Vulkan 使用的着色器代码格式是一种叫做 SPIR-V 的字节码,这一字节码格式可以在 Vulkan 和 OpenCL 上使用。可以用它来编写图形和计算着色器,在这里,我们将它用于编写图形管线的着色器。GPU 厂商的编译器将字节码转换为原生代码的工作复杂度远远低于直接编译较高级的类 C 代码。过去的经验告诉我们使用类 C 代码,比如GLSL 作为着色器代码,会因为不同 GPU 厂商对代码的不同解释而造成大量问题,并且类 C 代码的编译器实现要比字节码编译器复杂的多,GPU厂商实现的编译器也极有可能存在错误,不同 GPU 厂商的实现也差异巨大。而使用字节码格式,上述的这些问题可以在极大程度上减少。虽然,Vulkan 使用字节码格式作为着色器代码,但这并不意味着我们要直接书写字节码来编写着色器。Khronos 发布了一个独立于厂商的可以将 GLSL 代码转换为 SPIR-V 字节码的编译器。这个编译器可以验证我们的着色器代码是否完全符合标准,将 GLSL 代码转换为 SPIR-V 字节码。我们可以在应用程序运行时调用这个编译器,动态生成 SPIR-V 字节码,但在本教程,我们没有这样做。这一编译器已经被包含在了 LunarG的 Vulkan SDK 中,编译器可执行文件名称为 glslangValidator.exe。
GLSL 是一个类 C 的着色器语言。使用 GLSL 编写的程序包含了一个 main 函数,这一函数完成具体的运算操作。GLSL 使用全局变量进行输入输出,它包含了许多用于图形编程的特性,比如向量和矩阵支持,用于计算叉积的函数,用于矩阵与向量相乘的函数,用于计算反射向量的函数等等。GLSL 中的向量类型叫做 vec,后跟一个表示向量元素数的数字。比如,用于表示一个三维空间位置的向量的类型为 vec3。GLSL 允许我们访问向量的分量比如.x,也允许我们使用表达式来创建新的向量值,比如vec3(1.0, 2.0, 3.0).xy 会返回一个 vec2 类型的值。向量构造器也可以被组合使用,比如可以使用 vec3(vec2(1.0, 2.0), 3.0) 生成一个 vec3 类型的值。
顶点着色器对输入的每个顶点进行处理。它可以接收顶点属性作为输入,比如世界坐标,颜色,法线和纹理坐标。它的输出包括顶点最终的裁剪坐标和需要传递给片段着色器的顶点属性,比如颜色和纹理坐标。这些
值会被插值处理后传给顶点着色器。裁剪坐标是一个来自顶点着色器的四维向量,它的四个成分会被除以第四个成分来完成规范化。规范化后的坐标被映射到帧缓冲的坐标空间,如下图所示:
我们可以直接将顶点着色器输出的裁剪坐标的第四个成分设置为 1,然后作为规范设备坐标。这样裁剪坐标到规范设备坐标就不会对坐标进行任何变换。通常,顶点坐标被存储在一个顶点缓冲中,但对于 Vulkan 来说,创建顶点缓冲,然后填入数据要进行很多操作为了尽快让我们的三角形显示在屏幕上,我们暂时先直接将顶点坐标写入顶点着色器,就像这样:
#version 450
#extension GL_ARB_separate_shader_objects : enable
out gl_PerVertex{
vec4 gl_Position;
};
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];
}
着色器的 main 函数对于每个顶点执行一次。GLSL 内建的 gl_VertexIndex变量包含了当前顶点的索引。这一索引通常来说是用来引用顶点缓冲中的顶点数据,但在这里,我们用它来引用我们在着色器中硬编码的顶点数据。我们输出的裁剪坐标由代码中的 positions 数组给出了前两个成分,剩余两个成分被我们设置为了 0.0 和 1.0。为了让着色器代码可以在 Vulkan 下工作,我们需要使用 GL_ARB_separate_shader_objects 扩展。
我们的三角形由来自顶点着色器的三个顶点作为三角形的顶点构成,这一三角形范围内的屏幕像素会被使用片段着色器处理后的片段进行填充。
将顶点的颜色传递给片段着色器,由片段着色器将颜色值输出到帧缓冲上。在顶点着色器中添加颜色输出变量,并在 main 函数中写入颜色值到颜色输出变量:
#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);
}
一组对应的输入和输出变量可以使用不同的变量名称,编译器可以通过定义它们时使用的 location 将它们对应起来。片段着色器的 main 函数现在被我们修改为输出输入的颜色变量作为片段颜色。三角形除了顶点之
外的片段颜色会被插值处理。
新建.bat,添加如下内容保存,双击运行,就可以完成着色器代码的编译
D:/VulkanSDK/1.1.77.0/Bin32/glslangValidator.exe -V shader.vert
D:/VulkanSDK/1.1.77.0/Bin32/glslangValidator.exe -V shader.frag
pause
运行脚本后,看到两个新的文件 vert.spv 和 frag.spv。这两个文件的文件名由编译器自动推导出来,读者可以使用自己喜欢的名称重命名这两个文件。编译脚本的执行过程中可能出现一些缺少特性的警告,读者可以放心地忽略掉这些警告。
如果着色器代码存在语法错误,编译器会报告语法错误所在的行,以及错误出现的原因。
编译器还支持将SPIR-V 格式的字节码反向编译为便于人类阅读的代码格式。