[自問自答] 為何 DirectX11 傳遞矩陣到 Constant Buffers 前需要進行轉置

在我的一個專案中,使用了 c++ 與 DirectX11 直接實作 rendering pipeline,並使用函式庫DirectXMath,作為數學運算的主要工具。

DirectXMath 是一個支援 SIMD 的函式庫,其中一個特點是它以 row-major 的方式來儲存矩陣。

但不論是 GLSL 還是 HLSL,shader 中的 matrix 都是預設以 column-major 儲存,所以在 DirectX11 與 DirectXMath 的範例中告訴你,在把 DirectXMath 的矩陣類型傳遞到 shader 中使用前,必須先經過一次矩陣轉置的處理,並且在 HLSL program 中,要使用 mul(vector, matrix) 來進行座標轉換。

但理由只有就這樣?結論來的這麼容易?

Row-major 與 Column-major

兩者的差異在於矩陣儲存在記憶體的分配方式(如下圖),不影響矩陣的運作規則。

matrix1

同樣的儲存邏輯套用在向量上,row-vector 寫作 1 x n 的矩陣;column-vector 寫作 n x 1 的矩陣。

矩陣的乘法的規則是左邊矩陣的列數(column)必須等於右邊矩陣的行數(row),因此引申出轉換矩陣與向量乘法的左右差異(如下圖,以平移矩陣為例)。

matrix2

Row-major matrix 與 row-vector 是 DirectXMath 採用的規則,所以其中的變換矩陣都是以右乘的方式來疊加效果;而 OpenGL 與 Unity 的函式庫是採用 column-major matrix 與 column-vector,因此在變換矩陣的乘法為左乘。

可是,row-major matrix 與 row-vector 並不是綁定的,只要符合矩陣乘法中列數行數的規則,column-major matrix 與 row-vector 也能夠用來相乘。

Row-major 與 column-major 只影響了記憶體的分配方式,不影響矩陣的運作規則。

Shader 中的矩陣

Shader 的部分倒是相當統一,不論 OpenGL 的 GLSL,或者 HLSL 都預設以 column-major 方式儲存矩陣。

如果沒有特別去處理,那所有傳入到 HLSL constant buffers 中的矩陣都會被視為 column-major matrix。

設定矩陣到 constant buffers 中經過的只是單純的記憶體複製,如果本來是 row-major matrix,經過一次轉置運算後,才是矩陣形式相同,所對應的 column-major matrix 記憶體配置。

可是,row-vector 與 column-vector 所使用的轉換矩陣,形式是不相同的。

所以經過一次轉置運算後的 DirectXMath 矩陣(row-major),傳入 HLSL 中後用了不同的記憶體分配,但維持了未轉置前的形式,是準備給 row-vector 使用的矩陣,必須用在 row-vector 上才會有正確結果。

mul( ) 的左乘與右乘

根據 HLSL 的文件說明:

mul(x, y) 如果 x 是向量,視為 row-vector;如果 y 是向量,視為 column-vector。

加上矩陣一律都預設為 column-major matrix,可以引申出下面的結果。

  • mul(x, y) 如果 x 是矩陣 y 是向量,即是矩陣左乘向量 mul(column-major matrix, column-vector)。
  • mul(x, y) 如果 x 是向量 y 是矩陣,即是矩陣右乘向量 mul(row-vector, column-major matrix)。

這邊要注意的是,矩陣右乘向量的呼叫方式中,矩陣雖然是預設的 column-major 記憶體分配,但是這跟內容形式沒有直接關聯,其中若放入轉換矩陣,應該要是 row-vector 需要的形式 (位移量寫在row 4)。
明明是 column-major,卻儲存著 row-vector 所用的轉換矩陣,不就是上一段落中提到的內容嗎?

組合出結論

綜合上面的資訊,至少有兩種方式可以讓 shader 中能得到正確的座標轉換結果:

  • 在矩陣傳入 constant buffers 前不進行轉置,而 shader 中座標轉換寫為左乘 mul(matrix, vector),這個是類似 OpenGL 習慣的寫法。
  • 在矩陣傳入 constant buffers 前進行轉置,而 shader 中座標轉換寫為右乘 mul(vector, matrix),這個是 DirectX 11 範例中的寫法。

方式一中,本來 row-major 的矩陣不經處理就被視為 column-major 來解讀,這可以視為經過了一次轉置運算,本來給 row-vector 使用的轉換矩陣,被改成了 column-vector 所使用的轉換矩陣形式,因此使用左乘。
方式二中,經過一次的轉置運算,雖然記憶體分配方式改變,但矩陣的形式保持著給 row-vector 使用的模樣,所以使用了右乘。

根據 vstrakhgamedev.net 的留言,矩陣右乘向量會被編譯為 4 個 dot 指令,運算效率上會有一點點優勢。除了保持使用 row-vector 的規則,這點效能差異或許也是 DirectX 11 會在範例中使用方式二的原因之一。

參考連結