本文的内容是介绍Vulkan整合Imgui的方法。Imgui官方给出了一个整合vulkan的案例,用户可以直接使用该后端进行渲染,也可以自己写vulkan后端进行渲染。本文采取了自己写vulkan后端的方式。若想采用imgui官方的后端,可以把imgui_impl_vulkan.h/cpp文件包含到自己的工程中使用。
初始化
在程序的最开始,程序判断使用哪种api渲染。如果是vulkan渲染,就创建一个VulkanImguiRenderer实例。VulkanImguiRenderer类是我自定义的vullkan imgui渲染类,采用了单例的实现思路,可以直接用类名调用方法。
if (RendererAPI::GetAPI() == RendererAPI::API::OpenGL)
{
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 410");
}
else if (RendererAPI::GetAPI() == RendererAPI::API::Vulkan)
{
ImGui_ImplGlfw_InitForVulkan(window, true);
VulkanImguiRenderer::Create();
}
渲染开始
imgui渲染的方式是:先生成若干绘制命令,再把这些命令统一提交给后端的渲染器,让它执行。开始记录绘制命令需要调用ImGui::NewFrame()函数。记录结束后,使用ImGui::Render()方法提交命令。
if (RendererAPI::GetAPI() == RendererAPI::API::OpenGL)
{
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGuizmo::BeginFrame();
}
else if (RendererAPI::GetAPI() == RendererAPI::API::Vulkan)
{
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGuizmo::BeginFrame();//坐标轴绘制工具
}
Vulkan后端工作流程
本项目的Imgui渲染后端在VulkanImguiRenderer类中实现。它的工作流程如下:
- 初始化vulkan渲染资源,包括创建顶点和索引缓冲、加载shader,创建管线等;
- 加载字体相关贴图资源,创建该贴图的描述符集;
- 外部提交渲染命令;
- 更新缓冲区数据,主要是顶点和索引缓冲数据。如果有必要,需重建顶点和索引缓冲区;
- 读取渲染命令,执行vulkan渲染操作;
- 返回步骤3循环,直到程序结束。
初始化vulkan资源
首先,创建imgui渲染管线的描述符集布局。这个布局是和imgui的shader对应的,即在0号binding位上有一个VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER类型的资源。
setLayout = std::make_shared<VulkanDescriptorSetLayout>();
setLayout->AddBinding(0, DescriptorType::IMAGE);
setLayout->Build();
同时,Push Constant也是和imgui的shader一一对应的,格式如下:
struct PushConstBlock {
glm::vec2 scale;
glm::vec2 translate;
} pushConstBlock;
接下来,进行渲染管线的配置。配置部分可以参考imgui官方提供的案例和SaschaWillems的vulkan整合imgui案例。
configInfo.inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
configInfo.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
configInfo.inputAssemblyInfo.flags = 0;
configInfo.inputAssemblyInfo.primitiveRestartEnable = VK_FALSE;
configInfo.viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
configInfo.viewportInfo.viewportCount = 1;
configInfo.viewportInfo.scissorCount = 1;
configInfo.viewportInfo.flags = 0;
configInfo.rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
configInfo.rasterizationInfo.polygonMode = VK_POLYGON_MODE_FILL;
configInfo.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
configInfo.rasterizationInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
configInfo.rasterizationInfo.flags = 0;
configInfo.rasterizationInfo.depthClampEnable = VK_FALSE;
configInfo.rasterizationInfo.lineWidth = 1.0f;
configInfo.multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
configInfo.multisampleInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
configInfo.multisampleInfo.flags = 0;
// Enable blending
configInfo.colorBlendAttachment.blendEnable = VK_TRUE;
configInfo.colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
configInfo.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
configInfo.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
configInfo.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
configInfo.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
configInfo.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
configInfo.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
configInfo.colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
configInfo.colorBlendInfo.attachmentCount = 1;
configInfo.colorBlendInfo.pAttachments = &configInfo.colorBlendAttachment;
configInfo.depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
configInfo.depthStencilInfo.depthTestEnable = VK_FALSE;
configInfo.depthStencilInfo.depthWriteEnable = VK_FALSE;
configInfo.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
configInfo.depthStencilInfo.back.compareOp = VK_COMPARE_OP_ALWAYS;
configInfo.dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR };
configInfo.dynamicStateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
configInfo.dynamicStateInfo.pDynamicStates = configInfo.dynamicStateEnables.data();
configInfo.dynamicStateInfo.dynamicStateCount = static_cast<uint32_t>(configInfo.dynamicStateEnables.size());
configInfo.dynamicStateInfo.flags = 0;
VkVertexInputBindingDescription vInputBindDescription{};
vInputBindDescription.binding = 0;
vInputBindDescription.stride = sizeof(ImDrawVert);
vInputBindDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
// Vertex bindings an attributes based on ImGui vertex definition
std::vector<VkVertexInputBindingDescription> vertexInputBindings = {
vInputBindDescription,
};
VkVertexInputAttributeDescription vInputAttribDescription1{};
vInputAttribDescription1.location = 0;
vInputAttribDescription1.binding = 0;
vInputAttribDescription1.format = VK_FORMAT_R32G32_SFLOAT;
vInputAttribDescription1.offset = offsetof(ImDrawVert, pos);
VkVertexInputAttributeDescription vInputAttribDescription2{};
vInputAttribDescription2.location = 1;
vInputAttribDescription2.binding = 0;
vInputAttribDescription2.format = VK_FORMAT_R32G32_SFLOAT;
vInputAttribDescription2.offset = offsetof(ImDrawVert, uv);
VkVertexInputAttributeDescription vInputAttribDescription3{};
vInputAttribDescription3.location = 2;
vInputAttribDescription3.binding = 0;
vInputAttribDescription3.format = VK_FORMAT_R8G8B8A8_UNORM;
vInputAttribDescription3.offset = offsetof(ImDrawVert, col);
std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
vInputAttribDescription1,vInputAttribDescription2,vInputAttribDescription3
};
std::vector<VkDescriptorSetLayout>layouts = { setLayout->GetDescriptorSetLayout() };
pipeline = std::make_shared<VulkanPipeline>(VulkanRenderer::Get()->GetSwapChain()->GetRenderPass()->GetRenderPass(),
layouts,
std::make_shared <VulkanShader>("assets/shaders/ui.vert",
"assets/shaders/ui.frag"),
sizeof(PushConstBlock),
vertexInputBindings,
vertexInputAttributes,
&configInfo
);
绑定贴图资源
imgui绘制需要依赖于一张字体贴图资源,vulkan后端需要为这张字体贴图资源创建描述符集,以便在渲染的时候绑定。除此之外,用户自定义的图片如果想要使用imgui渲染,也需要创建描述符集,并把描述符集的地址通过ImGui::Image函数传递给后端。
ImGui::Image函数想要绘制贴图,需要把一个作为贴图句柄的ImTextureId参数传递进去。这个参数在不同的图形API中含义是不同的。 在imgui官方的vulkan后端实现文件中,这个参数被设定为VkDescriptorSet *的值。不过,我个人不太欣赏这种实现方式,因为Vulkan是多帧同时渲染的,所以在前端执行Imgui::Image函数的时候,用户怎么确定要绑定的描述符集是哪一帧的呢?因此,我选择把自己封装的VulkanDescriptorSet对象地址作为ImTextureID传递进去。
我使用一个哈希表存储所有的描述符集,用名字作为索引。用户可以在外部使用RegisterImage函数注册图像资源,即为图像生成描述符集;也可以在外部使用GetImageId命令获得描述符集的值。
class VulkanImguiRenderer
{
public:
......
static void RegisterImage(std::string name,std::shared_ptr<VulkanImageView> imageView) {
inst->registerImage(name,imageView);
}
static void RegisterImages(std::string name, std::vector<std::shared_ptr<VulkanImageView> >& imageView) {
inst->registerImages(name, imageView);
}
static std::shared_ptr<VulkanDescriptorSet> GetImageId(std::string name)
{
return inst->getImageId(name);
}
private:
......
void registerImage(std::string name,std::shared_ptr<VulkanImageView> imageView);
void registerImages(std::string name, std::vector<std::shared_ptr<VulkanImageView> >& imageView);
std::unordered_map<std::string,std::shared_ptr<VulkanDescriptorSet> >descriptorSets;
}
注册图片资源的代码如下:
void VulkanImguiRenderer::registerImage(std::string name, std::shared_ptr<VulkanImageView> imageView)
{
std::shared_ptr<VulkanDescriptorSet>tempSet = std::make_shared<VulkanDescriptorSet>(setLayout);
tempSet->WriteImage(0, imageView->GetDescriptorImageInfo().get());
tempSet->Update();
descriptorSets.insert(std::pair<std::string, std::shared_ptr<VulkanDescriptorSet> >(name, tempSet));
}
在外部的使用方法如下:
ImGui::Image((ImTextureID)(VulkanImguiRenderer::GetImageId("renderTarget").get()), ImVec2(400, 400));
更新资源数据
imgui在每一帧的绘制之前,都需要更新缓冲区的数据,尤其是顶点和索引缓冲区的数据。
由于imgui会随时对顶点和索引缓冲区进行扩容,所以为了实现多帧绘制的功能,需要把顶点和索引缓冲区为每一帧定义一个。我把顶点和索引缓冲区进行了封装,把多帧对应的缓冲区封装到了同一个类里。
ImDrawData* imDrawData = ImGui::GetDrawData();
if (imDrawData == nullptr)return;
// Note: Alignment is done inside buffer creation
VkDeviceSize vertexBufferSize = imDrawData->TotalVtxCount * sizeof(ImDrawVert);
VkDeviceSize indexBufferSize = imDrawData->TotalIdxCount * sizeof(ImDrawIdx);
if ((vertexBufferSize == 0) || (indexBufferSize == 0)) {
return;
}
if (vertexBuffer == nullptr)
{
vertexBuffer = std::make_shared<VulkanVertexBuffer>(imDrawData->TotalVtxCount, sizeof(ImDrawVert));
vertexBuffer->Map();
}
else if (vertexBuffer->GetVertexCount() != imDrawData->TotalVtxCount)
{
vertexBuffer->Destroy();
vertexBuffer->CreateBuffer(imDrawData->TotalVtxCount, sizeof(ImDrawVert));
vertexBuffer->Map();
}
if (indexBuffer == nullptr)
{
indexBuffer = std::make_shared<VulkanIndexBuffer>(imDrawData->TotalIdxCount, sizeof(ImDrawIdx));
indexBuffer->Map();
}
else if (indexBuffer->GetIndexCount() != imDrawData->TotalIdxCount)
{
indexBuffer->Destroy();
indexBuffer->CreateBuffer(imDrawData->TotalIdxCount, sizeof(ImDrawIdx));
indexBuffer->Map();
}
// Update buffers only if vertex or index count has been changed compared to current buffer size
// Upload data
ImDrawVert* vtxDst = (ImDrawVert*)vertexBuffer->GetMapped();
ImDrawIdx* idxDst = (ImDrawIdx*)indexBuffer->GetMapped();
for (int n = 0; n < imDrawData->CmdListsCount; n++) {
const ImDrawList* cmd_list = imDrawData->CmdLists[n];
memcpy(vtxDst, cmd_list->VtxBuffer.Data, cmd_list->VtxBuffer.Size * sizeof(ImDrawVert));
memcpy(idxDst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx));
vtxDst += cmd_list->VtxBuffer.Size;
idxDst += cmd_list->IdxBuffer.Size;
}
// Flush to make writes visible to GPU
vertexBuffer->Flush();
indexBuffer->Flush();
绘制数据
首先,需要绑定渲染管线、绑定Push Constant。接下来,使用ImGui::GetDrawData函数获取渲染命令。循环遍历每一条命令,检查命令的TextureId成员是否为空,如果为空,说明这是一条普通的绘制命令,需要绑定字体贴图对应的描述符集;否则,就需要绑定用户自定义图片的描述符集。注意,这个TextureId字段就是在ImGui::Image函数中传递进来的参数。我们直接把它强制转换为它原本的格式即可。
if (ImGui::GetDrawData() == nullptr)return;
auto commandBuffer = VulkanRenderer::Get()->GetCurrentCommandBuffer();
ImGuiIO& io = ImGui::GetIO();
pipeline->Bind();
std::shared_ptr<VulkanDescriptorSet> fontDescriptorSet = getImageId("font");
auto descriptorPtr = fontDescriptorSet->GetDescriptorSet(VulkanRenderer::Get()->GetFrameIndex());
vkCmdBindDescriptorSets(
VulkanRenderer::Get()->GetCurrentCommandBuffer(),
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline->GetPipelineLayout(),
0,
1,
&descriptorPtr,
0,
nullptr
);
VkViewport viewport{};
viewport.width = ImGui::GetIO().DisplaySize.x;
viewport.height = ImGui::GetIO().DisplaySize.y;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
// UI scale and translate via push constants
pushConstBlock.scale = glm::vec2(2.0f / io.DisplaySize.x, 2.0f / io.DisplaySize.y);
pushConstBlock.translate = glm::vec2(-1.0f);
pipeline->PushConstant(sizeof(PushConstBlock), &pushConstBlock);
// Render commands
ImDrawData* imDrawData = ImGui::GetDrawData();
int32_t vertexOffset = 0;
int32_t indexOffset = 0;
if (imDrawData->CmdListsCount > 0) {
VkDeviceSize offsets[1] = { 0 };
vertexBuffer->Bind();
indexBuffer->Bind(VK_INDEX_TYPE_UINT16);
for (int32_t i = 0; i < imDrawData->CmdListsCount; i++)
{
const ImDrawList* cmd_list = imDrawData->CmdLists[i];
for (int32_t j = 0; j < cmd_list->CmdBuffer.Size; j++)
{
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[j];
VkRect2D scissorRect;
scissorRect.offset.x = std::max((int32_t)(pcmd->ClipRect.x), 0);
scissorRect.offset.y = std::max((int32_t)(pcmd->ClipRect.y), 0);
scissorRect.extent.width = (uint32_t)(pcmd->ClipRect.z - pcmd->ClipRect.x);
scissorRect.extent.height = (uint32_t)(pcmd->ClipRect.w - pcmd->ClipRect.y);
vkCmdSetScissor(commandBuffer, 0, 1, &scissorRect);
// Bind DescriptorSet with font or user texture
auto imageDescriptorPtr = (VulkanDescriptorSet*)pcmd->TextureId;
if (imageDescriptorPtr != nullptr)
{
VkDescriptorSet set = imageDescriptorPtr->GetDescriptorSet(VulkanRenderer::Get()->GetFrameIndex());
pipeline->BindDescriptorSet(0, &set);
}
else {
pipeline->BindDescriptorSet(0, &descriptorPtr);
}
vkCmdDrawIndexed(commandBuffer, pcmd->ElemCount, 1, indexOffset, vertexOffset, 0);
indexOffset += pcmd->ElemCount;
}
vertexOffset += cmd_list->VtxBuffer.Size;
}
}
以上步骤都完成后,就可以成功绘制Imgui到屏幕上了。最后,我想总结一下我在绘制过程中踩的坑:
- 一定要为每一帧都创建一个顶点/索引缓冲区,否则,在试图销毁缓冲区进行扩容的时候vulkan会提示你该资源正在被使用,无法销毁;
- 记得初始化imgui的glfw后端(如果采用glfw开启窗口的话),该后端可用来处理输入回调函数,如果不初始化,imgui将无法进行任何操作。注:也可以自己编写输入回调函数。
- 不要使用vulkan官方教程里的渲染管线配置文件初始化imgui的渲染管线,二者具有一定差异。