[HLSL]HLSL 入门参考 (dx11龙书附录B译文)

时间:2022-07-05 14:49:13

原文:[HLSL]HLSL 入门参考 (dx11龙书附录B译文)

HLSL 高级着色语言 参考文档

龙书DirectX12现已推出中文版,其附录B的高级着色器语言参考的翻译质量比本文更高,有条件的读者可以去支持一下。

目录

变量类型

标量类型

  1. bool: 值为 true 或 false. 注意 HLSL 像 C++ 一样,提供 true 和 false 关键字以供使用.
  2. int: 32位有符号整数
  3. half: 16位有理数(浮点数)
  4. float: 32位有理数
  5. double: 64位有理数

Note: 一些平台可能不支持 int, half, 或 double. 这些平台会使用 float 来模拟.

矢量类型

  1. float2: float 组成的二维矢量.
  2. float3: float 组成的三维矢量.
  3. float4: float 组成的四维矢量.

Note: 可以将 float 替换成其他标量类型以组成新的矢量. 比如 int2, half3, bool4.

  可以通过像给数组或结构体初始化那样初始化矢量:

float3 v = {1.0f, 2.0f, 3.0f};
float2 w = float2(x, y);
float4 u = float4(w, 3.0f, 4.0f);
// u= (w.x, w.y, 3.0f, 4.0f)

  可以通过数组下标的语法访问矢量元素. 比如访问矢量 vec 的第i个元素,可以这样写:

vec[i] = 2.0f;

  另外,可以通过定义好了的元素名 x, y, z, w, r, g, b 和 a 像访问结构体那样访问矢量元素.

vec.x = vec.r = 1.0f;
vec.y = vec.g = 2.0f;
vec.z = vec.b = 3.0f;
vec.w = vec.a = 4.0f;

   名称 r, g, b 和 a 与名称 x, y, z 和 w 引用的元素完全一样. 当用矢量表示颜色时,使用符号RGBA会更好一些,因为这强调了矢量储存的是颜色的事实.

Swizzles

  有时候会要使用矢量 u=(ux,uy,uz,uw)\mathbf{u}=(u_x,u_y,u_z,u_w)u=(ux​,uy​,uz​,uw​) 的元素来填充矢量 v=(uw,uy,uy,ux)\mathbf{v}=(u_w, u_y, u_y, u_x)v=(uw​,uy​,uy​,ux​) .最直接的方法就是分别引用每个元素来分别赋值.不过 HLSL 提供了一种叫 Swizzles 的特殊的语法来进行这种仅仅变换次序的赋值:

float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v = u.wyyx;
//v = {4.0f, 2.0f, 2.0f, 1.0f}

  另一个例子:

float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v = u.wzyx;
// v = {4.0f, 3.0f, 2.0f, 1.0f}

  如果只需要复制部分元素的话可以这样子:

float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v.xy = u;
// v = {1.0f, 2.0f, 5.0f, 6.0f}

矩阵类型

  可以使用下列语法定义 m × n 的矩阵,其中 n 和 m 必须在 1 到 4 之间:

floatmxn matmxn;

  例如:

  1. float2x2: float 型元素组成的 2 × 2 矩阵.
  2. float3x3: float 型元素组成的 3 × 3 矩阵.
  3. float4x4: float 型元素组成的 4 × 4 矩阵.
  4. float3x4: float 型元素组成的 3 × 4 矩阵.

Note: 可以将 float 替换成其他标量类型以组成新的矩阵. 比如 int2x2, half3x3, bool4x4.

  可以使用两个数组下表的语法访问矩阵元素.例如访问 i 行 j 列元素:

M[i][j] = value;

  另外还可以像结构体那样访问矩阵元素:

