Vulkan 学习笔记(十五)渲染通道(Render passes)
在我们大功告成、把图形管线造出来之前,还得先跟Vulkan这位“暴躁老哥”交代一下:咱们渲染的时候要用到哪些帧缓冲附件。我们需要明确告诉它:会有多少个颜色缓冲、深度缓冲,每个缓冲用多少个采样点,以及在整个渲染过程中,这些数据该怎么处理。
所有这些乱七八糟的配置信息,都被打包封装进了一个叫 渲染通道 (Render Pass) 的对象里。为了伺候好这位爷,咱们得专门写一个 createRenderPass 函数。记得在 initVulkan 里调用它,位置要卡在 createGraphicsPipeline 之前,给管线留个“遗言”时间。
void initVulkan(){
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass(); // 就在这儿!
createGraphicsPipeline();
}
附件描述:给画布定规矩
咱们这案例里,就一个简简单单的颜色缓冲附件,也就是交换链里的其中一张图。
void createRenderPass(){
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat; // 格式得跟交换链一致
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; // 暂时不用多重采样,就1个
format 必须跟交换链图像的格式对上暗号。至于多重采样(Multisampling),那是后话,现在咱们先用单采样凑合着。
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
这里的 loadOp(加载操作)和 storeOp(存储操作)决定了渲染前和渲染后怎么处理数据。对于 loadOp,咱们有这几个选项:
- •
VK_ATTACHMENT_LOAD_OP_LOAD:保留附件里原有的东西(不清理)。 - •
VK_ATTACHMENT_LOAD_OP_CLEAR:上来就给清掉,变成一个固定的常量值。 - •
VK_ATTACHMENT_LOAD_OP_DONT_CARE:旧内容是啥无所谓,老子不在乎。
咱们打算画新帧之前把 framebuffer 清成黑色,所以用的是 Clear 操作。至于 storeOp,只有两个选择:
- •
VK_ATTACHMENT_STORE_OP_STORE:把渲染好的内容老老实存进内存,以后还能读。 - •
VK_ATTACHMENT_STORE_OP_DONT_CARE:渲染完这 framebuffer 就废了,内容 undefined(未定义)也无所谓。
咱们辛辛苦苦画了个三角形,当然是想把它显示在屏幕上瞅瞅,所以 storeOp 必须选 Store,不能选 Don't Care。
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
上面那俩是管颜色和深度数据的,下面这俩是管模板(Stencil)数据的。咱们这小程序用不着模板缓冲,所以加载和存储的结果都无所谓,直接 Don't Care 一视同仁。
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
在Vulkan里,纹理和帧缓冲都是 VkImage 对象,但它们在内存里的**布局(Layout)**会根据用途变来变去。
最常见的几种布局有:
- •
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:这图是用来当颜色附件画的。 - •
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:这图是用来在交换链里展示的。 - •
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:这图是用来当复制操作的目标(目的地)的。
虽然详细内容要到纹理那章才细讲,但现在你得知道:图像得先“过渡”到特定的布局,才能干对应的活。
initialLayout 指的是渲染通道开始前,这图像处于啥布局。finalLayout 指的是渲染一结束,这图像要自动变成啥布局。
咱们用 VK_IMAGE_LAYOUT_UNDEFINED(未定义)作为初始布局,意思就是“我不在乎它之前是啥样”。副作用就是图像内容不保证保留,不过没关系,反正咱们马上就要把它清掉。至于结束后的布局,咱们希望它准备好去交换链里“抛头露面”,所以用 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR。
子通道与附件引用:流水线上的工位
一个渲染通道可以包含好几个子通道(Subpasses)。这玩意儿就是后续的渲染操作,它们依赖于前一个通道的 framebuffer 内容。比如,你想搞个后处理特效,模糊一下再加个锐化,这就是一连串的子通道。
如果你把这些操作打包进同一个渲染通道,Vulkan 就能帮你重新安排操作顺序,省点内存带宽,跑得更快。
但咱们现在只是画个三角形,不整那些花里胡哨的,就用一个单子通道。
每个子通道都要引用一个或多个咱们刚才描述的附件。这些引用关系是用 VkAttachmentReference 结构体来定义的:
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0; // 引用附件数组里的第0号元素
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; // 咱想要的布局
attachment 参数指明引用哪个附件,也就是附件描述数组的下标。咱们数组里只有一个 VkAttachmentDescription,所以索引是 0。layout 指明在这个子通道里,咱们希望附件处于啥布局。Vulkan 会在子通道开始时自动把附件转成这个布局。咱们打算把它当颜色缓冲用,顾名思义,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 能给咱们最好的性能。
子通道本身是用 VkSubpassDescription 结构体描述的:
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
以后Vulkan可能还支持计算子通道,所以现在咱们得明确告诉它:这是个图形子通道(Graphics)。然后,咱们指定一下颜色附件的引用:
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
注意了!这个数组里的附件索引,是直接对应片段着色器里的代码的!比如着色器里的 layout(location = 0) out vec4 outColor,指的就是这个数组里索引为0的附件。
除了颜色附件,子通道还能引用别的类型的附件:
- •
pInputAttachments:着色器要读的附件。 - •
pResolveAttachments:多重采样时用的。 - •
pDepthStencilAttachment:存深度和模板数据的。 - •
pPreserveAttachments:这子通道不用,但数据得留着的附件。
终于到了:创建渲染通道
磨叽了这么久,附件也描述完了,子通道也定义好了,咱们终于可以创建渲染通道本尊了。在类里加个成员变量,就在 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);
// ...
}
这一通操作下来属实有点累,但好消息是:下一章咱们就能把这些东西捏合在一起,终于要造出那个传说中的图形管线对象了!
源码(https://vulkan-tutorial.com/code/11_render_passes.cpp)