SwallowJoe的博客

Be a real go-getter,
NEVER SETTLE!

0%

Vulkan入门(6)-创建管道的几个固定操作

简述

其他图形API为图形管道的大多数阶段提供了默认状态。但在Vulkan中,必须明确所有内容,从视口大小到颜色混合功能。
接下来我们试着填写配置这些固定功能操作的所有结构。

参考资料

一. Vertex input

VkPipelineVertexInputStateCreateInfo结构描述将传递给顶点着色器的顶点数据的格式。
它以两种方式描述了这一点:

  • 绑定( Bindings ):数据之间的间距以及数据是按顶点还是按实例(请参阅实例化)
  • 属性描述( Attribute descriptions ):传递给顶点着色器的属性的类型,从哪个绑定加载它们

因为我们直接在顶点着色器中对顶点数据进行硬编码,所以我们将填充此结构以指定现在没有要加载的顶点数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void createGraphicsPipeline() {
......
// 这里为管道指定着色器
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};

// 创建顶点着色器的数据输入
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 成员指向一个结构数组,描述前面提到的加载顶点数据的细节。
后续在学习顶点缓冲的时候详细分析。

二. Input assembly

VkPipelineInputAssemblyStateCreateInfo结构描述了两件事:

  1. 将从顶点绘制什么类型的几何, 由成员变量topology 指定。可以使用如下值:
    • 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:每个三角形的第二个和第三个顶点用作下一个三角形的前两个顶点
  2. 是否应启用基元重启。

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

如果是绘制三角形,创建如下的 VkPipelineInputAssemblyStateCreateInfo:

1
2
3
4
5
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

3.1 Viewports

视窗描述了输出将被渲染到的帧缓冲区域。 基本总是(0,0)到(宽度,高度),也就是窗口大小。

1
2
3
4
5
6
7
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。
虽然视窗定义了从图像到帧缓冲的转换,但剪刀矩形定义了实际存储像素的区域。
剪刀矩形外的任何像素都将被光栅化器丢弃。 它们的功能类似于过滤器而不是转换。
差异如下所示。 请注意,左边的剪刀矩形只是导致该图像的众多可能性之一,只要它比视口大:

图片

也就是说Viewport会缩放以显示完整的图片,而scissors会裁剪(或者说遮挡)图片内容。

3.2 Scissors

简单定义一个可以绘制整个帧缓冲的Scissor:

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

3.3 使用方式

现在使用VkPipelineViewportStateCreateInfo结构将此视口和剪刀矩形组合成视口状态。
可以在某些图形卡上使用多个视口和剪刀矩形,因此其成员需要引用它们的数组。使用多个需要启用GPU功能。

1
2
3
4
5
6
7
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType =
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;

四. Rasterizer (光栅化)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType =
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
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
  1. depthClampEnable: 设置为VK_TRUE,那么超出近平面和远平面的fragment将被夹住,而不是丢弃它们。这在某些特殊情况下很有用,比如阴影贴图。使用此功能需要启用GPU功能。
  2. rasterizerDiscardEnable: 设置为VK_TRUE,那么几何图形永远不会通过光栅化阶段。这基本上禁止任何输出到帧缓冲区。
  3. polygonMode: 多边形模态决定了如何为几何图形生成fragment。有以下几种模式:(使用FILL以外的任何模式都需要启用GPU功能)
    1. VK_POLYGON_MODE_FILL: 用fragment填充多边形的区域
    2. VK_POLYGON_MODE_LINE: 多边形边缘以直线的形式绘制
    3. VK_POLYGON_MODE_POINT: 用点绘制多边形顶点
  4. lineWidth: 用来描述线条的粗细。支持的最大线宽取决于硬件,任何超过1.0f的线路都需要启用宽带GPU功能。
  5. cullMode: 确定要使用的面消隐的类型。可以禁用消隐、消隐正面、消隐背面或两者兼有。
  6. frontFace: 指定要视为正面的面的顶点顺序,可以是顺时针或逆时针。

光栅化器可以通过添加一个常量值或根据fragment的坡度对深度值进行偏移来改变深度值。这有时用于阴影映射,一般不使用时只需将depthBiasEnable设置为VK_FALSE。

五. Multisampling 多重采样

VkPipelineMultisampleStateCreateInfo结构体用于配置vulkan中的多重采样。

