DirectX11 With Windows SDK--03 索引緩沖區、常量緩沖區


前言

一個立方體有8個頂點,然而繪制一個立方體需要畫12個三角形,如果按照前面的方法繪制的話,則需要提供36個頂點,而且這里面的頂點數據會重復4次甚至5次。這樣的繪制方法會占用大量的內存空間。

接下來會講另外一種繪制方法,可以只提供立方體的8個頂點數據,然后用一個索引數組來指代使用哪些頂點,按怎樣的順序繪制。

在閱讀本章之前,先要了解下面的內容:

章節
02 頂點/像素着色器的創建、頂點緩沖區
HLSL中矩陣的內存布局和mul函數探討

當然,我也建議你及早開始了解並上手圖形調試器,以幫助尋找在CPU調試時無法察覺到的問題:

章節
Visual Studio圖形調試器詳細使用教程

學習目標

  1. 掌握索引緩沖區的創建、使用
  2. 掌握常量緩沖區的創建、更新、使用
  3. 根據索引繪制出第一個立方體

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入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;
};

目前常量緩沖區有兩種運行時更新方式:

  1. 在創建資源的時候指定UsageD3D11_USAGE_DEFAULT,可以允許常量緩沖區從GPU寫入,需要用ID3D11DeviceContext::UpdateSubresource方法更新。
  2. 在創建資源的時候指定UsageD3D11_USAGE_DYNAMICCPUAccessFlagsD3D11_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結構體變量用於更新常量緩沖區,其中viewproj矩陣需要預先進行一次轉置以抵消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_DEFAULTD3D11_USAGE_STAGE方式創建的資源,並且不能用於深度模板緩沖區和支持多采樣的緩沖區。

ID3D11DeviceContext::UpdateSubresource的性能表現取決於是否出現與待更新緩沖區的資源競爭。例如,GPU正在執行繪制操作時占用了該緩沖區,然后CPU發出了對同一個緩沖區的UpdateSubresource操作。

  1. 當出現資源競爭時,UpdateSubresource會對源數據進行2次拷貝。第一次是CPU拷貝一份資源在臨時的內存空間讓GPU命令緩沖能夠訪問它,發生在該方法被返回之前。然后第二次由GPU從內存拷貝到不可映射的顯存區域。第二次拷貝通常是異步發生的,因為這是在GPU命令緩沖被刷新后執行的。
  2. 若沒有出現資源競爭,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));
}

效果如下:

練習題

粗體字部分為自定義題目

  1. 嘗試只用5個頂點繪制四棱錐
  2. 嘗試將四棱錐、立方體的頂點數據放在同一個頂點緩沖區,索引數據也放在同一個索引緩沖區,然后使用這兩個緩沖區來繪制出這兩個物體(讓四棱錐在左邊,立方體在右邊,可以修改頂點數據,也可以使用變換矩陣)
  3. 嘗試創建動態頂點緩沖區,然后通過MapUnmap的方式給頂點緩沖區寫入頂點數據。

關於資源的更新,具體可以了解下面兩個鏈接:

Efficient_Buffer_Management

how-to-use-updatesubresource-and-map-unmap

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM