Vulkan Tutorial 教程翻译(五) 绘制三角形 2.3 图形管线基础

图形管线基础

简介

后面的章节,我们将会安装配置一个用于渲染第一个三角形的图形管线。图形管线是一系列有序操作的集合,它获取你提供的网格的顶点和纹理,将其转换为渲染目标上的像素点。下图是一个简单的过程展示

输入装配阶段 会从你指定的缓冲区收集原始的顶点数据,也许也会使用索引缓冲,以此减少重复顶点的数据量。

顶点着色器 它是逐顶点运行的,通常在顶点着色器中完成顶点位置的转换,将模型坐标转为屏幕坐标。也会将每个顶点的数据传给着色器的下一个阶段。

细分着色器允许你基于确定的规则去细分集合体,从而让网格获得更高的质量。例如其通常用于将砖块台阶这些物体看起来不是那么平坦。

几何着色器被运行在每一个图元上(三角形,线段,点),它可以丢弃这些图元,或者生成更多的图元。这与细分着色器很像,但是更加灵活。但是,大多数应用中并没有用到它,因为它在除Intel之外的显卡上,性能表现不佳。

光栅化阶段,会将之前的图元拍散到片段中.片段是指将会被写入帧缓冲区的像素,任何超出屏幕显示范围的片段都会被丢弃掉,前面顶点着色器的输入,会在片段着色器中被插值。通常,如果片段着色器被其他的片原片段所覆盖,也会因为深度测试被丢弃掉。

片段着色器会被每一个之前步骤存活下来的片段所调用,以决定要像帧缓冲区中写入什么样的颜色值和深度值。它也可以使用从顶点着色器传来经过插值的数据,通常是纹理坐标、光照法线等。

颜色混合会针对映射到相同像素点的片段,执行混合操作,最简单的是覆盖掉原有值,或者增加原始值,抑或是进行透明度混合。

绿色标识出的阶段,被称为固定功能阶段,这些阶段允许你使用参数调整他们的操作,但是这些阶段是预先已经定义好的,不可修改。

另外的黄色标识的阶段是可编程的,这意味着你可以上传自己的代码给显卡,以完成任何你想要的操作。例如,可以使用片段着色器,实现从纹理到照明到光线追踪。。。,这些程序并行运行在许多GPU核上。

如果你之前用过诸如OpenGL或者Direct3D等其他图形API,你可以使用如 glBlendFunc 或者 OMSetBlendState 去修改图形管线的设置.Vulkan中的图形管线几乎是完全不可变的,所以如果你想去修改着色器、绑定不同的帧缓冲、修改混合模式,必须重建图形管线.缺点是你不得不创建大量的管线以代表不同的渲染操作,然而由于所有的管线都提前知晓了,驱动层就可以做更多定制性的优化了。

基于你的需求,有些可编程的阶段是可选的。例如,如果你只想绘制简单的几何图形,细分着色器和几何着色器可以被禁用。如果你仅对深度值感兴趣,可以禁用片段着色,这对于阴影贴图的生成很有用。

在下一章,我们会创建两个对于显示三角形所必须的可编程阶段: 顶点着色器 和 片段着色器。固定功能的设置,例如混合模式,视口变换,光栅化等都会在那之后予以讨论。最后一部分是关于帧缓冲的输入与输出规范。

创建一个 createGraphicsPipeline() 函数,在createImageViews 之后调用。我们整个一章都会在这个函数里进行。

C++ 复制代码
void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createGraphicsPipeline();
}

...

void createGraphicsPipeline() {

}

下一章会讨论,能真正在屏幕上绘制出内容的着色器模块。

着色器模块

GLSL 与 HLSL都是以文本形式提供着色器代码的,与这些API不同,Vulkan的着色器代码必须以二进制字节码的格式给出。这种格式被称作 SPIR-V,它被设计用于Vulkan 和 OpenCL ,这是一个可以被用于写计算和图像shader的格式,我们在此教程中会聚焦于着色器在图形管线中的应用。

使用比特码的好处是,对GPU生产商来说,实现由着色器代码到本地代码的编译器不会太过复杂。过去的经验表明,人类可读的着色器代码,例如GLSL,对其语义的解释标准是相当灵活的。如果你为一个GPU驱动写了一个着色器程序,在其他GPU上运行,可能会报出一个语法上的错误,或者更糟,由于编译器的bug,你的着色器运行效果是不一样的。直接使用 SPIR-V 格式的二进制码,很可能会避免这些问题。

然而,这并不意味着我们需要手写二进制码。Khronos已经为我们提供了一个独立于厂商的编译器,可以将glsl语言转换为SPIR-V的二进制格式。这个编译器会验证你的shader代码是否符合标准并会生成一个SPIR-V格式的二进制文件。也可以将这个编译器用作一个依赖库,用于在运行时动态生成二进制码,但在此教程中,我们并不会这么做。尽管我们可以直接使用 glslangValidator.exe ,但是这里还是使用谷歌提供的 glslc.exe 来代替。glslc的优势是,可以使用与gcc、Clang这些编译器相同的参数,并且它也包含了一些额外的功能,例如include, 这两个文件都是包含在 Vulkan的SDK中的,我们不必再去额外下载它们了。

