一、 目录与文件部署
从官网获取 IMGUI 代码库,在项目 extern
目录下新建 imgui
目录,将相关文件复制进去,构建出如下目录结构:
.
├── build
├── extern
│ ├── glfw
│ ├── glm
│ └── imgui
│ ├── backends
│ │ ├── imgui_impl_glfw.cpp
│ │ ├── imgui_impl_glfw.h
│ │ ├── imgui_impl_vulkan.cpp
│ │ └── imgui_impl_vulkan.h
│ ├── imconfig.h
│ ├── imgui_demo.cpp
│ ├── imgui_draw.cpp
│ ├── imgui_internal.h
│ ├── imgui_tables.cpp
│ ├── imgui_widgets.cpp
│ ├── imgui.cpp
│ ├── imgui.h
│ ├── imstb_rectpack.h
│ ├── imstb_textedit.h
│ ├── imstb_truetype.h
│ └── LICENSE.txt
└── stb
二、CMakeLists 配置 - 接入 ImGUI 依赖
(一)路径与文件设置
在 CMakeLists.txt
中,添加 ImGUI 相关配置:
# 设置外部库目录及各库路径
...
set(IMGUI_ROOT ${EXTERNAL_LIB_DIR}/imgui)
# 递归收集源文件
file(GLOB_RECURSE SOURCE_FILES src/*.cpp src/*.c)
(二)构建与链接 ImGUI 库
# 构建 imgui 静态库,指定实现文件
add_library(imgui STATIC
${IMGUI_ROOT}/imgui.cpp
${IMGUI_ROOT}/imgui_demo.cpp
${IMGUI_ROOT}/imgui_draw.cpp
${IMGUI_ROOT}/imgui_widgets.cpp
${IMGUI_ROOT}/imgui_tables.cpp
${IMGUI_ROOT}/backends/imgui_impl_glfw.cpp
${IMGUI_ROOT}/backends/imgui_impl_vulkan.cpp
)
# 配置 imgui 头文件包含目录
target_include_directories(imgui PUBLIC
${IMGUI_ROOT}
${GLFW_ROOT}/include
$<TARGET_PROPERTY:Vulkan::Vulkan,INTERFACE_INCLUDE_DIRECTORIES>
)
# 查找 Vulkan 库
find_package(Vulkan REQUIRED)
# 链接库到项目
target_link_libraries(${PROJECT_NAME} PRIVATE
Vulkan::Vulkan
imgui
${GLFW_LIB}
)
三、主代码集成 - main.cpp 中 ImGUI 接入
(一)头文件引入
...
#include <imgui/imgui.h>
#include <imgui/backends/imgui_impl_glfw.h>
#include <imgui/backends/imgui_impl_vulkan.h>
#include "renderer/VkRenderer.h"
#include "scene/Camera.h"
#include "ObjModelLoader.h"
#include "GltfModelLoader.h"
#include "HelloRect.h"
(二)初始化与回调
GLFWwindow* window;
const uint32_t WIDTH = 1920;
const uint32_t HEIGHT = 1000;
Camera camera(glm::vec3(5.0f, 5.0f, 5.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), 45.0f, 0.01f, 1000.0f);
VkContext vkcontext{};
// 帧缓冲大小变化回调
static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
vkcontext.framebufferResized = true;
}
// 窗口初始化
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(WIDTH, HEIGHT, "Hello Vulkan", nullptr, nullptr);
glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
}
(三)ImGUI 主题设置
// ImGUI 主题配置(含中文字体加载)
void setImguiTheme() {
float baseFontSize = 16.0f;
ImGuiIO& io = ImGui::GetIO();
ImFont* font = io.Fonts->AddFontFromFileTTF
(
"assets/fonts/msyh.ttc",
baseFontSize,
nullptr,
io.Fonts->GetGlyphRangesChineseFull()
);
IM_ASSERT(font != nullptr);
ImGui::StyleColorsDark();
ImGui::GetStyle().WindowRounding = 4.0f;
}
(四)主函数流程
int main() {
initWindow();
vkcontext.window = window;
vkcontext.camera = &camera;
// 加载模型并设置
MeshInstance* objModel = ObjModelLoader::loadModel("assets/models/viking_room.obj", "assets/models/viking_room.png");
objModel->modelMatrix = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 0.0f, 1.0f));
objModel->modelMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(3.0f, 3.0f, 3.0f));
vkcontext.meshInstances.push_back(objModel);
// Vulkan 初始化
if (!vkInit(&vkcontext)) {
throw std::runtime_error("Vulkan 初始化失败!");
}
// ImGUI 上下文等初始化
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
setImguiTheme();
ImGui_ImplGlfw_InitForVulkan(window, true);
ImGui_ImplVulkan_InitInfo init_info = {};
// 填充 init_info 字段...
ImGui_ImplVulkan_Init(&init_info);
// 上传 ImGUI 字体(独立命令缓冲)
VkCommandBufferAllocateInfo allocInfo{};
// 配置 allocInfo...
VkCommandBuffer font_cmd;
if (vkAllocateCommandBuffers(vkcontext.device, &allocInfo, &font_cmd) != VK_SUCCESS) {
throw std::runtime_error("无法为 ImGui 字体上传分配命令缓冲区!");
}
VkCommandBufferBeginInfo begin_info = {};
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin_info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(font_cmd, &begin_info);
ImGui_ImplVulkan_CreateFontsTexture();
vkEndCommandBuffer(font_cmd);
VkSubmitInfo submit_info = {};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &font_cmd;
vkQueueSubmit(vkcontext.graphicsQueue, 1, &submit_info, VK_NULL_HANDLE);
vkDeviceWaitIdle(vkcontext.device);
ImGui_ImplVulkan_DestroyFontsTexture();
// 主循环
try {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// 1. ImGui 新帧
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// 2. 构建UI
ImGui::ShowDemoWindow();
ImGui::Render();
vkRender(&vkcontext, ImGui::GetDrawData());
}
// 资源清理
vkDeviceWaitIdle(vkcontext.device);
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
vkClean(&vkcontext);
glfwDestroyWindow(window);
glfwTerminate();
for (MeshInstance* mesh: vkcontext.meshInstances) {
delete mesh;
}
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
四、渲染器代码更新 - VkRenderer 调整
(一)函数声明修改(VkRenderer.h)
void vkRender(VkContext* vkcontext, ImDrawData* imguiDrawData);
(二)描述符池与命令缓冲更新(VkRenderer.cpp)
1. 描述符池创建调整
// 创建描述符池,增大容量
{
std::array<VkDescriptorPoolSize, 2> poolSizes{};
size_t meshCount = ctx->meshInstances.size();
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES * meshCount)+ 1;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES * meshCount) + 1;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES * meshCount)+1;
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
if (vkCreateDescriptorPool(ctx->device, &poolInfo, nullptr, &ctx->descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("创建描述符池失败!");
}
}
2. 命令缓冲录制修改
void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex, ImDrawData* imguiDrawData, VkContext* ctx) {
// 原有模型绘制循环...
for (size_t i = 0; i < ctx->meshInstances.size(); ++i) {
// 模型绘制逻辑
}
// 绘制 ImGUI 内容
ImGui_ImplVulkan_RenderDrawData(imguiDrawData, commandBuffer);
vkCmdEndRenderPass(commandBuffer);
if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
throw std::runtime_error("录制命令缓冲失败!");
}
}
五、整合要点与总结
(一)核心流程
- Vulkan 基础准备:完成
VkInstance
、VkDevice
、SwapChain
等渲染器核心组件初始化,特别注意描述符池需预留足够空间给 ImGUI 与主渲染。 - ImGUI 初始化:通过
ImGui::CreateContext();
创建上下文,配置 IO(如中文字体、交互支持),设置主题样式,再初始化 GLFW + Vulkan 后端。 - 字体上传:需用独立命令缓冲,上传后通过
vkDeviceWaitIdle
保证同步,避免渲染异常。 - 主循环集成:每帧调用
NewFrame
、构建 UI(如ShowDemoWindow
)、Render
,最后在 Vulkan 命令缓冲中调用ImGui_ImplVulkan_RenderDrawData
绘制 UI。
(二)常见问题与解决
- 描述符池报错:若出现
vkFreeDescriptorSets
报错,检查是否添加VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
标志;若报空间不足(如错误码-1000069000
),需增大maxSets
和poolSizes
。 - 命令缓冲问题:字体上传必须用独立缓冲,否则易引发同步问题,导致渲染异常。
- 样式定制:通过
ImGui::GetStyle()
调整圆角、配色,结合ImGuiIO
加载自定义字体(如中文字体),灵活美化界面。
(三)关键总结
Vulkan 集成 ImGUI 需重点关注资源池管理(描述符池容量与标志)、命令缓冲同步(字体上传独立处理)、ImGUI 初始化流程。遇到验证层报错,优先排查描述符池配置;样式需求可通过 ImGUI 自身接口灵活实现,确保渲染流程与 UI 交互顺畅协同 。
当前代码分支:14_integrated_DearImGUI