Drawing a triangle/Graphics pipeline basics/Fixed functions

目录

Dynamic state

Vertex input

Input assembly

Viewports and scissors

Rasterizer

Multisampling

Depth and stencil testing

Color blending

Pipeline layout

Conclusion


较旧的图形API为图形管道的大部分阶段提供了默认状态。在Vulkan中,您必须明确大多数管道状态,因为它将被烘焙为不可变的管道状态对象。在本章中,我们将填写配置这些固定函数操作的所有结构。

Dynamic state

虽然大多数管道状态需要烘焙为管道状态,但实际上可以在绘制时不重新创建管道的情况下更改有限数量的状态。例如视口的大小、线宽和混合常数。如果要使用动态状态并保留这些特性,则必须填写VkPipelineDynamicStateCreateInfo结构,如下所示:

std::vector<VkDynamicState> dynamicStates = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_SCISSOR
};

VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();

这将导致忽略这些值的配置,并且您可以(并且需要)在绘图时指定数据。这将导致更灵活的设置,对于视口和裁剪状态等情况非常常见,这将导致在烘焙到管道状态时更复杂的设置。

Vertex input

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成员指向一个结构数组,这些结构描述了加载顶点数据的上述细节。将此结构添加到shaderStages数组后面的createGraphicsPipeine函数中。

Input assembly

VkPipelineInputAssemblyStateCreateInfo结构描述了两件事:将从顶点绘制什么样的几何体,以及是否应启用基本体重新启动。前者在拓扑成员中指定,可以具有如下值:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST:顶点的点
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST:每2个顶点的直线,不重复使用
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP:每条线的端点用作下一条线的起点
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST:每3个顶点的三角形,不重复使用
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP:每个三角形的第二个和第三个顶点用作下一个三角形的前两个顶点

通常,顶点是按顺序从顶点缓冲区按索引加载的,但对于元素缓冲区,您可以指定要自己使用的索引。这允许您执行优化,如重用顶点。如果将primitiveRestartEnable成员设置为VK_TRUE,则可以通过使用0xFFFF或0xFFFFFFFF的特殊索引,在_STRIP拓扑模式中拆分直线和三角形。

我们打算在本教程中绘制三角形,因此我们将坚持以下结构数据:

VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;

Viewports and scissors

视口基本上描述了输出将渲染到的帧缓冲区区域。这几乎总是(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。如果你没有做任何特殊的事情,那么你应该坚持标准值0.0f和1.0f。

当视口定义从图像到帧缓冲区的转换时,剪形矩形定义像素实际存储的区域。裁剪矩形外的任何像素都将被光栅化器丢弃。它们的功能类似于过滤器而不是转换。差异如下所示。请注意,只要裁剪矩形大于视口,它只是生成该图像的多种可能性之一。

因此,如果我们想绘制整个帧缓冲区,我们将指定一个完全覆盖它的剪形矩形:

VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;

视口和剪切矩形可以指定为管道的静态部分,也可以指定为命令缓冲区中的动态状态集。虽然前者更符合其他状态,但使视口和裁剪状态动态化通常很方便,因为它给了您更多的灵活性。这是非常常见的,所有实现都可以处理这种动态状态,而不会造成性能损失。

选择动态视口和剪式矩形时,需要为管道启用相应的动态状态:

std::vector<VkDynamicState> dynamicStates = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_SCISSOR
};

VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();

然后,您只需在管道创建时指定它们的计数:

VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.scissorCount = 1;

实际视口和裁剪矩形稍后将在绘图时设置。

使用动态状态,甚至可以在一个命令缓冲区内指定不同的视口和/或裁剪矩形。

如果没有动态状态,则需要使用VkPipelineViewportStateCreateInfo结构在管道中设置视口和裁剪矩形。这使得该管道的视口和裁剪矩形不可变。对这些值所需的任何更改都需要使用新值创建新管道。

VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;

无论您如何设置它们,都可以在某些显卡上使用多个视口和裁剪矩形,因此结构成员引用它们的阵列。使用多个需要启用GPU功能(请参阅逻辑设备创建)。

Rasterizer

