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

图形API范畴

左乘与右乘

用列向量\(x\)右乘矩阵\(A\)\[ \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{bmatrix} \begin{bmatrix} x_1\\ x_2\\ x_3 \end{bmatrix} = x_1 \begin{bmatrix} a_{11}\\ a_{21}\\ a_{31} \end{bmatrix} + x_2 \begin{bmatrix} a_{12}\\ a_{22}\\ a_{32} \end{bmatrix} + x_3 \begin{bmatrix} a_{13}\\ a_{23}\\ a_{33} \end{bmatrix} \] 相当于列向量\(x\)每一项作为系数对矩阵\(A\)中的列向量线性组合。

用行向量\(x\)左乘矩阵\(A\)\[ \begin{bmatrix} x_1 &x_2 &x_3 \end{bmatrix} \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{bmatrix} = x_1 \begin{bmatrix} a_{11} &a_{12} &a_{13} \end{bmatrix} + x_2 \begin{bmatrix} a_{21} &a_{22} &a_{23} \end{bmatrix} + x_3 \begin{bmatrix} a_{31} &a_{32} &a_{33} \end{bmatrix} \] 相当于行向量\(x\)每一项作为系数对矩阵\(A\)中的行向量线性组合。

矩阵\(A\)和矩阵\(B\)的乘法,可以看作\(B\)右乘\(A\)\[ \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{bmatrix} \begin{bmatrix} b_{11} & b_{12} & b_{13}\\ b_{21} & b_{22} & b_{23}\\ b_{31} & b_{32} & b_{33} \end{bmatrix} = \begin{bmatrix} b_{11} \begin{bmatrix} a_{11}\\ a_{21}\\ a_{31} \end{bmatrix} + b_{21} \begin{bmatrix} a_{12}\\ a_{22}\\ a_{32} \end{bmatrix} + b_{31} \begin{bmatrix} a_{13}\\ a_{23}\\ a_{33} \end{bmatrix} & b_{12} \begin{bmatrix} a_{11}\\ a_{21}\\ a_{31} \end{bmatrix} + b_{22} \begin{bmatrix} a_{12}\\ a_{22}\\ a_{32} \end{bmatrix} + b_{32} \begin{bmatrix} a_{13}\\ a_{23}\\ a_{33} \end{bmatrix} & b_{13} \begin{bmatrix} a_{11}\\ a_{21}\\ a_{31} \end{bmatrix} + b_{23} \begin{bmatrix} a_{12}\\ a_{22}\\ a_{32} \end{bmatrix} + b_{33} \begin{bmatrix} a_{13}\\ a_{23}\\ a_{33} \end{bmatrix} \end{bmatrix} \] 也可以看作\(A\)左乘\(B\)\[ \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{bmatrix} \begin{bmatrix} b_{11} & b_{12} & b_{13}\\ b_{21} & b_{22} & b_{23}\\ b_{31} & b_{32} & b_{33} \end{bmatrix} = \begin{bmatrix} a_{11}[b_{11} &b_{12} &b_{13}] +a_{12}[b_{21} &b_{22} &b_{23}] +a_{13}[b_{31} &b_{32} &b_{33}] \\ a_{21}[b_{11} &b_{12} &b_{13}] +a_{22}[b_{21} &b_{22} &b_{23}] +a_{23}[b_{31} &b_{32} &b_{33}] \\ a_{31}[b_{11} &b_{12} &b_{13}] +a_{32}[b_{21} &b_{22} &b_{23}] +a_{33}[b_{31} &b_{32} &b_{33}] \end{bmatrix} \]

行优先与列优先

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

例如数组\(m_{11}m_{12}m_{13}m_{14}m_{21}m_{22}m_{23}m_{24}m_{31}m_{32}m_{33}m_{34}m_{41}m_{42}m_{43} m_{44}\)

解释为行优先矩阵 \[ \mathbf{M}=\begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ m_{31} & m_{32} & m_{33} & m_{34}\\ m_{41} & m_{42} & m_{43} & m_{44}\end{bmatrix} \] 解释为列优先矩阵 \[ \mathbf{M}=\begin{bmatrix} m_{11} & m_{21} & m_{31} & m_{41} \\ m_{12} & m_{22} & m_{32} & m_{42} \\ m_{13} & m_{23} & m_{33} & m_{43}\\ m_{14} & m_{24} & m_{34} & m_{44} \end{bmatrix} \]

不同图形API下的情况

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

DirectX

DirectXMath中的的XMFLOAT4XMVECTOR均是行向量。

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

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

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

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

\(m_{11}m_{12}m_{13}m_{14}m_{21}m_{22}m_{23}m_{24}m_{31}m_{32}m_{33}m_{34}m_{41}m_{42}m_{43} m_{44}\)

传递到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);

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

假设在HLSL的cbuffer为:

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

如果g_Worldmatrixfloat4x4类型,由于是列优先矩阵,上面的4个寄存器存储的数据会被看作: \[ \begin{bmatrix} m_{11} & m_{21} & m_{31} & m_{41} \\ m_{12} & m_{22} & m_{32} & m_{42} \\ m_{13} & m_{23} & m_{33} & m_{43} \\ m_{14} & m_{24} & m_{34} & m_{44} \\ \end{bmatrix} \] 如果g_Worldrow_major matrixrow_major float4x4类型,则为行优先矩阵,上面的4个寄存器存储的数据则依然被视作: \[ \begin{bmatrix}m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ m_{31} & m_{32} & m_{33} & m_{34} \\ m_{41} & m_{42} & m_{43} & m_{44} \\ \end{bmatrix} \] 因此,将矩阵从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]

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

Unity Shader

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

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

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

其它图形API

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

参考资料

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

[2]《Unity Shader 入门精要》


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