// One-Based Indexing:
// 索引从1起计数
M._11 = M._12 = M._13 = M._14 = 0.0f;
M._21 = M._22 = M._23 = M._24 = 0.0f;
M._31 = M._32 = M._33 = M._34 = 0.0f;
M._41 = M._42 = M._43 = M._44 = 0.0f; // Zero-Based Indexing:
// 索引从0起计数
M._m00 = M._m01 = M._m02 = M._m03 = 0.0f;
M._m10 = M._m11 = M._m12 = M._m13 = 0.0f;
M._m20 = M._m21 = M._m22 = M._m23 = 0.0f;
M._m30 = M._m31 = M._m32 = M._m33 = 0.0f;

  有时候会需要引用矩阵的行矢量出来.可以只用一个数组下标来引用行矢量.比如从 3 × 3 矩阵 M\mathbf{M}M 中提出第i行的行矢量可以这样子:

float3 ithRow = M[i];
// 获取第i行行矢量

  下面的例子演示了向矩阵中插入三个矢量:

float3 N = normalize(pIn.normalW);
float3 T = normalize(pIn.tangentW - dot(pIn.tangentW, N)*N);
float3 B = cross(N,T);
float3x3 TBN;
TBN[0] = T; // 设置第 1 行行矢量
TBN[1] = B; // 设置第 2 行行矢量
TBN[2] = N; // 设置第 3 行行矢量

  还可以从行矢量生成矩阵:

float3 N = normalize(pIn.normalW);
float3 T = normalize(pIn.tangentW - dot(pIn.tangentW, N)*N);
float3 B = cross(N,T);
float3x3 TBN = float3x3(T, B, N);

Note:四维矢量和 4 × 4 矩阵除了可以用 float4 和 float4x4 表示,还可以直接用 vector 和 matrix 表示:

vector u = {1.0f, 2.0f, 3.0f, 4.0f};
matrix M; // 4 x 4 矩阵

数组

  可以用熟悉的 C++ 语法声明一个数组,例如:

float M[4][4];
half p[4];
float3 v[12]; // 12 个三维矢量

结构体

  结构体同样可以像 C++ 那样定义.不过 HLSL 的话就不允许有成员函数.这是一个结构体的例子:

struct SurfaceInfo {
float3 pos;
float3 normal;
float4 diffuse;
float4 spec;
};
SurfaceInfo v;
litColor += v.diffuse;
dot(lightVec, v.normal);
float specPower = max(v.spec.a, 1.0f);

typedef 关键字

  HLSL 的 typedef 跟 C++ .的几乎完全一样.例如给 vector<float,3> 一个别名可以这样:

typedef float3 point;

  然后可以将这样子的写法:

float3 myPoint;

改写成:

point myPoint;

  这是另外一个将 const 关键字(用起来跟C++一样的)进行 typedef 的演示:

typedef const float CFLOAT;

变量前缀

  以下关键字可以添加到变量前面作为前缀:

  1. static: 本质上讲是 extern 的反义词;这个关键字表示该变量不会暴露给 C++ ,是仅着色器程序内部可见的变量.

    static float3 v = {1.0f, 2.0f, 3.0f};

  2. uniform: 表示变量不改变每一个顶点或像素—使得变量对于所有顶点和像素都是常量,只能在 C++ 改变其值.变量只能在着色器程序外部初始化(例如从 C++)

  3. extern: 表示变量在 C++ 可见(例如 C++ 程序可在着色器程序外部访问这个变量).这里的全局变量默认为 uniform 和 extern.

  4. const: 该关键字与 C++ 的 const 关键字一样.表示该变量为常量,不可改变.

    const float pi = 3.14f;

类型转换 (Casting)

  HLSL 允许数据进行类型转换,而且类型检查非常宽松.HLSL的类型转换跟 C 语言的相似.例如,转换 matrix 为 float 可以这样:

float f = 5.0f;
float4x4 m = (float4x4)f;
// 给 m 的每一个元素赋 f

  标量到矩阵的类型转换会将标量给矩阵的的每一个元素都用这个标量赋值.看看下面的例子:

float3 n = float3(...);
float3 v = 2.0f*n - 1.0f;

   2.0f*n 是标量与矩阵的数乘,没有问题.然而要使式子成立,标量 1.0f 会扩充成矢量 (1.0f, 1.0f, 1.0f).因此式子相当于:

float3 v = 2.0f*n - float3(1.0f, 1.0f, 1.0f);

  这里的例子都能容易地推断出类型转换的情况.不过完整的类型转换规则还是得参阅 SDK 参考文档,搜索 “Casting and Conversion”.

关键字 和 (运)算符

关键字

  为了方便参考,现在列出 HLSL 所有的关键字:

- - - -
asm float pass true
bool for pixelshader typedef
compile half return uniform
const if sampler vector
decl in shared vertexshader
do inline static void
double inout string volatile
else int struct while
extern matrix technique
false out texture

  下面的关键字保留且未使用,未来可能成为正式关键字.

- - - -
auto dynamic_cast private template
break enum protected this
case explicit public throw
catch friend register try
char goto reinterpret_cast typename
class long short union
const_cast mutable signed unsigned
continue namespace sizeof using
default new static_cast virtual
delete operator switch -

(运)算符

  HLSL支持很多熟悉的 C++ 关键字. 除了下面要提到的少数的相异点,这些算符用起来跟 C++ 一毛一样.下表列出了所有的 HLSL 的算符.

- - - - - -
[ ] . > < <= >=
!= == ! && || ? :
+ += - -= * *=
/ /= % %= ++
= ( ) ,

  虽然这些算符用起来跟 C++ 很像,但是还是有相异点.

  1. 取模 % 算符可以在整数和有理数(浮点数)下工作.但是要进行取模,两个操作数要求要符号统一(比如都为正数或都为负数).
  2. 有到很多 HLSL 算符能对矢量或矩阵的每个元素单独进行,这是因为矢量和矩阵都内建在 HLSL 内部了,而且这些类型都由多个元素组成.通过使这些算符能对每个元素单独进行操作,可以扩展原本只能对标量进行运算的算符的用途,使它们支持矢量/矩阵分量加法,矢量/矩阵分量减法,矢量/矩阵相等性测试等.看下面的例子:

Note:像是在 C++ 里的一样,这些算符的行为可从它对标量的运算里猜出来.

float4 u = {1.0f, 0.0f, -3.0f, 1.0f};
float4 v = {-4.0f, 2.0f, 1.0f, 0.0f};
// 矢量分量加法(各元素相加)
float4 sum = u + v; // sum = (-3.0f, 2.0f, -2.0f, 1.0f)

  矢量分量自增(各元素自增)

// 自增前: sum = (-3.0f, 2.0f, -2.0f, 1.0f)
sum++;
// 自增后: sum = (-2.0f, 3.0f, -1.0f, 2.0f)

  矢量分量乘法

float4 u = {1.0f, 0.0f, -3.0f, 1.0f};
float4 v = {-4.0f, 2.0f, 1.0f, 0.0f};
// 矢量分量乘法(各元素相乘)
float4 product = u * v; // product = (-4.0f, 0.0f, -3.0f, 0.0f)

Warning: 对于两个矩阵

float4x4 A;
float4x4 B;

操作 A*B 会进行分量乘法,而非矩阵乘法,矩阵乘法需要用函数 mul 进行.

  比较算符同样会对每个元素进行,并且返回由 bool 型值组成的(布尔)矢量或矩阵. bool 型值会填充整个作为运算结果的矢量或矩阵.例如:

float4 u = { 1.0f, 0.0f, -3.0f, 1.0f};
float4 v = {-4.0f, 0.0f, 1.0f, 1.0f};
float4 b = (u == v);
// b = (false, true, false, true)

  最后,对二元算符可以做如下断定的结论:

  1. 如果二元算符的左操作数与右操作数的维度不同,那么维度小的操作数会被类型转换到其维度与另一个操作数的维度一致,这被称为 提升.例如, float 型的 x 和 float3 型的 y 进行 (x + y) 操作时, x 会被类型转换为 float3 型矢量,表达式最终结果的类型为 float3 型,其中 float 型的 x 类型会 提升 成 float3(x, x, x),这也是因为标量到矢量的类型转换预定好的.注意:对于未有定义类型转换的 提升 将会失败.例如, float2 到 float3 的 提升 将会失败,因为不存在矢量 float2 到矢量 float3 的类型转换.
  2. 如果二元算符的左操作数与右操作数的类型不同,那么类型 low 的一方的类型会被 提升 到与另一方相同.例如, int 型的 x 与 half 型的 y 进行 (x + y) 操作时, x 会被 提升 为 half 型,表达式最终结果的类型为 half.

流程语句

  HLSL 支持许多熟悉的 C++ if 语句,循环语句及其他常见的控制流程的语句.这些流程语句的语法跟 C++ 超像.

  • 返回语
return (expression);
  • if 语句
if(condition) {
statement(s);
}
  • if else 语句
if(condition) {
statement(s);
} else {
statement(s);
}
  • for 语句
for(initial; condition; increment) {
statement(s);
}
  • while 语句
while(condition) {
statement(s);
}
  • do while 语句
do {
statements(s);
}while(condition);

函数

自定义函数

  HLSL 的函数有以下特点:

  1. 用起来跟 C++ 很像.
  2. 参数跟在变量后面1
  3. 不支持递归
  4. 全都是内联函数

  并且 HLSL 还添加了给自定义函数用的额外的关键字.比如下面这个函数:

bool foo(
in const bool b, // input bool
out int r1, // output int
inout float r2 // input/output float
) {
if(b) { // 试探输入的数据
r1 = 5;
// 通过 r1 输出
} else {
r1 = 1;
// 通过 r1 输出
}
// 因为 r2 为 inout 所以可以用它作为 输入, 即读取数据
// 也可通过 r2 输出
r2 = r2 * r2 * r2;
return true;
}

  函数几乎就跟 C++ 的一样除了 in, out 和 inout 关键字.

  1. in: 在函数体执行之前,指示函数调用的该参数值会被复制一份副本给函数体来使用.一般没必要明确加上 in 因为参数默认就是 in.例如下面这两个例子等价:
float square(in float x) {
return x * x;
}

  去掉 in :

float square(float x) {
return x * x;
}
  1. out: 指示函数返回后会将函数体对该参数所有赋值应用到调用函数时的此处的变量.即好似直接对外部变量的引用操作,但是不可读.需要这个特性时这个关键字不能缺,因为 HLSL 不允许传递引用或指针.
void square(in float, out float) {
y = x * x;
}

  通过 x 输入一个要被平方的数,并用 x 自乘得到平方并通过 y 返回值.

  1. inout: in 和 out 的组合. 如果需要同时作为 in 和 out,那么用 inout.
void square(inout float x) {
x = x * x;
}

  通过 x 输入一个存有要被平方的数的变量,并用 x 自乘得到平方并通过 x 返回值.

内建函数

  HLSL 有用于 3D 图形编程的丰富的内建函数.下面列出部分内建函数:

  1. abs(x) - 返回 ∣x∣|x|∣x∣.
  2. ceil(x) - 返回最小的大于等于x的整数.
  3. cos(x) - 返回 cosxcos xcosx,其中 x 为弧度.
  4. clamp(x, a, b) - 修剪 x, 若 x 大于 b,则返回 b;若 x 小于 a,这返回 a,否则返回 x.
  5. clip(x) - 仅可在像素着色器中调用,如果输入向量中的任何元素小于0,则丢弃当前像素.
  6. cross(u, v) - 返回 u×v\mathbf{u} \times \mathbf{v}u×v
  7. ddx§ - Estimates screen space partial derivative ∂p/∂x. This allows you to determine how per pixel quantities p vary from pixel

    to pixel in the screen space x-direction.
  8. ddy§ - Estimates screen space partial derivative ∂p/∂y. This allows you to determine how per pixel quantities p vary from pixel

    to pixel in the screen space y-direction.
  9. degress(x) - 将弧度 x 转为角度
  10. determinant(M) - 返回矩阵的行列式的值
  11. distance(u, v) - 返回作为点的 u 和 v 的距离.
  12. dot(u, v) - 返回 u⋅v\mathbf{u} \cdot \mathbf{v}u⋅v.
  13. floor(x) - 返回最大的小于等于 x 的整数.
  14. frace(x) - 返回有理数(浮点数)的小数部分.
  15. length(x) - 返回 ∥v∥\rVert\mathbf{v}\rVert∥v∥.
  16. lerp(u, v, t) - 在 u 和 v 之间插值,要求 t∈[0,1]t \in [0,1]t∈[0,1],v > u 时返回 u+(v−u)tu+(v-u)tu+(v−u)t.
  17. log(x) - 返回 lnxln xlnx.
  18. log10(x) - 返回 log10xlog_{10}xlog10​x.
  19. log2(x) - 返回 log2xlog_2xlog2​x.
  20. max(x, y) - 返回 max{x,y}max\{x, y\}max{x,y}.
  21. min(x, y) - 返回 min{x,y}min\{x, y\}min{x,y}.
  22. mul(M, N) - 返回矩阵乘法 MN\mathbf{M}\mathbf{N}MN.注意要使该乘法要有意义.若 M 为矢量,则该函数为行矢量左乘矩阵.若 N 为矢量,则该函数为列矢量右乘矩阵.
  23. normalize(v) - 返回 v/∥v∥\mathbf{v}/\rVert\mathbf{v}\rVertv/∥v∥.
  24. pow(b, n) - 返回 bnb^nbn.
  25. radians(x) - 将角度 x 转为弧度
  26. saturate(x) - 返回 clamp(x, 0,0, 1.0).
  27. sin(x) - 返回 sinxsin xsinx.
  28. sincos(in x, out s, out c) - 通过 s 返回 $ sin x$, 通过 c 返回 cosxcos xcosx,其中 x 为弧度.
  29. sqrt(x) - 返回 x\sqrt{x}x​.
  30. reflect(v, n) - 返回 v 在以 n 为法向量的平面上反射的向量.
  31. reflact(v, n, eta) - 返回 v 在以 n 为法向量的 eta 为折射率的平面上折射的向量.
  32. rsqrt(x) - 返回 1x\frac{1}{\sqrt{x}}x​1​.
  33. tan(x) - 返回 tanxtan xtanx,其中 x 为弧度.
  34. transpose(M) - 返回转置矩阵 MT\mathbf{M}^TMT.
  35. Texture2D::Sample(S, texC) - Returns a color from a 2D texture map based on the SamplerState object S, and 2D texture coordinates texC.
  36. Texture2D::SampleLevel(S, texC, mipLevel) - Returns a color from a 2D texture map based on the SamplerState object S, 2D texture coordinates texC, and mipmap level mipLevel. This function differs from Texture2D::Sample in that the third parameter manually specifies the mipmap level to use. For example, we would specify 0 to access the topmost mipmap LOD.
  37. TextureCube::Sample(S, v) - Returns a color from a cube map based on the SamplerState object S and 3D lookup vector v.
  38. Texture2DArray::Sample(S, texC) - Returns a color from a 2D texture array based on the SamplerState object S (recall a sampler state specifies texture filters and texture address modes) and 3D texture coordinates texC, where the first two coordinates are the usual 2D texture coordinates and the third coordinate specifies the array index.

Note: 很多函数都根据它应该能处理的数据重载了,使之能处理所有应该能处理的内建类型的参数.例如 abs 应当能处理所有标量数据,因故重载了对所有标量类型的操作.另外就是外积 cross 函数应当只能处理三维矢量,所以它就只重载了对所有标量类型的三维矢量的操作(例如 int3, float3, double3 等).还有就是线性插值函数 lerp 应该能处理所有标量,二维矢量,三维矢量和四维矢量的数据,所以它重载了这些所有类型的操作.

Note: 如果给"标量"函数(例如 cos(x))一个非标量的参数,该函数会遍历所有元素,对所有元素都调用该函数.例如:

float3 v = float3(0.0f, 0.0f, 0.0f);
v = cos(v);

会对所有元素都调用 cos 函数.

Note: 关于更多参考文档,完整的内建函数清单请查阅 DirectX 参考文档,搜索 “HLSL Intrinsic Functions.”

译注

  • 仅供教研学习交流用,不准一声不响拿去闷声发大财.
  • 配合 Introduction to 3D Game Programming with DirectX 11龙书dx11译文版 享用风味更佳.
  • 中国的3D编程处境好惨啊.
  • 对了,译者四级还没过.

  1. Parameters are always passed by value. ↩︎