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
  • 从 1 开始的行列位置:
    • _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; 

矩阵排序

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

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

行主矩阵的布局如下所示:

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)