iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0

因為雲的數學與參數相比前面的複雜非常多,這邊會安裝 ImGui 來方便後面的數值變化展示。

ImGui 是什麼

在 C++ 既有 UI 相關的函式庫中,Qt 是最有名的那個,Qt 會依賴作業系統提供的視窗系統、輸入事件與平台功能來建立應用程式介面,功能齊全。但在目前的專案裡,就相當於要把這整套在花一層的包裝去整合在 DX 的參數控制,相對笨重。

而 Imgui 是直接整合在 DirectX、Vulkan 本身,因為這個特性,在即時調整 Renderer 或 Shader 等 GPU 相關參數時會更加方便。

下載

先在 vcpkg 新增 imgui

cd vcpkg
vcpkg add port imgui

下載完後記得刪除 Cache 讓 vcpkg 去下載環境。

範例

**// 新增 constant buffer 用來控制顏色
cbuffer TriangleConstants : register(b0)
{
    float4 triangleColor;
};

struct VSInput
{
    float3 position : POSITION;
};

struct VSOutput
{
    float4 position : SV_POSITION;
};

VSOutput VSMain(VSInput input)
{
    VSOutput output = (VSOutput)0;
    output.position = float4(input.position, 1.0f);
    return output;
}

float4 PSMain(VSOutput input) : SV_Target0
{
    return triangleColor;
}**
imgui_system.h

#pragma once

#include <Windows.h>
#include <array>
#include <d3d12.h>
#include <wrl/client.h>

class GraphicsEngine;
struct ImGui_ImplDX12_InitInfo;

namespace Skyline::UI
{
class ImGuiSystem
{
public:
    ImGuiSystem() = default;
    ~ImGuiSystem() noexcept;

    ImGuiSystem(const ImGuiSystem&) = delete;
    ImGuiSystem& operator=(const ImGuiSystem&) = delete;
    ImGuiSystem(ImGuiSystem&&) = delete;
    ImGuiSystem& operator=(ImGuiSystem&&) = delete;

    void initialize(HWND window, GraphicsEngine& graphicsEngine);
    void beginFrame();
    void render();
    void shutdown() noexcept;
    bool beginWindow(const char* title);
    void endWindow();
    bool colorSliders(const char* label, float color[3]);

  private:
    static constexpr UINT DescriptorCount = 32;

    static void allocateDescriptor(
        ImGui_ImplDX12_InitInfo* information,
        D3D12_CPU_DESCRIPTOR_HANDLE* cpuHandle,
        D3D12_GPU_DESCRIPTOR_HANDLE* gpuHandle);
    static void freeDescriptor(
        ImGui_ImplDX12_InitInfo* information,
        D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
        D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle);

    GraphicsEngine* m_graphicsEngine = nullptr;
    Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> m_descriptorHeap;
    std::array<bool, DescriptorCount> m_descriptorUsage{};
    UINT m_descriptorSize = 0;
    bool m_initialized = false;
};
}
imgui_system.cpp

#include "imgui_system.h"

#include <stdexcept>

#include <imgui.h>
#include <imgui_impl_dx12.h>
#include <imgui_impl_win32.h>

#include "graphics_engine.h"
#include "system.h"

    // 宣告 ImGui Win32 backend 提供的 Windows 訊息處理函式
    extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND window, UINT message, WPARAM wParam,
                                                                 LPARAM lParam);

