[DirectX] IMGUI
in Study on WinAPI&DirectX
IMGUI를 이용해서 에디터를 만들어낼 것이다.
IMGUI란?
IMGUI는 게임을 만들 때 사용하는 에디터이다.
상술한 링크에서 코드를 다운로드 받을 수 있다.
많은 버전 중에서 위의 버전은 도킹버전인데, 도킹버전을 선택한 이유는
- UI가 게임 창 밖으로 나갈 수 있다.
- UI를 붙였다가 떼었다가 할 수 있다.
많은 게임회사들에서는 자체 엔진이나 언리얼, 유니티 엔진을 쓰지만,
간단한 툴을 만들 때에는 IMGUI를 사용한다.
IMGUI 띄우기
imgui_demo.cpp
imgui_demo.cpp 코드가 데모, 예시 코드이다. 이 코드를 통해 어떤 기능이 수행되는지 참고하면 된다.
자기 프로젝트에서 IMGUI 초기화 하기
main.cpp에서 참조할 헤더파일
- imgui.h
- imgui_impl_win32.h
- imgui_impl_dx11.h
// Client 프로젝트의 main.cpp 중에서
// 전역 변수:
HINSTANCE hInst; // 현재 인스턴스입니다.
HWND g_hWnd;
// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
// _CrtSetBreakAlloc(270);
MyRegisterClass(hInstance);
// 애플리케이션 초기화를 수행합니다:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// CEngine 초기화
if (FAILED(CEngine::GetInst()->init(g_hWnd, 1280, 768)))
{
return 0;
}
// Editor 초기화
CEditorObjMgr::GetInst()->init();
// ImGui 초기화
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
//io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows
//io.ConfigViewportsNoAutoMerge = true;
//io.ConfigViewportsNoTaskBarIcon = true;
//io.ConfigViewportsNoDefaultParent = true;
//io.ConfigDockingAlwaysTabBar = true;
//io.ConfigDockingTransparentPayload = true;
//io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleFonts; // FIXME-DPI: Experimental. THIS CURRENTLY DOESN'T WORK AS EXPECTED. DON'T USE IN USER APP!
//io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleViewports; // FIXME-DPI: Experimental.
// Setup Dear ImGui style
ImGui::StyleColorsDark();
//ImGui::StyleColorsLight();
// When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
ImGuiStyle& style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// Setup Platform/Renderer backends
ImGui_ImplWin32_Init(g_hWnd);
ImGui_ImplDX11_Init(DEVICE, CONTEXT);
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_CLIENT));
MSG msg;
bool show_demo_window = true;
bool show_another_window = false;
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
while (true)
{
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (WM_QUIT == msg.message)
break;
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
CEngine::GetInst()->progress();
CEditorObjMgr::GetInst()->progress();
// ImGui Update
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
if (show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);
// 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
{
static float f = 0.0f;
static int counter = 0;
ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.
ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too)
ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state
ImGui::Checkbox("Another Window", &show_another_window);
ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f
ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color
if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated)
counter++;
ImGui::SameLine();
ImGui::Text("counter = %d", counter);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
ImGui::End();
}
// 3. Show another simple window.
if (show_another_window)
{
ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
ImGui::Text("Hello from another window!");
if (ImGui::Button("Close Me"))
show_another_window = false;
ImGui::End();
}
// ImGui Rendering
ImGui::Render();
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Update and Render additional Platform Windows
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
// 렌더 종료
CDevice::GetInst()->Present();
}
}
// ImGui Release
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
return (int) msg.wParam;
}
위와 같이 IMGUI 프로젝트의 main.cpp로부터 코드를 가져온다.
그리고 Client 프로젝트의 main.cpp에서 IMGUI의 이벤트, 메세지에 대한 처리 작업 또한 구현해주어야 한다.
// Client 프로젝트의 main.cpp 중에서
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(hWnd, message, wParam, lParam))
return true;
switch (message)
{
case WM_DPICHANGED:
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports)
{
//const int dpi = HIWORD(wParam);
//printf("WM_DPICHANGED to %d (%.0f%%)\n", dpi, (float)dpi / 96.0f * 100.0f);
const RECT* suggested_rect = (RECT*)lParam;
::SetWindowPos(hWnd, NULL, suggested_rect->left, suggested_rect->top, suggested_rect->right - suggested_rect->left, suggested_rect->bottom - suggested_rect->top, SWP_NOZORDER | SWP_NOACTIVATE);
}
break;
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 메뉴 선택을 구문 분석합니다:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
버그 수정하기
IMGUI가 창 밖으로 나갔다가 다시 들어오면 UI가 사라지는 현상이 발생한다. 이를 어떻게 수정할까?
현재 렌더타겟을 스왑체인이 관리하고 있다. 이게 IMGUI가 사라지는 것이랑 무슨 상관이 있을까?
상술하였듯이, IMGUI Docking 버전의 장점은 UI가 창 밖으로 나갈 수 있다는 것이다.
이 때, 창 밖으로 나간 UI가 새로운 창이 되는 것이며, 새로운 스왚체인을 생성하게 된다.
이말인 즉슨, 출력하는 윈도우, 렌더 타겟이 달라지게 된다.
이런 현상이 왜 발생했냐면, 디바이스를 초기화 할때
// CDevice.cpp 에서
// 출력 타겟 설정
m_Context->OMSetRenderTargets(1, m_RTV.GetAddressOf(), m_DSV.Get());
타겟을 오직 메인 창만을 가리키도록 설정했기 때문이다.
그래서 출력 타겟 지정을 CRenderMgr.cpp에서 실시해야한다.
이를 위해서 각기 코드를 아래와 같이 작성한다.
// CDevice.h 중에서
public:
void OMSet() { m_Context->OMSetRenderTargets(1, m_RTV.GetAddressOf(), m_DSV.Get());}
// CRenderMgr.cpp 파일 중에서 렌더링 함수
그래서 매번 렌더링 할때마다 렌더 타겟을 알맞게 바꿔주고, UI가 밖으로 나갔다가 들어왔다가 할 때마다 스왚체인이 새로 생성되어 안정적으로 구동이 된다.
IMGUI의 UI 별 구성 살펴보기
아래와 같이 데모코드가 구성되어 있다.
// ImGui Update
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
if (show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);
// 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
{
static float f = 0.0f;
static int counter = 0;
ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.
ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too)
ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state
ImGui::Checkbox("Another Window", &show_another_window);
ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f
ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color
if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated)
counter++;
ImGui::SameLine();
ImGui::Text("counter = %d", counter);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
ImGui::End();
}
// 3. Show another simple window.
if (show_another_window)
{
ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
ImGui::Text("Hello from another window!");
if (ImGui::Button("Close Me"))
show_another_window = false;
ImGui::End();
}
UI 띄우기
ImGui::Begin("TestUI");
UI의 이름을 부여할 경우 Begin 안에 String을 넣어준다.
UI에 내용 넣기
ImGui::Text("Test String")
UI 안에 내용을 넣고자 할 경우 위와 같이 함수에 String을 부여한다.
주의사항! 같은 String값이 있으면 안된다!
같은 String을 넣고 싶다면?
ImGui::Text("Test String##1")
ImGui::Text("Test String##2")
위와 같이 # 2개를 넣고 다른 번호를 부여하면 된다.
# 2개 뒤에 있는 문자는 노출이 되지 않는 점을 이용한다.
UI에 버튼 넣기
ImGui::Button("Test Button");
버튼을 만들고 버튼에 이름을 상기한 바와 같이 불일 수 있다.
UI 구성 완료하기
ImGui::End();
위 코드를 통해 UI 하나 구성을 완료한다.
창 디자인 해보기
bool bTestUI = true;
if(bTestUI)
{
ImGui::Begin("TestWindow", &bTestUI)
ImGui::Text("Test String");
ImGui::Button("Test Button");
if (ImGui::BeginChild("ChildUI"))
{
ImGui::Text("Child Part");
ImGui::EndChild();
}
ImGui::End();
}
그리고 결과는 아래 그림과 같다.
코드 별로 UI에 반영되는 것은 imgui_demo.cpp를 뜯어봄으로써 알 수 있다.