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.shaderFirstTriangle.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() {
    /*待后续填充*/
}

然后将三角形每个像素的颜色简单粗暴地指定为你喜欢的颜色,我使用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_LISTVK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIPVK_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;
}

运行程序,你看到的图像应当如下:

_images/ch2-3-1.png

下一节请看Ch7-1 初识顶点缓冲区