DirectX11--HLSL中矩陣的內存佈局和mul函數探討

前言

說實話,我感受這是一個大坑,不知道爲何要設計成這樣混亂的形式。html

在我用的時候,以row_major矩陣,而且mul函數以向量左乘矩陣的形式來繪製時的確可以正常顯示,並不會有什麼感受。可是也有人會遇到明明傳的矩陣沒有問題,卻怎麼樣都繪製不出的狀況;或者使用列矩陣,在mul函數用向量左乘的形式卻又能夠繪製出來的疑問。所以本文目的就是要掃清這些障礙。windows

ps. 本問題由淡一抹夕霞提供。函數

DirectX11 With Windows SDK完整目錄佈局

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。性能

一些線性代數基礎

行主矩陣與列主矩陣

首先要了解的是,行主矩陣是這樣的:優化

\[ \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}\]spa

列主矩陣是這樣的:.net

\[ \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}\]翻譯

行主矩陣通過一次轉置後就會變成列主矩陣設計

矩陣左乘與右乘

因爲矩陣乘法不知足交換律,則須要區分當前矩陣位於乘號的左邊仍是右邊。有時候常常都會聽到左乘右乘這兩個概念,下面是有關它們的含義:

左乘指的是該矩陣位於乘號的左邊,例如:行向量 左乘 矩陣,即行向量在乘號的左邊

右乘指的是該矩陣位於乘號的右邊,例如:列向量 右乘 矩陣,即列向量在乘號的右邊

ps. 向量也是矩陣

行向量v和矩陣M知足下面的關係:

\[ \mathbf{(vM)}^{T} = \mathbf{M}^{T} \mathbf{v}^{T} \]

C++和HLSL中矩陣的內存佈局

在C++的DirectXMath中,不管是XMFLOAT4X4,仍是使用函數生成的XMMATRIX,都是採用行主矩陣的存儲方式。在連續內存中的佈局是這樣的:
\[ 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}\]

在C++傳遞給HLSL的字節流數據是不會發生變化的,這一點能夠經過VS自帶的圖形調試器能夠察看。

可是數據傳遞給HLSL後,matrix(float4x4)的屬性決定如何去接受這些數據。

默認狀況下,matrix(float4x4)是列矩陣,這意味着它會按列主矩陣的形式進行選取,至關於進行了一次轉置。

若是想讓它按行主矩陣的形式進行選取,則應當在前面加上row_major修飾符以免"轉置"。

HLSL中的mul函數

微軟的官方文檔是這麼描述mul函數的(微軟官方文檔連接),這裏進行我的翻譯:

使用矩陣數學來進行矩陣x左乘矩陣y的運算,要求矩陣x的列數與矩陣y的行數相等。

若是x是一個向量,那麼它將被解釋爲行向量。

若是y是一個向量,那麼它將被解釋爲列向量。

表面上看起來很美滿,很智能,但稍有不慎就要在這裏踩大坑了。

dp4指令

dp4是一個彙編指令(微軟官方文檔連接),使用方法以下:

dp4 dst, src0, src1

其中 src0和src1是一個向量,計算它們的點乘並將結果傳給dst。

固然這裏並非要教你們怎麼寫彙編,而是怎麼看。

爲了瞭解mul函數是如何進行向量與矩陣的乘法運算,咱們須要探討一下它的彙編實現。這裏我所使用的是row_major矩陣。首先是向量做爲第一個參數的狀況:

能夠看到這種運算方式實際上倒是按照向量右乘矩陣的形式進行的運算。

而後是將向量做爲第二個參數的狀況(僅單純的參數交換):

不管是行向量左乘矩陣,仍是列向量右乘矩陣,在彙編層面上都是用dp4的形式進行計算,這是由於對矩陣來講在內存上是以4個行向量的形式存儲的,傳遞一行比傳遞一列更簡單,適合進行與列向量的運算,而且效率會更高。

可是交換兩個參數卻會致使運算結果/顯示結果的不一樣,這時候就要看看矩陣所存的值了。

先看一段HLSL代碼:

struct VertexPosNormalTex
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct VertexPosHWNormalTex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION; // 在世界中的位置
    float3 NormalW : NORMAL; // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

// 頂點着色器
VertexPosHWNormalTex VS(VertexPosNormalTex pIn)
{
    VertexPosHWNormalTex pOut;
    
    row_major matrix viewProj = mul(gView, gProj);

    pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
    pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
    pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
    pOut.Tex = pIn.Tex;
    return pOut;
}

咱們只考慮viewProj的初始化和pOut.PosH的賦值操做。

首先是viewProj本來的值:

這是向量左乘矩陣時四個向量寄存器的值(默認HLSL):

這是向量右乘矩陣時四個向量寄存器的值(將float4(pOut.PosW, 1.0f)viewProj交換):

能夠發現這裏面隱含了一次轉置操做。這裏的轉置不是憑空出現的,而是源自於這句話前面的代碼所產生的彙編(默認HLSL):

而將float4(pOut.PosW, 1.0f)viewProj交換後,則彙編代碼沒有了轉置操做:

所以,咱們能夠知道一個行向量左乘行主矩陣時,爲了知足mul函數使用dp4指令優化運算,極可能會預先對原來的矩陣進行轉置。其中r4 r5 r6 r3爲viewProj轉置後的矩陣,即將會左乘向量float4(pOut.PosW, 1.0f)。對於dp4來講,最好是可以對一個行向量和列矩陣(取列矩陣的行與行向量作點乘)操做,又或者是對一個行矩陣(取行矩陣的行與列向量作點乘)和列矩陣操做,這樣都能有效避免轉置。

總結

綜上所述,有三處地方可能會發生轉置:

  1. C++代碼端的轉置
  2. HLSL中matrix(float4x4)是列主矩陣時會發生轉置
  3. mul乘法內部是以列向量右乘矩陣的形式實現的,對於行向量左乘矩陣的狀況會發生轉置

通過組合,就一共有四種可以正常繪製的狀況:

  1. C++代碼端不進行轉置,HLSL中使用row_major matrix(行矩陣),mul函數讓向量放在左邊(行向量),這樣實際運算就是(行向量 X 行矩陣) 。這種方法易於理解,可是這樣作dp4運算取矩陣的列很不方便,在HLSL中會產生用於轉置矩陣的大量指令,性能上略有損失。
  2. C++代碼端進行轉置,HLSL中使用matrix(列矩陣) ,mul函數讓向量放在左邊(行向量),實際運算是(行向量 X 行矩陣) 而後行矩陣爲了使用dp4運算髮生了轉置成了列矩陣 。這是官方例程所使用的方式,這樣可使得dp4運算能夠直接取列矩陣的行,從而避免內部產生大量的轉置指令,但這樣就還須要C++那邊貢獻一次轉置。後續我會將教程的項目也使用這種方式。
  3. C++代碼端不進行轉置,HLSL中使用matrix(列矩陣),mul函數讓向量放在右邊(列向量),實際運算是(列矩陣 X 列向量)。這種方法的確可行,取列矩陣的行也比較方便,效率上又和2等同,就是HLSL那邊的矩陣乘法都要反過來寫,然而DX自己就是崇尚行主矩陣的,把OpenGL的習慣帶來這邊有點。。。
  4. C++代碼端進行轉置,HLSL中使用row_major matrix(行矩陣),mul函數讓向量放在右邊(列向量)。 就算這種方法也能夠繪製出來,但仍是很讓人難受,比第2點還難受,我甚至不想去說它。

也就是說,以組合1爲基準,任意改變其中兩個狀態(即轉置兩次)都不會影響最終結果。

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索