设置

在我们完成管线的创建之前,我们需要告诉 Vulkan 将在渲染时使用的帧缓冲附件。我们需要指定将有多少颜色和深度缓冲,每个缓冲使用多少个采样,以及它们的内容在整个渲染操作中应如何处理。所有这些信息都封装在一个渲染通道对象中,我们将为此创建一个新的 createRenderPass 函数。在 createGraphicsPipeline 之前从 initVulkan 调用此函数。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
}

...

void createRenderPass() {

}

附件描述

在我们的例子中,我们将只有一个颜色缓冲附件,由交换链中的一个图像表示。

void createRenderPass() {
    VkAttachmentDescription colorAttachment{};
    colorAttachment.format = swapChainImageFormat;
    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
}

颜色附件的 format 应该与交换链图像的格式匹配,并且我们还没有进行任何多重采样,因此我们将坚持使用 1 个采样。

colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

loadOpstoreOp 确定在渲染之前和渲染之后如何处理附件中的数据。对于 loadOp,我们有以下选择

  • VK_ATTACHMENT_LOAD_OP_LOAD:保留附件的现有内容
  • VK_ATTACHMENT_LOAD_OP_CLEAR:在开始时将值清除为常量
  • VK_ATTACHMENT_LOAD_OP_DONT_CARE:现有内容未定义;我们不在乎它们

在我们的例子中,我们将使用清除操作在绘制新帧之前将帧缓冲清除为黑色。对于 storeOp,只有两种可能性

  • VK_ATTACHMENT_STORE_OP_STORE:渲染的内容将存储在内存中,并且可以稍后读取
  • VK_ATTACHMENT_STORE_OP_DONT_CARE:渲染操作后,帧缓冲的内容将未定义

我们有兴趣在屏幕上看到渲染的三角形,因此我们在这里使用存储操作。

colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

loadOpstoreOp 应用于颜色和深度数据,而 stencilLoadOp / stencilStoreOp 应用于模板数据。我们的应用程序不会对模板缓冲执行任何操作,因此加载和存储的结果无关紧要。

colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

Vulkan 中的纹理和帧缓冲由 VkImage 对象表示,这些对象具有特定的像素格式,但是内存中像素的布局可能会根据您尝试对图像执行的操作而更改。

一些最常见的布局是

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:用作颜色附件的图像
  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:要在交换链中呈现的图像
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:用作内存复制操作目标的图像

我们将在纹理章节中更深入地讨论这个主题,但现在重要的是要知道图像需要转换为特定的布局,这些布局适合它们接下来要参与的操作。

initialLayout 指定图像在渲染通道开始之前将具有的布局。finalLayout 指定渲染通道完成时自动转换到的布局。对 initialLayout 使用 VK_IMAGE_LAYOUT_UNDEFINED 意味着我们不在乎图像之前的布局是什么。此特殊值的注意事项是不能保证图像的内容会被保留,但这没关系,因为我们无论如何都要清除它。我们希望图像在使用交换链渲染后即可用于呈现,这就是为什么我们使用 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 作为 finalLayout

子通道和附件引用

单个渲染通道可以包含多个子通道。子通道是后续的渲染操作,这些操作依赖于先前通道中帧缓冲的内容,例如,一个接一个应用的一系列后处理效果。如果将这些渲染操作分组到一个渲染通道中,则 Vulkan 能够重新排序操作并节省内存带宽,从而可能获得更好的性能。但是,对于我们的第一个三角形,我们将坚持使用单个子通道。

每个子通道都引用我们在前面的章节中使用结构描述的一个或多个附件。这些引用本身是 VkAttachmentReference 结构,如下所示

VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

attachment 参数通过其在附件描述数组中的索引来指定要引用的附件。我们的数组由单个 VkAttachmentDescription 组成,因此其索引为 0layout 指定我们希望附件在使用此引用的子通道期间具有的布局。当子通道启动时,Vulkan 将自动将附件转换为此布局。我们打算使用该附件充当颜色缓冲,并且 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 布局将为我们提供最佳性能,正如其名称所暗示的那样。

子通道使用 VkSubpassDescription 结构描述

VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

Vulkan 未来也可能支持计算子通道,因此我们必须明确说明这是一个图形子通道。接下来,我们指定对颜色附件的引用

subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;

此数组中附件的索引直接从片段着色器中使用 layout(location = 0) out vec4 outColor 指令引用!

以下其他类型的附件可以由子通道引用

  • pInputAttachments:从着色器读取的附件
  • pResolveAttachments:用于多重采样颜色附件的附件
  • pDepthStencilAttachment:用于深度和模板数据的附件
  • pPreserveAttachments:此子通道未使用的附件,但必须保留其数据的附件

渲染通道

现在已经描述了附件和引用它的基本子通道,我们可以创建渲染通道本身。创建一个新的类成员变量来保存 VkRenderPass 对象,就在 pipelineLayout 变量的上方

VkRenderPass renderPass;
VkPipelineLayout pipelineLayout;

然后可以通过使用附件和子通道数组填充 VkRenderPassCreateInfo 结构来创建渲染通道对象。VkAttachmentReference 对象使用此数组的索引引用附件。

VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;

if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
    throw std::runtime_error("failed to create render pass!");
}

就像管线布局一样,渲染通道将在整个程序中被引用,因此它应该只在最后清理

void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    vkDestroyRenderPass(device, renderPass, nullptr);
    ...
}

这有很多工作,但在下一章中,所有内容都将结合在一起,最终创建图形管线对象!

C++ 代码 / 顶点着色器 / 片段着色器