Per-Component 数学运算

使用 HLSL,可以在算法级别对着色器进行编程。 若要了解语言,需要了解如何声明变量和函数、使用内部函数、定义自定义数据类型并使用语义将着色器参数连接到其他着色器和管道。

了解如何在 HLSL 中创作着色器后,需要了解 API 调用,以便可以:为特定硬件编译着色器、初始化着色器常量,并在必要时初始化其他管道状态。

矢量类型

向量是一个数据结构,其中包含一到四个组件。

bool    bVector;   // scalar containing 1 Boolean
bool1   bVector;   // vector containing 1 Boolean
int1    iVector;   // vector containing 1 int
float3  fVector;   // vector containing 3 floats
double4 dVector;   // vector containing 4 doubles

紧跟数据类型的整数是向量上的组件数。

初始值设定项也可以包含在声明中。

bool    bVector = false;
int1    iVector = 1;
float3  fVector = { 0.2f, 0.3f, 0.4f };
double4 dVector = { 0.2, 0.3, 0.4, 0.5 };

或者,矢量类型可用于进行相同的声明:

vector <bool,   1> bVector = false;
vector <int,    1> iVector = 1;
vector <float,  3> fVector = { 0.2f, 0.3f, 0.4f };
vector <double, 4> dVector = { 0.2, 0.3, 0.4, 0.5 };

矢量类型使用尖括号来指定组件的类型和数量。

矢量最多包含四个组件,每个组件都可以使用两个命名集之一进行访问:

  • 位置集:x,y,z,w
  • 颜色集:r、g、b、a

这些语句都返回第三个组件中的值。

// Given
float4 pos = float4(0,0,2,1);

pos.z    // value is 2
pos.b    // value is 2

命名集可以使用一个或多个组件,但它们不能混合。

// Given
float4 pos = float4(0,0,2,1);
float2 temp;

temp = pos.xy  // valid
temp = pos.rg  // valid

temp = pos.xg  // NOT VALID because the position and color sets were used.

在读取组件时指定一个或多个矢量组件,称为重排。 例如:

float4 pos = float4(0,0,2,1);
float2 f_2D;
f_2D = pos.xy;   // read two components 
f_2D = pos.xz;   // read components in any order       
f_2D = pos.zx;

f_2D = pos.xx;   // components can be read more than once
f_2D = pos.yy;

掩码控制写入的组件数。

float4 pos = float4(0,0,2,1);
float4 f_4D;
f_4D    = pos;     // write four components          

f_4D.xz = pos.xz;  // write two components        
f_4D.zx = pos.xz;  // change the write order

f_4D.xzyw = pos.w; // write one component to more than one component
f_4D.wzyx = pos;

不能多次将分配写入同一组件。 因此,此语句的左侧无效:

f_4D.xx = pos.xy;   // cannot write to the same destination components 

此外,组件名称空间不能混合。 这是无效的组件写入:

f_4D.xg = pos.rgrg;    // invalid write: cannot mix component name spaces 

将矢量作为标量访问将访问矢量的第一个组件。 以下两个语句是等效的。

f_4D.a = pos * 5.0f;
f_4D.a = pos.r * 5.0f;

矩阵类型

矩阵是包含数据行和列的数据结构。 数据可以是任何标量数据类型,但是矩阵的每个元素都是相同的数据类型。 用追加到数据类型的逐列字符串指定行数和列数。

int1x1    iMatrix;   // integer matrix with 1 row,  1 column
int2x1    iMatrix;   // integer matrix with 2 rows, 1 column
...
int4x1    iMatrix;   // integer matrix with 4 rows, 1 column
...
int1x4    iMatrix;   // integer matrix with 1 row, 4 columns
double1x1 dMatrix;   // double matrix with 1 row,  1 column
double2x2 dMatrix;   // double matrix with 2 rows, 2 columns
double3x3 dMatrix;   // double matrix with 3 rows, 3 columns
double4x4 dMatrix;   // double matrix with 4 rows, 4 columns

最大行数或列数为 4;最小值为 1。

在声明矩阵时,可以初始化矩阵:

float2x2 fMatrix = { 0.0f, 0.1, // row 1
                     2.1f, 2.2f // row 2
                   };   

或者,矩阵类型可用于进行相同的声明:

matrix <float, 2, 2> fMatrix = { 0.0f, 0.1, // row 1
                                 2.1f, 2.2f // row 2
                               };

矩阵类型使用尖括号指定类型、行数和列数。 此示例创建一个浮点矩阵,其中包含两行和两列。 可以使用任何标量数据类型。

此声明定义了一个包含两行和三列的浮点值的矩阵(32 位浮点数):

matrix <float, 2, 3> fFloatMatrix;

矩阵包含以行和列形式组织的值,可以使用结构运算符“.”访问这些值,后跟两个命名集之一:

  • 从零开始的行列位置:
    • _m00、_m01、_m02、_m03
    • _m10、_m11、_m12、_m13
    • _m20、_m21、_m22、_m23
    • _m30、_m31、_m32、_m33
  • 基于一个行列的位置:
    • _11, _12, _13, _14
    • _21, _22, _23, _24
    • _31, _32, _33, _34
    • _41, _42, _43, _44

每个命名集以下划线开头,后跟行号和列号。 从零开始的约定还包括行号和列号之前的字母“m”。 下面是使用两个命名集访问矩阵的示例:

// given
float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   }; 

float f_1D;
f_1D = matrix._m00; // read the value in row 1, column 1: 1.0
f_1D = matrix._m11; // read the value in row 2, column 2: 2.1

f_1D = matrix._11;  // read the value in row 1, column 1: 1.0
f_1D = matrix._22;  // read the value in row 2, column 2: 2.1

与向量一样,命名集可以使用任一命名集中的一个或多个组件。

// Given
float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   };
float2 temp;

temp = fMatrix._m00_m11 // valid
temp = fMatrix._m11_m00 // valid
temp = fMatrix._11_22   // valid
temp = fMatrix._22_11   // valid

还可以使用数组访问表示法(从零开始的索引集)访问矩阵。 每个索引位于方括号内。 使用以下索引访问 4x4 矩阵:

  • [0][0], [0][1], [0][2], [0][3]
  • [1][0], [1][1], [1][2], [1][3]
  • [2][0], [2][1], [2][2], [2][3]
  • [3][0], [3][1], [3][2], [3][3]

下面是访问矩阵的示例:

float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   };
float temp;

temp = fMatrix[0][0] // single component read
temp = fMatrix[0][1] // single component read

请注意,结构运算符“.”不用于访问数组。 数组访问表示法不能使用重排读取多个组件。

float2 temp;
temp = fMatrix[0][0]_[0][1] // invalid, cannot read two components

但是,数组访问可以读取多组件向量。

float2 temp;
float2x2 fMatrix;
temp = fMatrix[0] // read the first row

与向量一样,读取多个矩阵组件称为重排。 可以分配多个组件,假设只使用一个名称空间。 这些都是有效的分配:

// Given these variables
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;

tempMatrix._m00_m11 = worldMatrix._m00_m11; // multiple components
tempMatrix._m00_m11 = worldMatrix.m13_m23;

tempMatrix._11_22_33 = worldMatrix._11_22_33; // any order on swizzles
tempMatrix._11_22_33 = worldMatrix._24_23_22;

掩码控制写入的组件数。

// Given
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;

tempMatrix._m00_m11 = worldMatrix._m00_m11; // write two components
tempMatrix._m23_m00 = worldMatrix._m00_m11;

不能多次将分配写入同一组件。 因此,此语句的左侧无效:

// cannot write to the same component more than once
tempMatrix._m00_m00 = worldMatrix._m00_m11;

此外,组件名称空间不能混合。 这是无效的组件写入:

// Invalid use of same component on left side
tempMatrix._11_m23 = worldMatrix._11_22; 

矩阵排序

默认情况下,统一参数的矩阵打包顺序设置为列主。 这意味着矩阵的每个列都存储在单个常量寄存器中。 另一方面,行主矩阵将矩阵的每一行打包在单个常量寄存器中。 可以使用 #pragmapack_matrix 指令或 row_majorcolumn_major 关键字更改矩阵打包。

矩阵中的数据在着色器运行时加载到着色器常量寄存器中。 矩阵数据的读取方式有两种选择:按行主要顺序或列主要顺序。 列主要顺序表示每个矩阵列将存储在单个常量寄存器中,而行主顺序意味着矩阵的每一行将存储在单个常量寄存器中。 对于矩阵使用多少个常量寄存器,这是一个重要考虑因素。

行主矩阵布局如下:

11
21
31
41

12
22
32
42

13
23
33
43

14
24
34
44

 

列主矩阵布局如下:

11
12
13
14

21
22
23
24

31
32
33
34

41
42
43
44

 

行主矩阵和列主要矩阵排序决定了矩阵组件从着色器输入中读取的顺序。 将数据写入常量寄存器后,矩阵顺序对在着色器代码中使用或访问数据的方式没有影响。 此外,着色器正文中声明的矩阵不会打包到常量寄存器中。 行主和列主打包顺序对构造函数的打包顺序没有影响(这始终遵循行主要排序)。

矩阵中的数据顺序可以在编译时声明,或者编译器将在运行时对数据进行排序,以便最有效的使用。

例子

HLSL 使用两种特殊类型:矢量类型和矩阵类型,以便更轻松地编程 2D 和 3D 图形。 其中每个类型都包含多个组件;矢量最多包含四个分量,一个矩阵最多包含 16 个组件。 在标准 HLSL 公式中使用向量和矩阵时,执行的数学运算旨在按分量工作。 例如,HLSL 实现此乘法:

float4 v = a*b;

作为四分量相乘。 结果是四个标量:

float4 v = a*b;

v.x = a.x*b.x;
v.y = a.y*b.y;
v.z = a.z*b.z;
v.w = a.w*b.w;

这是四个乘法,其中每个结果存储在 v 的单独组件中。 这称为四分乘。 HLSL 使用组件数学,这使得编写着色器非常高效。

这与通常作为生成单个标量的点积实现的乘法大相径庭:

v = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;

矩阵还在 HLSL 中使用每组件作:

float3x3 mat1,mat2;
...
float3x3 mat3 = mat1*mat2;

结果是两个矩阵(而不是标准 3x3 矩阵相乘)的按分量乘法。 每个分量矩阵相乘产生此第一个术语:

mat3.m00 = mat1.m00 * mat2._m00;

这不同于 3x3 矩阵乘法,这将产生此第一个术语:

// First component of a four-component matrix multiply
mat.m00 = mat1._m00 * mat2._m00 + 
          mat1._m01 * mat2._m10 + 
          mat1._m02 * mat2._m20 + 
          mat1._m03 * mat2._m30;

乘法函数的重载版本处理一个作数是向量,另一个作数是矩阵。 例如:vector * vector、vector * matrix、matrix * vector 和 matrix * matrix。 例如:

float4x3 World;

float4 main(float4 pos : SV_POSITION) : SV_POSITION
{
    float4 val;
    val.xyz = mul(pos,World);
    val.w = 0;

    return val;
}   

生成的结果与:

float4x3 World;

float4 main(float4 pos : SV_POSITION) : SV_POSITION
{
    float4 val;
    val.xyz = (float3) mul((float1x4)pos,World);
    val.w = 0;

    return val;
}   

本示例使用 (float1x4) 强制转换将 pos 向量转换为列向量。 通过强制转换或交换要相乘的参数的顺序来更改向量相当于转置矩阵。

自动强制转换会导致乘积函数和点内部函数返回与此处使用的相同结果:

{
  float4 val;
  return mul(val,val);
}

乘法的此结果为 1x4 * 4x1 = 1x1 向量。 这相当于点积:

{
  float4 val;
  return dot(val,val);
}

返回单个标量值。

数据类型 (DirectX HLSL)