W 키 한 번이 이동 도구로 바뀌기까지
앞 글에서는 ImGuizmo가 수정한 World 행렬을 Transform에 반영했습니다. 이제 사용자가 W를 누르면 이동 화살표, E를 누르면 회전 고리가 나타나게 해 보겠습니다. 키를 눌렀다는 OS 메시지와 SceneWindow가 사용하는 조작 모드 사이에 어떤 연결이 필요한지 따라가는 과정입니다.
같은 W라도 Game에서는 캐릭터 이동, Inspector의 텍스트 칸에서는 문자 입력에 쓰일 수 있습니다. 따라서 키 코드만 검사하지 않고 어느 창에서 조작하려는지도 함께 판단해야 합니다.
ImGuizmo 키 입력 시스템: Transform 조작 구현
영상에서는 Scene을 선택한 뒤 이동·회전·크기 도구가 바뀌는 부분을 봅니다. 화면에는 도구만 바뀌어 보이지만, 내부에서는 입력 이벤트가 만들어지고 핸들러가 SceneWindow의 조작 모드 값을 갱신합니다. 이 글은 저장소의 Win32·SimpleMath·Transform 경로를 기준으로 설명합니다.
1. 키 입력에서 기즈모까지
flowchart LR
W[Win32 WM_KEYDOWN / WM_KEYUP] --> A[EditorApplication::SetKeyPressed]
A --> E[KeyPressedEvent / KeyReleasedEvent]
E --> C[등록된 EventCallback]
C --> D[EventDispatcher]
D --> K[OnKeyPressed]
K --> G[Q / W / E / R 모드 변경]
G --> S[SceneWindow의 ImGuizmo::Manipulate]Mermaid
복사
현재 창 입력은 GLFW가 아니라 Editor_Window/main.cpp의 Win32 메시지 처리에서 옵니다. SetKeyPressed의 action 값은 0=Release, 1=Press, 2=Repeat 형태의 내부 약속입니다. GLFW와 숫자 모양이 같다고 해서 GLFW 창을 사용하는 것은 아닙니다.
OS 메시지를 엔진의 키 이벤트로 바꿉니다
WndProc는 Windows의 키 메시지에서 keyCode와 action을 구해 다음 함수에 전달합니다. 여기서는 W인지부터 검사하지 않습니다. 먼저 눌림·해제·반복이라는 공통 입력 형태로 바꿉니다. 이후 기즈모 외의 기능도 같은 KeyPressedEvent를 읽을 수 있습니다.
void EditorApplication::SetKeyPressed(int keyCode, int scancode, int action, int mods)
{
constexpr int RELEASE = 0;
constexpr int PRESS = 1;
constexpr int REPEAT = 2;
switch (action)
{
case RELEASE:
{
ya::KeyReleasedEvent event(static_cast<ya::eKeyCode>(keyCode));
if (EventCallback)
EventCallback(event);
}
break;
case PRESS:
{
ya::KeyPressedEvent event(static_cast<ya::eKeyCode>(keyCode), false);
if (EventCallback)
EventCallback(event);
}
break;
case REPEAT:
{
ya::KeyPressedEvent event(static_cast<ya::eKeyCode>(keyCode), true);
if (EventCallback)
EventCallback(event);
}
break;
}
}
C++
복사
W를 처음 눌러 action이 1이면 PRESS 분기로 들어가고, keyCode가 W이며 IsRepeat가 false인 이벤트가 만들어집니다. EventCallback은 초기화에서 EditorApplication::OnEvent에 연결되어 있습니다. 그곳의 Dispatcher가 KeyPressedEvent를 골라 OnKeyPressed를 호출합니다. 키를 놓아 action이 0이 되면 KeyReleasedEvent가 만들어져 눌린 상태를 끝내는 경로로 갑니다.
위 함수가 만든 이벤트는 스택 객체이며 콜백 호출 동안만 유효합니다. 이 주소를 저장해 다음 프레임에 사용하면 안 됩니다. 지연 큐에 넣는 GameObject 이벤트와 수명이 다릅니다.
반복 입력의 현재 한계: 이 함수는 Repeat=2를 처리하지만, 현재 main.cpp의 호출부는 Release=0과 Press=1만 전달합니다. 따라서 IsRepeat() 필터만으로 Win32 자동 반복을 걸러낸다고 설명할 수 없습니다. 개선하려면 WM_KEYDOWN의 lParam 비트 30(이전 키 상태)을 확인해 반복을 구분해야 합니다. 반복 간격은 사용자의 OS 설정에 따라 달라집니다.
// 개선 예시: 현재 저장소에 반영한 코드가 아닙니다.
const bool keyUp = (static_cast<UINT_PTR>(lParam) & (UINT_PTR(1) << 31)) != 0;
const bool wasDown = (static_cast<UINT_PTR>(lParam) & (UINT_PTR(1) << 30)) != 0;
const int action = keyUp ? 0 : (wasDown ? 2 : 1);
// 이후 기존 SetKeyPressed 호출에 action을 전달합니다.
C++
복사
비트 30은 이 메시지 직전에 키가 이미 눌려 있었는지 나타냅니다. 첫 WM_KEYDOWN은 0이므로 Press=1, 누르고 있는 동안 들어온 추가 WM_KEYDOWN은 1이므로 Repeat=2로 구분할 수 있습니다. 비트 31이 1인 해제는 Release=0을 우선합니다. 이 연결을 추가해야 아래 IsRepeat 검사에 의미 있는 입력이 들어옵니다.
2. Scene 포커스와 모드 변경
모드를 바꾸되 진행 중인 드래그는 유지합니다
입력을 받는 함수를 읽을 때는 switch만 보지 말고 앞의 반환 조건부터 봅니다. Scene에 포커스가 없으면 이 입력으로 Scene 도구를 바꾸지 않습니다. 기즈모 축을 끌고 있는 중에는 다른 조작 모드로 바꾸지 않아 드래그 도중 해석이 바뀌는 것을 막습니다.
bool EditorApplication::OnKeyPressed(ya::KeyPressedEvent& e)
{
auto* sceneWindow = static_cast<SceneWindow*>(EditorWindows.at(L"SceneWindow"));
if (!sceneWindow->IsViewportFocused()) return false;
if (e.IsRepeat())
return false;
bool control = ImGui::GetIO().KeyCtrl;
bool shift = ImGui::GetIO().KeyShift;
switch (e.GetKeyCode())
{
// Gizmos
case ya::eKeyCode::Q:
{
if (!ImGuizmo::IsUsing())
SetGuizmoType(-1);
break;
}
case ya::eKeyCode::W:
{
if (!ImGuizmo::IsUsing())
SetGuizmoType(ImGuizmo::OPERATION::TRANSLATE);
break;
}
case ya::eKeyCode::E:
{
if (!ImGuizmo::IsUsing())
SetGuizmoType(ImGuizmo::OPERATION::ROTATE);
break;
}
case ya::eKeyCode::R:
{
if (control)
{
//ScriptEngine::ReloadAssembly();
}
else
{
if (!ImGuizmo::IsUsing())
SetGuizmoType(ImGuizmo::OPERATION::SCALE);
}
break;
}
}
return true;
}
C++
복사
W 분기를 실행하면 SetGuizmoType이 SceneWindow에 TRANSLATE 값을 전달합니다. 다음 UI 프레임에서 SceneWindow가 그 값을 Manipulate의 operation 인자로 사용하므로 이동 화살표가 나타납니다. W 자체는 Transform의 Position을 바꾸지 않습니다. W는 도구를 선택하고, 실제 이동은 마우스 드래그 결과를 분해해 setter에 넣을 때 일어납니다.
키 | 현재 동작 |
Q | 기즈모 숨기기 (-1) |
W | 이동 (TRANSLATE) |
E | 회전 (ROTATE) |
R | 크기 (SCALE) |
Ctrl+R | 스크립트 리로드 자리만 있으며 호출은 주석 상태 |
Scene 포커스를 확인하고 ImGuizmo::IsUsing()으로 드래그 중 모드 변경을 막습니다. 위 실제 함수는 Scene에 포커스가 있으면 처리하지 않은 키도 마지막에 true를 반환합니다. 향후에는 실제 사용한 키만 소비하고, 텍스트 편집 중인 단축키 입력도 구분하는 편이 좋습니다. Ctrl+S 저장이나 Ctrl+Z Undo가 이 코드로 완성된 것은 아닙니다.
EventDispatcher::Dispatch<T>는 지금 받은 이벤트의 타입이 T인지 확인해 즉시 함수를 호출합니다. 반환값은 타입 일치 여부이고, Handled는 핸들러가 입력을 소비했는지 나타냅니다. 다음 레이어 전달을 멈추는 것은 호출부의 책임입니다. ImGui 플랫폼 백엔드에는 키 눌림과 해제 상태가 모두 전달되어야 합니다.
3. 화면에서 Transform을 바꾸는 순서
1.
Scene RT를 ImGui::Image로 표시하고 실제 이미지 영역을 구합니다.
2.
같은 Scene용 EditorCamera의 View·Projection을 준비합니다.
3.
선택한 오브젝트의 World 행렬과 조작 모드를 ImGuizmo::Manipulate에 전달합니다.
4.
변경된 행렬을 위치·회전·크기로 분해해 Transform에 적용합니다.
이동 축은 X=빨강, Y=초록, Z=파랑입니다. LOCAL은 오브젝트 축, WORLD는 월드 축을 기준으로 합니다. 현재 SceneWindow는 WORLD 모드로 호출합니다. 스냅을 사용할 때 이동·크기는 축별 값 3개, 회전은 각도 값을 준비하고 라이브러리에서 읽는 배열 크기를 맞춥니다.
행렬 규칙: YamYam Transform은 각도를 degree로 보관하고 행벡터 기준 Scale × Rotate × Translation으로 World를 만듭니다. GLM 열벡터 예제의 네 번째 열에 위치가 있다는 설명을 SimpleMath 메모리에 그대로 적용하면 안 됩니다. SimpleMath의 위치는 _41, _42, _43입니다. ImGuizmo 입력 전치를 무조건 추가하지 말고, 현재 SceneWindow가 사용하는 행렬 경로를 함께 확인합니다.
World 결과를 local Transform에 바로 쓰는 현재 방식은 부모 변환이 없는 오브젝트를 전제로 합니다. 부모 계층을 도입하면 World 결과에 부모 World의 역행렬을 적용해 local로 되돌려야 합니다. 0 스케일, 음수 스케일, shear, 오일러 각 불연속은 단순 TRS 분해에서 별도로 다룰 문제입니다.
기즈모를 그리는 조건과 새 입력을 받는 조건도 구분합니다. 마우스가 이미지 밖으로 나갔다고 기즈모를 즉시 숨기면 진행 중인 드래그가 끊길 수 있습니다. 패널 가시성·선택 상태로 표시 여부를 판단하고 입력 시작 위치와 드래그 지속 여부를 따로 관리합니다.
4. 한 번의 조작을 처음부터 끝까지 확인해 봅시다
Scene을 클릭하고 W를 눌러 이동 화살표를 표시한 다음 X축을 끌어 봅니다. W를 누른 시점에는 위치가 그대로이고, 끄는 동안에만 Position이 바뀌어야 합니다. 마우스를 놓은 뒤에도 위치가 유지되는 것은 Transform에 결과를 돌려주었기 때문입니다.
이번에는 E를 눌러 회전 도구로 바꾸고, 축을 드래그하는 도중 W를 눌러 봅니다. IsUsing 검사 때문에 진행 중인 조작 모드는 유지되어야 합니다. Q는 도구를 숨길 뿐 선택 객체의 Transform을 초기화하지 않습니다. Game에 포커스를 옮긴 상태와 키를 길게 누르는 상황까지 비교하면 포커스·모드·반복 필터의 역할을 각각 확인할 수 있습니다.
영상은 이 기능의 개발 당시 기록입니다. Undo/Redo, 복수 선택, 부모 계층 변환, 스크립트 리로드는 이 영상이나 예제만으로 구현 완료라고 보지 않습니다.
DX12의 RT 연결과 현재 화면: DX12 Texture·RenderTarget 구현과 Scene / Game 뷰 연결


