图形API中的矩阵表示与运算

图形API范畴

左乘与右乘

用列向量x右乘矩阵A[a11a12a13a21a22a23a31a32a33][x1x2x3]=x1[a11a21a31]+x2[a12a22a32]+x3[a13a23a33] 相当于列向量x每一项作为系数对矩阵A中的列向量线性组合。

用行向量x左乘矩阵A[x1x2x3][a11a12a13a21a22a23a31a32a33]=x1[a11a12a13]+x2[a21a22a23]+x3[a31a32a33] 相当于行向量x每一项作为系数对矩阵A中的行向量线性组合。

矩阵A和矩阵B的乘法,可以看作B右乘A[a11a12a13a21a22a23a31a32a33][b11b12b13b21b22b23b31b32b33]=[b11[a11a21a31]+b21[a12a22a32]+b31[a13a23a33]b12[a11a21a31]+b22[a12a22a32]+b32[a13a23a33]b13[a11a21a31]+b23[a12a22a32]+b33[a13a23a33]] 也可以看作A左乘B[a11a12a13a21a22a23a31a32a33][b11b12b13b21b22b23b31b32b33]=[a11[b11b12b13]+a12[b21b22b23]+a13[b31b32b33]a21[b11b12b13]+a22[b21b22b23]+a23[b31b32b33]a31[b11b12b13]+a32[b21b22b23]+a33[b31b32b33]]

行优先与列优先

同样将一维元素数组表示为矩阵,列优先矩阵指矩阵中的元素是按列解释的,行优先矩阵指矩阵中的元素是按行解释的。两种矩阵可以通过转置相互转换。(也翻译为行主序和列主序)

例如数组m11m12m13m14m21m22m23m24m31m32m33m34m41m42m43m44

解释为行优先矩阵 M=[m11m12m13m14m21m22m23m24m31m32m33m34m41m42m43m44] 解释为列优先矩阵 M=[m11m21m31m41m12m22m32m42m13m23m33m43m14m24m34m44]

不同图形API下的情况

前面讲了左乘与右乘、行优先与列优先,这两者在数学概念上是没有关系的。可以用列向量右乘一个行优先矩阵或者列优先矩阵,没什么限制,但是翻译为汇编代码后可以看出在实现上有区别。

DirectX

DirectXMath中的的XMFLOAT4XMVECTOR均是行向量。

向量与矩阵相乘需要使用函数:

1
2
3
4
XMVECTOR XM_CALLCONV XMVector4Transform(
[in] FXMVECTOR V,
[in] FXMMATRIX M
) noexcept;
C++

在DirectX中,矩阵乘法的顺序是从左到右,变换生效的先后顺序也是从左到右。

DirectXMath中的XMFLOAT4X4XMMATRIX均是行优先矩阵,它的数据流如下:

传递到HLSL后,若是传递给cb0的寄存器的前4个向量,那么它内存布局一定如下:

1
2
3
4
cb0[0].xyzw = (m11, m12, m13, m14);
cb0[1].xyzw = (m21, m22, m23, m24);
cb0[2].xyzw = (m31, m32, m33, m34);
cb0[3].xyzw = (m41, m42, m43, m44);
C++

而在HLSL中,默认的matrixfloat4x4采用的是列优先矩阵。

假设在HLSL的cbuffer为:

1
2
3
4
cbuffer cb : register(b0)
{
(row_major) matrix g_World;
}
C

如果g_Worldmatrixfloat4x4类型,由于是列优先矩阵,上面的4个寄存器存储的数据会被看作: 如果g_Worldrow_major matrixrow_major float4x4类型,则为行优先矩阵,上面的4个寄存器存储的数据则依然被视作: 因此,将矩阵从DirectX传递到HLSL中时需要注意,可能要经过转置。

HLSL中的mul函数mul(x,y)

  • 要求矩阵x的列数与矩阵y的行数相等。

  • 如果x是一个向量,那么它将被解释为行向量。

  • 如果y是一个向量,那么它将被解释为列向量。

它使用dp4指令优化运算。对于dp4来说,最好是能够对一个行向量列优先矩阵(取列优先矩阵的列,也就是取一行寄存器向量与行向量做点乘)操作,又或者是对一个行优先矩阵(取行优先矩阵的行与列向量做点乘)和列矩阵操作,这样能避免转置。

4种正常传递与运算矩阵的情况:

  1. C++代码端不进行转置,HLSL中使用row_major matrix(行优先矩阵),mul函数让向量放在左边(行向量),这样实际运算就是(行向量 X 行优先矩阵) 。这种方法易于理解,但是这样做dp4运算取矩阵的列很不方便,在HLSL中会产生用于转置矩阵的大量指令,性能上有损失。

  2. C++代码端进行转置,HLSL中使用matrix(列优先矩阵) ,mul函数让向量放在左边(行向量),这样就是(行向量 X 列优先矩阵),但C++这边需要进行一次矩阵转置,HLSL内部不产生转置 。这是官方例程所使用的方式,这样可以使得dp4运算可以直接取列主序矩阵的行,从而避免内部产生大量的转置指令。教程的项目也使用这种方式。

  3. C++代码端不进行转置,HLSL中使用matrix(列主序矩阵),mul函数让向量放在右边(列向量),实际运算是(列主序矩阵 X 列向量)。这种方法的确可行,取列矩阵的行也比较方便,效率上又和2等同,就是HLSL那边的矩阵乘法都要反过来写,然而DX本身就是崇尚行主矩阵的,把OpenGL的习惯带来这边有点。。。

  4. C++代码端进行转置,HLSL中使用row_major matrix(行主序矩阵),mul函数让向量放在右边(列向量),实际运算是(行主序矩阵 X 列向量)。 就算这种方法也可以绘制出来,但还是很让人难受,比第2点还难受,我甚至不想去说它。

引用自[1]

值得一提的是,按照矩阵预算律,对于矩阵和列向量等价于。由于mul函数会自动对向量进行转置,所以可以通过调换矩阵和向量的顺序避免手动转置矩阵,等价于

Unity Shader

Unity Shader用名为ShaderLab的声明性语言编写,实现了跨平台。其中的CGPROGRAM 代码片段是用常规 HLSL/Cg着色语言编写

在Unity Shader中,通常在变换顶点时,使用列向量右乘矩阵进行乘法,因为Unity提供的内置矩阵(如UNITY_MATRIX_MVP等)都是按列存储的。但有时也会使用左乘的方式,因为可以省去对矩阵转置的操作()。

Unity在脚本中提供了一种矩阵类型——Matrix4x4。脚本中的这个矩阵类型则是采用列优先的方式。Unity Shader中Cg的矩阵采用行优先

其它图形API

矩阵在OpenGL和GLSL中都是列优先的,不像DirectX和HLSL前者行优先、后者列优先。

参考资料

[1] DirectX11--HLSL中矩阵的内存布局和mul函数探讨 - X_Jun - 博客园 (cnblogs.com)

[2]《Unity Shader 入门精要》


图形API中的矩阵表示与运算
https://tech.reddish.fun/Article/matrix-in-graphics-API/
作者
bit704
发布于
2023年10月12日
许可协议