GLSL是一个与C语言类似的着色器语言。程序以main函数为入口,与其他程序使用参数作为输入,返回值作为输出不同,着色器程序使用全局变量去处理输入和输出。该语言包含许多特性是为图形程序设计的,例如内建的向量类型 vec*,矩阵类型 mat*,也提供了诸如,向量的叉乘,矩阵向量乘法,求一个向量的反射向量这些操作。向量类型以vec开头,跟上向量的维度,如vec2,vec3,vec4。举个例子,一个3D的位置数据可被存储为vec3,也可以通过.x .y .z 来访问单个成员,也可以从多个成员中创建出一个新的向量,例如,vec3(1.0, 2.0, 3.0).xy 会创建出一个 vec2 类型, 向量构造器也可以结合向量对象和标量值,例如一个vec3类型可以这样构造 vec3(vec2(1.0f , 2.0f) , 3.0f)

前面提到,我们需要实现一个顶点着色器和片段着色器来在屏幕上绘制出一个三角形,后面两章将会介绍着色器代码,在此之后,会展示怎样生成SPIR-V字节码,并将它们导入到程序中。

顶点着色器

顶点着色器运行在每一个输入的顶点上,顶点会携带属性进入顶点着色器,例如模型的空间坐标位置、颜色、法线、纹理坐标等,输出是最终在裁剪坐标系下的位置,以及需要传递给片段着色器的属性,例如颜色和纹理坐标。这些值会在光栅化阶段被插值,以实现平滑的效果。

裁剪坐标系是一个有顶点着色器生成的四维向量,随后将四维向量全都除以最后一个向量,转为归一化的设备坐标系,这些设备归一坐标是一个将帧缓冲映射到 x:[-1,1] y:[-1,1]的坐标系,如图

如果你之前涉猎过计算机图形学,应该对此很熟悉。如果之前使用过 OpenGL,应该注意到这里Y轴的方向被翻转了,Z轴的坐标使用了与DirectX3D 相同的范围 [0 , 1.0]

我们的第一个三角形程序并不会应用任何的变换,只需要直接指定三个顶点作为屏幕坐标,创建出如下的图形

通过在顶点着色器中,设置裁剪坐标系的最后一项为1的方式,可以直接输出屏幕归一坐标系,这种方式在裁剪坐标系到屏幕归一化坐标系的转换中不会修改任何的值。

通常这些坐标数据会被存储进一个顶点缓冲区,可是在Vulkan中创建顶点缓冲区,并为其填充数据,并不容易。因此我们决定推迟这些内容的介绍,转而采用一种非正统的方式,将数据直接内置在顶点着色器中,代码如下

glsl 复制代码
#version 450

vec2 position[3] = vec2[]{
    vec2(0.0f, -0.5f),
    vec2(0.5f, 0.5f),
    vec2(-0.5f, 0.5f)
};

void main(){
    gl_Position = vec4(position[gl_VertexIndex], 0.0f, 1.0f);
}

主函数会被每一个顶点调用,它内建了 gl_VertexIndex 变量标识当前顶点的索引,通常会把这个顶点应用于顶点缓冲区,可是在我们的例子里,我们把这个索引值用在硬编码的一个顶点数组数据中。每一个顶点的位置都是从这个常量数组中取得的,再结合上假的z 和 w 分量,去生成出一个裁剪坐标系下的位置数据,内建的 gl_Position 是最终的输出。

片段着色器

三角形是由顶点着色器提供的位置,填充片段构成的屏幕上的一块区域,片段着色器会在这些片段上被调用,去给帧缓冲区提供颜色和深度。一个简单的输出红色三角形的片段着色器如下

glsl 复制代码
#version 450

layout(location = 0) out vec4 outColor;

void main(){
    outColor = vec4(1.0f , 0.0f , 0.0f, 1.0f);
}

像顶点着色器一样,main函数被每一个片段所调用。在片段着色器中,颜色用一个4个分量的向量来表示,分别代表R、G、B 和 Aplha 通道,取值范围是 0 ~ 1, 与顶点着色使用 gl_Position 内建变量作为输出不同,片段着色器并没有这样的内建变量作为颜色的输出。你必须为每一个帧缓冲指定自己的输出变量,这行 layout(location = 0) 就代表了帧缓冲区的索引。红色被写入到连接了第一个也是唯一一个帧缓冲区的 outColor 变量中.

每个顶点设置颜色

一个纯红色的三角形看起来并不有趣,下面这个更漂亮的三角形如何实现呢?

