3D 通用 Windows 平臺 (UWP) 遊戲通常會使用多邊形來代表遊戲中的物件和表面。 組成這些多邊形物件和表面結構的頂點清單稱為網格。 在這裡,我們會建立一個立方體物件的基本網格,並將其提供給著色器管線以進行渲染和顯示。
重要 這裡所包含的範例程式代碼會使用類型(例如 DirectX::XMFLOAT3 和 DirectX::XMFLOAT4X4),以及 DirectXMath.h 中宣告的內嵌方法。 如果您要剪下並貼上此程式碼,請在您的專案中 <#include DirectXMath.h>。
您需要知道的事項
技術
先決條件
- 線性代數和三維座標系統的基本知識
- Visual Studio 2015 或更新版本的 Direct3D 範本
操作說明
這些步驟將示範如何建立基本網格立方體。
步驟 1:建構模型的網格
在大部分的遊戲中,遊戲物件的網格會從包含特定頂點數據的檔案載入。 這些頂點的排序取決於應用程式,但它們通常會被序列化為帶狀或風扇的形式。 頂點數據可能來自任何軟體來源,也可以手動建立。 您的遊戲需要解讀數據,並以頂點著色器可以有效處理的方式來準備這些數據。
在我們的範例中,我們使用簡單的網格來表示立方體。 Cube 就像管線中這個階段的任何物件網格一樣,會使用自己的座標系統來表示。 頂點著色器會採用其座標,並且藉由套用您提供的轉換矩陣,傳回同質座標系統中的最終 2D 檢視投影。
定義立方體的網格。 (或從檔案載入它。這取決於你。)
SimpleCubeVertex cubeVertices[] =
{
{ DirectX::XMFLOAT3(-0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 0.0f) }, // +Y (top face)
{ DirectX::XMFLOAT3( 0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 0.0f) },
{ DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 1.0f) },
{ DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 1.0f) },
{ DirectX::XMFLOAT3(-0.5f, -0.5f, 0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 1.0f) }, // -Y (bottom face)
{ DirectX::XMFLOAT3( 0.5f, -0.5f, 0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 1.0f) },
{ DirectX::XMFLOAT3( 0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 0.0f) },
{ DirectX::XMFLOAT3(-0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f) },
};
立方體的座標系統會將立方體的中央置於原點,使用左手座標系統,Y 軸從上至下延伸。 座標值會以 -1 與 1 之間的 32 位浮點數表示。
在每個括弧配對中,第二個 DirectX::XMFLOAT3值群組會將與頂點相關聯的色彩指定為 RGB 值。 例如,位於 (-0.5, 0.5, -0.5) 的第一個頂點有完整的綠色 (G 值設定為 1.0,而 “R” 和 “B” 值則設定為 0)。
因此,您有 8 個頂點,每個頂點都有特定的色彩。 每個頂點/色彩配對都是範例中頂點的完整數據。 當您指定我們的頂點緩衝區時,您必須記住這個特定的布局。 我們會將此輸入配置提供給頂點著色器,以便瞭解您的頂點數據。
步驟 2:設定輸入配置
現在,您已經有記憶體裡的頂點。 但是,您的圖形裝置有自己的記憶體,而且您可以使用 Direct3D 來存取它。 若要將頂點數據放入圖形裝置進行處理,您必須先清理途徑:也就是說,您需要宣告頂點數據的配置方式,讓圖形裝置可以在從遊戲中取得頂點數據時正確解譯。 若要這樣做,您可以使用 ID3D11InputLayout。
宣告並設定頂點緩衝區的輸入配置。
const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
ComPtr<ID3D11InputLayout> inputLayout;
m_d3dDevice->CreateInputLayout(
basicVertexLayoutDesc,
ARRAYSIZE(basicVertexLayoutDesc),
vertexShaderBytecode->Data,
vertexShaderBytecode->Length,
&inputLayout)
);
在此程式代碼中,您會指定頂點的配置,特別是頂點清單中每個元素包含的數據。 在這裡,在 basicVertexLayoutDesc中,您會指定兩個數據元件:
POSITION:這是用於提供給著色器的位置信息的 HLSL 語義。 在此程式代碼中,它是 DirectX::XMFLOAT3,或更具體地說,是一個包含三個 32 位元浮點數的結構,對應於 3D 座標(x、y、z)。 您也可以使用 float4,如果您提供同質的 "w" 座標,在這種情況下,您可以指定 DXGI_FORMAT_R32G32B32A32_FLOAT。 無論您使用 DirectX::XMFLOAT3 還是 float4,都取決於您的遊戲特定需求。 確保您的網格的頂點資料正確對應至您使用的格式!
每個座標值都會以物件座標空間中 -1 與 1 之間的浮點值表示。 當頂點著色器完成時,已轉換的頂點會位於齊次(透視修正)檢視投影空間中。
您機智地指出,列舉值表示的是 RGB,而不是 XYZ! 眼力真好! 在這兩種色彩數據和座標數據的情況下,您通常會使用 3 或 4 個元件值,所以為什麼不針對兩者使用相同的格式? HLSL 語意,而非格式名稱,表示著色器如何處理數據。
COLOR:這是色彩數據的 HLSL 語意。 就像 POSITION一樣,它是由 3 個 32 位浮點值 (DirectX::XMFLOAT3) 所組成。 每個值都包含色彩元件:紅色(r)、藍色(b)或綠色(g),以介於 0 到 1 之間的浮點數表示。
COLOR 值通常會在著色器管線的結尾,以 4 元件的 RGBA 值形式傳回。 在此範例中,您會將所有圖元的著色器管線中的 “A” Alpha 值設定為 1.0 (最大不透明度)。
如需格式的完整清單,請參閱 DXGI_FORMAT。 如需 HLSL 語意的完整清單,請參閱 語意。
呼叫 ID3D11Device::CreateInputLayout 並在 Direct3D 裝置上建立輸入配置。 現在,您必須建立可實際保存數據的緩衝區!
步驟 3:填入頂點緩衝區
頂點緩衝區包含網格中每個三角形的頂點清單。 此清單中每個頂點都必須是唯一的。 在我們的範例中,您有 8 個立方體的頂點。 頂點著色器會在圖形裝置上執行,並從頂點緩衝區讀取,並根據您在上一個步驟中指定的輸入配置來解譯數據。
在下一個範例中,您會提供緩衝區的描述和子資源,以告知 Direct3D 關於頂點數據的實體對應,以及如何在圖形裝置上的記憶體中加以處理。 這是必要的,因為您使用了一般的 ID3D11Buffer,它可以包含任何資料! 提供 D3D11_BUFFER_DESC 和 D3D11_SUBRESOURCE_DATA 結構,以確保 Direct3D 瞭解緩衝區的實體記憶體配置,包括緩衝區中每個頂點元素的大小,以及頂點清單的大小上限。 您也可以控制緩衝記憶體的存取權限,以及其遍歷方式,但這有點超出本教學課程的範圍。
設定緩衝區之後,您可以呼叫 ID3D11Device::CreateBuffer 來實際建立它。 顯然,如果您有多個物件,請為每個唯一模型建立緩衝區。
宣告並建立頂點緩衝區。
D3D11_BUFFER_DESC vertexBufferDesc = {0};
vertexBufferDesc.ByteWidth = sizeof(SimpleCubeVertex) * ARRAYSIZE(cubeVertices);
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vertexBufferData;
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
ComPtr<ID3D11Buffer> vertexBuffer;
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&vertexBuffer);
載入頂點。 但是處理這些頂點的順序為何? 當您將索引清單對應到頂點時,就會按照索引的順序進行處理,而這個順序正是頂點著色器處理它們的順序。
步驟 4:填充索引緩衝區
現在,您需要提供每個頂點的索引列表。 這些索引會對應至頂點緩衝區中頂點的位置,從 0 開始。 為了協助您將此可視化,請考慮網格中的每個唯一頂點都有指派的唯一數位,例如標識符。 這個標識碼是頂點緩衝區中頂點的整數位置。
在我們的範例 Cube 中,您有 8 個頂點,這會為側邊建立 6 個四邊形。 您會將四邊形分割成三角形,總共有12個三角形使用我們的8個頂點。 每個三角形有 3 個頂點,因此您的索引緩衝區中有 36 個項目。 在我們的範例中,此索引模式稱為三角形清單,當您設定基本拓撲時,會將 Direct3D 指示為 D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST。
這可能是列出索引值最沒有效率的方式,因為當三角形共用點和邊時會有許多重複。 例如,當三角形與菱形共用一條邊時,您會為四個頂點列出六個索引,如下所示:
建構菱形時,索引的順序為 、
- 三角形 1: [0, 1, 2]
- 三角形 2: [0, 2, 3]
在分條或風扇拓撲中,您會以在遍歷過程中消除許多冗餘的邊的方式排序頂點(例如影像中的索引 0 到索引 2 的邊)。對於大型網格,這可大幅減少頂點著色器的執行次數,並顯著提高效能。 不過,我們會保持簡單,並堅持使用三角形清單。
將頂點緩衝區的索引定義為簡單的三角形列表拓撲。
unsigned short cubeIndices[] =
{ 0, 1, 2,
0, 2, 3,
4, 5, 6,
4, 6, 7,
3, 2, 5,
3, 5, 4,
2, 1, 6,
2, 6, 5,
1, 7, 6,
1, 0, 7,
0, 3, 4,
0, 4, 7 };
當您只有 8 個頂點時,緩衝區中的 36 個索引元素非常多餘! 如果您選擇消除某些重複,並使用不同的頂點清單類型,例如條帶或扇形,則必須在向 ID3D11DeviceContext::IASetPrimitiveTopology 方法提供特定 D3D11_PRIMITIVE_TOPOLOGY 值時指定該類型。
如需不同索引清單技術的詳細資訊,請參閱 基本拓撲。
步驟 5:為您的轉換矩陣建立常數緩衝區
在您開始處理頂點之前,需要提供轉換矩陣,這些矩陣在執行時會用於乘以每個頂點。 對於大多數 3D 遊戲,有三個:
- 從物件(模型)座標系統轉換成整體世界座標系統的 4x4 矩陣。
- 從世界座標系統轉換成相機(檢視)座標系統的 4x4 矩陣。
- 從相機座標系統轉換成二維視圖投影座標系統的 4x4 矩陣。
這些矩陣會透過 常數緩衝區傳遞到著色器。 常數緩衝區是一個記憶體區域,在著色器管線下一次執行過程中保持不變,並且可以由 HLSL 程式碼中的著色器直接存取。 您可以定義每個常數緩衝區兩次:第一次是在遊戲的C++程序代碼中,並在著色器程式代碼的類似 C 型 HLSL 語法中至少定義一次。 這兩個宣告必須在類型和數據對齊方面直接對應。 當著色器使用 HLSL 宣告來解釋在 C++ 中宣告的數據,而類型不匹配或數據未對齊時,很容易引入難以發現的錯誤!
HLSL 不會變更常數緩衝區。 當您的遊戲更新特定資料時,您可以變更它們。 遊戲開發人員通常會建立四種類型的常數緩衝區:一種用於每幀更新;一種用於每個模型/物件更新;一種用於遊戲狀態每次刷新時的更新;以及一種用於在遊戲存續期間永遠不變的數據。
在此範例中,我們只有一組永遠不會變更的數據:三個矩陣通用的 DirectX::XMFLOAT4X4 數據。
附註 這裡的範例程式碼使用了列優先矩陣。 您可以在 HLSL 中使用 row_major 關鍵詞來使用行優先的矩陣,並確保您的源矩陣數據也是行優先的。 DirectXMath 使用數據列主要矩陣,可以直接搭配以 row_major 關鍵詞定義的 HLSL 矩陣使用。
針對您用來轉換每個頂點的三個矩陣,宣告並建立常數緩衝區。
struct ConstantBuffer
{
DirectX::XMFLOAT4X4 model;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
};
ComPtr<ID3D11Buffer> m_constantBuffer;
ConstantBuffer m_constantBufferData;
// ...
// Create a constant buffer for passing model, view, and projection matrices
// to the vertex shader. This allows us to rotate the cube and apply
// a perspective projection to it.
D3D11_BUFFER_DESC constantBufferDesc = {0};
constantBufferDesc.ByteWidth = sizeof(m_constantBufferData);
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags = 0;
constantBufferDesc.MiscFlags = 0;
constantBufferDesc.StructureByteStride = 0;
m_d3dDevice->CreateBuffer(
&constantBufferDesc,
nullptr,
&m_constantBuffer
);
m_constantBufferData.model = DirectX::XMFLOAT4X4( // Identity matrix, since you are not animating the object
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
);
// Specify the view (camera) transform corresponding to a camera position of
// X = 0, Y = 1, Z = 2.
m_constantBufferData.view = DirectX::XMFLOAT4X4(
-1.00000000f, 0.00000000f, 0.00000000f, 0.00000000f,
0.00000000f, 0.89442718f, 0.44721359f, 0.00000000f,
0.00000000f, 0.44721359f, -0.89442718f, -2.23606800f,
0.00000000f, 0.00000000f, 0.00000000f, 1.00000000f);
注意 您通常會在設定裝置特定資源時宣告投影矩陣,因為其乘積結果必須與目前的 2D 檢視區大小參數相符(這通常對應於顯示器的像素高度和寬度)。 如果這些變更,您必須據以縮放 x 和 Y 座標值。
// Finally, update the constant buffer perspective projection parameters
// to account for the size of the application window. In this sample,
// the parameters are fixed to a 70-degree field of view, with a depth
// range of 0.01 to 100.
float xScale = 1.42814801f;
float yScale = 1.42814801f;
if (backBufferDesc.Width > backBufferDesc.Height)
{
xScale = yScale *
static_cast<float>(backBufferDesc.Height) /
static_cast<float>(backBufferDesc.Width);
}
else
{
yScale = xScale *
static_cast<float>(backBufferDesc.Width) /
static_cast<float>(backBufferDesc.Height);
}
m_constantBufferData.projection = DirectX::XMFLOAT4X4(
xScale, 0.0f, 0.0f, 0.0f,
0.0f, yScale, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, -0.01f,
0.0f, 0.0f, -1.0f, 0.0f
);
當您在這裡時,請在ID3D11DeviceContext上設定頂點和索引緩衝區,以及您使用的拓撲。
// Set the vertex and index buffers, and specify the way they define geometry.
UINT stride = sizeof(SimpleCubeVertex);
UINT offset = 0;
m_d3dDeviceContext->IASetVertexBuffers(
0,
1,
vertexBuffer.GetAddressOf(),
&stride,
&offset);
m_d3dDeviceContext->IASetIndexBuffer(
indexBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0);
m_d3dDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
好吧! 輸入組件完成。 所有一切都已就緒以供渲染。 讓我們開始運行頂點著色器。
步驟 6:使用頂點著色器處理網格
現在您有頂點緩衝區,其中包含定義網格的頂點,以及定義頂點處理順序的索引緩衝區,您可以將它們傳送至頂點著色器。 以編譯的高階著色器語言表示的頂點著色器程式代碼會針對頂點緩衝區中的每個頂點執行一次,讓您執行每個頂點轉換。 最終結果通常是 2D 投影。
(您是否載入頂點著色器?如果沒有,請檢閱 如何在 DirectX 遊戲中載入資源。)
在這裡,您會建立頂點著色器...
// Set the vertex and pixel shader stage state.
m_d3dDeviceContext->VSSetShader(
vertexShader.Get(),
nullptr,
0);
...並設定常數緩衝區。
m_d3dDeviceContext->VSSetConstantBuffers(
0,
1,
m_constantBuffer.GetAddressOf());
以下是頂點著色器程序代碼,可處理從物件座標到世界座標的轉換,然後轉換成 2D 檢視投影座標系統。 您也會套用一些簡單的逐頂點光源技術,以增添美觀。 這會出現在頂點著色器的 HLSL 檔案中(在此範例中為 SimplerVertexShader.hlsl)。
cbuffer simpleConstantBuffer : register( b0 )
{
matrix model;
matrix view;
matrix projection;
};
struct VertexShaderInput
{
DirectX::XMFLOAT3 pos : POSITION;
DirectX::XMFLOAT3 color : COLOR;
};
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float4 color : COLOR;
};
PixelShaderInput SimpleVertexShader(VertexShaderInput input)
{
PixelShaderInput vertexShaderOutput;
float4 pos = float4(input.pos, 1.0f);
// Transform the vertex position into projection space.
pos = mul(pos, model);
pos = mul(pos, view);
pos = mul(pos, projection);
vertexShaderOutput.pos = pos;
// Pass the vertex color through to the pixel shader.
vertexShaderOutput.color = float4(input.color, 1.0f);
return vertexShaderOutput;
}
你看到在頂端的 cbuffer 嗎? 這就是 HLSL 類似於我們先前在C++程式代碼中宣告的相同常數緩衝區。 是嗎,VertexShaderInputstruct? 咦,這看起來就像你的輸入配置和頂點數據宣告! 請務必C++程序代碼中的常數緩衝區和頂點數據宣告符合 HLSL 程式代碼中的宣告,且包含符號、類型和數據對齊。
PixelShaderInput 指定頂點著色器主要函式所傳回之數據的資料結構。 當您完成處理頂點後,您將傳回 2D 投影空間中的頂點位置及用於逐頂點光照的色彩。 顯示卡會使用著色器輸出的數據來計算在管線下一個階段執行像素著色器時必須著色的「片段」(可能的像素)。
步驟 7:透過圖元著色器傳遞網格
通常,在圖形管線的這個階段,您會在物件的可見的投影表面上執行逐像素運算。 人們喜歡紋理。然而,為了示例的目的,您只需將它傳遞到這個階段即可。
首先,讓我們建立圖元著色器的實例。 像素著色器會對場景的 2D 投影中的每個像素進行運算,並為這些像素指定色彩。 在此情況下,我們會直接傳遞由頂點著色器返回的像素色彩。
設定圖元著色器。
m_d3dDeviceContext->PSSetShader( pixelShader.Get(), nullptr, 0 );
在 HLSL 中定義透傳圖元著色器。
struct PixelShaderInput
{
float4 pos : SV_POSITION;
};
float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
// Draw the entire triangle yellow.
return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
將此程式代碼放在 HLSL 檔案中,與頂點著色器 HLSL 分開(例如 SimplePixelShader.hlsl)。 此程式代碼會針對檢視區(即您正在繪製的螢幕部分)中每一個可見像素執行一次,其在記憶體中的表示與整個螢幕對應。 現在,您的圖形管線已完全定義!
步驟 8:點陣化和顯示網格
讓我們執行流程。 這很簡單:呼叫 ID3D11DeviceContext::DrawIndexed。
繪製該立方體!
// Draw the cube.
m_d3dDeviceContext->DrawIndexed( ARRAYSIZE(cubeIndices), 0, 0 );
在圖形卡內,每個頂點都會以索引緩衝區中指定的順序進行處理。 在您的程式代碼執行頂點著色器和定義 2D 片段之後,會叫用圖元著色器,並標示三角形。
現在,將 Cube 放在畫面上。
將該畫面緩衝區呈現在顯示器上。
// Present the rendered image to the window. Because the maximum frame latency is set to 1,
// the render loop is generally throttled to the screen refresh rate, typically around
// 60 Hz, by sleeping the app on Present until the screen is refreshed.
m_swapChain->Present(1, 0);
你完成了! 對於充滿模型的場景,您應使用多個頂點和索引緩衝區;此外,您甚至可能依據不同的模型類型來使用不同的著色器。 請記住,每個模型都有自己的座標系統,而且您需要使用您在常數緩衝區中定義的矩陣,將它們轉換成共用世界座標系統。
備註
本主題涵蓋建立及顯示您自行建立的簡單幾何。 如需從檔案載入更複雜的幾何並將其轉換成範例特定頂點緩衝區物件 (.vbo) 格式的詳細資訊,請參閱 如何在 DirectX 遊戲中載入資源。
相關主題
- 如何在 DirectX 遊戲 載入資源