namespace Skyline::UI
{
// 解構時自動釋放 ImGui 使用的資源
ImGuiSystem::~ImGuiSystem() noexcept
{
    // 關閉 ImGui 系統並清理資源
    shutdown();
}

// 初始化 ImGui 的 Win32 與 DirectX 12 backend
void ImGuiSystem::initialize(HWND window, GraphicsEngine& graphicsEngine)
{
    // 防止重複初始化
    if (m_initialized)
        // 系統已初始化時拋出邏輯錯誤
        throw std::logic_error("ImGuiSystem: The system is already initialized.");

    // 確認傳入有效的視窗 Handle
    if (window == nullptr)
        // 視窗無效時拋出參數錯誤
        throw std::invalid_argument("ImGuiSystem: A valid window is required.");

    // 取得 GraphicsEngine 使用的 D3D12 device
    ID3D12Device* device = graphicsEngine.getD3DDevice();

    // 取得提交 GPU 指令使用的 command queue
    ID3D12CommandQueue* commandQueue = graphicsEngine.getCommandQueue();

    // 確認 D3D12 device 已完成初始化
    if (device == nullptr)
        // device 無效時拋出參數錯誤
        throw std::invalid_argument("ImGuiSystem: An initialized D3D12 device is required.");

    // 確認 command queue 已完成初始化
    if (commandQueue == nullptr)
        // command queue 無效時拋出參數錯誤
        throw std::invalid_argument("ImGuiSystem: An initialized D3D12 command queue is required.");

    // 建立 descriptor heap 的設定資料並初始化為零
    D3D12_DESCRIPTOR_HEAP_DESC heapDescription{};

    // 設定為可存放 CBV、SRV 與 UAV 的 descriptor heap
    heapDescription.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

    // 設定 descriptor heap 可容納的 descriptor 數量
    heapDescription.NumDescriptors = DescriptorCount;

    // 允許 shader 存取這個 descriptor heap
    heapDescription.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;

    // 建立 ImGui 使用的 descriptor heap
    if (FAILED(device->CreateDescriptorHeap(&heapDescription, IID_PPV_ARGS(&m_descriptorHeap))))
        // 建立失敗時拋出執行階段錯誤
        throw std::runtime_error("ImGuiSystem: Failed to create the descriptor heap.");

    // 檢查目前 ImGui header 與 library 的版本是否相容
    IMGUI_CHECKVERSION();

    // 建立 ImGui context
    ImGui::CreateContext();

    // 套用 ImGui 預設深色主題
    ImGui::StyleColorsDark();

    // 初始化 ImGui 的 Win32 backend
    if (!ImGui_ImplWin32_Init(window))
    {
        // Win32 backend 初始化失敗時銷毀已建立的 ImGui context
        ImGui::DestroyContext();

        // 回報 Win32 backend 初始化失敗
        throw std::runtime_error("ImGuiSystem: Failed to initialize the Win32 backend.");
    }

    // 取得 CBV、SRV、UAV descriptor 在 heap 中的位元組間距
    m_descriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

    // 建立 DirectX 12 backend 的初始化資訊並初始化為零
    ImGui_ImplDX12_InitInfo initializationInformation{};

    // 指定 ImGui 使用的 D3D12 device
    initializationInformation.Device = device;

    // 指定 ImGui 使用的 command queue
    initializationInformation.CommandQueue = commandQueue;

    // 設定同時處理中的 frame 數量
    initializationInformation.NumFramesInFlight = GraphicsEngine::FRAME_BUFFER_COUNT;

    // 設定 Render Target 的像素格式
    initializationInformation.RTVFormat = DXGI_FORMAT_R8G8B8A8_UNORM;

    // 設定 Depth Stencil Buffer 的像素格式
    initializationInformation.DSVFormat = DXGI_FORMAT_D32_FLOAT;

    // 提供 ImGui 配置 SRV 時使用的 descriptor heap
    initializationInformation.SrvDescriptorHeap = m_descriptorHeap.Get();

    // 將目前 ImGuiSystem 實例傳給 descriptor callback 使用
    initializationInformation.UserData = this;

    // 指定 descriptor 配置 callback
    initializationInformation.SrvDescriptorAllocFn = allocateDescriptor;

    // 指定 descriptor 釋放 callback
    initializationInformation.SrvDescriptorFreeFn = freeDescriptor;

    // 初始化 ImGui 的 DirectX 12 backend
    if (!ImGui_ImplDX12_Init(&initializationInformation))
    {
        // DirectX 12 backend 初始化失敗時先關閉 Win32 backend
        ImGui_ImplWin32_Shutdown();

        // 銷毀 ImGui context
        ImGui::DestroyContext();

        // 回報 DirectX 12 backend 初始化失敗
        throw std::runtime_error("ImGuiSystem: Failed to initialize the DirectX 12 backend.");
    }

    // 保存 GraphicsEngine 的位址供後續 render 與 shutdown 使用
    m_graphicsEngine = &graphicsEngine;

    // 將 ImGui 的 Win32 訊息處理函式註冊到系統
    setWindowMessageHandler(ImGui_ImplWin32_WndProcHandler);

    // 標記 ImGui 系統已成功初始化
    m_initialized = true;
}

// 開始建立新的 ImGui frame
void ImGuiSystem::beginFrame()
{
    // 更新 DirectX 12 backend 的新 frame 狀態
    ImGui_ImplDX12_NewFrame();

    // 更新 Win32 backend 的新 frame 狀態
    ImGui_ImplWin32_NewFrame();

    // 開始新的 ImGui frame
    ImGui::NewFrame();
}

// 開始建立指定標題的 ImGui 視窗
bool ImGuiSystem::beginWindow(const char* title)
{
    // 建立視窗並回傳目前是否應繼續提交視窗內容
    return ImGui::Begin(title);
}

// 結束目前的 ImGui 視窗
void ImGuiSystem::endWindow()
{
    // 結束由 ImGui::Begin 建立的視窗
    ImGui::End();
}

// 建立控制 RGB 三個色彩分量的 slider
bool ImGuiSystem::colorSliders(const char* label, float color[3])
{
    // 將 label 推入 ImGui ID stack 避免不同元件發生 ID 衝突
    ImGui::PushID(label);

    // 顯示這組色彩控制項的文字標籤
    ImGui::TextUnformatted(label);

    // 建立紅色分量的 slider 並記錄數值是否被修改
    bool changed = ImGui::SliderFloat("Red", &color[0], 0.0f, 1.0f);

    // 建立綠色分量的 slider 並保留先前任一 slider 的修改狀態
    changed = ImGui::SliderFloat("Green", &color[1], 0.0f, 1.0f) || changed;

    // 建立藍色分量的 slider 並整合整組控制項的修改狀態
    changed = ImGui::SliderFloat("Blue", &color[2], 0.0f, 1.0f) || changed;

    // 移除先前推入 ID stack 的 label
    ImGui::PopID();

    // 回傳任一色彩分量是否被修改
    return changed;
}

// 將目前 ImGui frame 轉換並提交成 DirectX 12 繪製指令
void ImGuiSystem::render()
{
    // 完成目前 ImGui frame 並產生 draw data
    ImGui::Render();

    // 建立包含 ImGui descriptor heap 的陣列供 command list 綁定
    ID3D12DescriptorHeap* heaps[] = {m_descriptorHeap.Get()};

    // 取得目前用來記錄 GPU 繪圖命令的 command list
    ID3D12GraphicsCommandList* commandList = m_graphicsEngine->getCommandList();

    // 將 ImGui 使用的 shader-visible descriptor heap 綁定到 command list
    commandList->SetDescriptorHeaps(1, heaps);

    // 將 ImGui draw data 轉換成 DirectX 12 繪製命令
    ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList);
}