是通过将光栅化为同一像素的多个多边形的片段着色器结果组合在一起实现的,这也是抗锯齿的方式之一。主要是在图形边缘地区做多重采样,这是最明显的锯齿伪影发生的地方。

如果只有一个多边形映射到一个像素,它不需要多次运行片段着色器,因此它比简单地渲染到更高的分辨率然后缩小比例的开销小得多。

启用多重采样需要启用GPU功能。

1
2
3
4
5
6
7
8
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;
  1. sampleShadingEnable: 可用于启用采样着色。
  2. rasterizationSamples: 指定用于光栅化的样本数, 一共有7个可选
    1. VK_SAMPLE_COUNT_1_BIT: 指定每像素1个采样的图像。
    2. VK_SAMPLE_COUNT_2_BIT: 指定每像素2个采样的图像。
    3. VK_SAMPLE_COUNT_4_BIT: 指定每像素4个采样的图像。
    4. VK_SAMPLE_COUNT_8_BIT: 指定每像素8个采样的图像。
    5. VK_SAMPLE_COUNT_16_BIT: 指定每像素16个采样的图像。
    6. VK_SAMPLE_COUNT_32_BIT: 指定每像素32个采样的图像。
    7. VK_SAMPLE_COUNT_64_BIT: 指定每像素64个采样的图像。
  3. minSampleShading: 若sampleShadingEnable设置为VK_TRUE,则指定采样着色的最小部分。

后续详细研究,当前先暂时关闭该功能。

六. Color blending 颜色混合

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

  1. 将新旧值混合生成最终颜色
  2. 使用位运算组合新旧值

有两种类型的结构可以配置颜色混合。

  1. VkPipelineColorBlendAttachmentState: 包含每个附加帧缓冲区的配置,
  2. VkPipelineColorBlendStateCreateInfo: 包含全局颜色混合设置。

6.1 VkPipelineColorBlendAttachmentState 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

此每帧缓冲区结构允许您配置颜色混合的第一种方式。将要执行的操作使用以下伪代码演示原理:

1
2
3
4
5
6
7
8
9
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混合,我们希望新颜色与基于不透明度的旧颜色混合。最终颜色的计算如下:

1
2
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;

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

1
2
3
4
5
6
7
8
// 实现alpha混合
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中找到所有可用的操作:

VkBlendFactor RGB Blend Factors (Sr,Sg,Sb) or (Dr,Dg,Db) Alpha Blend Factor (Sa or Da)
VK_BLEND_FACTOR_ZERO (0,0,0) 0
VK_BLEND_FACTOR_ONE (1,1,1) 1
VK_BLEND_FACTOR_SRC_COLOR (Rs0,Gs0,Bs0) As0
VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR (1-Rs0,1-Gs0,1-Bs0) 1-As0
VK_BLEND_FACTOR_DST_COLOR (Rd,Gd,Bd) Ad
VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR (1-Rd,1-Gd,1-Bd) 1-Ad
VK_BLEND_FACTOR_SRC_ALPHA (As0,As0,As0) As0
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA (1-As0,1-As0,1-As0) 1-As0
VK_BLEND_FACTOR_DST_ALPHA (Ad,Ad,Ad) Ad
VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA (1-Ad,1-Ad,1-Ad) 1-Ad
VK_BLEND_FACTOR_CONSTANT_COLOR (Rc,Gc,Bc) Ac
VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR (1-Rc,1-Gc,1-Bc) 1-Ac
VK_BLEND_FACTOR_CONSTANT_ALPHA (Ac,Ac,Ac) Ac
VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA (1-Ac,1-Ac,1-Ac) 1-Ac
VK_BLEND_FACTOR_SRC_ALPHA_SATURATE (f,f,f); f = min(As0,1-Ad) 1
VK_BLEND_FACTOR_SRC1_COLOR (Rs1,Gs1,Bs1) As1
VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR (1-Rs1,1-Gs1,1-Bs1) 1-As1
VK_BLEND_FACTOR_SRC1_ALPHA (As1,As1,As1) As1
VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA (1-As1,1-As1,1-As1) 1-As1

