SwallowJoe的博客

Be a real go-getter,
NEVER SETTLE!

0%

Vulkan入门(7)-渲染过程及创建图形管道

简述

Render passes, 即渲染过程。在完成创建管道之前,我们需要告诉Vulkan渲染时将使用的帧缓冲区附件。

我们需要指定将有多少颜色和深度缓冲区,为每个缓冲区使用多少个样本,以及在整个渲染操作中应如何处理它们的内容。

所有这些信息都包装在一个render pass对象中,为其创建一个新的createRenderPass函数。在createGraphicsPipeline之前从initVulkan调用此函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void initVulkan() {
createInstance();
setupDebugCallback();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
}

void createRenderPass() {

}

Vulkan图形管线和计算管线的区别之一是,你使用图形管线来渲染出像素,组成图像以供处理或显示给用户。在复杂的图形应用程序中,图片经过很多遍构建,每一遍都生成场景的一部分,应用全帧效果如后期处理、合成、渲染用户界面元素等等。这样的一遍可以使用Vulkan 中 renderpass 对象表示。

一个单一的的renderpass对象封装了多个pass或者一系列最终图像的几个渲染阶段,renderpass对象包含输出图像所需的信息。

所有的绘制必须被包含在一个renderpass中。甚至,图形管线需要知道他们把渲染结果发往哪儿,因此,有必要在创建图形管线之前创建一个renderpass对象,告知正在生成图像的管线有关图像的信息。

参考资料

一. Attachment description 附件说明

在这里,我们仅有一个颜色缓冲区附件,由交换链中的一个图像表示。

1
2
3
4
5
void createRenderPass() {
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
}

颜色附件的格式应该与交换链图像的格式匹配,而且我们还没有对多重采样做任何事情,所以只要使用1个样本。

1
2
3
4
5
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

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

loadOp和storeOp决定在呈现之前和呈现之后如何处理附件中的数据。loadOp和storeOp应用于颜色和深度数据,以及stencilLoadOp/
stencilStoreOp应用于模具数据。这里不会对模板缓冲区执行任何操作,因此加载和存储的结果是无关的。

1.1 loadOp

  1. VK_ATTACHMENT_LOAD_OP_LOAD: 指定将保留渲染区域中图像的先前内容。对于深度/模具格式的附件,这将使用访问类型VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ位。对于颜色格式的附件,这将使用访问类型VK_ACCESS_COLOR_ATTACHMENT_READ位。
  2. VK_ATTACHMENT_LOAD_OP_CLEAR: 指定将渲染区域中的内容清除为统一值,该值在渲染过程实例开始时指定。对于深度/模板格式的附件,这将使用访问类型VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE位。对于颜色格式的附件,这将使用访问类型VK_ACCESS_COLOR_ATTACHMENT_WRITE位。
  3. VK_ATTACHMENT_LOAD_OP_DONT_CARE: 指定不需要保留区域中的前一个内容;附件的内容将在渲染区域内未定义。对于深度/模板格式的附件,这将使用访问类型VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE位。对于颜色格式的附件,这将使用访问类型VK_ACCESS_COLOR_ATTACHMENT_WRITE位。

1.2 storeOp

  1. VK_ATTACHMENT_STORE_OP_STORE: 指定在渲染过程中和渲染区域内生成的内容写入内存,以后可以读取
  2. VK_ATTACHMENT_STORE_OP_DONT_CARE: 指定渲染后不需要渲染区域内的内容,这些内容可能会被丢弃;附件的内容将在渲染区域内未定义。

1.3 内存中像素的布局

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

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

initialLayout指定在渲染过程开始之前图像将具有的布局。finalLayout最终布局指定渲染过程完成时自动转换到的布局。

对initialLayout使用VK_IMAGE_LAYOUT_UNDEFINED意味着我们不关心图像在以前的布局中是什么。这个特殊值的警告是图像的内容不能保证被保存,但这并不重要,因为我们无论如何都要清除它。我们希望图像在渲染后可以使用交换链进行显示,这就是为什么我们使用VK_IMAGE_LAYOUT_PRESENT_SRC_KHR作为finalLayout。

一些最常见的布局有:

  1. VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: 用作颜色附件的图像
  2. VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: 交换链中要显示的图像
  3. VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: 要用作内存复制操作目标的图像

我们将在“纹理”一章中更深入地讨论这个主题,但现在需要知道的是,图像需要转换到适合其下一步将要涉及的操作的特定布局。

二. Subpasses and attachment references 子过程和附件引用

单个渲染过程可以由多个子过程组成。子过程是依赖于先前过程中帧缓冲区的内容的后续呈现操作。

例如一系列相继应用的后处理效果。如果将这些渲染操作分组到一个渲染过程中,则Vulkan能够对操作重新排序,并节省内存带宽以获得可能更好的性能。

这里对于一个三角形,使用一个子过程。

每个子过程引用一个或多个附件,这些附件是我们使用前面部分中的结构描述的。这些引用本身就是VKatAttchmentReference结构,如下:

