Today I want to present a translation of a new chapter in the section on the Graphics pipeline basics called Fixed functions.
Content
Non-programmable pipeline stages
- Vertex input
- Input assembler
- Viewport and scissors
- Rasterizer
- Multisampling
- Depth test and stencil test
- Color mixing
- Dynamic state
- Pipeline Layout
- Conclusion
Early graphics APIs used default state for most stages of the graphics pipeline. In Vulkan, all states must be described explicitly, starting with the viewport size and ending with the color mixing function. In this chapter, we will set up the non-programmable pipeline stages.
Vertex input
The VkPipelineVertexInputStateCreateInfo structure describes the format of the vertex data that is passed to the vertex shader. There are two types of descriptions:
- Description of attributes: data type passed to the vertex shader, binding to the data buffer and offset in it
- Binding: the distance between data items and how the data and the output geometry are bound (per-instance or vertex binding) (see Geometry instancing )
Since we hardcoded the vertex data in the vertex shader, we will indicate that there is no data to load. To do this, let's fill in the structure
VkPipelineVertexInputStateCreateInfo
. We'll come back to this question later in the chapter on vertex buffers.
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
Members
pVertexBindingDescriptions
and
pVertexAttributeDescriptions
point to an array of structures that describe the above data for loading vertex attributes. Add this structure to the function
createGraphicsPipeline
right after
shaderStages
.
Input assembler
The VkPipelineInputAssemblyStateCreateInfo structure describes 2 things: what geometry is formed from vertices and whether restarting of geometry is allowed for geometries such as line strip and triangle strip. Geometry is specified in the field
topology
and can have the following values:
VK_PRIMITIVE_TOPOLOGY_POINT_LIST
: geometry is drawn as separate points, each vertex is a separate pointVK_PRIMITIVE_TOPOLOGY_LINE_LIST
: the geometry is drawn as a set of line segments, each pair of vertices forms a separate lineVK_PRIMITIVE_TOPOLOGY_LINE_STRIP
: the geometry is drawn as a continuous polyline, each subsequent vertex adds one segment to the polylineVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
: the geometry is drawn as a set of triangles, with every 3 vertices forming an independent triangleVK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
: ,
Usually vertices are loaded sequentially in the order in which you place them in the vertex buffer. However, with an index buffer, you can change the loading order. This allows for optimizations such as reusing vertices. If you
primitiveRestartEnable
specify a value in the field
VK_TRUE
, you can interrupt the lines and triangles with topology
VK_PRIMITIVE_TOPOLOGY_LINE_STRIP
and
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
and start drawing new primitives using the special index
0xFFFF
or
0xFFFFFFFF
.
In the tutorial, we will be drawing individual triangles, so we will use the following structure:
VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE;
Viewport and scissors
The viewport describes the area of ββthe framebuffer to which the output is rendered. Almost always the coordinates from
(0, 0)
to are set for the viewport
(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;
Be aware that the size of the swap chain and images may differ from the values
WIDTH
and the
HEIGHT
window. Later, the images from the swap chain will be used as framebuffers, so we must use exactly their size.
minDepth
and
maxDepth
determine the range of depth values ββfor the framebuffer. These values ββmust be in the range
[0,0f, 1,0f]
, and there
minDepth
may be more
maxDepth
. Use the default values ββ-
0.0f
and
1.0f
if you're not going to do anything out of the ordinary.
If the viewport determines how the image will be stretched in the framebuffer, then scissor determines which pixels will be saved. All pixels outside the scissor rectangle will be discarded during rasterization. The clipping rectangle is used to crop the image, not transform it. The difference is shown in the pictures below. Please note that the clipping rectangle on the left is just one of many possible options for obtaining such an image, as long as its size is larger than the size of the viewport.
In this tutorial, we want to render the image to the entire framebuffer, so we will specify that the scissor rectangle completely overlaps the viewport:
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
Now we need to combine the information about the viewport and the scissor using the VkPipelineViewportStateCreateInfo structure . On some video cards, several viewports and clipping rectangles can be used simultaneously, so information about them is transmitted as an array. To use several viewports at once, you need to enable the corresponding GPU option.
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
The rasterizer converts geometry from a vertex shader into multiple fragments. The depth test , face culling , scissor test is also performed here, and the method of filling polygons with fragments is configured: filling the entire polygon, or only the edges of polygons (wireframe rendering). All of this is configured in the VkPipelineRasterizationStateCreateInfo structure .
VkPipelineRasterizationStateCreateInfo rasterizer{}; rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizer.depthClampEnable = VK_FALSE;
If the field is
depthClampEnable
set
VK_TRUE
, the fragments of which are outside of the near and far planes, are not cut off, and pushes them. This can be useful, for example, when creating a shadow map. To use this parameter, you must enable the corresponding GPU option.
rasterizer.rasterizerDiscardEnable = VK_FALSE;
If
rasterizerDiscardEnable
set
VK_TRUE
, the rasterization stage is disabled and no output is passed to the framebuffer.
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
polygonMode
determines how chunks are generated. The following modes are available:
VK_POLYGON_MODE_FILL
: polygons are completely filled with fragmentsVK_POLYGON_MODE_LINE
: polygon edges are converted to linesVK_POLYGON_MODE_POINT
: polygon vertices are drawn as dots
To use these modes, except
VK_POLYGON_MODE_FILL
, you need to enable the corresponding GPU option.
rasterizer.lineWidth = 1.0f;
The field
lineWidth
sets the thickness of the segments. The maximum supported chunk width depends on your hardware, and thicker chunks
1,0f
require the GPU option to be enabled
wideLines
.
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
The parameter
cullMode
defines the face culling type. You can turn off clipping entirely, or turn on clipping for front and / or non-front faces. The variable
frontFace
determines the order in which the vertices are traversed (clockwise or counterclockwise) to define the front faces.
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional
The rasterizer can change the depth values ββby adding a constant value or offsetting the depth depending on the slope of the fragment. This is usually used when creating a shadow map. We don't need this, so we'll
depthBiasEnable
install it for
VK_FALSE
.
Multisampling
The VkPipelineMultisampleStateCreateInfo structure configures multisampling - one of the anti-aliasing methods . It works mainly on the edges, combining colors from different polygons that are rasterized into the same pixels. This allows you to get rid of the most visible artifacts. The main advantage of multisampling is that the fragment shader in most cases is executed only once per pixel, which is much better, for example, than rendering at a higher resolution and then reducing the size. To use multisampling, you must enable the corresponding GPU option.
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
Until we include it, we will return to it in one of the following articles.
Depth test and stencil test
When using a depth buffer and / or a stencil buffer, you need to configure them using VkPipelineDepthStencilStateCreateInfo . We don't need this yet, so we'll just pass it
nullptr
instead of a pointer to this structure. We'll come back to this in the chapter on the depth buffer.
Color mixing
The color returned by the fragment shader needs to be merged with the color already in the framebuffer. This process is called color mixing, and there are two ways to do it:
- Mix old and new value to get the output color
- Concatenate old and new value using bitwise operation
Two types of structures are used to configure color mixing: the VkPipelineColorBlendAttachmentState structure contains settings for each connected framebuffer, the VkPipelineColorBlendStateCreateInfo structure contains global color mixing settings. In our case, only one framebuffer is used:
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
The structure
VkPipelineColorBlendAttachmentState
allows you to customize color mixing in the first way. The following pseudocode best demonstrates all the operations performed:
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;
If
blendEnable
set
VK_FALSE
, the color from the fragment shader is passed unchanged. If set
VK_TRUE
, two blending operations are used to compute the new color. The final color is filtered using
colorWriteMask
to determine which channels of the output image are being written to.
The most common color blending is alpha blending, where the new color is blended with the old color based on transparency.
finalColor
is calculated as follows:
finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
This can be configured using the following options:
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;
All possible operations can be found in the VkBlendFactor and VkBlendOp enumerations in the specification.
The second structure refers to an array of structures for all framebuffers and allows mixing constants to be specified that can be used as mixing factors in the above calculations.
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
If you want to use the second mixing method (bitwise operation), set
VK_TRUE
for
logicOpEnable
. Then you can specify the bitwise operation in the field
logicOp
. Note that the first method will automatically become unavailable, as if each is connected to the framebuffer
blendEnable
has been found
VK_FALSE
! Note that it
colorWriteMask
is also used for bitwise operations to determine which channel content will be changed. You can turn off both modes, as we did, in this case the colors of the fragments will be written to the framebuffer without changes.
Dynamic state
Some states of the graphics pipeline can be changed without re-creating the pipeline, such as viewport size, chunk widths, and blending constants. To do this, fill in the VkPipelineDynamicStateCreateInfo structure :
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;
As a result, the values ββof these settings are not taken into account at the stage of creating the pipeline, and you need to specify them right at the time of rendering. We'll come back to this in the next chapters. You can use
nullptr
instead of a pointer to this structure if you don't want to use dynamic states.
Pipeline Layout
In shaders, you can use
uniform
-variables - global variables that can be changed dynamically to change the behavior of the shaders without having to re-create them. They are typically used to pass a transformation matrix to a vertex shader or to create texture samplers in a fragment shader.
These uniforms must be specified when creating the pipeline using the VkPipelineLayout object . Even though we won't be using these variables for now, we still need to create an empty pipeline layout.
Let's create a member of the class to hold the object, as we will later refer to it from other functions:
VkPipelineLayout pipelineLayout;
Then let's create an object in a function
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!");
}
The structure also specifies push constants, which are another way to pass dynamic variables to shaders. We will get to know them later. We will use the pipeline throughout the entire life cycle of the program, so we need to destroy it at the very end:
void cleanup() {
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
...
}
Conclusion
That's all there is to know about non-programmable states! It took a lot of work to set them up from scratch, but now you know almost everything that happens in the graphics pipeline!
To create a graphics pipeline, it remains to create the last object - the render pass.