本部分结果可参考 13_Fixed_Functions

Shader Modules 指定了渲染管线中可编程的Vertex / Fragment 着色器。在这一节中,会设定剩下的固定函数的一些操作,如 Viewport Size / Color Blending 模式,这些在 Vulklan 中都需要在创建渲染管线时设定,而在如 OpenGL 这样较老的图形 API 中,则可以在运行时修改。

GraphicsPipelineMgr 中定义一系列的函数分别创建最终 Pipeline 创建时所需要的各数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GraphicsPipelineMgr
{
public:
static void createGraphicsPipeline(const std::string& vertFileName, const std::string& fragFileName);
static void destroyPipelineLayout();
static VkPipelineLayout pipelineLayout;

private:
static VkPipelineShaderStageCreateInfo getShaderStageCreateInfo(VkShaderModule shaderModule, VkShaderStageFlagBits stage);
static VkPipelineVertexInputStateCreateInfo getVertexInputStateCreateInfo();
static VkPipelineInputAssemblyStateCreateInfo getInputAssemblyStateCreateInfo();
static VkPipelineViewportStateCreateInfo getViewportStateCreateInfo();
static VkPipelineRasterizationStateCreateInfo getRasterizationStateCreateInfo();
static VkPipelineMultisampleStateCreateInfo getMultisamplingStateCreateInfo();
static VkPipelineColorBlendStateCreateInfo getColorBlendStateCreateInfo();
static VkPipelineDynamicStateCreateInfo getVKDynamicStateCreateInfo();
static VkPipelineLayoutCreateInfo getPipelineLayoutCreateInfo();
};

将原 Ch 12 Shader Modules 中创建 Shader Module 的内容也抽象至函数 createShaderModule 中。

