本文章是由機器翻譯。

DirectX 要素

頂點著色器與轉換

Charles Petzold

下載代碼示例

Charles Petzold1975 年,理查 · 哈斯畫家畫了平建築物的一側在曼哈頓蘇荷區類似于經典的鑄鐵建築,包括在一個視窗中的這些功能作為一隻貓的門面。時間還沒有對這幅畫,但在其早期的日子裡,它看起來很真實和愚弄了許多人。

這樣的繪畫 — — 被稱為視-l' 建立,意思"欺騙眼睛"— — 使用陰影和網底來帶出平面二維表面上的立體感。我們傾向于被逗樂的這樣的幻覺,但同時也急切地想滿足自己,我們可以辨別訣竅。一個簡單的方法是試著觀察這幅畫從不同的角度來看是否它看上去相同。

當電腦程式會顯示一些似乎跨越 2D 和 3D 間線的圖形時發生類似的心理過程。是真的 3D 嗎?或只是 2D 帶有一些聰明的重疊和網底?我們無法移動我們的頭從一邊到一邊,確定事實,但我們也許能夠說服要旋轉的圖形,看會發生什麼的程式。

在圖像圖 1 看起來很像 ThreeTriangles 程式在本專欄的上一篇文章中所顯示的螢幕 (msdn.microsoft.com/magazine/dn768854)。但本專欄中,稱為 ThreeRotatingTriangles 的可下載程式的確會轉動三個三角形的組合。效果是以可視方式很有趣,並作為三個三角形移動在關係到彼此,該程式並建立是下去確實一些 3D 圖形處理。然而,如果你看看代碼,你會發現完全與 Direct2D,而不是使用 Direct2D 影響的強大功能的 Direct3D 仍然編寫的程式。

ThreeRotatingTriangles 程式顯示
圖 1 ThreeRotatingTriangles 程式顯示

資料進入效果

