Ch2-3 创建管线并绘制三角形
首先在main.cpp中定义下列变量和函数:
using namespace vulkan;//上一节中在main.cpp中全局范围内使用了命名空间 pipelineLayout pipelineLayout_triangle;//管线布局 pipeline pipeline_triangle;//管线 //该函数调用easyVulkan::CreateRpwf_Screen()并存储返回的引用到静态变量,避免重复调用easyVulkan::CreateRpwf_Screen() const auto& RenderPassAndFramebuffers() { static const auto& rpwf = easyVulkan::CreateRpwf_Screen(); return rpwf; } //该函数用于创建管线布局 void CreateLayout() { /*待后续填充*/ } //该函数用于创建管线 void CreatePipeline() { /*待后续填充*/ }
书写着色器
参见Ch4-1 着色器模组,我在该节中具体叙述何为着色器模组,并对它进行了简单封装,请先阅览该节并完成封装。
在项目目录下新建shader文件夹,在其中创建文件FirstTriangle.vert.shader和FirstTriangle.frag.shader。
书写一个最简单的顶点着色器
用Visual Studio或其他文本编辑器打开FirstTriangle.vert.shader,一上来首先在着色器的开头声明GLSL版本和着色器类型,并定义主函数。
#version 460 #pragma shader_stage(vertex) void main() { /*待后续填充*/ }
-
涉及到的语法:版本声明、接入点函数、预编译指令表示着色器类型
如何用最简单的方式绘制一个纯色三角形?只要画三角形,不需要考虑别的,那么通过写死的三个顶点坐标来绘制当然最省事。
#version 450 #pragma shader_stage(vertex) vec2 positions[3] = { { 0, -.5f }, { -.5f, .5f }, { .5f, .5f } }; void main() { /*待填充*/ }
已经写好了三个顶点的NDC坐标数据,接着只需按顶点的索引访问相应数据,然后输出到gl_Position了:
#version 450 #pragma shader_stage(vertex) vec2 positions[3] = { { 0, -.5f }, { -.5f, .5f }, { .5f, .5f } }; void main() { gl_Position = vec4(positions[gl_VertexIndex], 0, 1); }
-
关于gl_VertexIndex,参见顶点着色器的内置输入。
-
gl_Position是顶点着色器的内置输出,代表用于生成图元的顶点坐标,无需声明即可使用。
-
2D绘制中,z分量无所谓,w分量应为1。
于是一个最简单的顶点着色器就写好了,之后用vkCmdDraw(...)绘制时,指定绘制3个顶点以构建三角形,首个顶点索引为0,那么上述着色器便会依次生成在NDC中坐标为{ 0, -0.5, 0, 1 }
、{ -0.5, 0.5, 0, 1 }
、{ 0.5, 0.5, 0, 1 }
的三个顶点。
书写完着色器后,在shader文件夹下打开控制台(文件浏览器的地址栏中直接输入cmd并回车即可),用以下控制台指令将GLSL着色器编译到SPIR-V:
glslc.exe路径 FirstTriangle.vert.shader -c
-
生成的文件会叫FirstTriangle.vert.spv。
-
关于上述控制台语句的具体说明,参见从GLSL编译到SPIR-V。
-
可以跳过输入cmd的步骤,直接在地址栏中输入这串语句并回车即可(但这样一来控制台会在执行完语句后立刻消失,来不及看错误信息)。
书写一个最简单的片段着色器
打开FirstTriangle.frag.shader,还是声明语言版本、着色器类型和主函数:
#version 460 #pragma shader_stage(fragment) void main() { /*待后续填充*/ }
接着要定义输出,既然是一个最简单的片段着色器,那么只需要输出像素的颜色到唯一的颜色附件即可:
#version 460 #pragma shader_stage(fragment) layout(location = 0) out vec4 o_Color; void main() { /*待后续填充*/ }
-
涉及到的语法:图形着色器中通用的输入输出声明方式
-
输出到o_Color的是标准化的数值,通常的8位整数色值[0, 255]被线性映射到[0, 1]。
然后将三角形每个像素的颜色简单粗暴地指定为你喜欢的颜色,我使用vec4(0, 0.5, 1, 1)
,这是一半绿加全蓝不透明,看上去是一种蓝色。
如此一来,一个最简单的片段着色器也完成了:
#version 460 #pragma shader_stage(fragment) layout(location = 0) out vec4 o_Color; void main() { o_Color = vec4(0, 0.5, 1, 1); }
写完后编译,不做赘述。
创建着色器模组
填充CreatePipeline(),从先前写的顶点和片段着色器,以函数内静态变量的形式创建着色器模组,以及它们对应的管线阶段创建信息:
void CreatePipeline() { static shaderModule vert("shader/FirstTriangle.vert.spv"); static shaderModule frag("shader/FirstTriangle.frag.spv"); static VkPipelineShaderStageCreateInfo shaderStageCreateInfos_triangle[2] = { vert.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT), frag.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT) }; /*待后续填充*/ }
创建管线布局
参见Ch3-3 管线布局和管线,我在该节中具体叙述何为管线布局,并对它进行了简单封装,请先阅览该节并完成封装。
填充CreatePipelineLayout(),由于前述的着色器中没用到任何uniform缓冲区或push constant等可由CPU侧变更的常量,我们只需要创建一个没有任何描述符集,也没有任何push constant范围的管线布局即可:
void CreateLayout() { VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{}; pipelineLayout_triangle.Create(pipelineLayoutCreateInfo); }
创建管线
参见Ch3-3 管线布局和管线,我在该节中具体叙述何为管线,以及涉及到的所有管线状态,内容很长,请先完成封装并了解以下内容:
1.VkGraphicsPipelineCreateInfo有哪些成员
2.图元拓扑类型
3.视口和剪裁范围
4.采样点个数
5.VkPipelineColorBlendAttachmentState有哪些成员
你应当会创建一个新的头文件VKBase+.h,这个文件会用来写一些常用但又不那么基础的对象的封装。你可以在主函数里包含它,或者将包含在EasyVulkan.hpp中的VKBase.h替换为VKBase+.h。
继续填充CreatePipeline(),因为创建管线时,视口和剪裁范围与交换链图像大小相关,于是定义两个lambda,分别用来创建和销毁管线,以及用作创建和销毁交换链时的回调函数。
void CreatePipeline() { static shaderModule vert("shader/FirstTriangle.vert.spv"); static shaderModule frag("shader/FirstTriangle.frag.spv"); static VkPipelineShaderStageCreateInfo shaderStageCreateInfos_triangle[2] = { vert.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT), frag.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT) }; auto Create = [] { graphicsPipelineCreateInfoPack pipelineCiPack; /*待后续填充*/ pipeline_triangle.Create(pipelineCiPack); }; auto Destroy = [] { pipeline_triangle.~pipeline(); }; graphicsBase::Base().AddCallback_CreateSwapchain(Create); graphicsBase::Base().AddCallback_DestroySwapchain(Destroy); //调用Create()以创建管线 Create(); }
填写管线的创建信息,首先填写管线布局和渲染通道:
auto Create = [] { graphicsPipelineCreateInfoPack pipelineCiPack; pipelineCiPack.createInfo.layout = pipelineLayout_triangle; pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers().renderPass; //子通道只有一个,所以pipelineCiPack.createInfo.renderPass使用默认值0 /*待后续填充*/ pipeline_triangle.Create(pipelineCiPack); };
只绘制一个三角型,所以图元拓扑类型可为VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST或VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP或VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN:
pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
接着指定视口和剪裁范围,当然是填满屏幕:
pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(windowSize.width), float(windowSize.height), 0.f, 1.f); pipelineCiPack.scissors.emplace_back(VkOffset2D{}, windowSize);
不开多重采样,所以每个像素点采样一次:
pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
不开启混色,只指定RGBA四通道的写入遮罩为全部写入:
pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
由于之前的代码中,已经将管线阶段的创建信息定义为了函数内的静态变量,这里先通过UpdateAllArrays()更新所有涉及到的数组地址,然后手动指定可编程管线阶段的数量及其创建信息的地址:
pipelineCiPack.UpdateAllArrays();
pipelineCiPack.createInfo.stageCount = 2;
pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_triangle;
因为这一节的目标只是画出三角形,其余参数暂且不管,保留默认值。
至此,整个CreatePipeline()如下:
void CreatePipeline() { static shaderModule vert("shader/FirstTriangle.vert.spv"); static shaderModule frag("shader/FirstTriangle.frag.spv"); static VkPipelineShaderStageCreateInfo shaderStageCreateInfos_triangle[2] = { vert.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT), frag.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT) }; auto Create = [] { graphicsPipelineCreateInfoPack pipelineCiPack; pipelineCiPack.createInfo.layout = pipelineLayout_triangle; pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers().renderPass; pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(windowSize.width), float(windowSize.height), 0.f, 1.f); pipelineCiPack.scissors.emplace_back(VkOffset2D{}, windowSize); pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 }); pipelineCiPack.UpdateAllArrays(); pipelineCiPack.createInfo.stageCount = 2; pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_triangle; pipeline_triangle.Create(pipelineCiPack); }; auto Destroy = [] { pipeline_triangle.~pipeline(); }; graphicsBase::Base().AddCallback_CreateSwapchain(Create); graphicsBase::Base().AddCallback_DestroySwapchain(Destroy); Create(); }
绘制三角形
首先将主函数更新为:
int main() { if (!InitializeWindow({1280,720})) return -1; /*变更*/const auto& [renderPass, framebuffers] = RenderPassAndFramebuffers(); /*新增*/CreateLayout(); /*新增*/CreatePipeline(); fence fence; semaphore semaphore_imageIsAvailable; semaphore semaphore_renderingIsOver; commandBuffer commandBuffer; commandPool commandPool(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT); commandPool.AllocateBuffers(commandBuffer); VkClearValue clearColor = { .color = { 1.f, 0.f, 0.f, 1.f } };//红色 while (!glfwWindowShouldClose(pWindow)) { while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED)) glfwWaitEvents(); graphicsBase::Base().SwapImage(semaphore_imageIsAvailable); auto i = graphicsBase::Base().CurrentImageIndex(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); renderPass.CmdBegin(commandBuffer, framebuffers[i], { {}, windowSize }, clearColor); /*渲染命令,待填充*/ renderPass.CmdEnd(commandBuffer); commandBuffer.End(); graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence); graphicsBase::Base().PresentImage(semaphore_renderingIsOver); glfwPollEvents(); TitleFps(); fence.WaitAndReset(); } TerminateWindow(); return 0; }
绘制三角形需要两个命令,首先要在渲染通道中绑定管线,然后再使用绘制命令。
绑定管线
用vkCmdBindPipeline(...)绑定管线:
void VKAPI_CALL vkCmdBindPipeline(...) 的参数说明 |
|
---|---|
VkCommandBuffer commandBuffer |
命令缓冲区的handle |
VkPipelineBindPoint pipelineBindPoint |
指定管线的类型 |
VkPipeline pipeline |
要被绑定的管线的handle |
版本要求 |
VkPipelineBindPoint 的枚举项 |
---|---|
1.0 |
VK_PIPELINE_BIND_POINT_GRAPHICS 表示绑定图形管线 |
1.0 |
VK_PIPELINE_BIND_POINT_COMPUTE 表示绑定计算管线 |
用以下代码绑定先前创建的图形管线:
vkCmdBindPipeline(commandBuffer_graphics, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
绘制
vkCmdDraw(...)是最基本的绘制命令,它适用于直接绘制(而不是从缓冲区中读取绘制参数)且不需要索引缓冲区(index buffer)的情况。
void VKAPI_CALL vkCmdDraw(...) 的参数说明 |
|
---|---|
VkCommandBuffer commandBuffer |
命令缓冲区的handle |
uint32_t vertexCount |
要绘制的顶点个数 |
uint32_t instanceCount |
要绘制的图形实例个数 |
uint32_t firstVertex |
首个顶点的索引 |
uint32_t firstInstance |
首个图形实例的索引 |
用3个顶点绘制1个三角形实例,首个顶点和首个实例都是0(这里的执行结果与实例索引无关,也先不用管什么是图形实例)。
最终,主函数变为:
int main() { if (!InitializeWindow({1280,720})) return -1; const auto& [renderPass, framebuffers] = RenderPassAndFramebuffers(); CreateLayout(); CreatePipeline(); fence fence; semaphore semaphore_imageIsAvailable; semaphore semaphore_renderingIsOver; commandBuffer commandBuffer; commandPool commandPool(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT); commandPool.AllocateBuffers(commandBuffer); VkClearValue clearColor = { .color = { 1.f, 0.f, 0.f, 1.f } };//红色 while (!glfwWindowShouldClose(pWindow)) { while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED)) glfwWaitEvents(); graphicsBase::Base().SwapImage(semaphore_imageIsAvailable); auto i = graphicsBase::Base().CurrentImageIndex(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); renderPass.CmdBegin(commandBuffer, framebuffers[i], { {}, windowSize }, clearColor); /*新增*/vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle); /*新增*/vkCmdDraw(commandBuffer, 3, 1, 0, 0); renderPass.CmdEnd(commandBuffer); commandBuffer.End(); graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence); graphicsBase::Base().PresentImage(semaphore_renderingIsOver); glfwPollEvents(); TitleFps(); fence.WaitAndReset(); } TerminateWindow(); return 0; }
运行程序,你看到的图像应当如下:
下一节请看Ch7-1 初识顶点缓冲区。