固定功能
早期的图形 API 为图形管线的大多数阶段提供了默认状态。在 Vulkan 中,您必须明确大多数管线状态,因为它将被烘焙到不可变的管线状态对象中。在本章中,我们将填写所有结构来配置这些固定功能操作。
动态状态
虽然*大多数*管线状态需要烘焙到管线状态中,但*可以*在绘制时更改有限数量的状态而无需重新创建管线。例如,视口的大小、线宽和混合常量。如果您想使用动态状态并保留这些属性,那么您需要填写一个 VkPipelineDynamicStateCreateInfo 结构,如下所示:
std::vector dynamicStates = {
vk::DynamicState::eViewport,
vk::DynamicState::eScissor
};
vk::PipelineDynamicStateCreateInfo dynamicState{ .dynamicStateCount = static_cast<uint32_t>(dynamicStates.size()), .pDynamicStates = dynamicStates.data() };
这将导致这些值的配置被忽略,您将能够在(并且需要)在绘制时指定数据。这导致了一个更灵活的设置,并且对于视口和剪刀状态等广泛使用,当烘焙到管线状态中时会导致更复杂的设置。
顶点输入
VkPipelineVertexInputStateCreateInfo 结构描述了传递给顶点着色器的顶点数据的格式。它大致以两种方式描述这一点:
-
绑定:数据之间的间距以及数据是每顶点还是每实例(参见 实例化)
-
属性描述:传递给顶点着色器的属性类型,从哪个绑定加载它们以及在哪个偏移量
因为我们正在将顶点数据直接硬编码到顶点着色器中,所以我们将填写此结构以指定现在没有要加载的顶点数据。我们将在顶点缓冲区章节中回到这一点。
vk::PipelineVertexInputStateCreateInfo vertexInputInfo;
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 成员设置为 VK_TRUE,则可以通过使用特殊索引 0xFFFF 或 0xFFFFFFFF 在 _STRIP 拓扑模式中打断线和三角形。
在本教程中,我们打算绘制三角形,因此我们将坚持使用以下结构数据:
vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ .topology = vk::PrimitiveTopology::eTriangleList };
视口和剪刀
视口基本上描述了输出将渲染到的帧缓冲区的区域。这几乎总是从 (0, 0) 到 (width, height),在本教程中也是如此。
vk::Viewport{ 0.0f, 0.0f, static_cast<float>(swapChainExtent.width), static_cast<float>(swapChainExtent.height), 0.0f, 1.0f };
请记住,交换链及其图像的大小可能与窗口的 WIDTH 和 HEIGHT 不同。交换链图像稍后将用作帧缓冲区,因此我们应该坚持使用它们的大小。
minDepth 和 maxDepth 值指定用于帧缓冲区的深度值范围。这些值必须在 [0.0f, 1.0f] 范围内,但 minDepth 可以高于 maxDepth。如果您没有做任何特殊的事情,那么您应该坚持使用 0.0f 和 1.0f 的标准值。
虽然视口定义了从图像到帧缓冲区的转换,但剪刀矩形定义了实际存储像素的区域。光栅化器将丢弃剪刀矩形之外的任何像素。它们的功能更像过滤器而不是转换。下图说明了这种区别。请注意,左侧的剪刀矩形只是可能导致该图像的许多可能性之一,只要它大于视口。
因此,如果我们想绘制整个帧缓冲区,我们将指定一个完全覆盖它的剪刀矩形:
vk::Rect2D{ vk::Offset2D{ 0, 0 }, swapChainExtent }
视口和剪刀矩形可以作为管线的静态部分指定,也可以作为命令缓冲区中设置的动态状态。虽然前者与其他状态更一致,但通常更方便使视口和剪刀状态动态化,因为它为您提供了更大的灵活性。这是广泛使用的,所有实现都可以处理此动态状态而不会影响性能。
选择动态视口和剪刀矩形时,您需要为管线启用相应的动态状态:
std::vector dynamicStates = {
vk::DynamicState::eViewport,
vk::DynamicState::eScissor
};
vk::PipelineDynamicStateCreateInfo dynamicState({}, dynamicStates.size(), dynamicStates.data());
然后,您只需要在管线创建时指定它们的计数:
vk::PipelineViewportStateCreateInfo viewportState({}, 1, {}, 1);
实际的视口和剪刀矩形将在绘制时设置。
使用动态状态,甚至可以在单个命令缓冲区中指定不同的视口和/或剪刀矩形。
没有动态状态,视口和剪刀矩形需要使用 VkPipelineViewportStateCreateInfo 结构在管线中设置。这使得此管线的视口和剪刀矩形不可变。对这些值的任何更改都需要创建一个具有新值的新管线。
vk::PipelineViewportStateCreateInfo viewportState{ .viewportCount = 1, .scissorCount = 1 };
无论您如何设置它们,某些图形卡上可以使用多个视口和剪刀矩形,因此结构成员引用它们的数组。使用多个需要启用 GPU 功能(参见逻辑设备创建)。
光栅化器
光栅化器获取由顶点着色器形成的几何形状,并将其转换为片段着色器要着色的片段。它还执行 深度测试、https://en.wikipedia.org/wiki/Back-face_culling[面剔除] 和剪刀测试,并且可以配置为输出填充整个多边形的片段或仅边缘(线框渲染)。所有这些都使用 VkPipelineRasterizationStateCreateInfo 结构进行配置。
vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False);
如果 depthClampEnable 设置为 VK_TRUE,则超出近和远平面的片段将被钳制到它们,而不是丢弃它们。这在某些特殊情况下(如阴影贴图)很有用。使用此功能需要启用 GPU 功能。
vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False);
如果 rasterizerDiscardEnable 设置为 VK_TRUE,则几何图形永远不会通过光栅化阶段。这基本上禁用了对帧缓冲区的任何输出。
vk::PipelineRasterizationStateCreateInfo rasterizer{ .depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False,
.polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eBack,
.frontFace = vk::FrontFace::eClockwise, .depthBiasEnable = vk::False,
.depthBiasSlopeFactor = 1.0f, .lineWidth = 1.0f };
polygonMode 确定如何为几何图形生成片段。以下模式可用:
-
VK_POLYGON_MODE_FILL:用片段填充多边形的区域 -
VK_POLYGON_MODE_LINE:多边形边缘绘制为线 -
VK_POLYGON_MODE_POINT:多边形顶点绘制为点
使用除填充之外的任何模式都需要启用 GPU 功能。
rasterizer.lineWidth = 1.0f;
lineWidth 成员很简单,它描述了以片段数量为单位的线宽。支持的最大线宽取决于硬件,任何比 1.0f 粗的线都需要您启用 wideLines GPU 功能。
vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False, vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack, vk::FrontFace::eClockwise);
cullMode 变量确定要使用的面剔除类型。您可以禁用剔除,剔除正面,剔除背面或两者。frontFace 变量指定面被视为正面的顶点顺序,可以是顺时针或逆时针。
vk::PipelineRasterizationStateCreateInfo rasterizer({}, vk::False, vk::False, vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack, vk::FrontFace::eClockwise, vk::False);
光栅化器可以通过添加常数值或基于片段的斜率偏置它们来改变深度值。这有时用于阴影映射,但我们不会使用它。只需将 depthBiasEnable 设置为 VK_FALSE。
多重采样
VkPipelineMultisampleStateCreateInfo 结构配置多重采样,这是执行 抗锯齿 的方法之一。它通过组合光栅化到同一像素的多个多边形的片段着色器结果来工作。这主要发生在边缘,这也是最明显的锯齿伪影发生的地方。因为它不需要在只有一个多边形映射到像素时多次运行片段着色器,所以它比简单地渲染到更高分辨率然后缩小要便宜得多。启用它需要启用 GPU 功能。
vk::PipelineMultisampleStateCreateInfo multisampling{.rasterizationSamples = vk::SampleCountFlagBits::e1, .sampleShadingEnable = vk::False};
我们将在后面的章节中重新讨论多重采样,现在让我们保持禁用状态。
深度和模板测试
如果您正在使用深度和/或模板缓冲区,那么您还需要使用 VkPipelineDepthStencilStateCreateInfo 配置深度和模板测试。我们现在没有,所以我们可以简单地传递一个 nullptr 而不是指向这样一个结构的指针。我们将在深度缓冲章节中回到这一点。
颜色混合
在片段着色器返回颜色后,需要将其与帧缓冲区中已有的颜色组合。这种转换称为颜色混合,有两种方法可以做到这一点:
-
混合旧值和新值以产生最终颜色
-
使用按位操作组合旧值和新值
有两种类型的结构来配置颜色混合。第一个结构 VkPipelineColorBlendAttachmentState 包含每个附加帧缓冲区的配置,第二个结构 VkPipelineColorBlendStateCreateInfo 包含*全局*颜色混合设置。在我们的情况下,我们只有一个帧缓冲区:
vk::PipelineColorBlendAttachmentState colorBlendAttachment;
colorBlendAttachment.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
colorBlendAttachment.blendEnable = vk::False;
这个每帧缓冲区结构允许您配置第一种颜色混合方式。将使用以下伪代码演示将执行的操作:
if (blendEnable) {
finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
finalColor = newColor;
}
finalColor = finalColor & colorWriteMask;
如果 blendEnable 设置为 VK_FALSE,则片段着色器中的新颜色将未经修改地传递。否则,将执行两个混合操作来计算新颜色。结果颜色将与 colorWriteMask 进行 AND 运算,以确定实际传递的通道。
使用颜色混合的最常见方法是实现 alpha 混合,我们希望新颜色基于其不透明度与旧颜色混合。finalColor 应如下计算:
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
这可以通过以下参数实现:
colorBlendAttachment.blendEnable = vk::True;
colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd;
colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne;
colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eZero;
colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd;
您可以在规范中的 VkBlendFactor 和 VkBlendOp 枚举中找到所有可能的操作。
第二个结构引用所有帧缓冲区的结构数组,并允许您设置可以在上述计算中用作混合因子的混合常量。
vk::PipelineColorBlendStateCreateInfo colorBlending{.logicOpEnable = vk::False, .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment };
如果您想使用第二种混合方法(按位组合),那么您应该将 logicOpEnable 设置为 VK_TRUE。然后可以在 logicOp 字段中指定按位操作。请注意,这将自动禁用第一种方法,就像您为每个附加帧缓冲区将 blendEnable 设置为 VK_FALSE 一样!colorWriteMask 也将在此模式下用于确定帧缓冲区中实际受影响的通道。也可以禁用这两种模式,就像我们在这里所做的那样,在这种情况下,片段颜色将未经修改地写入帧缓冲区。
管线布局
您可以在着色器中使用 uniform 值,这些值是类似于动态状态变量的全局变量,可以在绘制时更改以改变着色器的行为,而无需重新创建它们。它们通常用于将变换矩阵传递给顶点着色器,或在片段着色器中创建纹理采样器。
这些统一值需要在管线创建期间通过创建 VkPipelineLayout 对象来指定。即使我们在未来的章节之前不会使用它们,我们仍然需要创建一个空的管线布局。
创建一个类成员来保存此对象,因为我们将在以后的时间点从其他函数引用它:
vk::raii::PipelineLayout pipelineLayout = nullptr;
然后在 createGraphicsPipeline 函数中创建对象:
vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 0, .pushConstantRangeCount = 0 };
pipelineLayout = vk::raii::PipelineLayout( device, pipelineLayoutInfo );
该结构还指定了*推送常量*,这是另一种将动态值传递给着色器的方法,我们可能会在未来的章节中讨论。
结论
这就是所有固定功能状态!从头开始设置所有这些需要大量工作,但优点是现在我们几乎完全了解图形管线中发生的一切!这减少了遇到意外行为的可能性,因为某些组件的默认状态不是您所期望的。
然而,在我们最终可以创建图形管线之前,还有一个对象需要创建,那就是 渲染通道。
C++ 代码 / Slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器