Vulkan官方英文原文:Shader modules - Vulkan Tutorial
对应的Vulkan技术规格说明书版本: Vulkan 1.3.2
Unlike earlier APIs, shader code in Vulkan has to be specified in a bytecode format as opposed to human-readable syntax like GLSL and HLSL. This bytecode format is called SPIR-V and is designed to be used with both Vulkan and OpenCL (both Khronos APIs). It is a format that can be used to write graphics and compute shaders, but we will focus on shaders used in Vulkan's graphics pipelines in this tutorial.
不像早期的各种图形API,在Vulkan中的shader代码是以(二进制)字节码的格式被使用而不像GLSL和HLSL这样易读的语法。这种字节码格式被称作 SPIR-V,并且他被设计为Vulkan和OpenCL这两者一起使用。这种代码可以写图形和计算shader,但是在这个教程中我们关注的是用在Vulkan图形管线的shader。
The advantage of using a bytecode format is that the compilers written by GPU vendors to turn shader code into native code are significantly less complex. The past has shown that with human-readable syntax like GLSL, some GPU vendors were rather flexible with their interpretation of the standard. If you happen to write non-trivial shaders with a GPU from one of these vendors, then you'd risk other vendor's drivers rejecting your code due to syntax errors, or worse, your shader running differently because of compiler bugs. With a straightforward bytecode format like SPIR-V that will hopefully be avoided.
使用字节码的优势是GPU厂商编写的shader编译器将shader代码转换为原生代码的复杂度显著降低。过去的经验表明像GLSL这样可读性强的语法,一些GPU厂商对于标准的解释是相当地灵活。假如你碰巧在这些厂商的GPU上写了特别实现的shader代码,那么你会有由于其他厂商的驱动不适配你写的shader代码的风险,可能导致语法错误,或糟糕的是,由于编译器自身存在的问题,你的shader运行过程存在差异。用像SPIR-V这种直接了当的字节码格式,这些问题大概率会避免掉。
However, that does not mean that we need to write this bytecode by hand. Khronos has released their own vendor-independent compiler that compiles GLSL to SPIR-V. This compiler is designed to verify that your shader code is fully standards compliant and produces one SPIR-V binary that you can ship with your program. You can also include this compiler as a library to produce SPIR-V at runtime, but we won't be doing that in this tutorial. Although we can use this compiler directly via glslangValidator.exe, we will be using glslc.exe by Google instead. The advantage of glslc is that it uses the same parameter format as well-known compilers like GCC and Clang and includes some extra functionality like includes. Both of them are already included in the Vulkan SDK, so you don't need to download anything extra.
但是,这并不意味着我们需要去手写这些字节码。Khronos 已经发布了他们自己的厂商无关的编译器,能将GLSL文件编译为SPIR-V字节码文件。设计的这个编译器用来验证你的shader代码是否完全符合标准,并且能生成一个运行在你的程序中的SPIR-V格式的二进制文件。你也可以将这个编译器当做一个库包括在你的程序中以便在运行时生成 SPIR-V 文件,但是当前教程中不会这么用。尽管我们能通过 glslangValidator.exe 直接的使用编译器,但是我们将改用 Google 的 glslc.exe。glslc的优势是它使用类似于知名的编译器(像GCC和Clang)的命令行参数格式,而且glslc包含了像 includes 这些额外的功能。他们两个(glslangValidator和glslc)已经包含在Vulkan SDK(安装文件)中了,因此你不需要额外下载。
GLSL is a shading language with a C-style syntax. Programs written in it have a main function that is invoked for every object. Instead of using parameters for input and a return value as output, GLSL uses global variables to handle input and output. The language includes many features to aid in graphics programming, like built-in vector and matrix primitives. Functions for operations like cross products, matrix-vector products and reflections around a vector are included. The vector type is called vec with a number indicating the amount of elements. For example, a 3D position would be stored in a vec3. It is possible to access single components through members like .x, but it's also possible to create a new vector from multiple components at the same time. For example, the expression vec3(1.0, 2.0, 3.0).xy would result in vec2. The constructors of vectors can also take combinations of vector objects and scalar values. For example, a vec3 can be constructed with vec3(vec2(1.0, 2.0), 3.0).
GLSL是一种C风格语法的着色语言。GLSL程序以main函数为入口函数。GLSL不会以参数作为输入和返回值作为输出,而是用全局变量来处理输入输出。该语言包含许多有助于图形编程的特性,像内建的矢量和矩阵基础运算。像叉乘,矩阵和矢量乘法,围绕一个矢量反射这些操作功能都包含在内。举个例子,一个3D坐标可以存放在一个vec3类型的变量里。它可以通过vec3成员来单独的访问一个分量例如 .x,但同时也可以用多个分量创建一个新的矢量。举个例子,表达式 vec3(1.0, 2.0, 3.0).xy 将生成一个 vec2 类型的变量。矢量的构造函数也接受是对象和标量值的组合。例如,一个 vec3 对象可以用 vec3(vec2(1.0, 2.0), 3.0) 的形式来构造。
As the previous chapter mentioned, we need to write a vertex shader and a fragment shader to get a triangle on the screen. The next two sections will cover the GLSL code of each of those and after that I'll show you how to produce two SPIR-V binaries and load them into the program.
如前一章节所述,我们需要写一个顶点shader和一个片段shader,以便在盘屏幕上呈现一个三角形。后续两小节的内容将涵盖这两个shader的GLSL代码,之后,我将展示如何生成两个 SPIR-V 格式的二进制文件并将他们载入程序中。
Vertex shader
顶点着色器
The vertex shader processes each incoming vertex. It takes its attributes, like world position, color, normal and texture coordinates as input. The output is the final position in clip coordinates and the attributes that need to be passed on to the fragment shader, like color and texture coordinates. These values will then be interpolated over the fragments by the rasterizer to produce a smooth gradient.
顶点着色器处理每一个进来的顶点数据。这些顶点数据的属性,例如世界坐标,颜色,法线和纹理坐标等作为输入。它的输出是裁剪坐标空间的最终位置和被传入片段着色器的属性数据,例如颜色和纹理坐标。这些数值通过光栅化操作过程的插值计算应用在片段上,以产生平滑的过渡。
A clip coordinate is a four dimensional vector from the vertex shader that is subsequently turned into a normalized device coordinate by dividing the whole vector by its last component. These normalized device coordinates are homogeneous coordinates that map the framebuffer to a [-1, 1] by [-1, 1] coordinate system that looks like the following:
裁剪坐标空间是顶点shader产生的一个四维向量,随后,用整个矢量除以矢量本身的最后一个分量( .w ),将此矢量转换到规范化设备坐标空间。规范化设备坐标空间是齐次坐标空间,是由[-1.0, 1.0]的坐标系统将帧缓冲区的位置坐标映射到[-1.0, 1.0]的标准值,如下所示:

You should already be familiar with these if you have dabbled in computer graphics before. If you have used OpenGL before, then you'll notice that the sign of the Y coordinates is now flipped. The Z coordinate now uses the same range as it does in Direct3D, from 0 to 1.
如果你以前了解计算机图形学的话,你应该已经熟悉了这些机制。如果你以前用过OpenGL,你会注意到一个不同之处:Y坐标轴被翻转了。Z坐标轴用法和Direct3D一样,裁剪空间取值范围是0.0 到 1.0(包含0.0和1.0)。
如下图所示,左侧图是OpenGL坐标系,右侧图是Vulkan坐标系。

For our first triangle we won't be applying any transformations, we'll just specify the positions of the three vertices directly as normalized device coordinates to create the following shape:
因为这是我们的第一个,所以我们没有应用任何空间变换,我们只是用规范化设备空间指定三个顶点的坐标位置来创建如下三角形:

We can directly output normalized device coordinates by outputting them as clip coordinates from the vertex shader with the last component set to 1. That way, the division to transform clip coordinates to normalized device coordinates will not change anything.
我们能通过顶点shader,在裁剪空间将vec4类型变量的顶点坐标的第四个分量设为1.0,直接输出规范化设备坐标。用这个方法,将裁剪空间坐标转换为规范化设备坐标的除法将不改变任何数值。
Normally these coordinates would be stored in a vertex buffer, but creating a vertex buffer in Vulkan and filling it with data is not trivial. Therefore, I've decided to postpone that until after we've had the satisfaction of seeing a triangle pop up on the screen. We're going to do something a little unorthodox in the meanwhile: include the coordinates directly inside the vertex shader. The code looks like this:
这些坐标应该存放在一个顶点缓冲区里面,但是在Vulkan中创建一个顶点缓冲区并且设置好数据却不是一个小事。因此,我决定了延缓它直到 我们已经很满意的看到屏幕上显示出一个三角形之后。与此同时,我们将做一点不专业的事情:在顶点shader中直接包含顶点坐标数据。shader代码看起来如下所示:
#version 450
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
The main function is invoked for every vertex. The built-in gl_VertexIndex variable contains the index of the current vertex. This is usually an index into the vertex buffer, but in our case it will be an index into a hardcoded array of vertex data. The position of each vertex is accessed from the constant array in the shader and combined with dummy z and w components to produce a position in clip coordinates. The built-in variable gl_Position functions as the output.
这个main函数是每个顶点shader的入口函数。内建的 gl_VertexIndex 变量包含了当前被顶点shader正在处理的顶点索引。通常来说这是一个顶点缓冲区中的索引,但是我们现在用的索引是硬编码数组的顶点数据的索引。每一个顶点坐标是可以从当前shader的常量数组被访问到,并且结合模拟的z和w分量生产一个vec4类型的裁剪坐标空间的顶点数据。内建的变量 gl_Position 用于输出顶点坐标在顶点shader中的计算结果。
Fragment shader
片段着色器
The triangle that is formed by the positions from the vertex shader fills an area on the screen with fragments. The fragment shader is invoked on these fragments to produce a color and depth for the framebuffer (or framebuffers). A simple fragment shader that outputs the color red for the entire triangle looks like this:
由顶点shader计算出来的顶点坐标形成的三角形用片段在屏幕中填充一个区域。片段着色器在这些片段上被调用,计算出颜色和深度写入帧缓冲。一个在整个三角形输中出红色的简单片段着色器如下所示:
#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
The main function is called for every fragment just like the vertex shader main function is called for every vertex. Colors in GLSL are 4-component vectors with the R, G, B and alpha channels within the [0, 1] range. Unlike gl_Position in the vertex shader, there is no built-in variable to output a color for the current fragment. You have to specify your own output variable for each framebuffer where the layout(location = 0) modifier specifies the index of the framebuffer. The color red is written to this outColor variable that is linked to the first (and only) framebuffer at index 0.
片段着色器中的main函数被每一个片段调用,和顶点着色器的main函数一样。颜色在GLSL是由R、G、B、Alpha四个颜色通道构成的4分量矢量,每个分量取值范围是[0.0,1.0]。不像顶点shader中的 gl_Position ,片段着色器中没有在当前片段上输出颜色的内建变量。你必须对每个目标帧缓冲区指定你自己的输出变量,这里的 layout(location = 0)修饰符指定了目标帧缓冲区的索引。上面glsl代码中的红色颜色写到 outColor 变量中,此变量对应索引为0的第一个(目前也只有一个)目标帧缓冲区。
Per-vertex colors
逐顶点颜色
Making the entire triangle red is not very interesting. Wouldn't something like the following look a lot nicer?
绘制一个纯红色三角形不怎么有意思,像下面这种三角形看起来不是更好吗?