一、 目录与文件部署
从官网获取 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} PRIVATEVulkan::Vulkanimgui ${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. 构建UIImGui::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