Link

管线

现在我们可以加载三角形所需的着色器,我们必须构建 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 个顶点。

运行它,您应该会看到一个红色三角形,背景闪烁蓝色。

triangle

恭喜您绘制了第一个三角形!我们现在可以继续使用着色器做一些有趣的事情,使其更有趣。

下一步:通过着色器阶段传递数据