这一节创建pipeline.
在SceneWidget.h添加bool CreatePipeline()函数,和VkPipeline m_graphicsPipeline的成员变量,用于保存生成的VkPipeline的指针。
在SceneWidget.cpp中实现CreatePipeline()这个函数,并在SceneWidget::Init()函数中调用CreatePipeline(),和在SceneWidget::UnInit()函数里调用vkDestroyPipeline(m_device, m_graphicsPipeline, nullptr);以清理资源。
在bool CreatePipeline()中,还是先写创建函数:VkResult result = vkCreateGraphicsPipelines()其参数:
第一个参数:m_device,之前创建好的逻辑设备。
第二个参数:要传入一个VkPipelineCache,这个东东,可以保存一些之前生成好的pipeline的信息,方便下次快速创建pipeline。但是在这个demo里,不需要。所以这里传入nullptr。
第三个参数:是传pipeline创建信息的个数。这里的值是1就行。
第四个参数:传入的是VkGraphicsPipelineCreateInfo创建信息的数组。需要在函数上面补一个VkGraphicsPipelineCreateInfo createInfo = {};的变量,然后补齐成员。
第五个参数:VkAllocationCallbacks,没有用到,所以传入nullptr。
第六个参数:是生成后的VkPipeline指针。传入m_graphicsPipeline的引用。
判断函数的返回值VkResult是不是VK_SUCCESS,如果是就说明创建成功了,否则为创建失败。
在vkCreateGraphicsPipelines函数的上面补充VkGraphicsPipelineCreateInfo createInfo = {}的结构,其数据成员赋值:
createInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;固定的
createInfo.pNext = nullptr;不需要扩展。
createInfo.flags = 0;先给默认值0。
createInfo.stageCount;这个值是下一个参数的个数,暂时空时。
createInfo.pStages;是VkPipelineShaderStageCreateInfo结构的数组。现在没有这个数据结构,需要在上面补充这个数据结构。
下面的几个参数一直到pDynamicState,都要补充相应的数据结构了。
createInfo.pVertexInputState;
createInfo.pInputAssemblyState;
createInfo.pTessellationState;
createInfo.pViewportState;
createInfo.pRasterizationState;
createInfo.pMultisampleState;
createInfo.pDepthStencilState;
createInfo.pColorBlendState;
createInfo.pDynamicState;
createInfo.layout;这个应该是创建出来的对象。
createInfo.renderPass = m_renderPass;之前创建好的。
createInfo.subpass = 0;是renderPass中使用的subpass的索引。之前就创建了一个subpass,所以这里为0。
createInfo.basePipelineHandle;以是现有的pipeline创建的pipeline,这里不需要,所以为nullptr。
createInfo.basePipelineIndex;同样这个索引指定用这个索引代表的pipeline来生成新的pipeline,这里不需要,但值不能为0了,所以先赋值为-1。
最后的这两个参数,在教程说明,当flags为:VK_PIPELINE_CREATE_DERIVATIVE_BIT时,才有作用,那么即使这里不赋值,保持默认也应该没有问题的。
补结构,声明VkPipelineShaderStageCreateInfo 结构。这里至少应该有两个,一个vertexShader 和一个fragmentShader。
VkPipelineShaderStageCreateInfo vertexShader = {}
type在这里的值是固定的,值如下:
vertexShader.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
不需要扩展,这里为nullptr
vertexShader.pNext = nullptr;
vertexShader.flags = 0;
看stage的定义,是一个检举,表示shader的类型,所以这里是VK_SHADER_STAGE_VERTEX_BIT
vertexShader.stage = VK_SHADER_STAGE_VERTEX_BIT;
VkShaderModule着色器的模块对象,和VkImage一样。所以module是由vk的create函数生成出来的。在前面先补一个函数,生成module。
vertexShader.module;
着色器的入口函数,glsl是以main函数为入口的。
vertexShader.pName = ”main“;
教程中的说明,是着色器常量,可以为同一段着色器代码,给常量设置不同的值。可以减少一些起条件分支,提高效率。这里不需要。
vertexShader.pSpecializationInfo = nullptr;
补充module的生成。
在SceneWidget.h中添加一个函数:VkShaderModule CreateShaderModul(const std::string& strShader); 传入一段shader的代码,生成module.
在SceneWidget.cpp中实现这个函数:
还是先写创建函数:
要返回的对象,如下:
VkShaderModule module = VK_NULL_HANDLE;
调用vkCreateShaderModule函数,其调用如下:
VkResult result = vkCreateShaderModule(m_device, &info,nullptr, &module);其中的参数:
第一个参数是之前创建好的逻辑设备m_device。
第二个参数是它对应的创建信息,VkShaderModuleCreateInfo info = {};传入的是地址。
第三个参数:是个记录了申请内存的地址的回调,这里用不到,所以是nullptr。
第四个参数,是生成对象。
需要判断result是否为VK_NULL_HANDLE。
所以这个函数上方补VkShaderModuleCreateInfo info = {}结构。
info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
info.pNext = nullptr;
info.flags = 0;
这是shader的字符串的字节数。
info.codeSize;
这个是shader的字符串的指针。
info.pCode;
在opengl里,可以写两个常量字符串,将shader的内容,以字符串的形式写出来就行。但是在vulkan里,使用的却是.spv的文件,是一个二进制的内容文件。所以在这里还是要写个函数来读取.spv的内容。
在SceneWidget.h中,声明一个静态函数用来读取spv文件,返回std::string:
std::string ReadSpvFile(const std::string& filePath);然后在cpp文件中实现它。需要在将fstream包含进文件中。
使用c++ 的文件流程,并以二进制的形式打开文件。std::ifstream file(filePath, std::ios::ate | std::ios::binary);刚打开的文件,文件指针在文件的开始位置。通过file.tellg()函数,跳到文件尾,计算跳过了多少字节,即为文件的大小。用这个大小,初始化一个字符串:size_t fileSize = (size_t)file.tellg();
std::string str;
str.resize(fileSize);
再将文件指针,跳到文件开始的位置,通过file.read函数,将文件内容都读取到str字符串中。
关闭文件。返回str。
在可执行文件目录下,创建两个文件,一个叫vertex.vert顶点shader;一个叫frag.frag,片段shader。
vertex.vert的内容:
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
}
最简单的顶点着色器,只传入顶点和颜色。
frag.frag的内容:
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor.xyz, 1.0);
}
只输出片段的颜色。
有了这两个文件后,还需要通过vulkan自带的工具将glsl的文件转换成.spv:
同样转换另一个frag.frag。转换完后,得到两个.spv的文件。
回到CreatePipeline函数,记录刚生成的两个文件的路径。
调用CreateShaderModule函数,并分别将上面的两个路径传进去。生成module
分别填充vertexShader.module = vertexModule;和fragmentShader.module = fragModule;
填充CreateShaderModule函数中的:
info.codeSize = vertexCode.size();
info.pCode = reinterpret_cast<const uint32_t*>( vertexCode.data()) ;需要强转一下,将char*类型转成uint32_t类型。
CreateShaderModule函数代码如下:
回到CreatePipeline函数,两个shader的module已经有了,把它们放到一个数组里:
std::vector< VkPipelineShaderStageCreateInfo > shaderCreateInfos ;然后将vertexShader和fragmentShader都push进去。
shaderCreateInfos.push_back(vertexShader);
shaderCreateInfos.push_back(fragmentShader);
所以:
createInfo.stageCount = shaderCreateInfos.size();
createInfo.pStages = shaderCreateInfos.data();
继续补充下面的几个:
createInfo.pVertexInputState看定义,这是个VkPipelineVertexInputStateCreateInfo结构。
VkPipelineVertexInputStateCreateInfo vertexInputStateCreateInfo = {}
设置结构体类型
vertexInputStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
扩展设置为nullptr.
vertexInputStateCreateInfo.pNext = nullptr;
标志设置为0
vertexInputStateCreateInfo.flags = 0;
这个值是下一个参数数组中的数目。所以要补数据结构:VkVertexInputBindingDescription inputBindingDescription = {}。从这个数据结构,可以看出是用来设置数据在shader中布局,即设置shader中的binding、location、format以及偏移offset的值。
现在需要定义一下需要传给vulkan的数据,绘制最简单的三角形,那么需要的顶点属性有位置和颜色。
在头文件中定义数据结构如下:
其中Vector3、Vector4表示三维向量和四维向量。Vertex为要使用的顶点数据结构,其中中包含顶点和颜色。
再inputBindingDescription这个对象进行赋值
binding的值一般为0就行。
inputBindingDescription.binding = 0;
数据在内存中占用的步长,也就是一共占用的字节数。
inputBindingDescription.stride = sizeof(Vertex);
数据类型,还可能是VK_VERTEX_INPUT_RATE_INSTANCE,在这里是vertex类型。
inputBindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
所以:
vertexInputStateCreateInfo.vertexBindingDescriptionCount = 1;
vertexInputStateCreateInfo.pVertexBindingDescriptions = &inputBindingDescription;
还需要补结构:
std::vector<VkVertexInputAttributeDescription> attributeDescriptions;由于只有顶点和颜色,所以这里的vector里存的是两个,一个顶点一个颜色。VkVertexInputAttributeDescription结构是在描述所有的顶点数据在内存中的排列情况。
具体赋值如下:
需要将内存重置为两个对象的空间。第一个存放位置数据,第二个存放颜色数据。
attributeDescriptions.resize(2);
binding还是0
attributeDescriptions[0].binding = 0;
location的值首次从0开始。
attributeDescriptions[0].location = 0;
数据格式是xyz的,且为float类型。
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
这个是数据缓存中相对于缓存起始位置的偏移量。从置数据开始,这里为0
attributeDescriptions[0].offset = 0;
attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
rgba的格式。
attributeDescriptions[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
偏移是跨过顶点数据。attributeDescriptions[1].offset = sizeof(Vector3);
所以:
vertexInputStateCreateInfo.vertexAttributeDescriptionCount = attributeDescriptions.size();
vertexInputStateCreateInfo.pVertexAttributeDescriptions = attributeDescriptions.data();
且:
createInfo.pVertexInputState = &vertexInputStateCreateInfo;
createInfo.pInputAssemblyState的值是一个VkPipelineInputAssemblyStateCreateInfo的数据结构,再添加:VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCreateInfo = {}。
这个结构体的内容指定了绘制的方式,即:点、线、折线、环线、三角形、三角形链、三角形扇。在vulkan里没有绘制四边形的,这一点与opengl是有区别的。现在对这个结构的属性赋值:
inputAssemblyStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssemblyStateCreateInfo.pNext = nullptr;
inputAssemblyStateCreateInfo.flags = 0;
这里指定绘制的图元是三角形。
inputAssemblyStateCreateInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
这个标志是在索引绘制时,可以通过一个特殊的索引值达到图元重启的目的。重新指定绘制的第一个索引。这里是直接绘制顶点,所以设置为false。
inputAssemblyStateCreateInfo.primitiveRestartEnable = VK_FALSE;
然后赋值给createInfo.pInputAssemblyState。
createInfo.pTessellationState这个属性是输入控制点的信息。在这里,不需要。所以设置为nullpltr;
再看下一个属性,createInfo.pViewportState,对应的是:VkPipelineViewportStateCreateInfo定义一个对象,viewportStateCreateInfo = {}并赋值:
viewportStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportStateCreateInfo.pNext = nullptr;
viewportStateCreateInfo.flags = 0;
viewPort的个数,这里先设置为1。
viewportStateCreateInfo.viewportCount = 1;
pViewports的值,需要一个VkViewport的数据结构,定义为viewPort = {};其x、y的值都从0开始,即x = 0,y = 0,width与height的值,与m_capabilities.currentExtent的width和height值一样。即:
viewPort.x = 0;
viewPort.y = 0;
viewPort.width = m_capabilities.currentExtent.width;
viewPort.height = m_capabilities.currentExtent.height;
viewPort.minDepth = 0.0f;
viewPort.maxDepth = 1.0f;
所以:
viewportStateCreateInfo.pViewports = &viewPort;
同样的下面这个参数,也设置一个。
viewportStateCreateInfo.scissorCount = 1;
添加一个结构:VkRect2D rect2d = {};
rect2d.extent = m_capabilities.currentExtent;
rect2d.offset = { 0, 0 };
所以:
viewportStateCreateInfo.pScissors = &rect2d;
createInfo.pViewportState = &viewportStateCreateInfo;
下一参数:
createInfo.pRasterizationState;栅格化,对应声明一个变量:
VkPipelineRasterizationStateCreateInfo rasterizationStateCreateInfo = {}
rasterizationStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizationStateCreateInfo.pNext = nullptr;
rasterizationStateCreateInfo.flags = 0;
如果是VK_TRUE会将超出远平面和近平面之外的片段截断在远近平面上。这一特性主要应用在阴影贴图中。所以这里设置为false。
rasterizationStateCreateInfo.depthClampEnable = VK_FALSE;
如果为true,则表示,所有的图元都将丢弃。所以这里设置为false。
rasterizationStateCreateInfo.rasterizerDiscardEnable = VK_FALSE;
填充模式,有点、线和面三种模式。这里选面模式。
rasterizationStateCreateInfo.polygonMode = VK_POLYGON_MODE_FILL;
裁剪模式,有正向裁剪、反向裁剪和不裁剪。这里选不裁剪。
rasterizationStateCreateInfo.cullMode = VK_CULL_MODE_NONE;
设置正方向,顺时针或逆时针,这里选逆时针。
rasterizationStateCreateInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
深度贴图会用到这个变量。所以这里设置false
rasterizationStateCreateInfo.depthBiasEnable = VK_FALSE ;
由于这个特性没有用到,所以下面的三个参数,都可以设置为0.0f.
rasterizationStateCreateInfo.depthBiasConstantFactor = 0.0f;
rasterizationStateCreateInfo.depthBiasClamp = 0.0f;
rasterizationStateCreateInfo.depthBiasSlopeFactor = 0.0f;
最后一个线宽,设置为1.0f。
rasterizationStateCreateInfo.lineWidth = 1.0f;
同样的,createInfo.pRasterizationState = &rasterizationStateCreateInfo。
createInfo.pMultisampleState对应的补结构:
VkPipelineMultisampleStateCreateInfo multisampleStateCreateInfo = {};
multisampleStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
栅格化时的采样,之前已获过的。
multisampleStateCreateInfo.rasterizationSamples = m_msaa;
剩下的属性保持默认就行。
一般在有阴影时,会启用。
multisampleStateCreateInfo.sampleShadingEnable
设置启用采时的最小比例。
multisampleStateCreateInfo.minSampleShading;
掩码的值。
multisampleStateCreateInfo.pSampleMask;
指定,是否输出第一个颜色分量中的alpha值。
multisampleStateCreateInfo.alphaToCoverageEnable;
是否将第一个颜色的alpha的值替换为多重采样中的一个。
multisampleStateCreateInfo.alphaToOneEnable;
自己理解翻译,可能有误。
为createInfo.pMultisampleState赋值为&multisampleStateCreateInfo。
添加VkPipelineDepthStencilStateCreateInfo数据结构,定义depthStencilStateCreateInfo = {}
深度模板测试结构的类型。
depthStencilStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
默认
depthStencilStateCreateInfo.pNext = nullptr;
默认
depthStencilStateCreateInfo.flags = 0;
是否打开深度测试,这里需要打开。
depthStencilStateCreateInfo.depthTestEnable = VK_TRUE;
是否允许写入深度,这里需要写入。
depthStencilStateCreateInfo.depthWriteEnable = VK_TRUE;
深度测试中使用的比较运算。
depthStencilStateCreateInfo.depthCompareOp = VK_COMPARE_OP_LESS;
是否启用深度边界测试,这里不需要。
depthStencilStateCreateInfo.depthBoundsTestEnable = VK_FALSE;
是否启用模板测试,这里不需要。
depthStencilStateCreateInfo.stencilTestEnable = VK_FALSE;
下面两个参数,front和back是控制模板测试的参数,其数据结构为VkStencilOpState属性:
VkStencilOp failOp;
VkStencilOp passOp;
VkStencilOp depthFailOp;
以上参数是VkStencilOp值,是指当满足条件时的操作。
VkCompareOp 是对比操作。
VkCompareOp compareOp;
uint32_t compareMask;
uint32_t writeMask;
uint32_t reference;
depthStencilStateCreateInfo.front;
depthStencilStateCreateInfo.back;
minDepthBounds和maxDepthBounds表示在开启深度边界测试时使用的最小深度边界值和最大深度边界值。
depthStencilStateCreateInfo.minDepthBounds;
depthStencilStateCreateInfo.maxDepthBounds;
由于这些特性,在这里没有启用,所以保持默认即可。
createInfo.pDepthStencilState = &depthStencilStateCreateInfo;
补充数据结构VkPipelineColorBlendStateCreateInfo colorBlendStateCreateInfo = {};,全局颜色混合设置,混合时,使用位运算组合旧值和新值。还有一个结构是VkPipelineColorBlendAttachmentState对绑定的帧缓冲区进行单独的混合运算,混合旧值和新值生成最终的颜色。
colorBlendStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlendStateCreateInfo.pNext = nullptr;
colorBlendStateCreateInfo.flags = 0;
是否开启逻辑操作。
colorBlendStateCreateInfo.logicOpEnable = VK_FALSE;
要应用的逻辑操作。
colorBlendStateCreateInfo.logicOp = VK_LOGIC_OP_COPY;
设置混合因子。
for (int i = 0; i < 4; ++i)
{
colorBlendStateCreateInfo.blendConstants[i] = 0.0f;
}
这里需要一个VkPipelineColorBlendAttachmentState的结构,定义colorBlendAttachmentState = {}并为属性赋值
colorBlendStateCreateInfo.attachmentCount = 1;
colorBlendStateCreateInfo.pAttachments = &colorBlendAttachmentState;
对于colorBlendAttachmentState的属性赋值如下:
最简单的例子,不需要混合,所以关闭混合。
colorBlendAttachmentState.blendEnable = VK_FALSE;
混合位允许进行四个通过写入颜色。
colorBlendAttachmentState.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
关闭混合了,以下几个参数,可以保持默认。
colorBlendAttachmentState.srcColorBlendFactor;
colorBlendAttachmentState.dstColorBlendFactor;
colorBlendAttachmentState.colorBlendOp;
colorBlendAttachmentState.srcAlphaBlendFactor;
colorBlendAttachmentState.dstAlphaBlendFactor;
colorBlendAttachmentState.alphaBlendOp;
所以:
createInfo.pColorBlendState = &colorBlendStateCreateInfo;
这里也不需自动改变状态,所以也不需要对createInfo.pDynamicState赋值,保持默认即可。
createInfo.layout的值需要创建,通过vkCreatePipelineLayout函数来创建。由于是通过函数创建出来的,所以还需要在最后进行清理,所以要将这个值记录下来。
在SceneWidget.h中添加一个成员变量,VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE;再添加一个创建函数:bool CreatePipelineLayout();
在CreatePipelineLayout函数中,定义一个VkPipelineLayoutCreateInfo layoutCreateInfo = {};
各属性赋值如下:
layoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layoutCreateInfo.pNext = nullptr;
layoutCreateInfo.flags = 0;
layoutCreateInfo.setLayoutCount = 1;
使用之前创建好的。
layoutCreateInfo.pSetLayouts = &m_descriptorLayout;
最后两个参数,没有用到,不用设置。
layoutCreateInfo.pushConstantRangeCount;
layoutCreateInfo.pPushConstantRanges;
然后调用:
VkResult result = vkCreatePipelineLayout(m_device, &layoutCreateInfo, nullptr, &m_pipelineLayout);
当创建失败时返回false。否则返回true。
然后在CreatePipeline函数中调用。
这时可以对createInfo.layout赋值为:m_pipelineLayout。
同样在SceneWidget::UnInit函数中,在vkDestroyPipeline(m_device, m_graphicsPipeline, nullptr);后面清理m_pipelineLayout。
在创建完Pipeline之后,清掉Module,即在函数的最后调用:
调用和清理都有了,那么现在就运行一下,看看结果:
依然没有错误 :)
如果觉得可能是有什么错没有报出来。那么可以故意将代码中的某个值写错,看一下会不会报错。