早期版本的 ThreeTriangles 程式的整體組織已經保存在 ThreeRotatingTriangles。提供 Direct2D 效果實現的類現在稱為旋轉­TriangleEffect,而不是 SimpleTriangleEffect,但它繼續執行 ID2D1EffectImpl (《 影響執行") 和 ID2D1DrawTransform 介面。

早些時候 SimpleTriangleEffect 根本不是多才多藝。它包含硬編碼要顯示三個重疊的三角形的頂點。RotatingTriangleEffect 允許頂點必須定義從班級的外面,並切實執行和頂點著色器已得到加強,以適應矩陣變換。

一般來說,影響實現如 RotatingTriangle­效果包含一個靜態方法,將其自身註冊通過調用 RegisterEffectFromString 並贊同類 id。 在 ThreeRotatingTriangles 程式中,ThreeRotating­TrianglesRenderer 類註冊影響其建構函式中調用此靜態方法。

ThreeRotatingTrianglesRenderer 還作為一個私有欄位定義一個 ID2D1Effect 類型的物件:

Microsoft::WRL::ComPtr<ID2D1Effect>
            m_rotatingTriangleEffect;

若要使用的效果,該程式必須通過引用對 CreateEffect 的調用中的效果的類 ID 創建此物件。在 ThreeRotating­TrianglesRenderer 類,這發生在 CreateDeviceDependent­資源的方法:

d2dContext->CreateEffect(
  CLSID_RotatingTriangleEffect, &m_rotatingTriangleEffect);

然後可以使用調用 DrawImage 方法呈現效果。這裡是 ThreeRotatingTrianglesRenderer 如何在其 Render 方法調用:

d2dContext->DrawImage(m_rotatingTriangleEffect.Get());

但還有更多可以做那些兩次調用之間。ID2D1Effect 來自 ID2D1Properties,具有允許程式在效果上設置屬性的命名 SetValue 和 GetValue 方法。這些屬性的範圍可以從簡單的效果選項表示布林標誌到大資料緩衝區。然而,不論是 SetValue 和 GetValue,這些是不是經常使用的。他們需要通過索引,確定特定的屬性和更大的程式清晰性通過而不使用 SetValueByName 和 GetValueByName 的方法。

請記住,這些 SetValueByName 和 GetValueByName 的方法是 ID2D1Effect 物件,即從 CreateEffect 調用返回的物件的一部分。ID2D1Effect 物件將這些屬性的值傳遞給你寫的影響實現類 — — 實現的 ID2D1EffectImpl 和 ID2D1DrawTransform 介面的類。這似乎有點迴旋,但它有這樣做,所以可以使用註冊的影響,不用對實施效果的類的訪問。

但這意味著如 RotatingTriangleEffect 類影響執行必須本身表明它可以接受各種類型的屬性,它必須提供用於設置和獲取這些屬性的方法。

此資料由效應實現時它會註冊自己使用 RegisterEffectFromString。在此調用中需要的是一些 XML 包含的名稱和類型的各種屬性影響執行支援。RotatingTriangle­支援四個屬性,使用下列名稱和資料類型的影響:

  • VertexData 型 blob,那就是,一個位元組的指標所引用的記憶體緩衝區。
  • ModelMatrix 型 matrix4x4。
  • ViewMatrix 型 matrix4x4。
  • ProjectionMatrix 型 matrix4x4。

這些資料類型的名稱是特定于 Direct2D 影響。當註冊時支援的屬性影響時,影響執行還必須提供一個 D2D1_VALUE_TYPE_BINDING 物件的陣列。此陣列中的物件的每個關聯的設置和獲取資料的效果實現的兩種方法命名的屬性,例如 VertexData。為 VertexData,這兩種方法被命名為 SetVertexData 和 GetVertexData。(當您定義這些 Get 方法時,請確保包含 const 關鍵字,或你會將完全無法判明這些怪異範本錯誤之一。)

同樣,RotatingTriangleEffect 類定義方法命名為 SetModelMatrix 和 GetModelMatrix,如此等等。這些方法不由任何應用程式調用 — — 實際上,他們私有的 RotatingTriangleEffect。相反,程式上的 ID2D1Effect 物件,然後調用 Set 和 Get 方法影響執行調用的 SetValueByName 和 GetValueByName 的方法。

頂點緩衝區

ThreeRotatingTrianglesRenderer 類寄存器旋轉­TrianglesEffect 在其建構函式,並呈現其 Render 方法中的效果。但那些兩次調用,之間呈現器類調用 ID2D1Effect 物件將資料傳遞到影響執行上的 SetValueByName。

第前面列出的四個效果屬性是 VertexData,它是用來定義頂點緩衝區的頂點集合。頂點緩衝區中的項數 Direct2D 效果必須是三個倍數,歸類成三角形。ThreeRotatingTriangles 程式將顯示三個頂點,每個只有三個三角形,但效果可以處理更大的緩衝區。

預期的 RotatingTriangle 的頂點緩衝區的格式­效果在 RotatingTriangleEffect.h 結構中定義:

struct PositionColorVertex
{
  DirectX::XMFLOAT3 position;
  DirectX::XMFLOAT3 color;
};

這是使用的 SimpleTriangleEffect,但略有不同定義相同的格式。圖 2 顯示了如何在 ThreeRotatingTrianglesRenderer 中的 CreateDeviceDependentResources 方法將轉移頂點陣列給影響後造成了一些影響。

圖 2 創建效果和頂點緩衝區設置

void ThreeRotatingTrianglesRenderer::CreateDeviceDependentResources()
{
  ID2D1DeviceContext1* d2dContext =
    m_deviceResources->GetD2DDeviceContext();
  // Create the effect
  DX::ThrowIfFailed(d2dContext->CreateEffect(
                  CLSID_RotatingTriangleEffect,
                  &m_rotatingTriangleEffect)
    );
  // Set the vertices
  std::vector<PositionColorVertex> vertices =
  {
    // Triangle 1
    { XMFLOAT3(0, -1000, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(985, -174, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(342, 940, 1000), XMFLOAT3(0, 0, 1) },
    // Triangle 2
    { XMFLOAT3(866, 500, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(-342, 940, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(-985, -174, 1000), XMFLOAT3(0, 0, 1) },
    // Triangle 3
    { XMFLOAT3(-866, 500, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(-643, -766, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(643, -766, 1000), XMFLOAT3(0, 0, 1) }
  };
  DX::ThrowIfFailed(
    m_rotatingTriangleEffect->SetValueByName(L"VertexData",
      (byte *) &vertices)
    );
  // Ready to render!
  m_readyToRender = true;
}

X 和 Y 座標基於正弦和余弦的角度在 40 度的增量與一個半徑為 1000。 Z 座標範圍從­–1,000,至 1000 為背景的前景。在早些時候的 SimpleTriangleEffect 我是非常小心,介於 0 和 1 之間設置 Z,因為到剪輯 3d 抄數輸出使用的約定。你會看到,這是沒有必要在這裡因為實際相機轉換將應用到頂點。

一個名為 VertexData 的 SetValueByName 調用會導致要調用 SetVertexBuffer 方法在 RotatingTriangleEffect 傳遞資料的 ID2D1Effect 物件。此方法強制轉換回其原始類型和調用 CreateVertexBuffer 來存儲資訊的方式,使它能夠傳遞到頂點著色器位元組的指標。

應用轉換

圖 3 ThreeRotatingTrianglesRenderer 計算三個矩陣變換,使 SetValueByName 的三個調用中顯示更新方法。代碼已經略有簡化為武俠 HRESULT 返回刪除檢查。

圖 3 設置三個變換矩陣

void ThreeRotatingTrianglesRenderer::Update(DX::StepTimer const& timer)
{
  if (!m_readyToRender)
      return;
  // Apply model matrix to rotate vertices
  float angle = float(XM_PIDIV4 * timer.GetTotalSeconds());
  XMMATRIX matrix = XMMatrixRotationY(angle);
  XMFLOAT4X4 float4x4;
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ModelMatrix", float4x4);
  // Apply view matrix
  matrix = XMMatrixLookAtRH(XMVectorSet(0, 0, -2000, 0),
                            XMVectorSet(0, 0, 0, 0),
                            XMVectorSet(0, 1, 0, 0));
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ViewMatrix", float4x4);
  // Base view width and height on coordinates of model
  float width = 2000;
  float height = 2000;
  // Adjust width and height for landscape and portrait modes
  Windows::Foundation::Size logicalSize = 
    m_deviceResources->GetLogicalSize();
  if (logicalSize.Width > logicalSize.Height)
      width *= logicalSize.Width / logicalSize.Height;
  else
      height *= logicalSize.Height / logicalSize.Width;
  // Apply projection matrix   
  matrix = XMMatrixOrthographicRH(width, height, 500, 4000);
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ProjectionMatrix", float4x4);
}

像往常一樣,調用 Update 時在視頻顯示的畫面播放速率。它計算的第一個矩陣應用到頂點可繞 Y 軸旋轉它們。第二個矩陣是轉移現場,所以觀眾是三維座標系統的原點和直沿 Z 軸方向看結果的標準攝像機視圖座標轉換。第三個是一個標準的投影矩陣,在 X 和 Y 座標之間的值被歸哪個結果­– 1 和 1,0 和 1 之間的 Z 座標。

這些矩陣必須應用於頂點座標。因此為什麼不該程式只是將它們乘以在 CreateDeviceDependentResources 方法中定義的頂點陣列的然後在 RotatingTrianglesEffect 中設置一個新的頂點緩衝區?

這是當然可以定義在視頻顯示的畫面播放速率發生變化的動態頂點緩衝區和在某些情況下它是必要。但如果只需要由矩陣變換修改頂點,動態頂點緩衝區並不是盡可能高效維護整個程式相同的緩衝區和應用管道中稍後轉換 — — 具體來說,在頂點著色器在視頻 GPU 上運行。

這意味著頂點著色器的每一幀的視頻顯示,需要新的矩陣變換,並在引發另一個問題:如何影響執行獲得資料到頂點著色器?

著色器固定緩衝區

資料是通過一種稱為一個恒定的緩衝機制從應用程式代碼轉入著色器。Don讓騙你去思考其內容的名稱保持不變在整個過程中的程式。絕對並非如此。很多時候恒定緩衝區更改與每一幀的視頻顯示。然而,固定緩衝區的內容是固定為每個幀中的所有頂點,恒定的緩衝區的格式在編譯時固定的程式。

頂點著色器固定緩衝區的格式定義在兩個地方:在 c + + 代碼和頂點著色器本身中。在影響實現中,它看起來像這樣:

struct VertexShaderConstantBuffer
{
  DirectX::XMFLOAT4X4 modelMatrix;
  DirectX::XMFLOAT4X4 viewMatrix;
  DirectX::XMFLOAT4X4 projectionMatrix;
} m_vertexShaderConstantBuffer;

當渲染器中的更新方法調用設置一個矩陣的 SetValueByName 時,ID2D1Effect 物件在 RotatingTrianglesEffect 類中調用適當的設置的方法。這些方法被命名為 SetModelMatrix,SetViewMatrix 和 SetProjection­矩陣,與他們簡單地轉移矩陣的 m_vertexShaderConstantBuffer 物件中的適當欄位。

ID2D1Effect 物件假定對 SetValueByName 的任何調用結果在變化中,大意是可能涉及相應變更為圖形的輸出,所以的 PrepareForRender 效應實現中調用方法。正是在這裡影響執行可以藉此機會叫 SetVertexShaderConstantBuffer,將 VertexShaderConstantBuffer 的內容轉移到頂點著色器。

新的頂點著色器

現在,最後,你可以看做的大部分工作在旋轉這些三個三角形的頂點,並使它們在 3D 空間中的高級著色語言 (HLSL) 代碼的工作。這是新的和改進的頂點著色器,顯示其整體在圖 4

圖 4 的頂點著色器的旋轉的三角效應

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 color : COLOR0;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Constant buffer provided by effect.
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
};
// Called for each vertex.
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinates
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Transfer the color
  output.color = input.color;
  return output;
}

請注意結構如何定義:在著色器 VertexShaderInput 是在 c + + 標頭檔中定義的 PositionColorVertex 結構相同的格式。VertexShaderConstantBuffer 是在 c + + 代碼中的名稱相同的結構相同的格式。VertexShaderOutput 結構匹配圖元著色器的 PixelShaderInput 結構。

在頂點著色器與 SimpleTriangleEffect 在上個月的列相關聯,ClipSpaceTransforms 緩衝區是提供自動從場景空間 (用於的三角形的頂點的圖元座標) 將轉換為剪輯空間,其中涉及標準化 X 和 Y 座標從-1 到 1,和 Z 座標,範圍從 0 到 1。

這不再是必要的所以我將其刪除沒有任何不幸的後果。相反,投影矩陣做相同的工作。正如你所看到的主要功能將三個矩陣應用到輸入的頂點的位置,並將結果設置為輸出結構的 clipSpaceOuput 欄位。

該 clipSpaceOutput 欄位是必需的。這是深度緩衝區如何管理的以及如何將結果映射到的顯示圖面。然而,VertexShaderOutput 結構的 sceneSpaceOutput 欄位不需要。如果您刪除該欄位 — — 和也從圖元著色器的 PixelShaderInput 結構中刪除欄位 — — 程式將運行相同。

列列專業

著色器將執行三個的位置由矩陣乘法:

pos = mul(pos, modelMatrix);
pos = mul(pos, viewMatrix);
pos = mul(pos, projectionMatrix);

在數學符號中這些乘法看起來像這樣:

m11   m12    m13   m14
m21   m22    m23   m24
m31   m32    m33   m34
m41   m42    m43   m44
|x   y   z   w| ×

當此計算乘法,,描述點 (x,y,z,w) 的四個數字相乘的矩陣 (m11、 m21、 m31,m41) 中的第一列的四個數字和四種產品進行求和,而過程然後繼續第二、 第三和第四列。

向量 (x,y,z,w) 由存儲在連續的記憶體中的四個數字組成。考慮實現並行處理的矩陣乘法的硬體。您認為它可能更快地並存執行這些四個乘法運算,如果每個列中的數位也都存儲在連續的記憶體嗎?這似乎很有可能,這意味著在記憶體中存儲矩陣值的最佳方法是順序 m11、 m21、 m31,m41、 m12、 m22 等等。

這就是所謂的列優先的順序。區塊開頭的第一列的矩陣,然後第二、 第三和第四位。而這是頂點著色器假設必須在記憶體中的矩陣組織執行這些乘法時。

然而,這不是 DirectX 通常存儲在記憶體中的矩陣方法。DirectX 數學庫中的 XMMATRIX 和 XMFLOAT4X4 的結構存儲矩陣按行優先的順序:m11、 m12、 m13、 m14、 m21、 m22 和等等。這看起來更自然的秩序,給我們很多人因為它是類似于我們讀的文本行的順序 — — 經過,然後下來。

無論如何,DirectX 和著色器代碼之間也矛盾之處,這就是為什麼你會注意到在代碼中圖 3 每個矩陣遭受到 XMMatrixTranspose 調用之前被送到頂點著色器。XMMatrixTranspose 函數轉換行主要矩陣列主要矩陣和背部再次如果你需要它。

這就是這個問題最常見的解決方案,但它不是唯一的解決辦法。或者,您可以指定一個標誌來編譯著色器行優先的順序,或者你可以離開乘法矩陣 untransposed 只是開關周圍矩陣和向量的順序:

pos = mul(viewMatrix, pos);

最後一步

三個三角形的網底當然演示頂點顏色插在每個三角形的表面,但結果是相當粗糙。當然我可以做得更好在使用此功能強大的網底工具來類比光的反射。這將是在這輪暴跌中進入 Direct2D 多才多藝世界的最後一步。


Charles Petzold 是長期貢獻 MSDN 雜誌和"程式設計視窗,第 6 版"的作者 (微軟出版社,2013年),一本關於編寫 Windows 8 應用程式書。 他的網站是 charlespetzold.com

感謝以下的微軟技術專家對本文的審閱:Doug埃裡克森