我们需要对着色器做一些修改来完成这个效果。首先需要为三个顶点中的每一个都设置一个不一样的颜色,顶点着色器现在可以包含一个颜色数组,与位置数组类似。

glsl 复制代码
vec3 colors[3] = vec3[]{
    vec3(1.0f , 0.0f , 0.0f),
    vec3(0.0f , 1.0f , 0.0f),
    vec3(0.0f , 0.0f , 1.0f)
};

现在我们可以把每个顶点的颜色传输给片段着色器,它可以将颜色插值后输出给帧缓冲。顶点着色器中增加一个输出,main函数中写入。

glsl 复制代码
layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

下一步,需要在片段着色器中添加一个匹配的输入。

glsl 复制代码
layout(location = 0) in vec3 fragColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

输入变量没有必要与顶点着色器中的输出取一样的名字,它们是通过索引(location指定)关联在一起的,main函数已经被修改成了输出的颜色加上一个透明度的值。如上面图片所示,fragColor的值,是三个顶点自动插值完成的,这样就实现了平滑的渐变效果。

编译着色器

在你的工程的根目录下创建一个 shaders 目录,存储顶点着色器的代码到文件 shader.vert ,再创建一个包含片段着色器的代码文件 shader.frag , GLSL并没有一个官方的扩展名,通常用vert、frag这两个扩展名来区分它们。

shader.vert的内容

glsl 复制代码
#version 450

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

shader.frag 文件内容

GLSL 复制代码
#version 450

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

现在可以用 glslc 将它们编译成 SPIR-V格式的字节码

Windows

创建一个批处理命令 compile.bat 内容如下

bat 复制代码
C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv
C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv
pause

用你自己的 glslc.exe 路径替换批处理命令中的,双击运行。

Linux

创建一个 shell文件 compile.sh,内容如下

sh 复制代码
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv

用你自己的 glslc 路径替换命令行中的,修改compile.sh的执行权限 chmod +x compile.sh ,然后运行它。

平台特定指令结束

这两行命令告诉编译器读取 GLSL 源码,使用-o 输出一个SPIR-V的二进制字节码.

如果你的着色器代码包含语法错误,编译器会告诉你哪一行报错,试着删除一个分号再运行.再试试去掉命令行的参数去运行编译器,去看看编译器支持哪些特性,也可以从SPIR-V格式输出一个人类可读的格式,以便于你去查看这个阶段,编译器做了哪些优化。

在命令行中去编译着色器的代码是最常用的一种使用方式,也是我们在此教程中会使用的,不过也支持从你的代码中直接编译着色器的代码,有关的Vulkan SDK包含在 libshaderc 中, 它是一个库,可以帮助你在程序中将GLSL代码编译成 SPIR-V 字节码。

载入着色器程序

现在我们已经可以生成SPIR-V格式的着色器了,是时候将它载入到我们的程序中,并在渲染管线的一些阶段使用着色器了,首先实现一个简单的载入二进制数据的辅助函数。

C++ 复制代码
#include <fstream>
...

static std::vector<char> readFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
        throw std::runtime_error("failed to open file!");
    }
}

readFile 函数会读取指定文件的所有字节,并作为 std::vector 来返回.我们以两个标志位来打开指定文件

  • ate : 从文件末尾开始读取
  • binary : 将文件作为二进制读取(避免文本字符的转换)

在文件末尾读取,我们就可以知道文件的大小,从而分配出足够的缓存。

C++ 复制代码
size_t fileSize = (size_t) file.tellg();
std::vector<char> buffer(fileSize);

之后我们便可以返回文件的开始处,一次读取出所有的字节

C++ 复制代码
file.seekg(0);
file.read(buffer.data(), fileSize);

最后,关闭文件返回字节数据.

C++ 复制代码
file.close();

return buffer;

现在,在createGraphicsPipeline中调用这个函数,去载入两个字节码。

C++ 复制代码
void createGraphicsPipeline() {
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");
}

确保着色器已经被正确的载入了,打印出缓冲的大小与文件实际的大小做对比,注意,这里读取的内容不需要设置结束符,因为它是二进制码,我们会显式地说明它的大小。

创建着色器模块

在我们把这些代码传递给管线之前,必须将它们包裹进一个 VkShaderModule 的对象中.我们创建一个辅助函数 createShaderModule 来做这些

C++ 复制代码
VkShaderModule createShaderModule(std::vector<char> &code){

}

函数接收一个字节码的缓存作为参数,利用它创建出 VkShaderModule.

创建着色器模块很简单,只需要指定一个指向字节码数据的指针,以及它的长度。这些信息在 VkShaderModuleCreateInfo 结构体中设置,一个需要注意的点是比特码的大小是以字节大小为单位的,但是参数接收的数据是32位的 uint32_t 指针类型,而不是读取时的 char 指针类型,因此我们需要将指针通过 reinterpret_cast 进行转换,当做出这样转化时,需要保证数据满足 uin32_t 类型的对齐要求,幸运的是,存储在 std::vector 容器中的数据,其分配时就已经确保了数据满足这样的要求。

