Dear ImGui是一個開源GUI框架。除了UI部分外,本身還支援簡單的鍵鼠互動。目前專案內建的是V1.87版本,大概半年時間會更新一次版本,並且對原始碼有小幅度調整。
注意:直接下載原始碼使用會導致19章之後的UI效果有誤,修改了原始碼
imgui_impl_dx11.cpp
,需要用專案原始碼中的替換下載的。具體原因參考文末
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。
在本專案的ImGui資料夾中內建了一個VS專案,你可以參考其做法,也可以直接使用它。具體要用到的標頭檔案和原始檔包括這些:
如果你是從網上下載的ImGui包,則需要在根目錄、backends裡面找出上圖這些標頭檔案和原始檔。
在將資料夾中列出的.cpp和.h拖入自建的ImGui專案後,進入專案屬性頁進行改動:
部分內容根據自己VS版本進行修改
然後我們需要生成x64 Debug和x64 Release版本的靜態庫,生成位置分別位於:
然後開啟自己之前新建的專案,在屬性頁中找到C/C++ → 附加包含目錄,新增ImGui的目錄進你的專案。
緊接著是連結器 → 輸入 → 附加依賴項中直接加入ImGui.lib。在Debug和Release下使用統一使用YourImGuiDir\$(Platform)\$(Configuration)\ImGui.lib
,前面是相對或絕對路徑均可。
在ImGui資料夾中已經包含了一個CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
aux_source_directory(. IMGUI_DIR_SRCS)
add_library(ImGui STATIC ${IMGUI_DIR_SRCS})
target_include_directories(ImGui PUBLIC .)
你可以將ImGui專案資料夾複製到你的專案路徑內,然後在你專案的CMakeList.txt加上這樣一句話就可以在你的解決方案中新增並使用ImGui庫了:
# ImGui
target_link_libraries(YourTargetName ImGui)
在D3DApp.h
中新增這三個標頭檔案
#include <imgui.h>
#include <imgui_impl_dx11.h>
#include <imgui_impl_win32.h>
新增D3DApp::InitImGui
方法:
bool D3DApp::InitImGui()
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // 允許鍵盤控制
io.ConfigWindowsMoveFromTitleBarOnly = true; // 僅允許標題拖動
// 設定Dear ImGui風格
ImGui::StyleColorsDark();
// 設定平臺/渲染器後端
ImGui_ImplWin32_Init(m_hMainWnd);
ImGui_ImplDX11_Init(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get());
return true;
}
在D3DApp::Init
中呼叫InitImGui
:
bool D3DApp::Init()
{
if (!InitMainWindow())
return false;
if (!InitDirect3D())
return false;
if (!InitImGui())
return false;
return true;
}
然後在D3DApp.cpp
的上方新增一句話來參照外部函數:
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
在訊息處理常式D3DApp::MsgProc
的開頭新增ImGui的處理:
LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(m_hMainWnd, msg, wParam, lParam))
return true;
// ...
}
在D3DApp::Run()
中,我們插入這三個函數用於啟動ImGui新一幀的記錄與繪製:
int D3DApp::Run()
{
MSG msg = { 0 };
m_Timer.Reset();
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
m_Timer.Tick();
if (!m_AppPaused)
{
CalculateFrameStats();
// 這裡新增
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
// --------
UpdateScene(m_Timer.DeltaTime());
DrawScene();
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
最後就是在GameApp::DrawScene()
中插入這兩句:
void GameApp::UpdateScene(float dt)
{
// 可以在這之前呼叫ImGui的UI部分
}
void GameApp::DrawScene()
{
// 可以在這之前呼叫ImGui的UI部分
// Direct3D 繪製部分
ImGui::Render();
// 下面這句話會觸發ImGui在Direct3D的繪製
// 因此需要在此之前將後備緩衝區繫結到渲染管線上
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
HR(m_pSwapChain->Present(0, 0));
}
這樣就完成了ImGui的初始化。接下來可以在UpdateScene
裡面放幾個ImGui的範例視窗看看:
void GameApp::UpdateScene(float dt)
{
// ImGui內部範例視窗
ImGui::ShowAboutWindow();
ImGui::ShowDemoWindow();
ImGui::ShowUserGuide();
}
出現這些視窗且可以操作的話就是成功了,但是在前面的程式碼中已經禁用了雙擊視窗區域摺疊的功能。
需要注意的是程式執行後,exe所在的路徑會生成一個imgui.ini
的檔案,用於記錄視窗的佈局,這樣下次開啟的話就會保持先前的視窗狀態。可以在將佈局弄好後,把imgui.ini
複製複製到專案路徑,然後通過cmake複製過去:
file(COPY imgui.ini DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
通常一個視窗的結構為:
static bool is_open = true;
if (ImGui::Begin("Window" /*, &is_open */)) // 視窗右上角加上X關閉
{
// 新增元件
}
ImGui::end();
以本專案的為例:
//
// 自定義視窗與操作
//
static float tx = 0.0f, ty = 0.0f, phi = 0.0f, theta = 0.0f, scale = 1.0f, fov = XM_PIDIV2;
static bool animateCube = true, customColor = false;
if (animateCube)
{
phi += 0.3f * dt, theta += 0.37f * dt;
phi = XMScalarModAngle(phi);
theta = XMScalarModAngle(theta);
}
if (ImGui::Begin("Use ImGui"))
{
ImGui::Checkbox("Animate Cube", &animateCube); // 核取方塊
ImGui::SameLine(0.0f, 25.0f); // 下一個控制元件在同一行往右25畫素單位
if (ImGui::Button("Reset Params")) // 按鈕
{
tx = ty = phi = theta = 0.0f;
scale = 1.0f;
fov = XM_PIDIV2;
}
ImGui::SliderFloat("Scale", &scale, 0.2f, 2.0f); // 拖動控制物體大小
ImGui::Text("Phi: %.2f degrees", XMConvertToDegrees(phi)); // 顯示文字,用於描述下面的控制元件
ImGui::SliderFloat("##1", &phi, -XM_PI, XM_PI, ""); // 不顯示控制元件標題,但使用##來避免標籤重複
// 空字串避免顯示數位
ImGui::Text("Theta: %.2f degrees", XMConvertToDegrees(theta));
// 另一種寫法是ImGui::PushID(2);
// 把裡面的##2刪去
ImGui::SliderFloat("##2", &theta, -XM_PI, XM_PI, "");
// 然後加上ImGui::PopID(2);
ImGui::Text("Position: (%.1f, %.1f, 0.0)", tx, ty);
ImGui::Text("FOV: %.2f degrees", XMConvertToDegrees(fov));
ImGui::SliderFloat("##3", &fov, XM_PIDIV4, XM_PI / 3 * 2, "");
if (ImGui::Checkbox("Use Custom Color", &customColor))
m_CBuffer.useCustomColor = customColor;
// 下面的控制元件受上面的核取方塊影響
if (customColor)
{
ImGui::ColorEdit3("Color", reinterpret_cast<float*>(&m_CBuffer.color)); // 編輯顏色
}
}
ImGui::End();
然後就可以得到這樣的一個視窗:
為了讓立方體顯示自己設定的顏色,著色器改為了下面這樣:
// Cube.hlsli
cbuffer ConstantBuffer : register(b0)
{
matrix g_World; // matrix可以用float4x4替代。不加row_major的情況下,矩陣預設為列主矩陣,
matrix g_View; // 可以在前面新增row_major表示行主矩陣
matrix g_Proj; // 該教學往後將使用預設的列主矩陣,但需要在C++程式碼端預先將矩陣進行轉置。
vector g_Color;
uint g_UseCustomColor;
}
struct VertexIn
{
float3 posL : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
// Cube_VS.hlsl
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = mul(float4(vIn.posL, 1.0f), g_World); // mul 才是矩陣乘法, 運運算元*要求操作物件為
vOut.posH = mul(vOut.posH, g_View); // 行列數相等的兩個矩陣,結果為
vOut.posH = mul(vOut.posH, g_Proj); // Cij = Aij * Bij
vOut.color = vIn.color; // 這裡alpha通道的值預設為1.0
return vOut;
}
// Cube_PS.hlsl
// 畫素著色器
float4 PS(VertexOut pIn) : SV_Target
{
return g_UseCustomColor ? g_Color : pIn.color;
}
然後常數緩衝區記得更新:
m_CBuffer.world = XMMatrixTranspose(
XMMatrixScalingFromVector(XMVectorReplicate(scale)) *
XMMatrixRotationX(phi) * XMMatrixRotationY(theta) *
XMMatrixTranslation(tx, ty, 0.0f));
m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(fov, AspectRatio(), 1.0f, 1000.0f));
// 更新常數緩衝區
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
現在嘗試一下效果:
通過ImGui本身提供的函數,我們能夠獲取到一些常用的鍵鼠事件:
ImVec2 pos = ImGui::GetCursorPos(); // 滑鼠位置
bool active = ImGui::IsMouseDragging(ImGuiMouseButton_Left); // 滑鼠左鍵是否在拖動
active = ImGui::IsMouseDown(ImGuiMouseButton_Right); // 滑鼠右鍵是否處於按下狀態
active = ImGui::IsKeyPressed(ImGuiKey_W); // 是否剛按下W鍵
active = ImGui::IsKeyReleased(ImGuiKey_S); // 是否剛鬆開S鍵
active = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left); // 是否雙擊左鍵
// ...
還有一些事件無法通過函數獲取的,我們可以使用ImGuiIO
來獲取:
ImGuiIO& io = ImGuiIO::GetIO();
auto& delta = io.MouseDelta; // 當前幀滑鼠位移量
io.MouseWheel; // 滑鼠滾輪
下面展示了利用ImGui的IO事件操作物體:
// 不允許在操作UI時操作物體
if (!ImGui::IsAnyItemActive())
{
// 滑鼠左鍵拖動平移
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left))
{
tx += io.MouseDelta.x * 0.01f;
ty -= io.MouseDelta.y * 0.01f;
}
// 滑鼠右鍵拖動旋轉
else if (ImGui::IsMouseDragging(ImGuiMouseButton_Right))
{
phi -= io.MouseDelta.y * 0.01f;
theta -= io.MouseDelta.x * 0.01f;
}
// 滑鼠滾輪縮放
else if (io.MouseWheel != 0.0f)
{
scale += 0.02f * io.MouseWheel;
if (scale > 2.0f)
scale = 2.0f;
else if (scale < 0.2f)
scale = 0.2f;
}
}
現在就可以操作這個立方體了:
至此ImGui就算是入門了。由於ImGui本身是沒有檔案的,讀者需要通過Dear ImGui Demo
的視窗來尋找自己需要的控制元件,然後在imgui_demo.cpp
中搜尋對應位置的控制元件以檢視它程式碼是怎麼用的。這裡也推薦一個網站方便檢索:ImGui Manual (pthom.github.io)
初學者可以跳過這一段,在遇到下面UI過亮的情況時候可以回頭看。
通常情況下我們使用的渲染目標格式是DXGI_FORMAT_R8G8B8A8_UNORM
的,而如果是DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
的話,會導致ImGui缺乏伽馬校正而過亮,就像下面的圖這樣:
ImGui本身是對sRGB無動於衷的。根據sRGB and linear color spaces · Issue #578 · ocornut/imgui,我們可以利用ImGui提供的沒有使用到的列舉值ImGuiConfigFlags_IsSRGB
,然後需要對ImGui的原始碼做一些修改。
在imgui_impl_dx11.cpp
大致382行的位置,我們使用如下程式碼替換vertexShader
,使得能夠根據ImGuiConfigFlags_IsSRGB
的設定與否來決定是否需要去除伽馬校正:
static const char* vertexShader = nullptr;
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_IsSRGB)
{
vertexShader =
"cbuffer vertexBuffer : register(b0) \
{\
float4x4 ProjectionMatrix; \
};\
struct VS_INPUT\
{\
float2 pos : POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
PS_INPUT main(VS_INPUT input)\
{\
PS_INPUT output;\
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
output.col = pow(input.col, 2.2f);\
output.uv = input.uv;\
return output;\
}";
}
else
{
vertexShader =
"cbuffer vertexBuffer : register(b0) \
{\
float4x4 ProjectionMatrix; \
};\
struct VS_INPUT\
{\
float2 pos : POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
PS_INPUT main(VS_INPUT input)\
{\
PS_INPUT output;\
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
output.col = input.col;\
output.uv = input.uv;\
return output;\
}";
}
然後在D3DApp::InitImGui
中新增ImGuiConfigFlags_IsSRGB
標誌:
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // 允許鍵盤控制
io.ConfigFlags |= ImGuiConfigFlags_IsSRGB; // 標記當前使用的是SRGB,目前對ImGui原始碼有修改
io.ConfigWindowsMoveFromTitleBarOnly = true; // 僅允許標題拖動
這樣顯示就正常了。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。