管线
现在我们可以加载三角形所需的着色器,我们必须构建 VkPipeline 来渲染它。
VkPipeline 是 Vulkan 中一个庞大的对象,它包含了 GPU 用于绘制的整个配置。构建它们可能非常昂贵,因为它会将着色器模块完全转换为 GPU 指令,并验证其设置。
一旦构建了管线,就可以将其绑定到命令缓冲区中,然后在绘制任何内容时,它将使用绑定的管线。
Vulkan 管线是一个巨大的对象,具有许多不同的配置结构,其中一些甚至运行指针和数组。因此,我们将创建一个专门用于构建管线的类,这将简化该过程。
在本教程中,我们将创建更多管线,因此拥有相对容易创建管线的方法将非常有用。
让我们从声明类开始。我们将把这个类添加到 vk_engine.h 头文件中,与 VulkanEngine 类放在一起。
vk_engine.h
class PipelineBuilder {
public:
std::vector<VkPipelineShaderStageCreateInfo> _shaderStages;
VkPipelineVertexInputStateCreateInfo _vertexInputInfo;
VkPipelineInputAssemblyStateCreateInfo _inputAssembly;
VkViewport _viewport;
VkRect2D _scissor;
VkPipelineRasterizationStateCreateInfo _rasterizer;
VkPipelineColorBlendAttachmentState _colorBlendAttachment;
VkPipelineMultisampleStateCreateInfo _multisampling;
VkPipelineLayout _pipelineLayout;
VkPipeline build_pipeline(VkDevice device, VkRenderPass pass);
};
管线构建器是一个类,其中存储了所有需要的 Vulkan 结构体(这是一个基本集合,还有更多,但目前这些是我们需要填充的)。以及一个 build_pipeline 函数,它将完成它并构建它。如果需要,您可以将构建器放在自己的文件中(推荐 vk_pipeline.h),但我们没有这样做以保持文件数量较少。
我们现在将转到 vk_initializers.h 并开始为每个结构体编写初始化器。
初始化器
着色器阶段
我们将从 VkPipelineShaderStageCreateInfo
的初始化器开始。此 CreateInfo 将保存有关管线的单个着色器阶段的信息。我们从着色器阶段和着色器模块构建它。
VkPipelineShaderStageCreateInfo vkinit::pipeline_shader_stage_create_info(VkShaderStageFlagBits stage, VkShaderModule shaderModule) {
VkPipelineShaderStageCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
info.pNext = nullptr;
//shader stage
info.stage = stage;
//module containing the code for this shader stage
info.module = shaderModule;
//the entry point of the shader
info.pName = "main";
return info;
}
我们将入口点硬编码为“main”。请记住上一篇文章中,着色器的入口点是 main()
函数。这允许我们控制它,但 main() 是相当标准的,所以我们就这样保持。
顶点输入状态
VkPipelineVertexInputStateCreateInfo
包含顶点缓冲区和顶点格式的信息。这相当于 opengl 上的 VAO 配置,但在目前我们没有使用它,所以我们将使用空状态对其进行初始化。在下一个教程章节中,我们将学习如何正确设置它。
VkPipelineVertexInputStateCreateInfo vkinit::vertex_input_state_create_info() {
VkPipelineVertexInputStateCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
info.pNext = nullptr;
//no vertex bindings or attributes
info.vertexBindingDescriptionCount = 0;
info.vertexAttributeDescriptionCount = 0;
return info;
}
输入汇编
VkPipelineInputAssemblyStateCreateInfo
包含要绘制的拓扑类型的配置。您可以在此处将其设置为绘制三角形、线条、点或其他类型,例如三角形列表。
VkPipelineInputAssemblyStateCreateInfo vkinit::input_assembly_create_info(VkPrimitiveTopology topology) {
VkPipelineInputAssemblyStateCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
info.pNext = nullptr;
info.topology = topology;
//we are not going to use primitive restart on the entire tutorial so leave it on false
info.primitiveRestartEnable = VK_FALSE;
return info;
}
在 info 中,我们只需要设置样板代码以及我们想要的拓扑类型。示例拓扑
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : 普通三角形绘制
- VK_PRIMITIVE_TOPOLOGY_POINT_LIST : 点
- VK_PRIMITIVE_TOPOLOGY_LINE_LIST : 线条列表
光栅化状态
VkPipelineRasterizationStateCreateInfo
。固定功能光栅化的配置。在这里,我们可以启用或禁用背面剔除,并设置线宽或线框绘制。
VkPipelineRasterizationStateCreateInfo vkinit::rasterization_state_create_info(VkPolygonMode polygonMode)
{
VkPipelineRasterizationStateCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
info.pNext = nullptr;
info.depthClampEnable = VK_FALSE;
//discards all primitives before the rasterization stage if enabled which we don't want
info.rasterizerDiscardEnable = VK_FALSE;
info.polygonMode = polygonMode;
info.lineWidth = 1.0f;
//no backface cull
info.cullMode = VK_CULL_MODE_NONE;
info.frontFace = VK_FRONT_FACE_CLOCKWISE;
//no depth bias
info.depthBiasEnable = VK_FALSE;
info.depthBiasConstantFactor = 0.0f;
info.depthBiasClamp = 0.0f;
info.depthBiasSlopeFactor = 0.0f;
return info;
}
我们将 polygonMode 保留为可编辑输入,以便能够在线框和实体绘制之间切换。
cullMode 用于剔除背面或正面,但在这里我们将默认设置为不剔除。我们在这里也没有使用任何深度偏移,所以我们将所有这些都设置为 0。
如果启用了 rasterizerDiscardEnable
,则图元(在我们的例子中是三角形)甚至在到达光栅化阶段之前就被丢弃,这意味着三角形永远不会被绘制到屏幕上。例如,如果您只对顶点处理阶段的副作用感兴趣,例如写入稍后从中读取的缓冲区,则可以启用此功能。但在我们的例子中,我们对绘制三角形感兴趣,所以我们将其禁用。
多重采样状态
VkPipelineMultisampleStateCreateInfo
允许我们为此管线配置 MSAA。我们不会在整个教程中使用 MSAA,因此我们将默认设置为 1 个样本并禁用 MSAA。如果您想启用 MSAA,则需要将 rasterizationSamples
设置为大于 1,并启用 sampleShading。请记住,为了使 MSAA 工作,您的渲染通道也必须支持它,这会使事情变得非常复杂。
VkPipelineMultisampleStateCreateInfo vkinit::multisampling_state_create_info()
{
VkPipelineMultisampleStateCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
info.pNext = nullptr;
info.sampleShadingEnable = VK_FALSE;
//multisampling defaulted to no multisampling (1 sample per pixel)
info.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
info.minSampleShading = 1.0f;
info.pSampleMask = nullptr;
info.alphaToCoverageEnable = VK_FALSE;
info.alphaToOneEnable = VK_FALSE;
return info;
}
颜色混合附件状态
VkPipelineColorBlendAttachmentState
控制此管线如何混合到给定的附件中。我们只渲染到 1 个附件,因此我们只需要其中一个,并默认设置为“不混合”并仅覆盖。在这里可以制作将与图像混合的对象。这个也没有 sType + pNext
VkPipelineColorBlendAttachmentState vkinit::color_blend_attachment_state() {
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;
return colorBlendAttachment;
}
完成 PipelineBuilder
现在我们有了所有结构体,我们需要填充 PipelineBuilder 的 build_pipeline 函数,该函数将上述所有内容组合到最终的 Info 结构体中以创建管线。
让我们首先将视口和裁剪矩形连接到 ViewportState 中,并设置 ColorBlenderStateCreateInfo
VkPipeline PipelineBuilder::build_pipeline(VkDevice device, VkRenderPass pass) {
//make viewport state from our stored viewport and scissor.
//at the moment we won't support multiple viewports or scissors
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.pNext = nullptr;
viewportState.viewportCount = 1;
viewportState.pViewports = &_viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &_scissor;
//setup dummy color blending. We aren't using transparent objects yet
//the blending is just "no blend", but we do write to the color attachment
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.pNext = nullptr;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &_colorBlendAttachment;
}
VkPipelineColorBlendStateCreateInfo
包含有关附件及其使用方式的信息。这个必须与片段着色器输出匹配。
是时候将所有内容连接到主 VkGraphicsPipelineCreateInfo 并创建它了
VkPipeline PipelineBuilder::build_pipeline(VkDevice device, VkRenderPass pass) {
// ... other code ...
//build the actual pipeline
//we now use all of the info structs we have been writing into into this one to create the pipeline
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.pNext = nullptr;
pipelineInfo.stageCount = _shaderStages.size();
pipelineInfo.pStages = _shaderStages.data();
pipelineInfo.pVertexInputState = &_vertexInputInfo;
pipelineInfo.pInputAssemblyState = &_inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &_rasterizer;
pipelineInfo.pMultisampleState = &_multisampling;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.layout = _pipelineLayout;
pipelineInfo.renderPass = pass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
//it's easy to error out on create graphics pipeline, so we handle it a bit better than the common VK_CHECK case
VkPipeline newPipeline;
if (vkCreateGraphicsPipelines(
device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &newPipeline) != VK_SUCCESS) {
std::cout << "failed to create pipeline\n";
return VK_NULL_HANDLE; // failed to create graphics pipeline
}
else
{
return newPipeline;
}
管线布局
除了所有状态结构体之外,我们的管线还需要一个 VkPipelineLayout 对象。与其他状态结构体不同,这是一个实际完整的 Vulkan 对象,需要与管线分开创建。
管线布局包含有关给定管线的着色器输入的信息。您可以在此处配置推送常量和描述符集,但在目前我们不需要它,因此我们将为我们的管线创建一个空的管线布局
我们还需要另一个 info 结构体,所以让我们添加它。
VkPipelineLayoutCreateInfo vkinit::pipeline_layout_create_info() {
VkPipelineLayoutCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
info.pNext = nullptr;
//empty defaults
info.flags = 0;
info.setLayoutCount = 0;
info.pSetLayouts = nullptr;
info.pushConstantRangeCount = 0;
info.pPushConstantRanges = nullptr;
return info;
}
我们将 pSetLayouts 和 pPushConstantRanges 都设置为 null,因为我们的着色器没有输入,但我们很快就会在此处添加一些内容。
我们需要将管线布局存储在某个地方,因为很多 Vulkan 命令都需要它,所以让我们在 VulkanEngine 类中为其添加一个成员
class VulkanEngine{
public:
// ... other stuff ....
VkPipelineLayout _trianglePipelineLayout;
private:
}
现在我们必须从我们的 init_pipelines() 函数创建它。
void VulkanEngine::init_pipelines()
{
//shader module loading
//build the pipeline layout that controls the inputs/outputs of the shader
//we are not using descriptor sets or other systems yet, so no need to use anything other than empty default
VkPipelineLayoutCreateInfo pipeline_layout_info = vkinit::pipeline_layout_create_info();
VK_CHECK(vkCreatePipelineLayout(_device, &pipeline_layout_info, nullptr, &_trianglePipelineLayout));
}
创建三角形管线
现在是时候将所有内容组装在一起并构建用于渲染三角形的管线了。
在 VulkanEngine 类上添加 _trianglePipeline 作为新变量
class VulkanEngine {
public:
// ... other objects
VkPipeline _trianglePipeline;
};
void VulkanEngine::init_pipelines()
{
// layout and shader modules creation
//build the stage-create-info for both vertex and fragment stages. This lets the pipeline know the shader modules per stage
PipelineBuilder pipelineBuilder;
pipelineBuilder._shaderStages.push_back(
vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_VERTEX_BIT, triangleVertexShader));
pipelineBuilder._shaderStages.push_back(
vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_FRAGMENT_BIT, triangleFragShader));
//vertex input controls how to read vertices from vertex buffers. We aren't using it yet
pipelineBuilder._vertexInputInfo = vkinit::vertex_input_state_create_info();
//input assembly is the configuration for drawing triangle lists, strips, or individual points.
//we are just going to draw triangle list
pipelineBuilder._inputAssembly = vkinit::input_assembly_create_info(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
//build viewport and scissor from the swapchain extents
pipelineBuilder._viewport.x = 0.0f;
pipelineBuilder._viewport.y = 0.0f;
pipelineBuilder._viewport.width = (float)_windowExtent.width;
pipelineBuilder._viewport.height = (float)_windowExtent.height;
pipelineBuilder._viewport.minDepth = 0.0f;
pipelineBuilder._viewport.maxDepth = 1.0f;
pipelineBuilder._scissor.offset = { 0, 0 };
pipelineBuilder._scissor.extent = _windowExtent;
//configure the rasterizer to draw filled triangles
pipelineBuilder._rasterizer = vkinit::rasterization_state_create_info(VK_POLYGON_MODE_FILL);
//we don't use multisampling, so just run the default one
pipelineBuilder._multisampling = vkinit::multisampling_state_create_info();
//a single blend attachment with no blending and writing to RGBA
pipelineBuilder._colorBlendAttachment = vkinit::color_blend_attachment_state();
//use the triangle layout we created
pipelineBuilder._pipelineLayout = _trianglePipelineLayout;
//finally build the pipeline
_trianglePipeline = pipelineBuilder.build_pipeline(_device, _renderPass);
}
我们终于创建了绘制三角形所需的管线,所以我们终于可以做到这一点了。
让我们转到我们的主 draw()
函数,并执行绘制。
我们需要在 VkCmdBeginRenderPass 和 vkCmdEndRenderPass 之间添加绘制命令
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
//once we start adding rendering commands, they will go here
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _trianglePipeline);
vkCmdDraw(cmd, 3, 1, 0, 0);
//finalize the render pass
vkCmdEndRenderPass(cmd);
vkCmdBingPipeline 设置要在后续渲染命令中使用的管线,我们在此处绑定三角形管线。
vkCmdDraw 执行绘制,在本例中,我们绘制 1 个对象,包含 3 个顶点。
运行它,您应该会看到一个红色三角形,背景闪烁蓝色。
恭喜您绘制了第一个三角形!我们现在可以继续使用着色器做一些有趣的事情,使其更有趣。
下一步:通过着色器阶段传递数据