1
2
3
VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
  1. attachment参数通过附件描述数组中的索引指定要引用的附件。我们的数组由一个VkAttachmentDescription组成,所以它的索引是0。
  2. layout指定在使用此引用的子过程期间希望附件具有的布局。当子进程启动时,Vulkan将自动将附件转换到此布局。使用附件作为一个颜色缓冲区,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL LAYOUT会给我们带来最好的性能,顾名思义。

子过程使用VkSubpassDescription结构进行描述:

1
2
3
4
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef; // 指定对颜色附件的引用

Vulkan将来也可能支持计算子过程,所以我们必须明确表示这是一个图形子过程。
此数组中附件的索引直接从具有layout(location=0)out vec4 outColor指令的片段着色器引用!

子过程可以引用以下类型的附件:

  1. pColorAttachments: 颜色附件
  2. pInputAttachments: 从着色器读取的附件
  3. pResolveAttachments: 用于多重采样颜色附件的附件
  4. pDepthStencilAttachment: 深度和模板数据附件
  5. pPreserveAttachments: 此子过程未使用但必须保留数据的附件

三. Render pass 渲染过程

上面已经描述了附件和引用它的基本子过程,我们就可以创建渲染过程本身了。

渲染过程表示附件、子过程和子过程之间的依赖关系的集合,并描述如何在子过程中使用附件。

创建一个新的类成员变量,将VkRenderPass对象保持在pipelineLayout变量的正上方:

1
2
VkRenderPass renderPass;
VkPipelineLayout pipelineLayout;

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

1
2
3
4
5
6
7
8
9
10
// 创建渲染过程
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!");
}

与管道布局一样,渲染过程将在整个程序中被引用,因此只应在结束时进行清理:

1
2
3
4
5
void cleanup() {
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
...
}

目前我们以及做了很多工作了,下一步就是创建图形管道对象了!

四. 创建 pipeline

快速回顾现在拥有的对象类型:

  1. shader stages: 定义图形管道可编程阶段功能的着色器模块
  2. Fixed-function state: 定义管道固定函数阶段的所有结构,如输入程序集、光栅化器、视口和颜色混合
  3. Pipeline layout: 可在绘制时更新的着色器引用的统一值和推送值
  4. Render pass: 管道阶段引用的附件及其用法

所有这些组合都充分定义了图形管道的功能,因此现在可以开始在createGraphicsPipeline函数末尾填充VkGraphicsPipelineCreateInfo结构。
但是是在调用vkDestroyShaderModule之前,因为在创建过程中使用仍然需要使用这些着色器!

VkGraphicsPipelineCreateInfo结构包括一个shader-reate-info结构数组,其中包含所有所需的活动着色器阶段、定义所有相关固定函数阶段的创建信息以及管道布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
// 首先引用VkPipelineShaderStageCreateInfo结构的数组。
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr; // Optional
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = nullptr; // Optional
// 然后引用所有描述固定函数阶段的结构。
pipelineInfo.layout = pipelineLayout;
// 之后是管道布局,它是一个Vulkan句柄而不是结构指针。
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;

最后,我们有了渲染过程的引用以及使用这个图形管道的子过程的索引。也可以将其他渲染过程用于此管道而不是此特定实例,但它们必须与renderPass兼容。下面描述了兼容性的要求,但这里不使用该特性。

实际上还有两个参数:basePipelineHandle和basePipelineIndex。

Vulkan允许通过从现有管道派生来创建新的图形管道。管道衍生品的想法是,当管道具有与现有管道具有许多相同功能时,设置管道成本更低,并且可以更快地在同一父管道之间切换。
可以指定现有管道的句柄(带有basePipelineHandle),也可以引用另一个将由basePipelineIndex的索引创建的管道。

现在只有一个管道,所以只需要指定一个null句柄和一个无效的索引。只有在VkGraphicsPipelineCreateInfo的flags字段中指定VK_PIPELINE_CREATE_UBIT标志时,才使用这些值。

1
2
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
pipelineInfo.basePipelineIndex = -1; // Optional

通过创建一个类成员来保存VkPipeline对象:

1
2
3
4
5
6
7
VkPipeline graphicsPipeline;

// 创建图形管道
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1,
&pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
throw std::runtime_error("failed to create graphics pipeline!");
}

vkCreateGraphicsPipelines实际上比Vulkan中常用的对象创建函数有更多的参数。
它的设计是支持在一次调用中获取多个VkGraphicsPipelineCreateInfo对象并创建多个VkPipeline对象。

第二个参数(我们为其传递了VK_NULL_HANDLE参数)引用了一个可选的VkPipelineCache对象。管道缓存可用于在对vkCreateGraphicsPipelines的多个调用中存储和重用与管道创建相关的数据,如果缓存存储到文件中,甚至可以跨程序执行。这使得以后可以大大加快管道的创建速度。我们将在管道缓存中讨论这个问题。

所有常见的绘图操作都需要图形管道,因此也只能在程序结束时销毁:

1
2
3
4
5
void cleanup() {
vkDestroyPipeline(device, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
...
}

现在运行程序来确认所有这些艰苦的工作已经成功地创建了管道!我们已经非常接近看到屏幕上弹出一些东西(实际现在还没有看到任何东西,黑乎乎一片)。

接下来将从交换链图像设置实际的帧缓冲区,并准备绘图命令。

欢迎关注我的其它发布渠道