C++ 复制代码
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t *>(code.data());

调用 vkCreateShaderModule 去创建 VkShaderModule

C++ 复制代码
VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
    throw std::runtime_error("failed to create shader module!");
}

参数与之前创建Vulkan对象的函数参数是一样的,逻辑设备,创建信息,可选的分配回调,以及创建出的返回对象指针,代码的缓存可以在创建完对象后被立即释放。不要忘记返回创建的对象

C++ 复制代码
return shaderModule;

着色器模块仅仅是对字节码的一个简单包装,将SPIR-V字节码编译链接成CPU可运行的二进制文件,在图形管线创建之前并不会完成。这也意味着当我们创建完图形管线后,就可以立刻销毁着色器模块了,这也解释了为什么我们使用的是本地变量作为着色器模块,而不是类成员变量。

C++ 复制代码
void createGraphicsPipleline(){
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");

    VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
    VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

清除操作在函数末尾通过两个 vkDestroyShaderModule 调用来完成.所有本章后面添加的代码,都在这两个函数调用之前

C++ 复制代码
 ...
    vkDestroyShaderModule(device, fragShaderModule, nullptr);
    vkDestroyShaderModule(device, vertShaderModule, nullptr);
}

着色阶段的创建

为了实际使用这些着色器,我们需要将使用 VkPipelineShaderStageCreateInfo 结构体将着色器关联到管线阶段,它是创建实际渲染管线操作的一部分。

我们在createGraphicsPipeline函数中填写这个结构体,将它作为顶点着色器

C++ 复制代码
VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStafeInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;

第一步,告诉Vulkan,将要使用的是着色器的哪一个阶段,前面介绍的每一个可编程阶段,都有一个枚举类型表示。

C++ 复制代码
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";

后面的两个参数,指定了着色器模块应该包含的代码,以及被调用的函数,即理解的函数入口。这意味着可以将一个顶点着色器与多个片段着色器绑定到一起,并且使用不同的入口点来区分着色器的行为。本例中,我们使用标准的 main 函数。

还有一个可选成员 pSpecicalizationInfo , 虽然我们这里并不使用它,但是它是值得被讨论一下的,它允许你去指定着色器常量值。你可以使用单个的着色器模块,它的行为可以在管线创建时,通过不同的常量值来指定。这比起在渲染时配置着色器,具有更高的效率,因为编译器可以做,诸如消除与此变量关联的 if 语句 这样的优化操作。如果并没有这样的常量,可以设置这个成员时 nullptr,也是我们在初始化这个结构体时自动设置的值。

修改结构体的填写来实现片段着色器的创建

C++ 复制代码
VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";

最后,创建包含这两个结构体的数组,我们会在之后实际创建管线的阶段,引用这个数组。

C++ 复制代码
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderInfo};

这就是对管线可编程阶段的所有介绍了。下一章,我们会介绍管线的固定功能阶段。

固定功能

早先的图形API对大多数的图形管线状态都设置了默认值,但在Vulkan中,你必须显式地声明出大多数地图形管线状态,因为它们会被当成不可变的管线对象.本章,我们会填写所有的固定功能的结构体以完成管线的配置。

动态状态

尽管大多数的管线状态都需要被写死,仍然有一些状态可以在渲染时被改变而不用重建渲染管线,例如视口的大小,线段宽度和混合的颜色。如果你想使用这些动态状态,将这些属性排除在固定功能外,需要填写 VkPipelineDynamicStateCreateInfo 结构体.

C++ 复制代码
std::vector<VkDynamicState> dynamicStates = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_SCISSOR
};

VkDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();

这将会让这些值被忽略,你可以(也必须)在绘制时,指定这些值。这种方式更加的灵活,相比于将一些诸如裁剪,视口的状态写死进渲染管线中。

顶点输入

VkPipelineVertexInputStateCreateInfo 结构体描述了将要传送给顶点着色器的顶点数据的格式,大体用两种方法描述这些:

  • 绑定(Bindings) : 数据之间的间隔,是否数据是逐顶点的还是逐实例的。
  • 属性描述 : 传递给顶点着色器的属性类型,从哪个绑定点偏移多少去加载。

因为我们已经直接在顶点着色器中做了数据的硬编码,所以现在这个结构体并不需要顶点数据,会在后面的章节再来设置它

C++ 复制代码
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

成员 pVertexBindingDescriptions 和 pVertexAttributeDescriptions ,都是一个指向结构体数组的类型,用来在加载顶点数据时,描述数据的细节。在 createGraphicsPipeline 函数中,将这个结构体初始化的代码加在 shaderStage 逻辑之后。

输入装配

