瞭解 Direct3D 11 渲染管線
先前,您已了解如何建立一個可用於繪圖的視窗,以便在 使用 DirectX 裝置資源。 現在,您將瞭解如何建置圖形管線,以及您可以在其中連結的位置。
您會記得有兩個 Direct3D 介面來定義圖形管線:ID3D11Device,提供 GPU 及其資源的虛擬表示;以及 ID3D11DeviceContext,代表管線的圖形處理。 一般而言,您會使用 ID3D11Device 實例來設定及取得開始處理場景中圖形所需的 GPU 資源,並使用 ID3D11DeviceContext,在圖形管線中每個適當的著色器階段處理這些資源。 您通常不常呼叫 ID3D11Device 方法,也就是說,只有在您設定場景或裝置變更時。 另一方面,您每次處理畫面以顯示時,都會呼叫 ID3D11DeviceContext。
此範例會建立並設定適用於顯示簡單旋轉、頂點陰影立方體的最小圖形管線。 它展示了大約顯示所需資源的最小集合。 當您在這裡閱讀資訊時,請注意指定範例的限制,您可能必須擴充它以支援您想要轉譯的場景。
本範例涵蓋兩個圖形C++類別:裝置資源管理員類別和 3D 場景轉譯器類別。 本主題特別著重於 3D 場景轉譯器。
Cube 轉譯器有何用途?
圖形管線是由 3D 場景轉譯器類別所定義。 場景轉譯器能夠:
- 定義常數緩衝區來儲存統一數據。
- 定義頂點緩衝區來保存物件頂點數據,以及對應的索引緩衝區,讓頂點著色器能夠正確行走三角形。
- 建立紋理資源和資源檢視。
- 載入著色器物件。
- 更新圖形數據以顯示每個畫面。
- 將圖形轉譯(繪製)至交換鏈結。
前四個程式通常會使用 ID3D11Device 介面方法來初始化和管理圖形資源,最後兩個程式會使用 ID3D11DeviceContext 介面方法來管理和執行圖形管線。
Renderer 類別的實例會在主要項目類別上建立及管理為成員變數。 DeviceResources 實例是透過共享指標來管理,並跨越數個類別,包括主要專案類別、App 視圖提供者類別,以及 渲染器。 如果您以您自己的類別取代 Renderer,請考慮將 DeviceResources 實例宣告並指派為共用指標成員:
std::shared_ptr<DX::DeviceResources> m_deviceResources;
在 App 類別的 Initialize 方法中建立 DeviceResources 實例之後,只需將指標傳入類別建構函式(或其他初始化方法)。 如果您想要主要類別完全擁有 DeviceResources 實例,也可以傳遞 weak_ptr 參考作為替代方案。
建立立方體渲染器
在此範例中,我們會使用下列方法來組織場景轉譯器類別:
- CreateDeviceDependentResources:每當必須初始化或重新啟動場景時呼叫。 此方法會載入初始頂點數據、紋理、著色器和其他資源,並建構初始常數和頂點緩衝區。 一般而言,這裡的大部分工作都是使用 ID3D11Device 方法來完成,而不是 ID3D11DeviceContext 方法。
- CreateWindowSizeDependentResources:每當窗口狀態變更時呼叫,例如重設大小發生或方向變更時。 此方法會重建轉換矩陣,例如用於相機的轉換矩陣。
- 更新:通常從管理立即遊戲狀態的程式部分呼叫;在此範例中,我們只是從 Main 類別中呼叫它。 讓此方法讀取任何影響轉譯的遊戲狀態資訊,例如物件位置或動畫畫面的更新,以及任何全域遊戲數據,例如光線等級或遊戲物理變更。 這些輸入可用來更新每個畫面格常數緩衝區和對象數據。
- 轉譯:通常從管理遊戲迴圈的程式部分呼叫;在此情況下,它會從Main 類別呼叫。 此方法會建構圖形管線:它會綁定著色器、將緩衝區和資源綁定到著色器階段,並調用當前畫框的繪圖。
這些方法構成了使用 Direct3D 和您的資產來呈現場景的行為主體。 如果您使用新的轉譯類別擴充此範例,請在主要項目類別上宣告它。 因此,這:
std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;
變成這樣:
std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;
同樣地,請注意,這個範例假設方法在實作中具有相同的簽章。 如果簽章已變更,請檢閱 Main 迴圈,並據此進行修改。
讓我們更詳細地看看場景渲染方法。
建立裝置相依資源
CreateDeviceDependentResources 會使用 ID3D11Device 呼叫來合併所有初始化場景及其資源的作業。 這個方法假設 Direct3D 裝置剛剛針對場景初始化(或已重新建立)。 它會重新建立或重載所有場景特定的圖形資源,例如頂點和像素著色器、對象的頂點和索引緩衝區,以及任何其他資源(例如紋理及其對應的檢視)。
以下是 CreateDeviceDependentResources的範例程序代碼:
void Renderer::CreateDeviceDependentResources()
{
// Compile shaders using the Effects library.
auto CreateShadersTask = Concurrency::create_task(
[this]( )
{
CreateShaders();
}
);
// Load the geometry for the spinning cube.
auto CreateCubeTask = CreateShadersTask.then(
[this]()
{
CreateCube();
}
);
}
void Renderer::CreateWindowSizeDependentResources()
{
// Create the view matrix and the perspective matrix.
CreateViewAndPerspective();
}
每當您從磁碟載入資源(例如編譯的著色器物件(CSO 或 .cso)檔案或紋理)時,請以異步方式載入。 這可讓您讓其他工作同時進行(就像其他設定工作一樣),而且因為主要循環並未遭到封鎖,所以您可以繼續向用戶顯示一些視覺效果有趣的專案(例如您遊戲的載入動畫)。 此範例使用從 Windows 8 開始可用的並行::工作 API;請注意用來封裝異步載入工作的 Lambda 語法。 這些 Lambda 代表稱為 off-thread 的函式,因此明確擷取目前類別物件的指標(此)。
以下是如何載入著色器位元組程式代碼的範例:
HRESULT hr = S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();
// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;
size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];
fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
bytes,
bytesRead,
nullptr,
&m_pVertexShader
);
D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
{ "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 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayout
);
delete bytes;
bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
bytes,
bytesRead,
nullptr,
m_pPixelShader.GetAddressOf()
);
delete bytes;
CD3D11_BUFFER_DESC cbDesc(
sizeof(ConstantBufferStruct),
D3D11_BIND_CONSTANT_BUFFER
);
hr = device->CreateBuffer(
&cbDesc,
nullptr,
m_pConstantBuffer.GetAddressOf()
);
fclose(vShader);
fclose(pShader);
以下是如何建立頂點和索引緩衝區的範例:
HRESULT Renderer::CreateCube()
{
HRESULT hr = S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();
// Create cube geometry.
VertexPositionColor CubeVertices[] =
{
{DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3( 0, 0, 0),},
{DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3( 0, 0, 1),},
{DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3( 0, 1, 0),},
{DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3( 0, 1, 1),},
{DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3( 1, 0, 0),},
{DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3( 1, 0, 1),},
{DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3( 1, 1, 0),},
{DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3( 1, 1, 1),},
};
// Create vertex buffer:
CD3D11_BUFFER_DESC vDesc(
sizeof(CubeVertices),
D3D11_BIND_VERTEX_BUFFER
);
D3D11_SUBRESOURCE_DATA vData;
ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
vData.pSysMem = CubeVertices;
vData.SysMemPitch = 0;
vData.SysMemSlicePitch = 0;
hr = device->CreateBuffer(
&vDesc,
&vData,
&m_pVertexBuffer
);
// Create index buffer:
unsigned short CubeIndices [] =
{
0,2,1, // -x
1,2,3,
4,5,6, // +x
5,7,6,
0,1,5, // -y
0,5,4,
2,6,7, // +y
2,7,3,
0,4,6, // -z
0,6,2,
1,3,7, // +z
1,7,5,
};
m_indexCount = ARRAYSIZE(CubeIndices);
CD3D11_BUFFER_DESC iDesc(
sizeof(CubeIndices),
D3D11_BIND_INDEX_BUFFER
);
D3D11_SUBRESOURCE_DATA iData;
ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
iData.pSysMem = CubeIndices;
iData.SysMemPitch = 0;
iData.SysMemSlicePitch = 0;
hr = device->CreateBuffer(
&iDesc,
&iData,
&m_pIndexBuffer
);
return hr;
}
此範例不會載入任何網格或紋理。 您必須建立方法來載入遊戲特有的網格和紋理類型,並以異步方式呼叫它們。
在這裡也填入每個場景常數緩衝區的初始值。 每個場景常數緩衝區的範例包括固定燈或其他靜態場景元素和數據。
實作 CreateWindowSizeDependentResources 方法
每次視窗大小、方向或解析度變更時,都會呼叫 CreateWindowSizeDependentResources 方法。
視窗大小資源會像這樣更新:靜態訊息處理程序會接收數個可能事件之一,以指出視窗狀態的變更。 接著,您的主要迴圈會通知事件,並在主要類別實例上呼叫 CreateWindowSizeDependentResources,然後呼叫 CreateWindowSizeDependentResources 場景轉譯器類別的實作。
此方法的主要作業是確定視覺效果不會因為視窗屬性中的變更而變得混淆或無效。 在此範例中,我們會使用重新調整或重新導向視窗的新檢視欄位來更新專案矩陣。
我們已經在 DeviceResources 中看到用於建立視窗資源的程式碼,那就是交換鏈(包含後台緩衝區)和渲染目標視圖。 以下是渲染器如何建立外觀比例相依轉換:
void Renderer::CreateViewAndPerspective()
{
// Use DirectXMath to create view and perspective matrices.
DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
DirectX::XMVECTOR at = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
DirectX::XMVECTOR up = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);
DirectX::XMStoreFloat4x4(
&m_constantBufferData.view,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixLookAtRH(
eye,
at,
up
)
)
);
float aspectRatioX = m_deviceResources->GetAspectRatio();
float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;
DirectX::XMStoreFloat4x4(
&m_constantBufferData.projection,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixPerspectiveFovRH(
2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
aspectRatioX,
0.01f,
100.0f
)
)
);
}
如果您的場景有相依於外觀比例的特定元件配置,這就是重新排列元件以符合該外觀比例的地方。 您也可以在這裡變更後續處理行為的設定。
實作更新方法
每個遊戲迴圈中會呼叫一次 Update 方法 - 在此範例中,這個方法由主類別中的同名方法呼叫。 它有一個簡單的用途:根據上一個畫面格之後經過的時間量(或經過的時間步驟)更新場景的幾何結構和遊戲狀態。 在此範例中,我們只會在每個畫面上旋轉 Cube 一次。 在實際的遊戲場景中,此方法包含更多程式碼,用於檢查遊戲狀態、更新每個畫面(或其他動態)常數緩衝區、幾何緩衝區和其他記憶體內部資產。 由於 CPU 與 GPU 之間的通訊會產生額外負荷,因此請確定您只會更新自最後一個畫面以來實際變更的緩衝區 - 您可以視需要將常數緩衝區分組或分割,讓這個更有效率。
void Renderer::Update()
{
// Rotate the cube 1 degree per frame.
DirectX::XMStoreFloat4x4(
&m_constantBufferData.world,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixRotationY(
DirectX::XMConvertToRadians(
(float) m_frameCount++
)
)
)
);
if (m_frameCount == MAXUINT) m_frameCount = 0;
}
在此情況下,輪替 使用 Cube 的新轉換矩陣來更新常數緩衝區。 矩陣會在頂點著色器階段中對每個頂點進行乘法運算。 由於此方法會在每一個框架中呼叫,因此這是一個彙總更新動態常數和頂點緩衝區的方法的好地方,或執行任何其他作業,以便使場景中的物件為圖形管線轉換做好準備。
實作 Render 方法
呼叫 Update之後,每個遊戲迴圈會呼叫這個方法一次。 如同 Update,也會從 main 類別呼叫 Render 方法。 這是使用 ID3D11DeviceContext 實例上的方法,為框架建構和處理圖形管線的方法。 這最終會在對 ID3D11DeviceContext::DrawIndexed進行最後呼叫。 請務必瞭解,這個呼叫(或定義於 ID3D11DeviceContext的其他類似 Draw* 呼叫)實際上執行管道運作。 具體來說,這是當 Direct3D 與 GPU 通訊以設定繪圖狀態、執行每個管線階段,並將像素結果寫入轉譯目標緩衝區資源,以供交換鏈結顯示時。 由於 CPU 和 GPU 之間的通訊會產生額外負荷,因此,如果可以的話,請將多個繪製呼叫合併成單一呼叫,特別是如果您的場景有許多轉譯的物件。
void Renderer::Render()
{
// Use the Direct3D device context to draw.
ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();
ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();
context->UpdateSubresource(
m_pConstantBuffer.Get(),
0,
nullptr,
&m_constantBufferData,
0,
0
);
// Clear the render target and the z-buffer.
const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
context->ClearRenderTargetView(
renderTarget,
teal
);
context->ClearDepthStencilView(
depthStencil,
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
1.0f,
0);
// Set the render target.
context->OMSetRenderTargets(
1,
&renderTarget,
depthStencil
);
// Set up the IA stage by setting the input topology and layout.
UINT stride = sizeof(VertexPositionColor);
UINT offset = 0;
context->IASetVertexBuffers(
0,
1,
m_pVertexBuffer.GetAddressOf(),
&stride,
&offset
);
context->IASetIndexBuffer(
m_pIndexBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0
);
context->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
);
context->IASetInputLayout(m_pInputLayout.Get());
// Set up the vertex shader stage.
context->VSSetShader(
m_pVertexShader.Get(),
nullptr,
0
);
context->VSSetConstantBuffers(
0,
1,
m_pConstantBuffer.GetAddressOf()
);
// Set up the pixel shader stage.
context->PSSetShader(
m_pPixelShader.Get(),
nullptr,
0
);
// Calling Draw tells Direct3D to start sending commands to the graphics device.
context->DrawIndexed(
m_indexCount,
0,
0
);
}
最好依序在上下文中設定各種圖形管線階段。 一般而言,訂單為:
- 視需要重新整理具有新數據的常數緩衝區資源(使用來自 Update的數據)。
- 輸入元件 (IA):這是我們附加定義場景幾何的頂點和索引緩衝區的位置。 您必須為場景中的每個物件附加每個頂點和索引緩衝區。 因為這個範例只有 Cube,所以很簡單。
- 頂點著色器 (VS):附加任何頂點著色器,這些頂點著色器會轉換頂點緩衝區中的數據,並附加頂點著色器的常數緩衝區。
- 圖元著色器(PS):附加任何圖元著色器,這些著色器將在點陣化場景中執行個別圖元作業,併為圖元著色器附加裝置資源(常數緩衝區、紋理等等)。
- 輸出合併(OM):這是著色器運算完畢後像素混合的階段。 這是規則的例外狀況,因為您在設定其他階段之前附加深度模版和渲染目標。 如果您有額外的頂點和像素著色器可生成紋理,例如陰影圖、高度圖或其他取樣技術,則可能會有多個模板和目標。在這種情況下,每次繪圖通過都需要在呼叫繪製函式之前設定適當的目標。
接下來,在最後一節(使用著色器和著色器資源),我們將探討著色器,並討論 Direct3D 如何執行它們。
相關主題
-
下一個