// 關閉 ImGui 系統並釋放相關資源
void ImGuiSystem::shutdown() noexcept
{
    // 尚未初始化時不需要執行任何清理
    if (!m_initialized)
        return;

    // 移除 ImGui 的 Windows 訊息處理函式
    setWindowMessageHandler(nullptr);

    // 等待目前繪圖工作完成以避免資源仍被 GPU 使用
    m_graphicsEngine->waitDraw();

    // 關閉 ImGui DirectX 12 backend
    ImGui_ImplDX12_Shutdown();

    // 關閉 ImGui Win32 backend
    ImGui_ImplWin32_Shutdown();

    // 銷毀 ImGui context
    ImGui::DestroyContext();

    // 釋放 descriptor heap
    m_descriptorHeap.Reset();

    // 將所有 descriptor 使用狀態重設為未使用
    m_descriptorUsage.fill(false);

    // 清除 descriptor 的位元組間距
    m_descriptorSize = 0;

    // 清除保存的 GraphicsEngine 指標
    m_graphicsEngine = nullptr;

    // 標記 ImGui 系統目前未初始化
    m_initialized = false;
}

// 從 ImGui 專用 descriptor heap 中配置一組 CPU 與 GPU descriptor handle
void ImGuiSystem::allocateDescriptor(ImGui_ImplDX12_InitInfo* information, D3D12_CPU_DESCRIPTOR_HANDLE* cpuHandle,
                                     D3D12_GPU_DESCRIPTOR_HANDLE* gpuHandle)
{
    // 從 UserData 取回這個 callback 所屬的 ImGuiSystem 實例
    auto& system = *static_cast<ImGuiSystem*>(information->UserData);

    // 逐一尋找尚未被使用的 descriptor
    for (UINT index = 0; index < DescriptorCount; ++index)
    {
        // 已被使用的 descriptor 直接跳過
        if (system.m_descriptorUsage[index])
            continue;

        // 將目前 descriptor 標記為已配置
        system.m_descriptorUsage[index] = true;

        // 取得 descriptor heap 起點的 CPU handle
        *cpuHandle = system.m_descriptorHeap->GetCPUDescriptorHandleForHeapStart();

        // 取得 descriptor heap 起點的 GPU handle
        *gpuHandle = system.m_descriptorHeap->GetGPUDescriptorHandleForHeapStart();

        // 將 CPU handle 移動到目前 descriptor 的位置
        cpuHandle->ptr += static_cast<SIZE_T>(index) * system.m_descriptorSize;

        // 將 GPU handle 移動到目前 descriptor 的位置
        gpuHandle->ptr += static_cast<UINT64>(index) * system.m_descriptorSize;

        // descriptor 配置完成後離開函式
        return;
    }

    // descriptor heap 已滿時在 Debug 模式觸發 assertion
    IM_ASSERT(false && "ImGuiSystem: The descriptor heap is full.");

    // 配置失敗時將 CPU handle 清空
    *cpuHandle = {};

    // 配置失敗時將 GPU handle 清空
    *gpuHandle = {};
}