VkPipelineInputAssemblyStateCreateInfo 结构体描述了两个东西,1.需要从顶点绘制哪一种几何体 2.是否需要打开图元重启功能. 前者通过 topology 成员来指定,它有如下这些取值:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST : 通过顶点绘制点精灵
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST : 每两个顶点绘制一条线段,没有重用的顶点
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP : 仍是绘制直线,但是每一条线段结束的顶点用于下一条线段开始的顶点
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : 每三个顶点绘制一个三角形,没有顶点重用
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : 每一个三角形的第二第三个顶点作为后一个三角形的前两个顶点

通常,顶点按索引的顺序从顶点缓冲区被载入,但是你也可以使用元素缓冲区,加载自己的索引值.这可以实现针对顶点重用的优化,如果这时,你设置了 primitiveRestartEnable 成员为 VK_TRUE, 在以 _STRIP 为拓扑结构的绘制中,就可以断开连续绘制的效果,通常这里把索引设置为 0xFFFF 或者 0xFFFFFFFF.

本教程中,我们绘制的是三角形,所以结构体需要做如下设置

C++ 复制代码
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;

视口与裁剪

视口描述了帧缓冲区渲染输出的范围,它几乎总是从 (0, 0) 到 (宽,高),教程里我们这样设置

C++ 复制代码
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<uint32_t>(swapChainExtent.width);
viewport.height = static_cast<uint32_t>(swapChainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;

注意,这里使用的是交换链的宽高,它的大小可能是与窗口的宽高是不一样的。交换链的图像后面会作为帧缓冲区使用,所以这里我们可以写死这个尺寸。

minDepth 和 maxDepth 指定了使用帧缓冲区的深度值范围,这些值必须被设定在 [0.0f, 1.0f]之间,但是最小值可能会大于最大值。如果你不需要做任何特殊设置,可以将这个值固定在标准的 0.0 到 1.0 。

视口定义了图像到帧缓冲区的转换,裁剪矩形定义了哪些像素点会被存储下来。任何在裁剪矩形之外的像素点都会被光栅器丢弃掉。这个功能相比于转换器更像是一个过滤器.下图展示了它们的不同。注意左侧的裁剪矩形只是众多可能中的一个,因为它是大于视口的。

所以如果我们想要绘制整个帧缓冲区,需要指定裁剪矩形覆盖到整个帧缓冲区.

C++ 复制代码
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;

视口与裁剪,既可以在创建管线的时候静态设置,也可以在命令缓冲中作为动态状态被设置。动态设置有更好的灵活性,这是很常见的,所有驱动都可以处理这种动态状态而没有性能上的损失。

当选择动态的视口与裁剪区域时, 需要显式的打开管线的状态设置。

C++ 复制代码
std::vector<VkDynamicState> dynamicStates = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_SCISSOR
};

VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();

然后在管线创建的时候指定数量

C++ 复制代码
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.scissorCount = 1;

真正的视口及裁剪数据会在渲染时,被装载进来。

甚至,动态状态能为同一个命令缓冲区指定不同的视口和裁剪矩形。

没有动态状态,视口和裁剪矩形需要在 VkPipelineViewportStateCreateInfo 结构体中指定,这会让视口和裁剪矩形在此管线下变成不可变的。如果要修改,只能用新的值创建出新的管线。

C++ 复制代码
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;

无论如何设置它们,在一些显卡上可能会设置多个视口和裁剪矩形,所以引用对象是一个数组格式。如果要使用多个,需要查询GPU硬件是否支持此特性。

光栅器

光栅器获取从顶点着色器提供的几何形状,并将其转换成需要被片段着色器着色的片段。它也会实现深度测试,面剔除和裁剪测试,它也可以配置输出的片段是填充几何体还是仅绘制边沿(线框绘制模式),所有的配置都是用 VkPipelineRasterizationStateCreateInfo 结构体描述

C++ 复制代码
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;

如果 depthClampEnable 被设置成VK_TRUE,对于那些超出近平面和远平面范围的片段,会进行截断而不是舍弃。这对于阴影贴图的场景是很有用的。使用此特性需要检查GPU是否支持。

C++ 复制代码
rasterizer.rasterizerDiscardEnable = VK_FALSE;

如果 rasterizerDiscardEnable 设置为 VK_TRUE,集合体永远不会通过光栅化阶段,这基本不会给帧缓冲输出任何内容。

C++ 复制代码
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

polygonMode 会决定有多少片段被用于生成几何体。有以下几个取值。

  • VK_POLYGON_MODE_FILL : 用片段填充多边形区域
  • VK_POLYGON_MODE_LINE : 多边形边沿被绘制
  • VK_POLYGON_MODE_POINT : 多边形的顶点以点的形式绘制

使用任何除了FILL之外的模式,都需要GPU的支持.

C++ 复制代码
rasterizer.lineWidth = 1.0f;

lineWidth 的意义很明显,它用大量的片段描述线段的粗细,线宽的最大值依赖于硬件的支持,任何大于1.0的宽度,都需要先查询GPU是否支持 wideLines 属性。