此时 createGraphicsPipeline 函数的代码如下所示,后续的部分即将讲解函数实现中的各部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void GraphicsPipelineMgr::createGraphicsPipeline(const std::string& vertFileName, const std::string& fragFileName)
{
// Create shader modules
VkShaderModule vertShaderModule = ShadersMgr::createShaderModule(vertFileName);
VkShaderModule fragShaderModule = ShadersMgr::createShaderModule(fragFileName);

// Create shader stages
VkPipelineShaderStageCreateInfo shaderStages[] = {getShaderStageCreateInfo(vertShaderModule, VK_SHADER_STAGE_VERTEX_BIT),
getShaderStageCreateInfo(fragShaderModule, VK_SHADER_STAGE_FRAGMENT_BIT)};

// Fixed functions
VkPipelineVertexInputStateCreateInfo vertexInput = getVertexInputStateCreateInfo();
VkPipelineInputAssemblyStateCreateInfo inputAssembly = getInputAssemblyStateCreateInfo();
VkPipelineViewportStateCreateInfo viewportState = getViewportStateCreateInfo();
VkPipelineRasterizationStateCreateInfo rasterizer = getRasterizationStateCreateInfo();
VkPipelineMultisampleStateCreateInfo multisampling = getMultisamplingStateCreateInfo();
VkPipelineColorBlendStateCreateInfo colorBlending = getColorBlendStateCreateInfo();
VkPipelineDynamicStateCreateInfo dynamicState = getVKDynamicStateCreateInfo();

// Pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = getPipelineLayoutCreateInfo();
if (vkCreatePipelineLayout(LogicDevicesMgr::device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS)
throw std::runtime_error("Failed to create pipeline layout!");

// Clean up shader modules
ShadersMgr::destroyShaderModule(vertShaderModule);
ShadersMgr::destroyShaderModule(fragShaderModule);
}

#Vertex Input State

可以通过 VkPipelineVertexInputStateCreateInfo 描述需要传递给顶点着色器的顶点数据的格式,其中主要需要描述两部分:

  • Bindings: 描述顶点数据之前的间隔,以及顶点数据是逐顶点的还是逐 Instance 的。
  • Attribute Descriptions:描述传递给顶点 Shader 的属性(如位置 / 颜色等等)该从哪个 binding 中读取,以及偏移量是多少。

VkPipelineVertexInputStateCreateInfo 的创建代码如下所示:

1
2
3
4
5
6
7
8
9
10
VkPipelineVertexInputStateCreateInfo GraphicsPipelineMgr::getVertexInputStateCreateInfo()
{
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
return vertexInputInfo;
}

因为在 Ch 12 Shader Modules 中顶点的数据是直接记录在顶点中的,所以这里实际上并没有顶点数据需要将 CPU 传递到顶点 Shader 中。因此上述代码中 Descriptions 都为 nullptrCount 都为 0。

#Input Assembly State

可以通过 VkPipelineInputAssemblyStateCreateInfo 描述从顶点信息中,需要构建出怎么样的几何,以及是否需要进行 图元重启。

一个常见的 VkPipelineInputAssemblyStateCreateInfo 构造函数如下所示:

1
2
3
4
5
6
7
8
VkPipelineInputAssemblyStateCreateInfo GraphicsPipelineMgr::getInputAssemblyStateCreateInfo()
{
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
return inputAssembly;
}

其中 topology 与 OpenGL 中的 Layout 类似,在 Vulkan 中有以下选项:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST: 由顶点直接构成点
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST: 每两个顶点构成一个直线,即 ABCD,会构成 ABCD 两条线段。
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: 每个顶点与前一个顶点构成一条直线,即 ABC 会构成 ABBC 两条线段。
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: 每三个顶点构成一个三角形,即 ABCDEF,会构成 ABCDEF 两个三角形。
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: 每个顶点与前两个顶点构成一个三角形,即 ABCD 会构成 ABCBCD 两个三角形

primitiveRestartEnable 决定是否开启图元重启功能,当设为 VK_TRUE 后,当 indices 为 0xFFFF0xFFFFFFFF 会开始重启。

#Viewports and Scissors

之后需要设定 Viewport Test 和 Scissor Test:

在 Vulkan 中,Viewport 和 Scissor 的信息都是通过 VkPipelineViewportStateCreateInfo 进行描述的。

Viewport 部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
VkPipelineViewportStateCreateInfo GraphicsPipelineMgr::getViewportStateCreateInfo()
{
// ...
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;
// ...
}

其中

  • x, y, width, height 设定 Viewport 的范围。这里设定为全屏,即与之前创建 SwapChain 的大小是一样的。
  • minDepthmaxDepth 定义 Framebuffer 的深度值范围,这里将范围设定为最通常的 [0,0,1.0][0,0, 1.0]

Scissor 定义部分如下:

1
2
3
4
5
6
7
8
VkPipelineViewportStateCreateInfo GraphicsPipelineMgr::getViewportStateCreateInfo()
{
// ...
VkRect2D scissor{};
scissor.offset = { 0,0 };
scissor.extent = swapChainExtent;
// ...
}

当 Viewport 和 Scissor 都设定完后,需要将它们通过 VkPipelineViewportStateCreateInfo 结合在一起:

1
2
3
4
5
6
7
8
9
10
11
VkPipelineViewportStateCreateInfo GraphicsPipelineMgr::getViewportStateCreateInfo()
{
// ...
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
// ...
}

#Rasterization State

在现代 GPU 中,会在光栅化阶段进行 Depth Testing / Face Culling 等操作:

Vulkan 中可以通过 VkPipelineRasterizationStateCreateInfo 对上述一系列测试及光栅化本身进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VkPipelineRasterizationStateCreateInfo GraphicsPipelineMgr::getRasterizationStateCreateInfo()
{
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;
rasterizer.depthBiasClamp = 0.0f;
rasterizer.depthBiasSlopeFactor = 0.0f;
return rasterizer;
}

depthBiasClamp 表示是否需要将 Discard 的 片段 Clamp 到 near 和 far 平面。这个功能在一些特殊场景,如计算阴影贴图时需要。

rasterizerDiscardEnable 表示所有的几何是否都不需要通过光栅化阶段,如果设为 VK_TRUE,基本就不会有任何输出到 Framebuffer。

polygonMode 决定怎么根据 Geometry 生成 Fragment,这个数值有以下选项:

  • VK_POLYGON_MODE_FILL:用 Fragment 填充整个 Polygon
  • VK_POLYGON_MODE_LINE:用 LIne 绘制 Polygon 的边缘
  • VK_POLYGON_MODE_POINT:用点绘制 Polygon 的顶点

cullMode 用来指定剔除前向面还是背向面,frontFace 用来指定怎么样的顶点顺序会被认为是前向面。

depthBiasEnable 及后续参数是用来是否需要为 Fragment 的深度添加一个偏移值,整个参数也通常用来被用来绘制 Shadow Mapping。

#Multisampling State

可通过 VkPipelineMultisampleStateCreateInfo 配置 MSAA,目前设置为 MSAA 关闭,在后续章节中会进一步说明各配置项:

1
2
3
4
5
6
7
8
9
10
11
12
VkPipelineMultisampleStateCreateInfo GraphicsPipelineMgr::getMultisamplingStateCreateInfo()
{
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;
multisampling.pSampleMask = nullptr;
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
return multisampling;
}

#Depth and Stencil Testing

如果需要使用 Depth 和 Stencil Buffer,则需要配置 VkPipelineDepthStencilStateCreateInfo 对象,目前章节中还不需要,因此不进行配置。

#Color Blending

在 Vulkan 中色彩混合需要通过两个结构体配置 VkPipelineColorBlendAttachmentStateVkPipelineColorBlendStateCreateInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
VkPipelineColorBlendStateCreateInfo GraphicsPipelineMgr::getColorBlendStateCreateInfo()
{
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;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;

return colorBlending;
}

其中 VkPipelineColorBlendAttachmentState 配置每一个 Attachment 的混合方式,即针对一个 Attachment 中的每一个颜色通道,是否需要进行混合操作,以及混合的方式。

VkPipelineColorBlendStateCreateInfo 配置所有 Attachments 全局的混合配置,其中主要通过 logicOp 设置的位运算将各 Attachments 结果混合在一起。另外 blendConstants 用来配置混合操作的常量值,如果在配置 VkPipelineColorBlendAttachmentState 时设置的 Blend Factor 是和常数相关的,如 VK_BLEND_FACTOR_CONSTANT_COLOR,则需要在这里配置常量值。

#Dynamic State

部分之前部分创建的数据,可以在运行时被修改。VkDynamicState 用于标识那些可以在渲染过程中动态改变而无需重新创建管线对象的状态。这种设计提高了灵活性和性能,避免因为小的状态变更就必须生成新的固定管线对象,其定义方式如下:

1
2
3
4
5
6
7
8
9
10
VkPipelineDynamicStateCreateInfo GraphicsPipelineMgr::getVKDynamicStateCreateInfo()
{
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;
return dynamicState;
}

对于被定义在 VkDynamicState 中的变量,它们在创建 Pipeline 时定义的配置会被无视,且必须在绘制时被赋值。

#Pipeline layout

对于 Shader 中需要使用的 uniform 变量,需要在创建 Pipeline 时设定 VkPipelineLayout 对象。虽然目前并不需要对 Shader 中传递 uniform 对象,但仍然要提供空 VkPipelineLayout

1
2
3
4
5
6
7
8
9
10
VkPipelineLayoutCreateInfo GraphicsPipelineMgr::getPipelineLayoutCreateInfo()
{
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pSetLayouts = nullptr;
pipelineLayoutInfo.pushConstantRangeCount = 0;
pipelineLayoutInfo.pPushConstantRanges = nullptr;
return pipelineLayoutInfo;
}

因为 Pipeline Viewport 可以直接创建,所以可以在创建 Pipeline 时直接创建 Viewport Layout

1
2
3
4
5
6
7
8
9
10
11
void GraphicsPipelineMgr::createGraphicsPipeline(const std::string& vertFileName, const std::string& fragFileName)
{
// ...

// Pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = getPipelineLayoutCreateInfo();
if (vkCreatePipelineLayout(LogicDevicesMgr::device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS)
throw std::runtime_error("Failed to create pipeline layout!");

// ...
}

因为创建的 Pipeline Layout 对象会在程序的生命周期中一直存在,因此需要在 cleanup 中被删除

1
2
3
4
5
6
7
8
9
10
void GraphicsPipelineMgr::destroyPipelineLayout()
{
vkDestroyPipelineLayout(LogicDevicesMgr::device, pipelineLayout, nullptr);
}

void HelloTriangleApplication::cleanup()
{
GraphicsPipelineMgr::destroyPipelineLayout();
// ...
}