Link

随着引擎的增长,我们需要一种可交互的界面,我们可以使用它来添加滑块、按钮和显示数据。

为此,我们将向项目中添加库“dear Imgui”。这是一个库,可以非常容易地添加带有滑块、按钮、可编辑文本的窗口和界面……而无需设置 UI 文件或处理复杂的系统。

立即 GPU 命令

编辑直到修复:本文的这一部分将被移走,新版本的 imgui 不需要立即命令来上传。我们仍然需要在教程的后面使用立即命令。

Imgui 将要求我们在正常的绘制循环之外运行一些命令。这将是我们在引擎上多次需要用于不同用途的东西。我们将实现一个 immediate_submit 函数,该函数使用栅栏和与我们在绘制中使用的不同的命令缓冲区,将一些命令发送到 GPU,而无需与交换链或渲染逻辑同步。

让我们将这些结构添加到 VulkanEngine 类中

class VulkanEngine{
public:
    // immediate submit structures
    VkFence _immFence;
    VkCommandBuffer _immCommandBuffer;
    VkCommandPool _immCommandPool;

	
	void immediate_submit(std::function<void(VkCommandBuffer cmd)>&& function);

private:
	void init_imgui();
}

我们有一个栅栏和一个命令缓冲区及其池。 immediate_submit 函数接受一个 std 函数作为回调,以便与 lambda 一起使用。也添加 init_imgui() 函数,并将其添加到 init() 函数调用链的末尾。暂时将其留空。

我们需要为立即提交创建这些同步结构,所以让我们进入 init_commands() 函数并挂钩命令部分。

void VulkanEngine::init_commands()
{
	VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_immCommandPool));

	// allocate the command buffer for immediate submits
	VkCommandBufferAllocateInfo cmdAllocInfo = vkinit::command_buffer_allocate_info(_immCommandPool, 1);

	VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_immCommandBuffer));

	_mainDeletionQueue.push_function([=]() { 
	vkDestroyCommandPool(_device, _immCommandPool, nullptr);
	});

}

这与我们对每帧命令所做的事情相同,但这次我们直接将其放入删除队列以进行清理。

现在我们需要创建栅栏,我们将将其添加到 init_sync_structures() 中。将其添加到末尾

void VulkanEngine::init_sync_structures()
{
	VK_CHECK(vkCreateFence(_device, &fenceCreateInfo, nullptr, &_immFence));
	_mainDeletionQueue.push_function([=]() { vkDestroyFence(_device, _immFence, nullptr); });
}

我们将使用与每帧栅栏相同的 fenceCreateInfo。与命令相同,我们也将直接将其销毁函数添加到删除队列。

现在实现 immediate_submit 函数

void VulkanEngine::immediate_submit(std::function<void(VkCommandBuffer cmd)>&& function)
{
	VK_CHECK(vkResetFences(_device, 1, &_immFence));
	VK_CHECK(vkResetCommandBuffer(_immCommandBuffer, 0));

	VkCommandBuffer cmd = _immCommandBuffer;

	VkCommandBufferBeginInfo cmdBeginInfo = vkinit::command_buffer_begin_info(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

	VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo));

	function(cmd);

	VK_CHECK(vkEndCommandBuffer(cmd));

	VkCommandBufferSubmitInfo cmdinfo = vkinit::command_buffer_submit_info(cmd);
	VkSubmitInfo2 submit = vkinit::submit_info(&cmdinfo, nullptr, nullptr);

	// submit command buffer to the queue and execute it.
	//  _renderFence will now block until the graphic commands finish execution
	VK_CHECK(vkQueueSubmit2(_graphicsQueue, 1, &submit, _immFence));

	VK_CHECK(vkWaitForFences(_device, 1, &_immFence, true, 9999999999));
}

请注意,此函数与我们在 gpu 上执行命令的方式非常相似,几乎相同。

它与相同的事情非常接近,只是我们没有将提交与交换链同步。

我们将使用此函数进行数据上传和渲染循环之外的其他“即时”操作。改进它的一种方法是在与图形队列不同的队列上运行它,这样我们就可以将此执行与主渲染循环重叠。

IMGUI 设置

现在让我们进行 imgui 初始化。

我们首先需要将一些包含项添加到 vk_engine.cpp

#include "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_vulkan.h"

它是主要的 imgui 头文件,然后是 SDL 2 和 vulkan 后端的实现头文件。

现在到初始化函数