C++ 复制代码
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

cullMode 设置哪一种类型的面需要被剔除,也可以禁用剔除,剔除前面或者双面都剔除。frontFace 表述顶点以何种顺序来表示前后,是顺时针还是逆时针。

C++ 复制代码
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional

光栅器可以修改深度值,加上一个常量值或者根据片段的斜率对其添加一个偏置。这有些时候会用在阴影贴图中,但我们现在并不使用它,仅设置 depthBiasEnable 为 VK_FALSE 。

多重采样

结构体 VkPipelineMultisampleStateCreateInfo 用于配置多重采样,这是一种实现抗锯齿的方案。它是通过将多边形光栅化到同一个像素的结果做融合来实现的。主要发生在多边形的边缘处,也是伪影最容易出现的地方。如果多边形只映射到一个像素,片段着色器就不需要运行多次,这相比于将画面渲染到一个较高的分辨率然后降低采样的方式,消耗更低。要使用这个特性需要查询GPU硬件是否支持。

C++ 复制代码
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

我们后面章节再介绍多重采样,现在先将其禁用。

深度与模板测试

如果你使用了深度或模板缓冲区,也需要使用 VkPipelineDepthStencilStateCreateInfo 来配置深度或 模板测试。我们现在不用,先全部传 nullptr , 后面会回来介绍深度缓冲区。

颜色混合

片段着色器最终生成的颜色,会与帧缓冲区里原来的颜色混合,这个转换便是颜色混合,有两种方式:

  • 混合老的值与新的值去生成一个最终值
  • 通过位操作联合老值和新值

有两种类型的结构体可用于颜色混合,VkPipelineColorBlendAttachmentState 可以配置与之关联的帧缓冲,VkPipelineColorBlendStateCreateInfo 是一个全局的颜色混合配置,这个例子中只有一个帧缓冲区。

C++ 复制代码
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

这个可以为逐帧缓冲设置混合模式的结构体,它实现的混合操作如以下伪代码所示

C++ 复制代码
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;

如果 blendEnable 被设置成VK_FALSE,从片段着色器传来的颜色不会做任何修改。否则,就会执行两次混合操作来计算出一个新的颜色,最终的颜色还会通过和 colorWriteMask 掩码的与操作,来决定会写入到哪一个颜色通道中。

最常用的是透明度混合,我们希望能用老的颜色和新的颜色按透明度进行混合。finalColor 最终按以下伪代码来计算出来

C++ 复制代码
finalColor.rgb = newAlpha * newColor + (1-newAlpha)* oldColor;
finalColor.a = newAlpha.a;

这可用以下的参数来实现

C++ 复制代码
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;

可以在 VkBlendFactor 和 VkBlendOp 枚举中找到所有可能的操作。

第二个针对所有帧缓冲区的设置方式,引用了一个结构体数组作为参数,可以设置混合常量作为前面提到的混合因子。

C++ 复制代码
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

如果你想使用第二种混合方式,可以将 logicOpEnable 设置为 VK_TRUE,位操作可以在 logicOp 字段进行设置,注意,这会禁用第一种方法。在这种模式中,colorWriteMask 也可以被设置,决定影响帧缓冲的哪些颜色通道。也可以将两种方式都禁用,正如我们现在做的这样,片段着色器的输出颜色会不经修改地写入帧缓冲区。

管线布局

可以在着色器中使用同一变量 uniform , 它是一种类似于全局状态的变量,可以在渲染一帧时,动态改变着色器的行为,而不需要重建着色器。通常用来位顶点着色器传递变换矩阵,或者为片段着色器创建纹理采样器。

这些 uniform 值需要在管线创建的时候,通过 VkPipelineLayout 对象来指定。尽管我们目前并没有用到它,还是需要创建一个空管线布局。

创建一个类成员去存储这个对象,我们会在后面的其他函数中引用它。

C++ 复制代码
VkPipelineLayout pipelineLayout;

在 createGraphicsPipeline 函数中创建这个对象:

C++ 复制代码
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!");
}

这个结构体也会指定推送常量,它是为着色器传递动态值的另外一种方式。管线布局对象会在应用的整个生命周期被引用,所以需要在程序结束时被销毁。

C++ 复制代码
void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    ...
}

总结

这便是管线的所有固定功能了,我们做了大量的繁琐工作,好处是我们现在几乎完全认识到了图形管线的工作内容,这减少了很多异常的行为,因为默认状态可能并不是你所期望的。

在最终创建图形管线时,还有一件事,那便是,渲染通道(render pass)

渲染通道

安装

在最终创建渲染管线之前,需要告诉Vulkan,用于渲染的帧缓冲区的附件信息。需要指定有多少个颜色和深度缓冲区,它们需要被采样多少次以及整个渲染过程中如何处理它们的内容。所有这些信息都被包裹进了一个名为渲染通道的对象中。我们在 createGraphicsPipeline函数前创建一个 createRenderPass 函数.