// 將先前配置的 descriptor 標記為可再次使用
void ImGuiSystem::freeDescriptor(ImGui_ImplDX12_InitInfo* information, D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
                                 D3D12_GPU_DESCRIPTOR_HANDLE)
{
    // 從 UserData 取回這個 callback 所屬的 ImGuiSystem 實例
    auto& system = *static_cast<ImGuiSystem*>(information->UserData);

    // 取得 descriptor heap 起點的 CPU handle 位址
    const SIZE_T heapStart = system.m_descriptorHeap->GetCPUDescriptorHandleForHeapStart().ptr;

    // 計算欲釋放 descriptor 與 heap 起點之間的位元組距離
    const SIZE_T offset = cpuHandle.ptr - heapStart;

    // 根據 offset 與 descriptor 大小換算出 descriptor index
    const UINT index = static_cast<UINT>(offset / system.m_descriptorSize);

    // 確認計算出的 index 位於 descriptor heap 範圍內
    IM_ASSERT(index < DescriptorCount);

    // 將 descriptor 標記為未使用以供後續重新配置
    system.m_descriptorUsage[index] = false;
}
}
main.cpp

#include <cstdlib>
#include <directx/d3dx12_core.h>
#include <exception>
#include <stdexcept>

#include "constant_buffer.h"
#include "graphics_engine.h"
#include "my_engine.h"
#include "skyline_debugger.h"
#include "system.h"
#include "ui/imgui_system.h"

namespace
{
struct SimpleVertex
{
    float x;
    float y;
    float z;
};

//控制顏色的 struct
struct TriangleConstants
{
    float color[4];
};

void initializeRootSignature(RootSignature& rootSignature)
{
    rootSignature.init(
        D3D12_FILTER_MIN_MAG_MIP_LINEAR,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP);
}

D3D12_GRAPHICS_PIPELINE_STATE_DESC createPipelineDescription(
    ID3D12RootSignature* rootSignature,
    ID3DBlob* vertexShader,
    ID3DBlob* pixelShader)
{
    if (rootSignature == nullptr)
        throw std::invalid_argument("DayX_ImGui: A root signature is required.");
    if (vertexShader == nullptr)
        throw std::invalid_argument("DayX_ImGui: A vertex shader is required.");
    if (pixelShader == nullptr)
        throw std::invalid_argument("DayX_ImGui: A pixel shader is required.");

    static constexpr D3D12_INPUT_ELEMENT_DESC inputElements[] = {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    };

    D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};
    description.InputLayout = {inputElements, _countof(inputElements)};
    description.pRootSignature = rootSignature;
    description.VS = CD3DX12_SHADER_BYTECODE(vertexShader);
    description.PS = CD3DX12_SHADER_BYTECODE(pixelShader);
    description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
    description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    description.DepthStencilState.DepthEnable = FALSE;
    description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
    description.DepthStencilState.StencilEnable = FALSE;
    description.SampleMask = UINT_MAX;
    description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    description.NumRenderTargets = 1;
    description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    description.DSVFormat = DXGI_FORMAT_D32_FLOAT;
    description.SampleDesc.Count = 1;
    return description;
}
}

