7.4 创建简单的图形管线
可调用和第六章“着色器和管线”中创建计算管线的函数类似的函数来创建图形管线。然而,你可以看到,图形管线包含很多个着色阶段和固定功能处理单元,所以,对于图形管线的相应的描述就复杂的多了。可调用vkCreateGraphicsPipelines()来创建图形管线,其原型如下:
VkResultvkCreateGraphicsPipelines (
VkDevice device,
VkPipelineCache pipelineCache,
uint32_t createInfoCount,
const VkGraphicsPipelineCreateInfo* pCreateInfos,
const VkAllocationCallbacks* pAllocator,
VkPipeline* pPipelines);
如你所见,vkCreateGraphicsPipelines()的原型和vkCreateComputePipelines()很类似。都接受一个设备(device),管线缓存的handle(pipelineCache),一个createInfo数组,包含数组的长度(pCreateInfosand createInfoCount)。这才是这个函数难搞的地方。VkGraphicsPipelineCreateInfo是一个很大、复杂的数据类型,它包含了多个其他结构的指针和你已经创建的对象的handle。深呼吸一下。VkGraphicsPipelineCreateInfo的定义是:
typedefstruct VkGraphicsPipelineCreateInfo {
VkStructureType sType;
const void* pNext;
VkPipelineCreateFlags flags;
uint32_t stageCount;
const VkPipelineShaderStageCreateInfo* pStages;
const VkPipelineVertexInputStateCreateInfo* pVertexInputState;
const VkPipelineInputAssemblyStateCreateInfo* pInputAssemblyState;
const VkPipelineTessellationStateCreateInfo* pTessellationState;
const VkPipelineViewportStateCreateInfo* pViewportState;
const VkPipelineRasterizationStateCreateInfo* pRasterizationState;
const VkPipelineMultisampleStateCreateInfo* pMultisampleState;
const VkPipelineDepthStencilStateCreateInfo* pDepthStencilState;
const VkPipelineColorBlendStateCreateInfo* pColorBlendState;
const VkPipelineDynamicStateCreateInfo* pDynamicState;
VkPipelineLayout layout;
VkRenderPass renderPass;
uint32_t subpass;
VkPipeline basePipelineHandle;
int32_t basePipelineIndex;
}VkGraphicsPipelineCreateInfo;
上面提醒过的,VkGraphicsPipelineCreateInfo是一个很大的数据结构,带有很多个其他数据的指针。然而,可简单的把它分解成小块,很多个创建附加信息是可选的,可设置为nullptr。和Vulkan中其他的创建信息类型一样,VkGraphicsPipelineCreateInfo从sType域和pNext开始。VkGraphicsPipelineCreateInfo的sType是VK_GRAPHICS_PIPELINE_CREATE_INFO,pNext可以置为nullptr,除非使用了拓展。
flags域包含管线如何被使用的信息。在当前的Vulkan版本中已经定义了三个标志位,如下:
• VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT tells Vulkan that thispipeline is
not going to be used inperformance-critical applications and that you would prefer to receive a
ready-to-go pipeline object quickly ratherthan have Vulkan spend a lot of time optimizing the
pipeline. You might use this for thingslike simple shaders for displaying splash screens or user
interface elements that you want to displayquickly.
• VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT and
VK_PIPELINE_CREATE_DERIVATIVE_BIT are usedwith derivative pipelines. This is a
feature whereby you can group similarpipelines and tell Vulkan that you’ll switch rapidly
among them. TheVK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT flag tells Vulkan
that you will want to create derivatives ofthe new pipeline, and
VK_PIPELINE_CREATE_DERIVATIVE_BIT tellsVulkan that this pipeline is a pipeline.
7.4.1 图形着色器阶段
VkGraphicsPipelineCreateInfo接下来两个域,stageCount和 pStages,是你把着色器传递到管线的目标位置。pStages是一个指向长度为stageCount、类型为VkPipelineShaderStageCreateInfo的数组的指针,数组每一个元素描述了一个着色阶段。这些和你在VkComputePipelineCreateInfo定义中看到的类似,除了现在变成了一个数组。VkPipelineShaderStageCreateInfo的定义是:
typedefstruct VkPipelineShaderStageCreateInfo {
VkStructureType sType;
const void* pNext;
VkPipelineShaderStageCreateFlags flags;
VkShaderStageFlagBits stage;
VkShaderModule module;
const char* pName;
const VkSpecializationInfo* pSpecializationInfo;
}VkPipelineShaderStageCreateInfo;
所有的图形管线至少包含一个顶点着色器,且这个顶点着色器总是管线的第一个着色阶段。因此,VkGraphicsPipelineCreateInfo的pStages应该指向一个描述了顶点着色器的VkPipelineShaderStageCreateInfo类型数据。VkPipelineShaderStageCreateInfo中的参数应该和你在第六章“着色器和管线”中创建计算管线时的参数一样含义。module应该是一个着色器模块,至少包含了顶点着色器,pName应该是该模块中顶点着色器的执行入口。
因为在我们简单的管线
Listing 7.2:Creating a Simple Graphics Pipeline
VkPipelineShaderStageCreateInfoshaderStageCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,// sType
nullptr, // pNext
0, // flags
VK_SHADER_STAGE_VERTEX_BIT, // stage
module, // module
"main", // pName
nullptr // pSpecializationInfo
};
static const
VkPipelineVertexInputStateCreateInfovertexInputStateCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,// sType
nullptr, // pNext
0, // flags
0, //
vertexBindingDescriptionCount
nullptr, // pVertexBindingDescriptions
0, //
vertexAttributeDescriptionCount
nullptr // pVertexAttributeDescriptions
};
static const
VkPipelineInputAssemblyStateCreateInfoinputAssemblyStateCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,//sType
nullptr, // pNext
0, // flags
VK_PRIMITIVE_TOPOLOGY_POINT_LIST, //topology
VK_FALSE // primitiveRestartEnable
};
static const
VkViewportdummyViewport =
{
0.0f, 0.0f, // x, y
1.0f, 1.0f, // width, height
0.1f, 1000.0f // minDepth, maxDepth
};
static const
VkRect2DdummyScissor =
{
{ 0, 0 }, // offset
{ 1, 1 } // extent
};
static const
VkPipelineViewportStateCreateInfoviewportStateCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,// sType
nullptr, // pNext
0, // flags
1, // viewportCount
&dummyViewport, // pViewports
1, // scissorCount
&dummyScissor // pScissors
};
static const
VkPipelineRasterizationStateCreateInforasterizationStateCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,// sType
nullptr, // pNext
0, // flags
VK_FALSE, // depthClampEnable
VK_TRUE, // rasterizerDiscardEnable
VK_POLYGON_MODE_FILL, // polygonMode
VK_CULL_MODE_NONE, // cullMode
VK_FRONT_FACE_COUNTER_CLOCKWISE, //frontFace
VK_FALSE, // depthBiasEnable
0.0f, // depthBiasConstantFactor
0.0f, // depthBiasClamp
0.0f, // depthBiasSlopeFactor
0.0f // lineWidth
};
static const
VkGraphicsPipelineCreateInfographicsPipelineCreateInfo =
{
VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,// sType
nullptr, // pNext
0, // flags
1, // stageCount
&shaderStageCreateInfo, // pStages
&vertexInputStateCreateInfo, //pVertexInputState
&inputAssemblyStateCreateInfo, //pInputAssemblyState
nullptr, // pTessellationState
&viewportStateCreateInfo, //pViewportState
&rasterizationStateCreateInfo, //pRasterizationState
nullptr, // pMultisampleState
nullptr, // pDepthStencilState
nullptr, // pColorBlendState
nullptr, // pDynamicState
VK_NULL_HANDLE, // layout
renderpass, // renderPass
0, // subpass
VK_NULL_HANDLE, // basePipelineHandle
0, // basePipelineIndex
};
result =vkCreateGraphicsPipelines(device,
VK_NULL_HANDLE,
1,
&graphicsPipelineCreateInfo,
nullptr,
&pipeline);
当然,大多数时候,你不会使用一个仅包含顶点着色器的图形管线。如本章前面所介绍的,管线最多由五个着色器阶段组成。这些阶段如下:
• The vertex shader, specified as VK_SHADER_STAGE_VERTEX_BIT,processes one vertex at a
time and passes it to the next logicalstage in the pipeline.
• The tessellation control shader, specified as
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT,processes one control point at a
time but has access to all of the data thatmakes up the patch. It can be considered to be a patch
shader, and it produces the tessellationfactors and per-patch data associated with the patch.
• The tessellation evaluation shader, specified using
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT,processes one tessellated
vertex at a time. In many applications, itevaluates the patch function at each point—hence, the
name. It also has access to the full patchdata produced by the tessellation control shader.
• The geometry shader, specified using VK_SHADER_STAGE_GEOMETRY_BIT,executes once
for each primitive that passes through thepipeline: points, lines, or triangles. It can produce new
primitives or throw them away rather thanpassing them on. It can also change the type of a
primitive as it passes by.
• The fragment shader, specified using VK_SHADER_STAGE_FRAGMENT_BIT,executes once
per fragment, after rasterization. It isprimarily responsible for computing the final color of each
pixel.
大多数直接渲染将包含顶点着色器和片元着色器。每一个阶段从上一个阶段消耗数据并向下一个阶段传递数据,形成一个管线。在一些情况下,着色器的输入由固定功能单元提供,优势输出被固定功能单元消耗。不管数据的来源和去向,在着色器内声明输入和输出的方式是相同的。
在SPIR-V中向着色器声明一个输入,变量声明时必须被Input修饰。同样,在着色器内声明输出,变量声明时需要被Output修饰。不像GLSL,特定用途的输入和输出并没有SPIR-V预定义的名字。它们以自己的目的被修饰。然后,你用GLSL写着色器,并用编译器编译为SPIR-V。编译器将识别内置的变量,并把它们翻译为生成的SPIR-V着色器里合适的带有修饰符声明的输入、输入变量。
7.4.2 顶点输入状态
要渲染真实的几何对象,你需要给Vulkan管线提供数据。你可以使用SPIR-V提供的顶点和实例索引,来可控的生成几何数据或者从缓冲区取得几何数据。或者,你可描述集合数据在内存中的布局,Vulkan可以帮你获取,直接给入到着色器内。
要想这么做,需要使用VkGraphicsPipelineCreateInfo的pVertexInputState成员,它是VkPipelineVertexInputStateCreateInfo类型的数据,定义如下:
typedefstruct VkPipelineVertexInputStateCreateInfo {
VkStructureType sType;
const void* pNext;
VkPipelineVertexInputStateCreateFlags flags;
uint32_t vertexBindingDescriptionCount;
const VkVertexInputBindingDescription* pVertexBindingDescriptions;
uint32_t vertexAttributeDescriptionCount;
const VkVertexInputAttributeDescription* pVertexAttributeDescriptions;
} VkPipelineVertexInputStateCreateInfo;
VkPipelineVertexInputStateCreateInfo结构同样以sType和pNext域开始,各自应置为VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO和nullptr。VkPipelineVertexInputStateCreateInfo的flags域被保留,应置为0。
顶点输入状态可划分为可绑定到含有数据缓冲区的顶点绑定集,和描述顶点在缓冲区中如何布局的顶点属性集。和顶点缓冲区绑定的缓冲区有时被称为顶点缓冲区。注意,真的不存在这个所谓的可存储顶点数据的顶点缓冲区。用来存储顶点数据的缓冲区的唯一要求就是创建的时候带有VK_BUFFER_USAGE_VERTEX_BUFFER_BIT参数。
vertexBindingDescriptionCount是管线使用的顶点绑定的个数,pVertexBindingDescriptions是指向了一个VkVertexInputBindingDescription类型的数组的指针,每一个元素描述了一个绑定。VkVertexInputBindingDescription定义是:
typedefstruct VkVertexInputBindingDescription {
uint32_t binding;
uint32_t stride;
VkVertexInputRate inputRate;
}VkVertexInputBindingDescription;
binding域是这个结构数据描述的绑定的索引。每一个管线可以访问到一定数量的顶点缓冲区绑定,他们的索引并不需要是连续的。只要管线使用的每一个绑定都被描述了,就无需在指定的管线中描述绑定。
VkVertexInputBindingDescription数组能寻址到的最后一个绑定索引必须比设备支持的最大绑定个数要小。这个限制被Vulkan标准保证至少是16。对于一些机器,可能会更高。然而,你可以检查设备的VkPhysicalDeviceLimits数据的maxVertexInputBindings成员来获知这个最高值。可调用vkGetPhysicalDeviceProperties()函数来获取VkPhysicalDeviceLimits数据。
每一个绑定可以被看作一个缓冲区对象中一个数组。数组的步长—亦即每一个元素的起始位置的距离,以byte为单位—通过stride指定。如果顶点数组通过一个数据类型的数组指定,stride参数必须包含该数据类型的大小,即使着色器并不会使用每一个它的每一个成员。对于任何类型的绑定,stride的最大值都是Vulkan实现决定的,但是Vulkan标准保证至少是2048字节。如果想要使用更大步长的顶点数据,你需要查询设备是否支持这个步长。
可检查VkPhysicalDeviceLimits的maxVertexInputBindingStride域来获知受支持的最大步长。
还有,Vulkan可以顶点索引的函数或者实例索引的函数来遍历数组。这是通过inputRate域来指定的,它的值可以是 VK_VERTEX_INPUT_RATE_VERTEX 或者VK_VERTEX_INPUT_RATE_INSTANCE。
每一个顶点属性都是存储在顶点缓冲区的一个数据类型的数据。一个顶点缓冲区的每一个顶点属性都有相同的步进速率和数组步长,但是在数据内部有自己的数据类型和偏移量。这是通过VkVertexInputAttributeDescription类型数据来描述的。数组的指针通过VkPipelineVertexInputStateCreateInfo的pVertexAttributeDescriptions域来传递,数组中元素的个数(顶点属性的个数)通过vertexAttributeDescriptionCount传递。VkVertexInputAttributeDescription定义是:
typedefstruct VkVertexInputAttributeDescription {
uint32_t location;
uint32_t binding;
VkFormat format;
uint32_t offset;
}VkVertexInputAttributeDescription;
每一个属性都有location,可在顶点着色器内引用到该属性。顶点属性位置并不需要是连续的,也无需描述每一个顶点属性位置,只要管线使用的所有属性都被描述了。属性的位置通过VkVertexInputAttributeDescription的location成员来描述。
指定了被绑定的缓冲区的此次绑定,亦即可从之获取属性数据绑定,通过binding参数指定,并且它应和之前描述过的VkVertexInputBindingDescription类型数据的每个元素指定的绑定想匹配。顶点数据的格式通过format指定,每个数据的便宜量通过offset指定。
如同数据结构的总大小有上限一样,每一个属性在数据结构内的偏移量也有上限。Vulkan标准保证至少是2047个字节,这个大小足以保证每一个结构的最后面能刚好放一个byte,从而数据结构大小不超过最大值2048自己。如果你需要使用比这个更大的数值,你需要检查设备的能力是否可以处理。设备的VkPhysicalDeviceLimits的maxVertexInputAttributeOffset域包含了offset可以使用的最大值。你可以调用vkGetPhysicalDeviceProperties()函数来获取该值。
Listing 7.3展示了如何创建一个C++数据结构,并使用VkVertexInputBindingDescription和VkVertexInputBindingDescription来描述它,以便你可以用它来把顶点数据传递给Vulkan。
Listing7.3: Describing Vertex Input Data
typedefstruct vertex_t
{
vmath::vec4 position;
vmath::vec3 normal;
vmath::vec2 texcoord;
} vertex;
static const
VkVertexInputBindingDescription vertexInputBindings[] =
{
{ 0, sizeof(vertex),VK_VERTEX_INPUT_RATE_VERTEX } // Buffer
};
static const
VkVertexInputAttributeDescription vertexAttributes[] =
{
{ 0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0 }, //Position
{ 1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(vertex, normal) },//Normal
{ 2, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(vertex, texcoord) } // TexCoord
};
static const
VkPipelineVertexInputStateCreateInfo vertexInputStateCreateInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, // sType
nullptr, // pNext
0, // flags
vkcore::utils::arraysize(vertexInputBindings), //vertexBindingDescriptionCount
vertexInputBindings, //pVertexBindingDescriptions
vkcore::utils::arraysize(vertexAttributes),//vertexAttributeDescriptionCount
vertexAttributes //
pVertexAttributeDescriptions
};
在单个顶点着色器内可以使用的输入属性的最大个数依赖于设备,但是被保证至少是16.这是pVertexInputAttributeDescriptions数组中VkVertexInputAttributeDescription元素个数的上限。一些Vulkan实现也许支持更大的数量。可检查设备的VkPhysicalDeviceLimits数据的maxVertexInputAttributes域,来获知顶点着色器可以使用的输入的最大个数。
顶点数据是从你绑定到命令缓冲区的顶点缓冲区中读取的,然后传递给顶点着色器。对于将处理这些顶点数据的顶点着色器,它必须声明和已定义的顶点属性对应的输入。在你的SPIR-V顶点着色器中创建一个带Input存储标志的变量即可做到。在GLSL着色器中,使用in类型的变量即可表达。
每一个输入必须被赋予一个location。在GLSL中这是通过location限定符来指定的,翻译到SPIR-V中就是应用到输入的Location修饰符。Listing 7.4展示了声明了多个输入的GLSL顶点着色器的片段。glslangvalidator生成的SPIR-V如Listing 7.5所示。
Theshader shown in Listing 7.5 is incomplete, as it has been edited to make thedeclared inputs clearer.
#version450 core
layout (location = 0) in vec3 i_position;
layout (location = 1) in vec2 i_uv;
void main(void)
{
gl_Position = vec4(i_position, 1.0f);
}
Listing 7.5: Declaring Inputs to a Vertex Shader (SPIR-V)
; SPIR-V
; Version: 1.0
; Generator: Khronos Glslang Reference Front End; 1
; Bound: 30
; Schema: 0
OpCapability Shader
%1 = OpExtInstImport "GLSL.std.450"
OpMemoryModel Logical GLSL450
OpEntryPoint Vertex %4 "main" %13 %18 %29
OpSource GLSL 450
OpName %18 "i_position" ;; Name of i_position
OpName %29 "i_uv" ;; Name of i_uv
OpDecorate %18 Location 0 ;; Location of i_position
OpDecorate %29 Location 1 ;; Location of i_uv
...
%6 = OpTypeFloat 32 ;; %6 is 32-bit floating-point type
%16 = OpTypeVector %6 3 ;; %16 is a vector of 3 32-bit floats
(vec3)
%17 = OpTypePointer Input %16
%18 = OpVariable %17 Input ;; %18 is i _position - input pointer to vec3
%27 = OpTypeVector %6 2 ;; %27 is a vector of 2 32-bit floats
%28 = OpTypePointer Input %27
%29 = OpVariable %28 Input ;; %29 is i _uv - input pointer to vec2
...
也可以
7.4.3 输入组装
7.4.4 细分状态
7.4.5 视口状态
7.4.6 栅格化状态
7.4.7 多采样状态
7.4.8 深度和stencil测试状态
7.4.9 颜色混合状态