Vulkan教程 - 10 创建图形管线

在我们完成管线创建之前,我们需要告诉Vulkan渲染将要用到的帧缓冲附件的信息。我们需要明确有多少颜色和深度缓冲,每个又有多少采样以及它们的内容应该如何通过渲染操作来进行处理。所有这些信息都包装在渲染通道(render pass)对象中,我们就创建一个新的方法createRenderPass,在initVulkan中调用它,且它在createGraphicsPipeline之前。

我们这里仅有一个颜色缓冲附件,就是来自交换链的一个图像。

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

颜色附件的format参数应该和交换链图像的格式匹配,且我们目前没有多重采样,所以就保持一个采样。

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:当前存在的上下文是未定义的,且我们也不关心它们。

我们这里将会在绘制新的帧之前使用清除操作来清除帧缓冲到黑色。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;

loadOp和storeOp应用到颜色和深度数据,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明确了当渲染通道完成后要自动转移到的布局。用VK_IMAGE_LAYOUT_UNDEFINED作为initialLayout表示我们不关心之前图像布局。需要注意到该特殊值的一点是,图像内容不保证被保留,但是这没什么影响,因为我们反正还是要清除它的。我们想要图像为使用交换链渲染后的呈现准备就绪,所以用VK_IMAGE_LAYOUT_PRESENT_SRC_KHR作为finalLayout。

单渲染通道可以由多个子通道组成。子通道是依赖于之前通道的帧缓冲内容的后续渲染操作,例如一个接一个应用的一系列后期处理效果。如果你把这些渲染操作合并成一个渲染通道,那么Vulkan能够重新对这些操作排序,并保存内存带宽以便更好地提升性能。我们的第一个三角形就还是用单个子通道。

每个子通道引用一个或多个附件,这些引用就是些类似下面的VkAttachmentReference结构体:

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

attachment参数表明通过附件描述数组的索引来确定引用哪一个附件。我们的数组是由单个VkAttachmentDescription组成,因此索引就是0。布局表明了我们想要附件在使用该引用的子通道的适合用哪个布局。当子通道开启的时候,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;

渲染通道对象就可以根据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!");
}

如管线布局一样,渲染通道也是整个程序生命周期中都被引用的,所以在最后的cleanup中清理,紧跟管线布局清理之后调用:

vkDestroyRenderPass(device, renderPass, nullptr);

现在我们能把前面章节所有的结构体和对象都组合起来创建图形管线了!现在回顾下我们都有哪些对象:

着色器阶段:着色器模块定义了图形管线可编程阶段的功能;

固定管线状态:所有的结构体定义了管线的固定功能阶段,例如输入组装,光栅器,视口和颜色混合;

管线布局:由着色器引用的可以在绘制时更新的统一和可压入的值;

渲染通道:由管线阶段引用的附件以及它们的用法。

所有这些组合完整定义了图形管线的功能,所以我们现在开始填充VkGraphicsPipelineCreateInfo结构体,就在createGraphicsPipeline的末尾处。

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

之后是管线布局,它是一个Vulkan句柄而不是一个结构体指针:

pipelineInfo.layout = pipelineLayout;

设置好到渲染通道的引用以及子通道的索引:

pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;

本管线也可以使用别的渲染通道而不一定是这个特定的实例,但是要和renderPass兼容。兼容的要求是要在这里描述的,只是本教程不用而已。

pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;  // optional
pipelineInfo.basePipelineIndex = -1;  // optional

实际上还有两个参数,basePipelineHandle和basePipelineIndex。Vulkan允许你通过派生一个已有的管线来创建新的图形管线。管线派生的想法是因为如果二者有很多相同的功能,这样比建立管线更加节省开销,而且从同一个父对象切换管线也会更快。你可以通过basePipelineHandle指明一个已存在管线的句柄,或者引用另一个管线,也就是要通过basePipelineIndex加上索引来创建的管线。现在只有一个管线,我们就指定一个空句柄和无效的索引。这些值仅仅在VkGraphicsPipelineCreateInfo的flags字段的VK_PIPELINE_CREATE_DERIVATIVE_BIT标记也指定的情况下才有用。

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

VkPipeline graphicsPipeline;

最终可以创建图形管线了:

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

vkCreateGraphicsPipelines实际有更多的参数,它设计的时候就是接收多个VkGraphicsPipelineCreateInfo对象然后一次调用就会创建多个VkPipeline对象。

第二个参数,我们已经传了VK_NULL_HANDLE,引用了一个可选的VkPipelineCache对象。管线缓冲可被用于存储和重用管线创建有关的数据,横跨多次vkCreateGraphicsPipelines调用,如果存储到了文件甚至横跨程序执行。这让极大提高管线传将速度成为可能,以后管线缓冲章节再深究。

图形管线是所有常用绘制操作都要的,所以它也应该在程序结束的时候清理掉:

vkDestroyPipeline(device, graphicsPipeline, nullptr);

这个就写在cleanup方法的第一行。现在运行程序,确认所有这些艰苦的工作能够最终成功创建管线。我们现在离看到屏幕显示东西已经很近了(其实快一百三十页了,居然还没看到三角形),下面的章节会设置来自交换链的真正的的帧缓冲并准备绘制命令。

你可能感兴趣的:(Vulkan)