int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR commandLine, int showCommand)
{
    try
    {
        SkylineDebugger::Initialize("DayX_ImGui");
        initWindow(instance, previous, commandLine, showCommand, TEXT("DayX_ImGui"));
        if (g_hWnd == nullptr)
            throw std::runtime_error("DayX_ImGui: Failed to create the application window.");

        GraphicsEngine graphicsEngine;
        graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);
        SkylineDebugger::ConfigureD3D12(graphicsEngine.getD3DDevice());

        RootSignature rootSignature;
        initializeRootSignature(rootSignature);

        Shader vertexShader;
        Shader pixelShader;
        vertexShader.loadVS("assets/shaders/imgui_triangle.hlsl", "VSMain");
        pixelShader.loadPS("assets/shaders/imgui_triangle.hlsl", "PSMain");

        PipelineState pipelineState;
        pipelineState.init(createPipelineDescription(
            rootSignature.get(), vertexShader.getCompiledBlob(), pixelShader.getCompiledBlob()));

        const SimpleVertex vertices[] = {
            {-0.5f, -0.5f, 0.0f},
            {0.0f, 0.5f, 0.0f},
            {0.5f, -0.5f, 0.0f},
        };
        const std::uint16_t indices[] = {0, 1, 2};

        VertexBuffer vertexBuffer;
        vertexBuffer.init(_countof(vertices), sizeof(vertices[0]));
        vertexBuffer.copy(vertices);

        IndexBuffer indexBuffer;
        indexBuffer.init(_countof(indices), sizeof(indices[0]));
        indexBuffer.copy(indices);

        //初始化 Constant Buffer
        TriangleConstants triangleConstants{{1.0f, 0.2f, 0.1f, 1.0f}};
        ConstantBuffer constantBuffer;
        constantBuffer.init(sizeof(triangleConstants), &triangleConstants);

        RenderContext& renderContext = graphicsEngine.getRenderContext();
        renderContext.SetCommandList(graphicsEngine.getCommandList());

        //更新 imgui 
        Skyline::UI::ImGuiSystem imguiSystem;
        imguiSystem.initialize(g_hWnd, graphicsEngine);

        while (dispatchWindowMessage())
        {
            graphicsEngine.beginRender();
            imguiSystem.beginFrame();

            if (imguiSystem.beginWindow("Triangle controls"))
                imguiSystem.colorSliders("Triangle color", triangleConstants.color);
            imguiSystem.endWindow();

            constantBuffer.copyToVRAM(triangleConstants);

            renderContext.setRootSignature(rootSignature);
            renderContext.setPipelineState(pipelineState);
            renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
            renderContext.setVertexBuffer(vertexBuffer);
            renderContext.setIndexBuffer(indexBuffer);
            renderContext.setGraphicsRootConstantBufferView(0, constantBuffer.getGPUVirtualAddress());
            renderContext.drawIndexed(3);

            imguiSystem.render();
            graphicsEngine.endRender();

            SkylineDebugger::LogD3D12Messages(graphicsEngine.getD3DDevice());
        }

        imguiSystem.shutdown();
        SkylineDebugger::Shutdown();
        return EXIT_SUCCESS;
    }
    catch (const std::exception& error)
    {
        SkylineDebugger::ShowFatalError("DayX_ImGui initialization failed", error);
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
    catch (...)
    {
        SkylineDebugger::ShowFatalError("DayX_ImGui initialization failed", "An unknown fatal error occurred.");
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
}

結果


上一篇
Day 18:Compute Pipeline 介紹
下一篇
Day 20:Compute Shader、RWStructuredBuffer
系列文
因為 AI 看不懂老舊程式,只好乖乖從零開始學 DirectX 12 與 HLSL23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言