光栅化器从顶点着色器中获取由顶点成形的几何体,并将其转化为片段,由片段着色器着色。它还执行深度测试、面部剔除和裁剪测试,并且可以配置为输出填充整个多边形或仅边缘的片段(线框渲染)。所有这些都是使用VkPipelineRasterizationStateCreateInfo结构配置的。

VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;

如果depthClampEnable设置为VK_TRUE,则超出近平面和远平面的片段将被钳制到它们,而不是丢弃它们。这在某些特殊情况下很有用,例如阴影贴图。使用此功能需要启用GPU功能。

rasterizer.rasterizerDiscardEnable = VK_FALSE;

如果光栅化器DiscardEnable设置为VK_TRUE,则几何体永远不会通过光栅化器阶段。这基本上禁用了对帧缓冲区的任何输出。

rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

polygonMode确定如何为几何体生成片段。以下模式可用:

  • VK_POLYGON_MODE_FILL:用片段填充多边形区域
  • VK_POLYGON_MODE_LINE:多边形边绘制为线
  • VK_POLYGON_MODE_POINT:多边形顶点绘制为点

 使用填充以外的任何模式都需要启用GPU功能。

rasterizer.lineWidth = 1.0f;

lineWidth成员很简单,它以片段的数量来描述线条的厚度。支持的最大线宽取决于硬件,任何大于1.0f的线条都需要启用wideLines GPU功能。

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

cullMode变量确定要使用的面部剔除类型。可以禁用剔除、剔除正面、剔除背面或两者兼而有之。frontFace变量指定要视为前向面的顶点顺序,可以是顺时针或逆时针。

rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional

光栅化器可以通过添加一个常量值或基于片段的斜率对其进行偏置来更改深度值。这有时用于阴影映射,但我们不会使用它。只需将depthBiasEnable设置为VK_FALSE即可。

Multisampling

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; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional

我们将在下一章重新讨论多重采样,现在让我们禁用它。

Depth and stencil testing

如果使用的是深度和/或模具缓冲区,则还需要使用VkPipelineDepthStencilStateCreateInfo配置深度和模具测试。我们现在还没有,所以我们可以简单地传递一个nullptr,而不是指向这样一个结构的指针。我们将在深度缓冲章节中继续讨论。

Color blending

片段着色器返回颜色后,需要将其与帧缓冲区中已存在的颜色组合。这种转换称为颜色混合,有两种方法可以实现:

  • 混合新旧值以产生最终颜色
  • 使用按位操作组合新旧值

有两种类型的结构可以配置颜色混合。第一个结构VkPipelineColorBlendAttachmentState包含每个附加帧缓冲区的配置,第二个结构Vk PipelineCoColorBlendStateCreateInfo包含全局颜色混合设置。在我们的例子中,我们只有一个帧缓冲区:

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; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
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) <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.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;

这可以通过以下参数实现:

colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_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枚举中找到所有可能的操作。

第二个结构引用了所有帧缓冲区的结构数组,并允许您设置混合常数,这些常数可以用作上述计算中的混合因子。

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

如果要使用第二种混合方法(按位组合),则应将logicOpEnable设置为VK_TRUE。然后可以在logicOp字段中指定按位操作。注意,这将自动禁用第一个方法,就像您为每个附加的帧缓冲区将blendEnable设置为VK_FALSE一样!colorWriteMask也将用于此模式,以确定帧缓冲区中的哪些通道将受到实际影响。也可以禁用这两种模式,正如我们在这里所做的那样,在这种情况下,片段颜色将不经修改地写入帧缓冲区。

Pipeline layout

可以在着色器中使用统一的值,这些值是类似于动态状态变量的全局值,可以在绘图时更改,以更改着色器的行为,而无需重新创建它们。它们通常用于将变换矩阵传递给顶点着色器,或在片段着色器中创建纹理采样器。

在管道创建过程中,需要通过创建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常量,这是将动态值传递给着色器的另一种方式,我们可能会在未来的章节中讨论。管道布局将在程序的整个生命周期内被引用,因此应在最后销毁:

void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    ...
}

Conclusion

这就是所有的固定函数状态!从头开始设置这一切需要大量的工作,但好处是我们现在几乎完全了解图形管道中正在发生的一切!这减少了发生意外行为的可能性,因为某些组件的默认状态不是您所期望的。

然而,在我们最终创建图形管道之前,还有一个对象需要创建,那就是渲染过程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值