前言
一個立方體有8個頂點,然而繪制一個立方體需要畫12個三角形,如果按照前面的方法繪制的話,則需要提供36個頂點,而且這里面的頂點數據會重復4次甚至5次。這樣的繪制方法會占用大量的內存空間。
接下來會講另外一種繪制方法,可以只提供立方體的8個頂點數據,然后用一個索引數組來指代使用哪些頂點,按怎樣的順序繪制。
在閱讀本章之前,先要了解下面的內容:
| 章節 |
|---|
| 02 頂點/像素着色器的創建、頂點緩沖區 |
| HLSL中矩陣的內存布局和mul函數探討 |
當然,我也建議你及早開始了解並上手圖形調試器,以幫助尋找在CPU調試時無法察覺到的問題:
| 章節 |
|---|
| Visual Studio圖形調試器詳細使用教程 |
學習目標:
- 掌握索引緩沖區的創建、使用
- 掌握常量緩沖區的創建、更新、使用
- 根據索引繪制出第一個立方體
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
索引緩沖區(Index Buffer)
使用索引緩沖區進行替代指定順序繪制,可以有效減少頂點緩沖區的占用空間,避免提供大量重復的頂點數據。
在使用索引緩沖區前,先講初始化頂點數組,如下:
// ******************
// 設置立方體頂點
// 5________ 6
// /| /|
// /_|_____/ |
// 1|4|_ _ 2|_|7
// | / | /
// |/______|/
// 0 3
VertexPosColor vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
};
然后頂點緩沖區的創建和使用和之前一樣:
// 設置頂點緩沖區描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof vertices;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
// 新建頂點緩沖區
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = vertices;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
// 輸入裝配階段的頂點緩沖區設置
UINT stride = sizeof(VertexPosColor); // 跨越字節數
UINT offset = 0; // 起始偏移量
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
現在索引數組的初始化如下:
// 索引數組
DWORD indices[] = {
// 正面
0, 1, 2,
2, 3, 0,
// 左面
4, 5, 1,
1, 0, 4,
// 頂面
1, 5, 6,
6, 2, 1,
// 背面
7, 6, 5,
5, 4, 7,
// 右面
3, 2, 6,
6, 7, 3,
// 底面
4, 0, 3,
3, 7, 4
};
然后填充緩沖區描述信息並創建索引緩沖區:
// 設置索引緩沖區描述
D3D11_BUFFER_DESC ibd;
ZeroMemory(&ibd, sizeof(ibd));
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof indices;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
// 新建索引緩沖區
InitData.pSysMem = indices;
HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));
ID3D11DeviceContext::IASetIndexBuffer方法--渲染管線輸入裝配階段設置索引緩沖區
void ID3D11DeviceContext::IASetIndexBuffer(
ID3D11Buffer *pIndexBuffer, // [In]索引緩沖區
DXGI_FORMAT Format, // [In]數據格式
UINT Offset); // [In]字節偏移量
在裝配的時候你需要指定每個索引所占的字節數:
| DXGI_FORMAT | 字節數 | 索引范圍 |
|---|---|---|
| DXGI_FORMAT_R8_UINT | 1 | 0-255 |
| DXGI_FORMAT_R16_UINT | 2 | 0-65535 |
| DXGI_FORMAT_R32_UINT | 4 | 0-2147483647 |
於是我們可以這樣:
// 輸入裝配階段的索引緩沖區設置
m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
注意:當前更新將統一使用的索引類型為32位無符號整數。
常量緩沖區(Constant Buffer)
在HLSL中,常量緩沖區的變量類似於C++這邊的全局常量,供着色器代碼使用。下面是一個HLSL常量緩沖區示例:
cbuffer ConstantBuffer : register(b0)
{
matrix g_World;
matrix g_View;
matrix g_Proj;
}
cbuffer 用於聲明一個常量緩沖區
matrix 等價於 float4x4,同樣有vector等價於float4.其中D3D中的矩陣默認是行主矩陣形式,但是到了HLSL的matrix默認是列主矩陣形式。
register(b0) 指的是該常量緩沖區位於寄存器索引為0的緩沖區
而在C++應用層,常量緩沖區的對應結構體可以為:
struct ConstantBuffer
{
XMMATRIX world;
XMMATRIX view;
XMMATRIX proj;
};
目前常量緩沖區有兩種運行時更新方式:
- 在創建資源的時候指定
Usage為D3D11_USAGE_DEFAULT,可以允許常量緩沖區從GPU寫入,需要用ID3D11DeviceContext::UpdateSubresource方法更新。 - 在創建資源的時候指定
Usage為D3D11_USAGE_DYNAMIC、CPUAccessFlags為D3D11_CPU_ACCESS_WRITE,允許常量緩沖區從CPU寫入,首先通過ID3D11DeviceContext::Map方法獲取內存映射,然后再更新到映射好的內存區域,最后通過ID3D11DeviceContext::Unmap方法解除占用。
不僅常量緩沖區,一般的緩沖區和紋理資源更新都可以使用上述兩種方式。前者適合更新不頻繁(隔一段時間更新),或者僅一次更新的數據。而后者更適合於需要頻繁更新,如每幾幀更新一次,或每幀更新一次或多次的資源。
由於常量緩沖區大多數需要頻繁更新,因此后續都將主要使用DYNAMIC更新。創建支持DYNAMIC更新的緩沖區過程如下:
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DYNAMIC;
cbd.ByteWidth = sizeof(ConstantBuffer);
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
// 新建常量緩沖區,不使用初始數據
HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffer.GetAddressOf()));
注意:在創建常量緩沖區時,描述參數
ByteWidth必須為16的倍數,因為HLSL的常量緩沖區本身以及對它的讀寫操作需要嚴格按16字節對齊
現在來了解更新緩沖區的兩種方法所需要用到的函數。
DYNAMIC更新
ID3D11DeviceContext::Map[1]函數--獲取指向緩沖區中數據的指針並拒絕GPU對該緩沖區的訪問
HRESULT ID3D11DeviceContext::Map(
ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的資源對象
UINT Subresource, // [In]緩沖區資源填0
D3D11_MAP MapType, // [In]D3D11_MAP枚舉值,指定讀寫相關操作
UINT MapFlags, // [In]填0,CPU需要等待GPU使用完畢當前緩沖區
D3D11_MAPPED_SUBRESOURCE *pMappedResource // [Out]獲取到的已經映射到緩沖區的內存
);
D3D11_MAP枚舉值類型的成員如下:
| D3D11_MAP成員 | 含義 |
|---|---|
| D3D11_MAP_READ | 映射到內存的資源用於讀取。該資源在創建的時候必須綁定了 D3D11_CPU_ACCESS_READ標簽 |
| D3D11_MAP_WRITE | 映射到內存的資源用於寫入。該資源在創建的時候必須綁定了 D3D11_CPU_ACCESS_WRITE標簽 |
| D3D11_MAP_READ_WRITE | 映射到內存的資源用於讀寫。該資源在創建的時候必須綁定了 D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE標簽 |
| D3D11_MAP_WRITE_DISCARD | 映射到內存的資源用於寫入,之前的資源數據將會被拋棄。該 資源在創建的時候必須綁定了D3D11_CPU_ACCESS_WRITE和 D3D11_USAGE_DYNAMIC標簽 |
| D3D11_MAP_WRITE_NO_OVERWRITE | 映射到內存的資源用於寫入,但不能復寫已經存在的資源。 該枚舉值只能用於頂點/索引緩沖區。該資源在創建的時候需要 有D3D11_CPU_ACCESS_WRITE標簽,在Direct3D 11不能用於 設置了D3D11_BIND_CONSTANT_BUFFER標簽的資源,但在 11.1后可以。具體可以查閱MSDN文檔 |
最后映射出來的內存我們可以通過memcpy_s函數來更新。
默認情況下,若待訪問資源仍在被GPU使用,CPU會阻塞直到能夠訪問該資源。
注意:千萬不要對只支持寫操作的映射內存區域進行讀取操作!否則這會招致十分顯著的性能損失。即便是像這樣的C++代碼:
*((int*)MappedResource.pData) = 0都會引發讀操作從而觸發性能損失。這在x86匯編表示為AND DWORD PTR [EAX], 0。
ID3D11DeviceContext::Unmap函數--讓指向資源的指針無效並重新啟用GPU對該資源的訪問權限
void ID3D11DeviceContext::Unmap(
ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的資源對象
UINT Subresource // [In]緩沖區資源填0
);
現在需要利用mBuffer結構體變量用於更新常量緩沖區,其中view和proj矩陣需要預先進行一次轉置以抵消HLSL列主矩陣的轉置,至於world矩陣已經是單位矩陣就不需要了:
m_CBuffer.world = XMMatrixIdentity(); // 單位矩陣的轉置是它本身
m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
));
m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
DEFAULT更新
ID3D11DeviceContext::UpdateSubresource方法[1]--更新緩沖區的數據
該方法可以用於更新允許GPU寫入的資源:
void ID3D11DeviceContext::UpdateSubresource(
ID3D11Resource *pDstResource, // [In]需要更新的常量緩沖區
UINT DstSubresource, // [In]緩沖區資源填0
const D3D11_BOX *pDstBox, // [In]忽略,填nullptr
const void *pSrcData, // [In]用於更新的數據源
UINT SrcRowPitch, // [In]忽略,填0
UINT SrcDepthPitch); // [In]忽略,填0
該方法僅可以用於以D3D11_USAGE_DEFAULT或D3D11_USAGE_STAGE方式創建的資源,並且不能用於深度模板緩沖區和支持多采樣的緩沖區。
ID3D11DeviceContext::UpdateSubresource的性能表現取決於是否出現與待更新緩沖區的資源競爭。例如,GPU正在執行繪制操作時占用了該緩沖區,然后CPU發出了對同一個緩沖區的UpdateSubresource操作。
- 當出現資源競爭時,
UpdateSubresource會對源數據進行2次拷貝。第一次是CPU拷貝一份資源在臨時的內存空間讓GPU命令緩沖能夠訪問它,發生在該方法被返回之前。然后第二次由GPU從內存拷貝到不可映射的顯存區域。第二次拷貝通常是異步發生的,因為這是在GPU命令緩沖被刷新后執行的。 - 若沒有出現資源競爭,
UpdateSubresource的行為取決於CPU認為怎樣會更快:像第一步那樣執行,或者直接從CPU拷貝到最終的顯存資源位置。具體行為還是要依賴於系統。
該方法本身涉及到CPU的拷貝操作,CPU運行開銷會比較大一些,而且還需要留有足夠的顯存空間。
注意:如果用於更新着色器的常量緩沖區,不能對其中的數據部分更新,必須完整地進行數據的更新。
使用該方法只需要一句代碼就可以更新:
m_CBuffer.world = XMMatrixIdentity(); // 單位矩陣的轉置是它本身
m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
));
m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
m_pd3dImmediateContext->UpdateSubresource(m_pConstantBuffer.Get(), 0, nullptr, &m_CBuffer, 0, 0);
ID3D11DeviceContext::*SSetConstantBuffers方法--渲染管線某一着色階段設置常量緩沖區
這里的*可以是V, H, D, C, G, P六種可編程渲染管線階段,函數的形參都基本一致。
現在更新了數據后,我們還需要給頂點着色階段設置常量緩沖區供使用。
void ID3D11DeviceContext::VSSetConstantBuffers(
UINT StartSlot, // [In]放入緩沖區的起始索引,例如上面指定了b0,則這里應為0
UINT NumBuffers, // [In]設置的緩沖區數目
ID3D11Buffer *const *ppConstantBuffers); // [In]用於設置的緩沖區數組
綁定常量緩沖區的操作通常只需要調用一次即可:
m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());
HLSL代碼
該例程所用的HLSL代碼如下:
//Cube.hlsli
cbuffer ConstantBuffer : register(b0)
{
matrix World;
matrix View;
matrix Proj;
}
struct VertexIn
{
float3 posL : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
// Cube_VS.hlsl
#include "Cube.hlsli"
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = mul(float4(vIn.posL, 1.0f), gWorld); // mul 才是矩陣乘法, 運算符*要求操作對象為
vOut.posH = mul(vOut.posH, gView); // 行列數相等的兩個矩陣,結果為
vOut.posH = mul(vOut.posH, gProj); // Cij = Aij * Bij
vOut.color = vIn.color; // 這里alpha通道的值默認為1.0
return vOut;
}
// Cube_PS.hlsl
#include "Cube.hlsli"
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
注意:在HLSL中,矩陣乘法不能用*運算符,該運算符要求兩個矩陣行列數相同,運算的結果也是一個同行列數的矩陣,運算過程為:
Cij = Aij * Bij。應該使用mul函數進行替代。
GameApp::UpdateScene方法--逐幀更新數據
繪制之前我們得讓立方體轉起來,不然就只能看到立方體的正面。在這里我們讓立方體同時繞X軸和Y軸旋轉,修改世界矩陣即可(無需太在意旋轉矩陣的設置是否合理,僅僅是為了演示效果):
void GameApp::UpdateScene(float dt)
{
static float phi = 0.0f, theta = 0.0f;
phi += 0.0001f, theta += 0.00015f;
m_CBuffer.world = XMMatrixTranspose(XMMatrixRotationX(phi) * XMMatrixRotationY(theta));
// 更新常量緩沖區,讓立方體轉起來
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
}
GameApp::DrawScene方法
ID3D11DeviceContext::DrawIndexed方法--根據頂點和索引緩沖區進行繪制
在輸入裝配階段指定好了頂點緩沖區、索引緩沖區和原始拓補類型后,再綁定常量緩沖區到頂點着色階段,最后就可以使用ID3D11DeviceContext::DrawIndexed方法來繪制:
void ID3D11DeviceContext::DrawIndexed(
UINT IndexCount, // 索引數目
UINT StartIndexLocation, // 起始索引位置
INT BaseVertexLocation); // 起始頂點位置
舉個例子,如果按下述方式調用:
m_pd3dImmediateContext->DrawIndexed(6, 6, 4);
假設頂點緩沖區有12個頂點,索引緩沖區有12個索引,存放的值為{11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
上面的調用意味着頂點緩沖區我們從索引4作為我們的基准索引,然后索引緩沖區則是使用了索引6到11對應的索引值,即{5, 4, 3, 2, 1, 0},然后加上基准索引值4為最終取得的原來頂點緩沖區索引為{9, 8, 7, 6, 5, 4}的頂點。
現在我們要繪制12個三角形,構成立方體。GameApp::DrawScene方法實現如下:
void GameApp::DrawScene()
{
assert(m_pd3dImmediateContext);
assert(m_pSwapChain);
static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; // RGBA = (0,0,0,255)
m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&black));
m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// 繪制立方體
m_pd3dImmediateContext->DrawIndexed(36, 0, 0);
HR(m_pSwapChain->Present(0, 0));
}
效果如下:

練習題
粗體字部分為自定義題目
- 嘗試只用5個頂點繪制四棱錐

- 嘗試將四棱錐、立方體的頂點數據放在同一個頂點緩沖區,索引數據也放在同一個索引緩沖區,然后使用這兩個緩沖區來繪制出這兩個物體(讓四棱錐在左邊,立方體在右邊,可以修改頂點數據,也可以使用變換矩陣)
- 嘗試創建動態頂點緩沖區,然后通過
Map和Unmap的方式給頂點緩沖區寫入頂點數據。
關於資源的更新,具體可以了解下面兩個鏈接:
how-to-use-updatesubresource-and-map-unmap
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