备注:

  1. Rs0、Gs0、Bs0和As0分别表示与正在混合的颜色附件相对应的片段输出位置的第一源颜色R、G、B和A分量。
  2. Rs1、Gs1、Bs1和As1分别表示第二源颜色R、G、B和A分量,用于对应于被混合的颜色附件的片段输出位置,这些分量在双源混合模式中使用。
  3. Rd、Gd、Bd和Ad表示目的颜色的R、G、B和A分量。也就是说,此片段/样本的相应颜色附件中当前的颜色。
  4. Rc、Gc、Bc和Ac分别表示混合常数R、G、B和A组分。

选择源和目标混合因子后,它们连同源和目标组件一起传递给混合操作。RGB和alpha组件可以使用不同的操作。指定操作的VkBlendOp的可能值为:

VkBlendOp RGB Components Alpha Component
VK_BLEND_OP_ADD R = Rs0 × Sr + Rd × Dr
G = Gs0 × Sg + Gd × Dg
B = Bs0 × Sb + Bd × Db
A = As0 × Sa + Ad × Da
VK_BLEND_OP_SUBTRACT R = Rs0 × Sr - Rd × Dr
G = Gs0 × Sg - Gd × Dg
B = Bs0 × Sb - Bd × Db
A = As0 × Sa - Ad × Da
VK_BLEND_OP_REVERSE_SUBTRACT R = Rd × Dr - Rs0 × Sr
G = Gd × Dg - Gs0 × Sg
B = Bd × Db - Bs0 × Sb
A = Ad × Da - As0 × Sa
VK_BLEND_OP_MIN R = min(Rs0,Rd)
G = min(Gs0,Gd)
B = min(Bs0,Bd)
A = min(As0,Ad)
VK_BLEND_OP_MAX R = max(Rs0,Rd)
G = max(Gs0,Gd)
B = max(Bs0,Bd)
A = max(As0,Ad)

备注:

  1. Rs0、Gs0、Bs0和As0分别表示第一源颜色R、G、B和A分量。
  2. Rd、Gd、Bd和Ad表示目的颜色的R、G、B和A分量。也就是说,此片段/样本的相应颜色附件中当前的颜色。
  3. Sr、Sg、Sb和Sa分别表示源混合因子R、G、B和A组分。
  4. Dr、Dg、Db和Da分别表示目标混合因子R、G、B和A分量。

6.2 VkPipelineColorBlendStateCreateInfo 全局颜色混合设置

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

1
2
3
4
5
6
7
8
9
10
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也将用于此模式,以确定帧缓冲区中的哪些通道将实际受到影响。也可以禁用这两种模式,这种情况下,片段颜色将被不修改地写入帧缓冲区。

七. Dynamic state 可动态修改状态

一小部分状态是可以不需要重新创建管道,直接修改的;比如viewport的大小,线宽和颜色混合常数等。

可以使用 VkPipelineDynamicStateCreateInfo 来更改这些动态参数:

1
2
3
4
5
6
7
8
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;

这将导致忽略这些值的配置,并要求在绘图时指定相应配置。

八. Pipeline layout 管道布置图

可以在着色器中使用统一的值,类似于可以在绘制时更改的动态状态变量,以更改着色器的行为,而无需重新创建它们。

通常用于将变换矩阵传递给顶点着色器,或在片段着色器中创建纹理采样器。

在创建管道期间,需要通过创建VkPipelineLayout对象来指定这些统一值。创建一个类成员来保存这个对象,稍后会从其他函数中引用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VkPipelineLayout pipelineLayout;

void 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常量,这是将动态值传递给着色器的另一种方式,我们将在以后的章节中讨论。

管道布局将在整个程序的整个生命周期内被引用,因此在结束时应销毁:

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

九. 小结

所有的固定函数状态都是这样!这是一个很大的工作,以设置所有这些从头开始,但好处是,我们现在几乎完全了解一切正在进行的图形管道!
这减少了运行到意外行为的机会,因为某些组件的默认状态不是您所期望的。不过,在我们最终创建图形管道之前,还有一个对象需要创建,那就是渲染过程。

总结一下到目前位为止,创建图形管道应该做的操作:

  1. Vertex Input 描述将传递给顶点着色器的顶点数据的格式
  2. Input assembly 描述顶点绘制几何类型
  3. Viewports and scissors 描述将被渲染到的帧缓冲区域
  4. Rasterizer 光栅化配置
  5. Multisampling 配置多重采样
  6. Color blending 配置颜色混合
  7. Pipeline layout 创建管道布置图

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