C++ 复制代码
void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
}

...

void createRenderPass() {

}

附件描述

在本程序中,我们使用单个的颜色缓冲附件来代表交换链中的图像。

C++ 复制代码
void createRenderPass(){
    VkAttachmentDescription colorAttachment{};
    colorAttachment.format = swapChainImageFormat;
    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
}

颜色附件的格式需要与交换链的图像格式相匹配,我们不会多多重采样,所以这里设置为1次采样.

C++ 复制代码
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_LOAD_OP_STORE;

loadOp 和 storeOp 决定了附件中的数据在渲染前和渲染后会做什么,针对 loadOp 有以下的选择:

  • VK_ATTACHMENT_LOAD_OP_LOAD : 保存附件中已经存在的内容。
  • VK_ATTACHMENT_LOAD_OP_CLEAR : 在渲染开始前清理设置为一个常量
  • VK_ATTACHMENT_LOAD_OP_DONT_CARE : 已经存在的内容未被定义,但我们并不在乎。

在我们的程序中,我们准备在绘制一帧图像前,将帧缓冲的颜色清理为黑色。 针对 storeOp 有两个选择 :

  • VK_ATTACHMENT_STORE_OP_STORE : 渲染的内容可以被存入内存,可以在之后被读取出来。
  • VK_ATTACHMENT_STORE_OP_DONT_CARE : 帧缓冲的内容在渲染操作后是未定义的。

我们需要在屏幕上看到渲染出的三角形,所以这里选择保存操作。

C++ 复制代码
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

loadOp 和 StoreOp 应用于颜色和深度数据上,stencilLoadOp 和 stencilStoreOp 应用与模板数据上.我们并没有使用模板附件,所以这里设置为 DONT_CARE。

C++ 复制代码
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

Vulkan中的纹理和帧缓冲都是通过一个特定像素格式的 VkImage 来表示的,然而像素在内存中的布局,可能会因为你对图像的操作而改变。

一些常用的布局:

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : 被用作颜色附件的布局。
  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : 图像被用于呈现交换链的呈现。
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL : 图像被用来作为内存拷贝操作的接收对象。

我们会在后续的纹理章节中更加深入的讨论,现在最重要的是要知道,图像需要被转化为后续操作可以支持的布局。

initialLayout 指定了在渲染通道开始前图像的布局,finalLayout 指定了渲染通道结束后图像自动转化到的布局。使用 VK_IMAGE_LAYOUT_UNDEFINED 设置 initialLayout 表示我们并不关心渲染通道执行之前图像是什么样的,这个值表示图像内容并不会被保留,因为我们后面会清理掉它。我们想让图像为之后的显示做好准备,所以这里使用了 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR .

子通道和附件引用

单个的渲染通道可以由多个子通道组成。子通道是一系列依赖帧缓冲内容的渲染操作,例如,一个滤镜的效果可以被其他的子通道所使用。如果你组织这些操作到一个渲染过程中,Vulkan就可以重排这些操作,以优化内存带宽,获得更高的性能。对于我们的第一个三角形例子,我们仅使用单个子通道。

每一个子通道会引用一个以上的附件,VkAttachmentReference 描述引用的结构体如下

C++ 复制代码
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

attachment 参数指定了引用附件在附件描述数组中的索引值。我们的数组仅有一个 VkAttachmentDescription 对象,所以索引是0。 layout 指定了使用这个附件引用时,希望使用哪个布局,Vulkan会在这个子通道开始时,自动转化为指定的布局。我们准备将这个附件作为颜色缓冲区使用,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 正如其名字暗示的那样,会给我们提供最优的性能。

子通道使用结构体 VkSubpassDescription 来描述

C++ 复制代码
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

Vulkan未来可能还会支持计算子过程,所以现在必须显式地申明其为图形子过程,下一步,我们指定颜色附件.

C++ 复制代码
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;

这个数组中的附件索引,直接引用于片段着色器中的 layout(location = 0) out vec4 outColor

还有其他种类的附件可以被子通道所引用:

  • pInputAttachments : 附件从着色器中读取.
  • pResolveAttachments : 附件用于多重颜色采样.
  • pDepthStencilAttachment : 附件来自于深度和模板数据.
  • pPreserveAttachments : 附件并不被这个子通道所使用,而是一些需要被保存的数据.

渲染通道 (render pass)

现在,附件和基本的子通道引用都被介绍过了,我们可以创建渲染通道了.在pipelineLayout前面新建一个新的成员变量去存储 VkRenderPass .

C++ 复制代码
VkRenderPass renderPass;
VkPipelineLayout pipelineLayout;

渲染通道对象通过 VkRenderPassCreateInfo 结构体创建,需要提供附件与子通道数组对象。前面提到的 VkAttachmentReference 对象引用的附件索引,就是针对这些数组的。