void VulkanEngine::init_imgui()
{
	// 1: create descriptor pool for IMGUI
	//  the size of the pool is very oversize, but it's copied from imgui demo
	//  itself.
	VkDescriptorPoolSize pool_sizes[] = { { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 },
		{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 },
		{ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 },
		{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 },
		{ VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 },
		{ VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 },
		{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 },
		{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 },
		{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 },
		{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 },
		{ VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } };

	VkDescriptorPoolCreateInfo pool_info = {};
	pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
	pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
	pool_info.maxSets = 1000;
	pool_info.poolSizeCount = (uint32_t)std::size(pool_sizes);
	pool_info.pPoolSizes = pool_sizes;

	VkDescriptorPool imguiPool;
	VK_CHECK(vkCreateDescriptorPool(_device, &pool_info, nullptr, &imguiPool));

	// 2: initialize imgui library

	// this initializes the core structures of imgui
	ImGui::CreateContext();

	// this initializes imgui for SDL
	ImGui_ImplSDL2_InitForVulkan(_window);

	// this initializes imgui for Vulkan
	ImGui_ImplVulkan_InitInfo init_info = {};
	init_info.Instance = _instance;
	init_info.PhysicalDevice = _chosenGPU;
	init_info.Device = _device;
	init_info.Queue = _graphicsQueue;
	init_info.DescriptorPool = imguiPool;
	init_info.MinImageCount = 3;
	init_info.ImageCount = 3;
	init_info.UseDynamicRendering = true;

	//dynamic rendering parameters for imgui to use
	init_info.PipelineRenderingCreateInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO};
	init_info.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
	init_info.PipelineRenderingCreateInfo.pColorAttachmentFormats = &_swapchainImageFormat;
	

	init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;

	ImGui_ImplVulkan_Init(&init_info);

	ImGui_ImplVulkan_CreateFontsTexture();

	// add the destroy the imgui created structures
	_mainDeletionQueue.push_function([=]() {
		ImGui_ImplVulkan_Shutdown();
		vkDestroyDescriptorPool(_device, imguiPool, nullptr);
	});
}

VulkanEngine::init() 的末尾,在 init_pipelines(); 之后调用此函数
此代码改编自 imgui 演示。我们首先需要创建一些 imgui 想要的结构,例如它自己的描述符池。这里的描述符池存储了 1000 个不同类型描述符的大量数据,所以有点过头了。这不会成为问题,只是空间效率稍低。

然后我们调用 CreateContext()ImGui_ImplSDL2_InitForVulkanImGui_ImplVulkan_Init。这些函数将初始化我们需要的 imgui 的不同部分。在 vulkan 函数中,我们需要挂钩一些东西,例如我们的设备、实例、队列。

一个重要的方面是我们需要将 UseDynamicRendering 设置为 true,并将 ColorAttachmentFormat 设置为我们的交换链格式,这是因为我们将不使用 vulkan 渲染通道,而是使用动态渲染,vulkan 1.3 功能。与计算着色器不同,我们将直接将 dear imgui 绘制到交换链中。

在调用 ImGui_ImplVulkan_Init 之后,我们需要执行立即提交以上传字体纹理。一旦执行完成,我们调用 DestroyFontUploadObjects,以便 imgui 删除这些临时结构。最后,我们将清理代码添加到销毁队列中。

Imgui 渲染循环

Imgui 现在已初始化,但我们需要将其挂钩到渲染循环中。

我们首先要做的是将其代码添加到 run() 函数中

//Handle events on queue
while (SDL_PollEvent(&e) != 0) {
    //close the window when user alt-f4s or clicks the X button			
    if (e.type == SDL_QUIT) bQuit = true;

    if (e.type == SDL_WINDOWEVENT) {

        if (e.window.event == SDL_WINDOWEVENT_MINIMIZED) {
            stop_rendering = true;
        }
        if (e.window.event == SDL_WINDOWEVENT_RESTORED) {
            stop_rendering = false;
        }
    }

    //send SDL event to imgui for handling
    ImGui_ImplSDL2_ProcessEvent(&e);
}

//do not draw if we are minimized
if (stop_rendering) {
    //throttle the speed to avoid the endless spinning
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    continue;
}		

// imgui new frame
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();

//some imgui UI to test
ImGui::ShowDemoWindow();

//make imgui calculate internal draw structures
ImGui::Render();

//our draw function
draw();

我们必须将 SDL 事件传递到 imgui,以便从我们的 pollEvent 循环中进行处理。之后,我们需要为 imgui 上的新帧调用 3 个函数。完成此操作后,我们现在可以执行我们的 UI 命令。我们现在将其保留在演示窗口中。当我们调用 ImGui::Render() 时,它会计算 imgui 绘制帧所需的顶点/绘制/等等,但它本身不执行任何绘制。为了绘制它,我们将从我们的 draw() 函数中继续它。

