以下代码实现简单的顶点着色器 (vertex shader):
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
void VS(float3 iPosL : POSITION,
float4 iColor : COLOR,
out float4 oPosH : SV_POSITION,
out float4 oColor : COLOR)
{
// 把顶点变换到齐次裁剪空间
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
// 直接将顶点的颜色信息传至像素着色器
oColor = iColor;
}
在 Direct3D 中,编写着色器的语言为高级着色语言 (High Level Shading Language,HLSL)。一般情况下,着色器通常以 .hlsl 为扩展名的文本文件中编写。
顶点着色器就是上例中命名为 VS 的函数。我们可以给顶点着色器起任意合法的函数名。上述顶点着色器共有 4 个参数,前两个为输入参数,后两个为输出参数(通过关键字 out 来表示)。HLSL 没有引用 (reference) 和指针 (pointer) 的概念,所以需要借助结构体或多个输出参数才能够从函数中返回多个数值。在HLSL中所有的函数都是内联 (inline) 函数。
前两个输入参数分别对应于绘制立方体所自定义顶点结构体中的两个数据成员,也构成了顶点着色器的输入签名 (input signature)。参数语义 “:POSITION” 和 “:COLOR” 用于将顶点结构体中的元素映射到顶点着色器的相应输入参数,如下图所示:
通过 D3D12_INPUT_ELEMENT_DESC 数组为每个顶点元素都指定了与之关联的语义。顶点着色器中的每个参数也各附有一个语义,该语义用于使顶点元素与顶点着色器参数逐一匹配
输出参数也附有各自的语义 (“:SV_POSITION”和“:COLOR”),并以此作为纽带,将顶点着色器的输出参数映射到下个处理阶段(几何着色器或像素着色器)中所对应的输入参数。注意,SV_POSITION 语义比较特殊 (SV代表系统值,即system value),它所修饰的顶点着色器输出元素存有齐次裁剪空间中的顶点位置信息。因此,我们必须为输出位置信息的参数附上 SV_POSITION语义,使 GPU 可以在进行例如裁剪、深度测试和光栅化等处理之时,借此实现其他属性所无法介入的有关运算。值得注意的是,对于任何不具有系统值的输出参数而言,我们都可以根据需求以合法的语义名修饰它。
上述顶点着色器函数的第一行代码是通过将顶点与 4*4 矩阵 gWorldViewProj 相乘,使其坐标由局部空间变换到齐次裁剪空间:
// 将顶点坐标变换到齐次裁剪空间
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
借助构造语法 float4(iPosL, 1.0f) 即可构建一个等价于 float4(iPosL.x, iPosL.y, iPosL.z, 1.0f) 的 4D向量。我们知道,顶点的位置是一个点而非向量,所以将向量的第 4 个分量设置为 1 (w=1)。并以float2 与 float3 类型分别表示 2D 和 3D 向量。矩阵变量 gWorldViewProj 存于常量缓冲区(constant buffer) 内,我们会在 6.6 节中对它进行相关讨论。内置函数 (built-in function,也译作内建函数、内部函数等) mul 则用于计算向量与矩阵之间的乘法。顺便提一下,mul 函数可以根据不同规模的矩阵乘法而重载。例如,我们可以用 mul 函数进行两个 4*4 矩阵的乘法、两个 3*3 矩阵的乘法或者一个 1*3 向量与 3*3 矩阵的乘法等。着色器函数的最后一行代码把输入的颜色直接复制给输出参数,继而将该颜色传递到渲染流水线的下个阶段:
oColor = iColor;
我们可以把函数的返回类型和输入签名替换为结构体(从而取代过长的参数列表),即将以上顶点着色器改写为另一种等价实现:
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// 将顶点变换到齐次裁剪空间
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// 直接将顶点颜色传至像素着色器
vout.Color = vin.Color;
return vout;
}
注意:如果没有使用几何着色器,那么顶点着色器必须用 SV_POSITION 语义来输出顶点在齐次裁剪空间中的位置,因为(在没有使用几何着色器的情况下)执行完顶点着色器之后,硬件期望获取顶点位于齐次裁剪空间之中的坐标。如果使用了几何着色器,则可以把输出顶点在齐次裁剪空间中位置的工作交给它来处理。
注意:在顶点着色器(或几何着色器)中是无法进行透视除法的,此阶段只能实现投影矩阵这一环节的运算。而透视除法将在后面交由硬件执行。