老式图形API为多数图形管线提供了默认状态。而在Vulkan中你必须明确所有的东西,从视口大小到混合函数。本章我们会填充所有的结构体来配置这些固定管线操作。
VkPipelineVertexInputStateCreateInfo结构体描述了将要传给顶点着色器的顶点数据的格式,它主要通过以下两种方式描述:
绑定:数据间的距离以及数据是否是逐顶点或者逐实例的;
属性描述:传给顶点着色器的属性的种类,从哪个绑定加载以及从哪个偏移处开始。
因为我们在顶点着色器中进行硬编码,我们会填充该结构体来明确暂时没有顶点数据要加载,等顶点缓冲章节再回来看:
VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr; // optional
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr; // optional
pVertexBindingDescriptions和pVertexAttributeDescriptions成员指向一组结构体,描述了前面提到的加载顶点数据的细节。上面这段代码添加到createGraphicsPipeline的shaderStages数组后面。
VkPipelineInputAssemblyStateCreateInfo结构体描述了两个事情:我们要从这些顶点中绘制什么样的几何对象,以及是否需要启用图元重启。前者在topology成员变量中指定,有如下值:
VK_PRIMITIVE_TOPOLOGY_POINT_LIST:点来自于顶点;
VK_PRIMITIVE_TOPOLOGY_LINE_LIST:线来自于两个顶点,且顶点不重用;
VK_PRIMITIVE_TOPOLOGY_LINE_STRIP:每条线的终点作为下一条线的起点;
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST:三角形来自于三个顶点,且顶点不重用;
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP:每个三角形的第二和第三个顶点作为下个三角形的头两个顶点。
通常顶点会根据索引从顶点缓冲中依次加载,但是有了元素缓冲,你可以明确自己要用的顶点。这就允许你进行顶点重用之类的优化了。如果你设置了primitiveRestartEnable成员为真,就可以在_STRIP拓扑模式中使用特殊索引如0xFFFF或0xFFFFFFFF打破线和三角形。
我们整个教程就是打算画个三角形,所以我们会用下面的数据构建结构体:
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
视口大致描述了输出要渲染到的帧缓冲区域。这基本上都是(0, 0)到(width, height),本教程就是这样:
VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float)swapChainExtent.width;
viewport.height = (float)swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
记住交换链的大小和它的图像可能会和窗口的宽高不一样。交换链图像将会被用作后续的帧缓冲,所以就保持他们的大小。
minDepth和maxDepth的值表明了帧缓冲要用的深度值范围。这些值必须在[0.0f, 1.0f]之间,但是minDepth可能比maxDepth要高。
虽然视口定义了从图像到帧缓冲的变换,裁剪矩形定义了像素实际上存储到什么区域。任何在裁剪矩形外边的像素都会被光栅器丢弃。说是变换器,实际上它们更像是一个过滤器:
本教程我们就想要简单绘制整个帧缓冲,所以我们会指定一个能将其完全覆盖的裁剪矩形:
VkRect2D scissor = {};
scissor.offset = { 0, 0 };
scissor.extent = swapChainExtent;
现在本视口和裁剪矩形需要用VkPipelineViewportStateCreateInfo结构体绑定到一个视口状态。某些显卡可以使用多个视口和裁剪矩形,所以它的成员会引用一组视口和裁剪矩形。多重使用则要求启用一个GPU特性(参考逻辑设备创建):
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
光栅器使用来自顶点着色器的顶点构建的几何对象转换成供片段着色器着色的片段。它也会进行深度测试,面剔除和裁剪测试,可以配置输出充满整个多边形或者仅仅是边(网格线渲染)的片段。这些都通过VkPipelineRasterizationStateCreateInfo结构体创建:
VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
如果depthClampEnable设置为真,那么在近处或者远处平面外的片段会被被截断到它们上面而不是被丢弃。这对于一些特殊情况比较有用,比如阴影映射图。用这个的话需要启用一个GPU特性:
rasterizer.rasterizerDiscardEnable = VK_FALSE;
这里如果设置为真了,那么几何图形就不会传递到光栅器阶段。这样就基本禁用了到帧缓冲的任何输出:
polygonMode决定了如何为几何图形生成片段:
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
这里有以下几种模式可选:
VK_POLYGON_MODE_FILL:用片段填充多边形区域;
VK_POLYGON_MODE_LINE:多边形的边画作线;
VK_POLYGON_MODE_POINT:多边形的顶点画作点。
使用任何非填充模式需要启用一个GPU特性:
rasterizer.lineWidth = 1.0f;
lineWidth成员就比较直白了,它描述的是根据片段数量来说的线的宽度。支持的最大线宽度取决于硬件,任何比1.0f大的宽度需要启用GPU的wideLines特性。
剔除模式cullMode变量决定了面剔除要用的类型。你可以禁用面剔除,剔除前面,剔除后面或者全剔除。frontFace变量明确了考虑面是否为前面的时候的顶点顺序,可以顺时针或者逆时针:
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
光栅器可以通过添加一个常量或者根据片段斜率偏离来改变深度值,这有时候用于阴影映射,我们这里不用,所以就置为假即可:
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // optional
rasterizer.depthBiasClamp = 0.0f; // optional
rasterizer.depthBiasSlopeFactor = 0.0f; // optional
多重采样通过VkPipelineMultisampleStateCreateInfo配置,这是进行抗锯齿的方式之一。它通过组合光栅化同一像素的多个多边形的片段着色器的结果来工作,这主要发生在边上,也是最容易注意到的走样发生的地方。因为如果只有一个多边形映射到一个像素的话,它就不用多次运行片段着色器,这比仅仅渲染到高分辨率然后压缩尺寸会节省得多开销。启用的话需要GPU特性开启如下:
VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // optional
multisampling.pSampleMask = nullptr; // optional
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
当前我们先禁用了,以后再来看多重采样。
如果你要用深度或者模板缓冲,你也需要使用VkPipelineDepthStencilStateCreateInfo配置深度和模板测试。我们现在并没有用,所以就传个nullptr即可,以后回来看深度缓冲。
片段着色器返回一个颜色后,它需要与已经在帧缓冲中的颜色进行组合。该变换就是颜色混合,有两种方式处理:
将新的和旧的混合产生最终颜色;
用按位操作组合新旧颜色值。
有两种结构体来配置颜色混合。第一种是VkPipelineColorBlendAttachmentState,包含了每个附着的帧缓冲的配置。第二种VkPipelineColorBlendStateCreateInfo包含了全局颜色混合设置。我们这里只有一个帧缓冲:
VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // optional
该逐帧缓冲结构体让你能配置第一种颜色混合,将要进行的操作能用下面的伪代码解释:
if (blendEnable) {
finalColor.rgb = (srcColorBlendFactor * newColor.rgb)
(dstColorBlendFactor * oldColor.rgb);
finalColor.a = (srcAlphaBlendFactor * newColor.a)
(dstAlphaBlendFactor * oldColor.a);
} else {
finalColor = newColor;
}
finalColor = finalColor & colorWriteMask;
blendEnable设置为假的情况下,来自片段着色器的新颜色会不经修改地进行传递。否则,执行这两个混合操作来计算新的颜色。最终颜色和colorWriteMask进行按位与操作,确定通过哪些通道传输。
最常用的颜色混合方式是实现alpha混合,就是想新颜色基于旧颜色的透明度进行混合。那么最终颜色就是这么计算的:
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
这可以用下面的参数实现:
colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC1_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
你可以在VkBlendFactor和VkBlendOp枚举中找到所有可能的操作。
第二个结构体引用所有帧缓冲的结构体数组并允许你设置混合常数作为前面提到的计算中的混合因子。
如果你想要使用第二种混合方法(按位结合),那么你应该设置logicOpEnable为真,按位运算类型接着在logicOp中指明。注意这会自动禁用第一种方式,如同你对每个附着的帧缓冲设置了blendEnable为假一样。这个模式中也会使用colorWriteMask,以确定实际要影响帧缓冲的哪一个通道。也可以两种模式都不用,像我们这里做的一样,这样片段颜色就会不加修改的写入帧缓冲中。
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // optional
colorBlending.blendConstants[1] = 0.0f; // optional
colorBlending.blendConstants[2] = 0.0f; // optional
colorBlending.blendConstants[3] = 0.0f; // optional
我们在前面结构体中指定的一些状态可以不用重建管线就进行修改。例如视口大小,线宽度和混合常数。如果你想这么做,那么先要像这样填充一个VkPipelineDynamicStateCreateInfo结构体:
VkDynamicState dynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_LINE_WIDTH
};
VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
这会导致这些值的配置被忽略,然后在绘制的时候再指定这些数据。我们以后再回来看这部分内容,没有动态状态的时候可以将其替换成nullptr。
你可以在着色器中使用统一的值,就是一些全局变量,和动态状态变量类似,可以在绘制的时候进行修改来改变其行为而不用重新创建它们。它们通常用于传递变换矩阵给顶点着色器,或者在片段着色器中创建材质采样。
这些统一的值需要在管线创建的过程中通过创建一个VkPipelineLayout对象来指明。虽然以后的章节中才会用到,但是我们还是要创建一个空的管线布局。
首先创建一个类成员来存储该对象,因为我们以后会在其他方法中引用它:
VkPipelineLayout pipelineLayout;
然后在createGraphicsPipeline中创建该对象:
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // optional
pipelineLayoutInfo.pSetLayouts = nullptr; // optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // optional
pipelineLayoutInfo.pPushConstantRanges = nullptr; // optional
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create pipeline layout!");
}
该结构体也指明了push constants,这是另一个传递动态值给着色器的方法。管线布局的引用会贯穿整个程序生命周期,所以在最后的cleanup中销毁:
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
这行代码就放在cleanup最开始的地方。
这就是固定管线状态的所有内容了。但是还有一个对象要创建,就是渲染通道,之后才能创建图形管线,这就是后面的内容了。