动态渲染

Imgui 将使用带有网格和着色器的实际 gpu 绘制进行绘制,它不会像我们目前正在做的那样进行计算绘制。要绘制几何体,需要在渲染通道内完成。但是我们不使用渲染通道,因为我们将使用动态渲染,这是 vulkan 1.3 功能。我们不调用 VkCmdBeginRenderpass 并为其提供 VkRenderPass 对象,而是调用 VkBeginRendering,并使用包含将图像绘制到其中的所需设置的 VkRenderingInfo。

VkRenderingInfo 指向多个 VkRenderingAttachmentInfo,用于我们的目标图像以绘制到其中,所以让我们开始将它写入初始化器中。

VkRenderingAttachmentInfo vkinit::attachment_info(
    VkImageView view, VkClearValue* clear ,VkImageLayout layout /*= VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL*/)
{
    VkRenderingAttachmentInfo colorAttachment {};
    colorAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
    colorAttachment.pNext = nullptr;

    colorAttachment.imageView = view;
    colorAttachment.imageLayout = layout;
    colorAttachment.loadOp = clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
    colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    if (clear) {
        colorAttachment.clearValue = *clear;
    }

    return colorAttachment;
}

对于我们的附件信息,我们将具有清除值作为可选指针,这样我们就可以执行清除或跳过它并加载图像。

我们需要像往常一样使用所有这些渲染命令来挂钩图像视图和布局。重要的部分是 loadOP 和 storeOP。这控制了渲染目标在此附件中在渲染通道(动态渲染通道和类渲染通道)中使用时会发生什么。对于加载选项,我们有 LOAD,它将保留该图像中的数据。Clear 将在开始时将其设置为我们的清除值,而 dont-care 我们计划替换每个像素,因此 gpu 可以跳过从内存加载它。

对于我们的存储操作,我们将使用硬编码的存储,因为我们希望保存我们的绘制命令。

完成附件信息后,我们可以创建 VkRenderingInfo。向 VulkanEngine 类添加一个新函数 draw_imgui(),以绘制渲染 imgui 的渲染通道。

void VulkanEngine::draw_imgui(VkCommandBuffer cmd, VkImageView targetImageView)
{
	VkRenderingAttachmentInfo colorAttachment = vkinit::attachment_info(targetImageView, nullptr, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
	VkRenderingInfo renderInfo = vkinit::rendering_info(_swapchainExtent, &colorAttachment, nullptr);

	vkCmdBeginRendering(cmd, &renderInfo);

	ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd);

	vkCmdEndRendering(cmd);
}

我们将获取渲染范围以设置要绘制的像素矩形,并且我们将发送颜色附件和深度附件。我们现在不需要深度附件,那是为了以后。

然后我们需要从我们的 draw() 函数中调用它。

	// execute a copy from the draw image into the swapchain
	vkutil::copy_image_to_image(cmd, _drawImage.image, _swapchainImages[swapchainImageIndex], _drawExtent, _swapchainExtent);

	// set swapchain image layout to Attachment Optimal so we can draw it
	vkutil::transition_image(cmd, _swapchainImages[swapchainImageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);

	//draw imgui into the swapchain image
	draw_imgui(cmd,  _swapchainImageViews[swapchainImageIndex]);

	// set swapchain image layout to Present so we can draw it
	vkutil::transition_image(cmd, _swapchainImages[swapchainImageIndex], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);

	//finalize the command buffer (we can no longer add commands, but it can now be executed)
	VK_CHECK(vkEndCommandBuffer(cmd));

该 copy_image 命令与之前相同,我们正在替换 VkEndCommandBuffer 调用结束之前的所有后续命令。

之前,我们将交换链图像从传输布局转换为呈现布局,但现在我们将其更改为 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。这是您在调用此处之类的渲染命令时应使用的布局。

之后,我们构建 VkRenderingInfo 并向其发送单个颜色附件。该颜色附件将指向我们目标的交换链图像。

现在我们有了 VkRenderInfo,我们可以调用 vkCmdBeginRendering,这会开始一个渲染通道,我们现在可以执行绘制命令。我们使用我们的命令缓冲区调用 imgui vulkan 后端,这将使 imgui 将其绘制命令记录到缓冲区中。完成后,我们可以调用 vkCmdEndRendering 以结束渲染通道。

之后,我们将交换链图像从 attachment-optimal 转换为呈现模式,并最终结束命令缓冲区。

如果您此时运行应用程序,您将拥有可以使用的 imgui 演示窗口。

让我们继续并将我们的新调试 UI 挂钩到着色器。

下一步: 推送常量和新着色器