本部分结果可参考 12_Shader_Modules

与早期图形 API 不同的是,Vulkan 的 Shader 必须是一个字节文件(bytecode format),而非像 GLSL 或 HLSL 这样可读性高的文本。Vulkan 需要的字节文件类型被称为 SPIR-V

使用字节文件的好处在于,GPU 厂商所制作的将 Shader 转换为 native code 的编译器会简单很多。原先对于 GLSL 这样的文本,每一家 GPU 厂商对于标准的理解可能存在偏差,因此如果在某家 GPU 上能成功运行的某些冷门的 Syntex,很可能在另一家上就无法运行或存在错误效果。

虽然 Vulkan 要求 SPIR-V ,但这不意味着开发者需要手写这些字节文件。Khronos 提供了与厂商无关的编译器的标准,可以将 GLSL 编译为 SPIR-V。
这里会使用谷歌推出的 glslc.exe 进行翻译

#Vertex Shader

如在 OpenGL 中定义的 顶点数据 一样,Vulkan 的顶点数据同样在 NDC 空间中,如下所示。只不过 Vulkan 和 OpenGL 对于 NDC 空间 Y 轴定义是相反的(OpenGL 将底部 Y 值定义为 1-1,而 Vulkan 定义为 11):

Vulkan NDC Space

为了绘制一个三角形,定义如下的 Vertex Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#version 450

#extension GL_ARB_separate_shader_objects: enable
#extension GL_KHR_vulkan_glsl: enable

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];
}

为了暂时保持教程的简洁性,这里直接将顶点数据写在 Vertex Shader 中,避免了传输顶点数据这些 CPU-> GPU 操作的定义。

#Fragment Shader

定义的 Fragment Shader 如下:

1
2
3
4
5
6
7
8
9
10
11
#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, 1.0);
}

#编译 Shaders

因为 glslc 已经包含在了 Vulkan 的 SDK 中,因此可以通过类似如下的语句,直接将 shader 编译为 SPIR-V

1
2
C:\VulkanSDK\1.3.204.1\Bin\glslc.exe shader.vert -o vert.spv
C:\VulkanSDK\1.3.204.1\Bin\glslc.exe shader.frag -o frag.spv

这样就能将之前章节中的 glsl shader 文件编译为 .spv 后缀的 SPIR-V 字节码。

本教程中可以使用脚本 CompileShaders 将一个目录下的所有 shader 文件编译为 SPIR-V 字节码。使用方法如下:

1
2
. .\CompileShader.ps1
Compile-Shaders <FolderPath>

对于文件夹内的文件,名字将保留并且后缀改为 .spv,且加上 Vert/Frag。例如 shader.vert 将被编译为 shaderVert.spv

#读取 Shaders

定义类 ShadersMgr 管理 Shader 的创建和销毁。为了方便起见,Shader 的创建和销毁都在 createGraphicsPipeline 中进行。

1
2
3
4
5
6
7
8
9
class ShadersMgr
{
public:
static VkShaderModule createShaderModule(const std::string& fileName);
static void destroyShaderModule(VkShaderModule shaderModule);

private:
static std::vector<char> readFile(const std::string& fileName);
};

为了读取之前编译好的 SPIR-V 字节码,首先定义帮助函数 ShadersMgr::readFile

1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<char> ShadersMgr::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 " + fileName);

const size_t fileSize = file.tellg();
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
}

readFile 中,打开文件时使用了 flag atebinary,后者是因为读取的 SPIR-V Shader 是二进制文件,因此设定为 binary 可以减少文本转换。
前者是表明从文件的最后打开,这样的话,在文件一打开时就能知道文件的总长度。因此可以直接通过 tellg 获取长度。

#创建 VK Shader Modules

在将获取到的 Shader 传递给渲染管线之前,比如要将它封装在 VkShaderModule 中,该类用以封装和管理已编译好的着色器代码,为此创造函数 ShadersMgr::createShaderModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VkShaderModule ShadersMgr::createShaderModule(const std::string& fileName)
{
auto code = readFile(fileName);

VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

VkShaderModule shaderModule;
if (vkCreateShaderModule(LogicDevicesMgr::device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS)
throw std::runtime_error("Failed to create shader module!");

return shaderModule;
}

类似的,需要创建 destroyShaderModule 函数来销毁 Shader Module:

1
2
3
4
void ShadersMgr::destroyShaderModule(VkShaderModule shaderModule)
{
vkDestroyShaderModule(LogicDevicesMgr::device, shaderModule, nullptr);
}

#Shader Stage 创建

为了真正的使用创建出来的 Shader Module,需要将其指定给管线的特定阶段(如顶点阶段/着色阶段)。指定的过程,可以通过 VkPipelineShaderStageCreateInfo 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void GraphicsPipelineMgr::createGraphicsPipeline(const std::string& vertFileName, const std::string& fragFileName)
{
VkShaderModule vertShaderModule = ShadersMgr::createShaderModule(vertFileName);
VkShaderModule fragShaderModule = ShadersMgr::createShaderModule(fragFileName);

VkPipelineShaderStageCreateInfo vertShaderStageCreateInfo = {};
vertShaderStageCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageCreateInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageCreateInfo.module = vertShaderModule;
vertShaderStageCreateInfo.pName = "main";

VkPipelineShaderStageCreateInfo fragShaderStageCreateInfo = {};
fragShaderStageCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageCreateInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageCreateInfo.module = fragShaderModule;
fragShaderStageCreateInfo.pName = "main";

VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageCreateInfo, fragShaderStageCreateInfo};

ShadersMgr::destroyShaderModule(vertShaderModule);
ShadersMgr::destroyShaderModule(fragShaderModule);
}

这里修改了 createGraphicsPipeline 函数的签名,增加了两个参数 vertFileNamefragFileName,用来传递编译好的 .spv 文件路径。

此时在 initVulkan 中调用 createGraphicsPipeline 时,传入编译好的 Shader 文件路径用以创建 Graphics Pipeline:

1
2
3
4
5
6
7
void HelloTriangleApplication::initVulkan()
{
// ...

GraphicsPipelineMgr::createGraphicsPipeline("Shaders/TriangleVert.spv",
"Shaders/TriangleFrag.spv");
}

其中 stage 用来指定这个 StageCreateInfo 属于那个阶段。pName 用来指定 Shader 的入口,即 Shader 函数中的入口函数,如上面代码中的 main

pName 的存在意味着可以在一个 Shader 中定义不同的入口还是表示不同的 Shader。比如可以在一个 Shader 中定义 mainmain2,然后在不同的 Stage 中使用不同的入口函数。

在上述代码的最后,将两个创建出来的 Stage 存在在一起构成 shaderStages,该变量在进一步创建 GraphicsPipeline 时会需要用到。