瞭解 Direct3D 11 轉譯管線
先前,您已瞭解如何建立視窗,以用於使用 DirectX 裝置資源進行繪圖。 現在,您將瞭解如何建置圖形管線,以及您可以在其中連結的位置。
您記得有兩個 Direct3D 介面可定義圖形管線: ID3D11Device,其提供 GPU 及其資源的虛擬標記法;和 ID3D11DeviceCoNtext,代表管線的圖形處理。 一般而言,您會使用 ID3D11Device 實例來設定及取得您開始處理場景中圖形所需的 GPU 資源,並使用 ID3D11DeviceCoNtext 來處理圖形管線中每個適當著色器階段的資源。 您通常會不常呼叫 ID3D11Device 方法,也就是說,只有在您設定場景或裝置變更時。 另一方面,每次處理框架進行顯示時,您都會呼叫 ID3D11DeviceCoNtext 。
此範例會建立並設定適合用來顯示簡單旋轉頂點著色 Cube 的最小圖形管線。 其示範大約是顯示所需的最小資源集。 當您在這裡閱讀資訊時,請注意指定範例的限制,其中您可能必須擴充它以支援您想要呈現的場景。
本範例涵蓋圖形的兩個 C++ 類別:裝置資源管理員類別和 3D 場景轉譯器類別。 本主題特別著重于 3D 場景轉譯器。
Cube 轉譯器會怎麼做?
圖形管線是由 3D 場景轉譯器類別所定義。 場景轉譯器能夠:
- 定義常數緩衝區來儲存您的統一資料。
- 定義頂點緩衝區來保存物件頂點資料,以及對應的索引緩衝區,讓頂點著色器能夠正確執行三角形。
- 建立紋理資源和資源檢視。
- 載入著色器物件。
- 更新圖形資料以顯示每個框架。
- 將圖形轉譯 (繪製) 至交換鏈結。
前四個進程通常會使用 ID3D11Device 介面方法來初始化和管理圖形資源,最後兩個進程會使用 ID3D11DeviceCoNtext 介面方法來管理和執行圖形管線。
轉譯器類別的實例會在主要專案類別上建立及管理為成員變數。 DeviceResources實例是當做數個類別的共用指標來管理,包括主要專案類別、應用程式檢視提供者類別和轉譯器。 如果您將 Renderer 取代為自己的類別,請考慮宣告 DeviceResources 實例並將其指派為共用指標成員:
std::shared_ptr<DX::DeviceResources> m_deviceResources;
只要在 App類別的Initialize方法中建立DeviceResources實例之後,將指標傳遞至類別建構函式 (或其他初始化方法) 。 如果您想要讓主要類別完全擁有DeviceResources實例,您也可以傳遞weak_ptr參考。
建立 Cube 轉譯器
在此範例中,我們會使用下列方法來組織場景轉譯器類別:
- 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 實作。
此方法的主要工作是確定視覺效果不會因為視窗屬性變更而變得混淆或不正確。 在此範例中,我們會使用已調整大小或重新導向視窗的新檢視欄位來更新專案矩陣 (FOV) 。
我們已看到在 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 方法
每個遊戲迴圈呼叫 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, 轉譯 方法也會從主要類別呼叫。 這是使用 ID3D11DeviceCoNtext 實例上方法建構和處理框架圖形管線的方法。 這會在 ID3D11DeviceCoNtext::D rawIndexed的最終呼叫中產生提示。 請務必瞭解,此呼叫 (或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 的執行方式。
相關主題
-
接下來