Link

在 GPU 上运行代码

我们现在已经实现了渲染循环,所以下一步是绘制一些东西。

与其现在绘制几何体,不如使用计算着色器将数据写入图像,然后将该图像显示到屏幕上。使用计算着色器计算图像是一种非常常见的用例,您可以在引擎中复杂的后处理链中看到。当程序员要进行光线追踪或其他非几何图形绘制时,这也非常常见。

VkPipeline

在 Vulkan 中,要在 GPU 上执行代码,我们需要设置一个管线。管线有两种类型:图形管线和计算管线。计算管线要简单得多,因为它们只需要着色器代码的数据,以及用于数据绑定的描述符的布局。另一方面,图形管线必须为 GPU 中所有的固定功能硬件配置大量的状态,例如颜色混合、深度测试或几何格式。我们将在下一章中使用它们。

两种类型的管线共享着色器模块和布局,它们的构建方式相同。

VkShaderModule

VkShaderModule 是一个已处理的着色器文件。我们从预编译的 SpirV 文件创建它。在 Vulkan 中,与 OpenGL 不同,驱动程序不直接接受 GLSL 中的着色器代码。有一些扩展允许这样做,但这不是标准做法。因此,我们需要提前将 GLSL 文件编译成已编译的 SpirV 文件。作为 Vulkan SDK 的一部分,我们有 glslangValidator 程序,它用于将 GLSL 编译成 SpirV。如果您查看 vkguide 代码库上主项目的 CMakeLists.txt,您可以看到它从 /shaders/ 文件夹中获取所有着色器文件,并将它们作为自定义命令添加到编译目标中的部分。

每当您想编译本教程中的着色器时,您需要构建 Shaders 目标。这将编译您的所有着色器文件。由于 CMake 的限制,当您添加新的着色器文件时,您需要重新运行 CMake 配置步骤,以便 CMake 可以选择它们。在其他引擎上,常见的做法是拥有一个可执行文件或 .bat 或其他类似的脚本,以便在引擎打开时自动编译着色器。

也可以使用 HLSL 而不是 GLSL 来编写 Vulkan 着色器。许多项目都喜欢这样做,但我们不会在本教程中这样做。如果您想了解如何操作,可以查看 Vulkan 官方网站上的 HLSL In Vulkan

描述符集

为了给着色器提供数据,我们需要设置一些绑定。在 Vulkan 中,将图像和缓冲区等对象绑定到着色器需要描述符集。可以将单个描述符视为指向资源的句柄或指针。该资源可以是缓冲区或图像,并且还包含其他信息,例如缓冲区的大小,或者如果是图像,则包含采样器类型。VkDescriptorSet 是这些绑定在一起的指针的集合。Vulkan 不允许您在着色器中绑定单个资源。描述符集是从 VkDescriptorPool 分配的,使用 VkDescriptorLayout,其中包含有关该描述符集包含的内容的信息(例如,2 个图像)。分配描述符集后,您可以使用 vkUpdateDescriptorSets 更新其数据,该函数接受 VkWriteDescriptorSet 数组。一旦您拥有完全配置的描述符集,就可以使用 VkBindDescriptorSets 将其绑定到您的管线,并且其数据将在着色器中可用。

在本章中,我们将把一个绘制图像连接到计算着色器,以便着色器可以写入其中。在本教程中,我们将编写一些抽象来简化此流程。一个管线可以有多个槽位来绑定一些描述符集。Vulkan 规范保证我们将至少有 4 个集合,这将是我们在本教程中要实现的目标。根据 GPU 供应商的说法,每个描述符集槽位都有成本,因此我们拥有的越少越好。在本教程的后面,我们将使用描述符集 #0 来始终绑定一些全局场景数据,其中将包含一些 uniform 缓冲区和一些特殊纹理。描述符集 #1 将用于每个对象的数据。

推送常量

除了用于将着色器连接到缓冲区和图像的描述符集之外,Vulkan 还提供了一个选项,可以在记录命令缓冲区时直接写入少量字节的数据。推送常量是 Vulkan 独有的机制,它保留了少量内存,以便直接绑定到着色器。这种方法速度很快,但空间也非常有限,必须在编码命令时写入。它的主要用途是您可以为其提供一些每个对象的数据,如果您的对象不需要太多数据,但根据 GPU 供应商的说法,它们最佳的用例是将一些索引发送到着色器,以便用于访问一些更大的数据缓冲区。

管线布局

着色器有一些它需要的输入,这就是 VkPipelineLayout 的用途。要创建一个,我们必须为其提供它需要的描述符槽位的 VkDescriptorSetLayout,以及定义其推送常量用法的 PushConstantRange。图形和计算管线的 PipelineLayouts 以相同的方式创建,并且必须在管线本身之前创建。

计算管线

要构建计算管线,我们首先需要为其创建管线布局,然后为其代码挂钩单个着色器模块。构建完成后,我们可以通过首先调用 VkCmdBindPipeline,然后调用 VkCmdDispatch 来执行计算着色器。

计算着色器具有特定的编程模型。当我们调用 vkCmdDispatch 时,我们以 X * Y * Z 方式给 Vulkan 提供要在 3 个维度中启动的工作组数量。在我们的示例中,我们将使用它来绘制图像,因此我们仅使用其中 2 个维度,这样我们就可以为图像中的每组像素执行一个工作组。

在着色器本身内部,我们可以看到 layout (local_size_x = 16, local_size_y = 16) in; 通过这样做,我们正在设置单个工作组的大小。这意味着对于来自 vkCmdDispatch 的每个工作单元,我们将有 16x16 个执行通道,这非常适合写入 16x16 像素的正方形。

在着色器代码中,我们可以通过 gl_LocalInvocationID 变量访问通道索引。还有 gl_GlobalInvocationID 和 gl_WorkGroupID。通过使用这些变量,我们可以找出我们从每个通道写入的确切像素。

现在让我们在代码中设置所有这些,并使我们的第一个着色器工作起来。

下一步: Vulkan 着色器 - 代码