C++ 复制代码
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rendnerPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;

if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
    throw std::runtime_error("failed to create render pass!");
}

与管线布局一样,渲染通道可以被整个程序引用,所以需要在程序结束时,将其清理掉.

C++ 复制代码
void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    vkDestroyRenderPass(device, renderPass, nullptr);
    ...
}

下一节,所有介绍的对象会汇聚到一起,创建出图形管线对象。

结论

我们现在可以结合前面章节介绍的所有结构体与对象创建出图形管线了!这是我们现在拥有的对象,先快速回顾一下:

  • 着色器阶段 : 着色器模块定义了图形管线的可编程阶段
  • 固定功能模块 : 定义了图形管线的固定功能阶段,如输入装配,光栅化,视口和颜色混合。
  • 管线布局 : 着色器中引用的统一变量和推送常量可以在渲染一帧时被更新
  • 渲染通道 : 管线引用的附件及它们的用途

结合以上这些,我们现在可以在 createGraphicsPipeline 函数填写 VkCreateGraphicsPipelineCreateInfo 结构体,注意,创建需要放在 vkDestroyShaderModule 函数调用之前,因为在创建管线时,我们仍需要使用这些着色器模块。

C++ 复制代码
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;

现在开始设置 VkPipelineShaderStageCreateInfo 引用结构体。

C++ 复制代码
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr; // Optional
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = &dynamicState;

然后我们引用了所有在固定功能阶段描述的结构体。

C++ 复制代码
pipelineInfo.layout = pipelineLayout;

然后设置管线布局,这是一个句柄而不是之前那样的指针。

C++ 复制代码
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;

最后,设置渲染通道,并设置了这个图形管线会使用的子通道的索引。也可以在渲染管线中使用其他的渲染通道,可是,其他的通道必须与现在这个是兼容的。我们现在并不需要使用这个特性。

C++ 复制代码
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
pipelineInfo.basePipelineIndex = -1; // Optional

还有两个额外的参数 basePipelineHandle 和 basePipelineIndex ,Vulkan 允许从已经存在的管线中创建出一个新的图形管线,当有很多功能是一样的时候,派生出的管线消耗资源更少,有相同父管线之间的切换速度也会更快。既可以通过 basePipelineHandle 指定已经存在的管线的句柄,也可以通过 basePipelineIndex 引用索引创建的管线。目前只有单管线,所以我们简单的设置为空,索引给一个负值。这些值仅在 flag 标志位 VK_PIPELINE_CREATE_DERIVATIVE_BIT 被设置时起作用。

最后创建类成员接收 VkPipeline 对象.

C++ 复制代码
VkPipeline graphicsPipeline;

创建出真正的图形管线

C++ 复制代码
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
    throw std::runtime_error("failed to create graphics pipeline!");
}

vkCreateGraphicsPipelines 函数相比其他的Vulkan对象创建函数,有着更多的参数,它被设计可以接收多个 VkGraphicsPipelineCreateInfo 以在一次调用中创建出多个管线对象。

第二个参数,我们现在传的是 VK_NULL_HANDLE ,它是一个可选的 VkPipelineCache 对象,管线缓存对象可以在多次 创建调用时用来存储和重用有关数据,如果将数据存储成文件,甚至可以跨程序运行。这可被用于改善管线创建的速度。

正常的绘制操作都需要图形管线,所以只有在程序结束时,才能销毁它。

C++ 复制代码
void cleanup() {
    vkDestroyPipeline(device, graphicsPipeline, nullptr);
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    ...
}

现在运行你的程序,确保成功得到了渲染管线。我们已经很接近在屏幕上显示出东西了,在后面的章节,我们会安装真实的来自于交换链的帧缓冲,并准备绘制命令。

相关推荐
林太白2 分钟前
Rust认识安装
前端·后端·rust
掘金酱3 分钟前
🔥 稀土掘金 x Trae 夏日寻宝之旅火热进行ing:做任务赢大疆pocket3、Apple watch等丰富大礼
前端·后端·trae
1024小神3 分钟前
tauri项目添加多文件下载功能,并支持下载进度回调显示在前端页面上
前端·javascript
Ace_31750887764 分钟前
义乌购拍立淘API接入指南
前端
不想说话的麋鹿10 分钟前
《NestJS 实战:RBAC 系统管理模块开发 (四)》:用户绑定
前端·后端·全栈
我是谁谁23 分钟前
JavaScript 中的 Map、WeakMap、Set 详解
前端
laperter34 分钟前
vue3项目第三篇
前端
呆呆的心36 分钟前
深入剖析 JavaScript 数据类型与 Symbol 类型的独特魅力😃
前端·javascript·面试
嘉小华38 分钟前
Kotlin委托机制详解
前端
有仙则茗40 分钟前
process.cwd()和__dirname有什么区别
前端·javascript·node.js