DirectX11 With Windows SDK--20 硬件實例化與視錐體裁剪


前言

這一章將了解如何在DirectX 11利用硬件實例化技術高效地繪制重復的物體,以及使用視錐體裁剪技術提前將位於視錐體外的物體進行排除。

在此之前需要額外了解的章節如下:

章節回顧
18 使用DirectXCollision庫進行碰撞檢測
19 模型加載:obj格式的讀取及使用二進制文件提升讀取效率

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

硬件實例化(Hardware Instancing)

硬件實例化指的是在場景中繪制同一個物體多次,但是是以不同的位置、旋轉、縮放、材質以及紋理來繪制(比如一棵樹可能會被多次使用以構建出一片森林)。在以前,每次實例繪制(Draw方法)都會引發一次頂點緩沖區和索引緩沖區經過輸入裝配階段傳遞進渲染管線中,大量重復的繪制則意味着多次反復的輸入裝配操作,會引發十分龐大的性能開銷。事實上在繪制同樣物體的時候頂點緩沖區和索引緩沖區應當只需要傳遞一次,然后真正需要多次傳遞的也應該是像世界矩陣、材質、紋理等這些可能會經常變化的數據。

要能夠實現上面的這種操作,還需要圖形庫底層API本身能夠支持按對象繪制。對於每個對象,我們必須設置它們各自的材質、世界矩陣等,然后才是調用繪制命令。盡管在Direct3D 10和后續的版本已經將原本Direct3D 9的一些API重新設計以盡可能最小化性能上的開銷,部分多余的開銷仍然存在。因此,Direct3D提供了一種機制,不需要通過API上的額外性能開銷來實現實例化,我們稱之為硬件實例化。

為什么要擔憂API性能開銷呢?Direct3D 9應用程序通常因為API導致在CPU上遇到瓶頸,而不是在GPU。以前關卡設計師喜歡使用單一材質和紋理來繪制許多對象,因為對於它們來說需要經常去單獨改變它的狀態並且去調用繪制。場景將會被限制在幾千次的調用繪制以維持實時渲染的速度,主要在於這里的每次API調用都會引起高級別的CPU性能開銷。現在圖形引擎可以使用批處理技術以最小化繪制調用的次數。硬件實例化是API幫助執行批處理的一個方面。

多頂點緩沖區輸入

之前我們提到,在輸入裝配階段中提供了16個輸入槽,這意味着可以同時綁定16個頂點緩沖區作為輸入。那這時候如果我們使用多個頂點緩沖區作為輸入會產生什么樣的結果呢?

頂點按數據類型拆分成多個頂點緩沖區

這里做一個鋪墊,以前我們在輸入裝配階段只使用了1個輸入槽。現在假定我們有如下頂點緩沖區結構:

索引 頂點位置 頂點法向量 頂點顏色
0 P1 N1
1 P2 N2
2 P3 N3

這里我們也可以使用2個輸入槽,第一個頂點緩沖區存放頂點位置,第二個頂點緩沖區存放頂點法向量和頂點顏色:

索引 頂點位置
0 P1
1 P2
2 P3
索引 頂點法向量 頂點顏色
0 N1
1 N2
2 N3

先回顧一下頂點輸入布局描述的結構:

 typedef struct D3D11_INPUT_ELEMENT_DESC
 {
    LPCSTR SemanticName;    // 語義名
    UINT SemanticIndex;     // 語義名對應的索引值
    DXGI_FORMAT Format;     // DXGI數據格式
    UINT InputSlot;         // 輸入槽
    UINT AlignedByteOffset; // 對齊的字節偏移量
    D3D11_INPUT_CLASSIFICATION InputSlotClass;  // 輸入槽類別(此時為頂點)
    UINT InstanceDataStepRate;  // 忽略(0)
 } 	D3D11_INPUT_ELEMENT_DESC;

然后我們就可以在輸入布局中這樣指定(這里先簡單提及一下):

語義 語義索引 數據格式 輸入槽 該輸入槽對應的字節偏移
POSITION 0 R32G32B32_FLOAT 0 0
NORMAL 0 R32G32B32_FLOAT 1 0
COLOR 0 R32G32B32A32_FLOAT 1 12

這樣,下面在HLSL的結構體數據實際上來源於兩個輸入槽:

struct VertexPosNormalColor
{
	float3 pos : POSITION;		// 來自輸入槽0
	float3 normal : NORMAL;		// 來自輸入槽1
	float4 color : COLOR;		// 來自輸入槽1
};

然后,輸入裝配器就會根據輸入布局,以及索引值來抽取對應數據,最終構造出來的頂點數據流和一開始給出的表格數據是一致的。即便你把第二個頂點緩沖區再按頂點法向量和頂點顏色拆分成兩個新的頂點緩沖區,使用三輸入槽產生的結果也是一致的。如果你只能拿到連續的頂點位置數據、連續的法向量數據、連續的紋理坐標數據話,可以考慮使用上述方案。

頂點與實例數據的組合 與 流式實例化數據

現在,我們需要着重觀察D3D11_INPUT_ELEMENT_DESC的最后兩個成員:

 typedef struct D3D11_INPUT_ELEMENT_DESC
 {
    LPCSTR SemanticName;    // 語義名
    UINT SemanticIndex;     // 語義名對應的索引值
    DXGI_FORMAT Format;     // DXGI數據格式
    UINT InputSlot;         // 輸入槽
    UINT AlignedByteOffset; // 對齊的字節偏移量
    D3D11_INPUT_CLASSIFICATION InputSlotClass;  // 輸入槽類別(頂點/實例)
    UINT InstanceDataStepRate;  // 實例數據步進值
 } 	D3D11_INPUT_ELEMENT_DESC;

1.InputSlotClass:指定輸入的元素是作為頂點元素還是實例元素。枚舉值含義如下:

枚舉值 含義
D3D11_INPUT_PER_VERTEX_DATA 作為頂點元素
D3D11_INPUT_PER_INSTANCE_DATA 作為實例元素

2.InstanceDataStepRate:指定每份實例數據繪制出多少個實例。例如,假如你想繪制6個實例,但提供了只夠繪制3個實例的數據,1份實例數據繪制出1種顏色,分別為紅、綠、藍。那么我們可以設置該成員的值為2,使得前兩個實例繪制成紅色,中間兩個實例繪制成綠色,后兩個實例繪制成藍色。通常在繪制實例的時候我們會將該成員的值設為1,保證1份數據繪制出1個實例。對於頂點成員來說,設置該成員的值為0.

在前面的例子,我們知道一個結構體的數據可以來自多個輸入槽,現在要使用硬件實例化,我們需要使用至少兩個輸入槽(其中至少一個頂點緩沖區,至少一個實例緩沖區)

現在我們需要使用的頂點與實例數據組合的結構體如下:

struct InstancePosNormalTex
{
    float3 PosL : POSITION;		// 來自輸入槽0
    float3 NormalL : NORMAL;	// 來自輸入槽0
    float2 Tex : TEXCOORD;		// 來自輸入槽0
    matrix World : World;		// 來自輸入槽1
    matrix WorldInvTranspose : WorldInvTranspose;	// 來自輸入槽1
};

輸出的結構體和以前一樣:

struct VertexPosHWNormalTex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION;  // 在世界中的位置
    float3 NormalW : NORMAL; // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

現在頂點着色器代碼變化如下:

VertexPosHWNormalTex VS(InstancePosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    
    matrix viewProj = mul(g_View, g_Proj);
    vector posW = mul(float4(vIn.PosL, 1.0f), vIn.World);

    vOut.PosW = posW.xyz;
    vOut.PosH = mul(posW, viewProj);
    vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose);
    vOut.Tex = vIn.Tex;
    return vOut;
}

至於像素着色器,和上一章為模型所使用的着色器的保持一致。

對於前面的結構體InstancePosNormalTex,與之對應的輸入成員描述數組如下:

D3D11_INPUT_ELEMENT_DESC basicInstLayout[] = {
	{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "World", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "World", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "World", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "World", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "WorldInvTranspose", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "WorldInvTranspose", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 80, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "WorldInvTranspose", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 96, D3D11_INPUT_PER_INSTANCE_DATA, 1},
	{ "WorldInvTranspose", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 112, D3D11_INPUT_PER_INSTANCE_DATA, 1}
};

因為DXGI_FORMAT一次最多僅能夠表達128位(16字節)數據,在對應矩陣的語義時,需要重復描述4次,區別在於語義索引為0-3.

頂點的數據占用輸入槽0,而實例數據占用的則是輸入槽1。這樣就需要我們使用兩個緩沖區以提供給輸入裝配階段。其中第一個作為頂點緩沖區,而第二個作為實例緩沖區以存放有關實例的數據,綁定到輸入裝配階段的方法如下:

struct VertexPosNormalTex
{
	DirectX::XMFLOAT3 pos;
	DirectX::XMFLOAT3 normal;
	DirectX::XMFLOAT2 tex;
	static const D3D11_INPUT_ELEMENT_DESC inputLayout[3];
};

struct InstancedData
{
	XMMATRIX world;
	XMMATRIX worldInvTranspose;
};

// ...
UINT strides[2] = { sizeof(VertexPosNormalTex), sizeof(InstancedData) };
UINT offsets[2] = { 0, 0 };
ID3D11Buffer * buffers[2] = { vertexBuffer.Get(), instancedBuffer.Get() };

// 設置頂點/索引緩沖區
deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets);
deviceContext->IASetInputLayout(instancePosNormalTexLayout.Get());

實例ID

系統值SV_InstanceID可以告訴我們當前進行繪制的頂點來自哪個實例。通常在繪制N個實例的情況下,第一個實例的索引值為0,一直到最后一個實例索引值為N - 1.它可以應用在需要個性化的地方,比如使用一個紋理數組,然后不同的索引去映射到對應的紋理,以繪制出網格模型相同,但紋理不一致的物體。

按實例進行繪制

ID3D11DeviceContext::DrawIndexedInstanced方法--帶索引數組的實例繪制

通常我們使用ID3D11DeviceContext::DrawIndexedInstanced方法來繪制實例數據:

void ID3D11DeviceContext::DrawIndexedInstanced(
    UINT IndexCountPerInstance,     // [In]每個實例繪制要用到的索引數目
    UINT InstanceCount,             // [In]繪制的實例數目
    UINT StartIndexLocation,        // [In]起始索引偏移值
    INT BaseVertexLocation,         // [In]起始頂點偏移值
    UINT StartInstanceLocation      // [In]起始實例偏移值
);

下面是一個調用示例:

deviceContext->DrawIndexedInstanced(part.indexCount, numInsts, 0, 0, 0);

ID3D11DeviceContext::DrawInstanced方法--實例繪制

若沒有索引數組,也可以用ID3D11DeviceContext::DrawInstanced方法來進行繪制

void ID3D11DeviceContext::DrawInstanced(
    UINT VertexCountPerInstance,    // [In]每個實例繪制要用到的頂點數目
    UINT InstanceCount,             // [In]繪制的實例數目
    UINT StartVertexLocation,       // [In]起始頂點偏移值
    UINT StartInstanceLocation      // [In]起始實例偏移值
);

在調用實例化繪制后,輸入裝配器會根據所有頂點輸入槽與實例輸入槽進行笛卡爾積的排列組合,這里舉個復雜的例子,有5個輸入槽,其中頂點相關的輸入槽含3個元素,實例相關的輸入槽含2個元素:

輸入槽索引 0 1 2 3 4
按頂點/實例 頂點 頂點 頂點 實例 實例
數據類型 頂點位置 頂點法向量 紋理坐標 世界矩陣 世界矩陣逆轉置
索引0 P0 N0 T0 W0 WInvT0
索引1 P1 N1 T1 W1 WInvT1
索引2 P2 N2 T2 ------ ----------

最終產生的實例數據流如下表,含3x2=6組結構體數據:

實例ID 頂點ID 頂點位置 頂點法向量 紋理坐標 世界矩陣 世界矩陣逆轉置
0 0 P0 N0 T0 W0 WInv0
0 1 P1 N1 T1 W0 WInv0
0 2 P2 N2 T2 W0 WInv0
1 0 P0 N0 T0 W1 WInv1
1 1 P1 N1 T1 W1 WInv1
1 2 P2 N2 T2 W1 WInv1

實例緩沖區的創建

和之前創建頂點/索引緩沖區的方式一樣,我們需要創建一個ID3D11Buffer,只不過在緩沖區描述中,我們需要將其指定為動態緩沖區(即D3D11_BIND_VERTEX_BUFFER),並且要指定D3D11_CPU_ACCESS_WRITE

// 設置實例緩沖區描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_DYNAMIC;
vbd.ByteWidth = count * (UINT)sizeof(InstancedData);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
// 新建實例緩沖區
HR(device->CreateBuffer(&vbd, nullptr, m_pInstancedBuffer.ReleaseAndGetAddressOf()));

因為我們不需要訪問里面的數據,因此不用添加D3D11_CPU_ACCESS_READ標記。

實例緩沖區數據的修改

若需要修改實例緩沖區的內容,則需要使用ID3D11DeviceContext::Map方法將其映射到CPU內存當中。對於使用了D3D11_USAGE_DYNAMIC標簽的動態緩沖區來說,在更新的時候只能使用D3D11_MAP_WRITE_DISCARD標簽,而不能使用D3D11_MAP_WRITE或者D3D11_MAP_READ_WRITE標簽。

將需要提交上去的實例數據存放到映射好的CPU內存區間后,使用ID3D11DeviceContext::Unmap方法將實例數據更新到顯存中以應用。

D3D11_MAPPED_SUBRESOURCE mappedData;
HR(deviceContext->Map(m_pInstancedBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
InstancedData * iter = reinterpret_cast<InstancedData *>(mappedData.pData);
// 省略寫入細節...

deviceContext->Unmap(m_pInstancedBuffer.Get(), 0);

視錐體裁剪

在前面的所有章節中,頂點的拋棄通常發生在光柵化階段。這意味着如果一份模型數據的所有頂點在經過矩陣變換后都不會落在屏幕區域內的話,這些頂點數據將會經歷頂點着色階段,可能會經過曲面細分階段和幾何着色階段,然后在光柵化階段的時候才拋棄。讓這些不會被繪制的頂點還要走過這么漫長的階段才被拋棄,可以說是一種非常低效的行為。

視錐體裁剪,就是在將這些模型的相關數據提交給渲染管線之前,生成一個包圍盒,與攝像機觀察空間的視錐體進行碰撞檢測。若為相交或者包含,則說明該模型對象是可見的,需要被繪制出來,反之則應當拒絕對該對象的繪制調用,或者不傳入該實例對象相關的數據。這樣做可以節省GPU資源以避免大量對不可見對象的繪制,對CPU的性能開銷也不大。

可以說,若一個場景中的模型數目越多,或者視錐體的可視范圍越小,那么視錐體裁剪的效益越大。

查看上圖,可以知道的是物體A和D沒有與視錐體發生碰撞,因此需要排除掉物體A的實例數據。而物體B和E與視錐體有相交,物體C則被視錐體所包含,這三個物體的實例數據都應當傳遞給實例緩沖區。

視錐體裁剪有三種等價的代碼表現形式。需要已知當前物體的包圍盒、世界變換矩陣、觀察矩陣和投影矩陣。其中投影矩陣本身可以構造出視錐體包圍盒。

下面有關視錐體裁剪的方法都放進了Collision.h中。

方法1

現在已知物體的包圍盒位於自身的局部坐標系,我們可以使用世界變換矩陣將其變換到世界空間中。同樣,由投影矩陣構造出來的視錐體包圍盒也位於自身局部坐標系中,而觀察矩陣實質上是從世界矩陣變換到視錐體所處的局部坐標系中。因此,我們可以使用觀察矩陣的逆矩陣,將視錐體包圍盒也變換到世界空間中。這樣就好似物體與視錐體都位於世界空間中,可以進行碰撞檢測了:

std::vector<Transform> XM_CALLCONV Collision::FrustumCulling(
	const std::vector<Transform>& transforms, const DirectX::BoundingBox& localBox, DirectX::FXMMATRIX View, DirectX::CXMMATRIX Proj)
{
	std::vector<Transform> acceptedData;

	BoundingFrustum frustum;
	BoundingFrustum::CreateFromMatrix(frustum, Proj);

	BoundingOrientedBox localOrientedBox, orientedBox;
	BoundingOrientedBox::CreateFromBoundingBox(localOrientedBox, localBox);
	for (auto& t : transforms)
	{
		XMMATRIX W = t.GetLocalToWorldMatrixXM();
		// 將有向包圍盒從局部坐標系變換到視錐體所在的局部坐標系(觀察坐標系)中
		localOrientedBox.Transform(orientedBox, W * View);
		// 相交檢測
		if (frustum.Intersects(orientedBox))
			acceptedData.push_back(t);
	}

	return acceptedData;
}

方法2

該方法對應的正是龍書中所使用的裁剪方法,基本思路為:分別對觀察矩陣和世界變換矩陣求逆,然后使用觀察逆矩陣將視錐體從自身坐標系搬移到世界坐標系,再使用世界變換的逆矩陣將其從世界坐標系搬移到物體自身坐標系來與物體進行碰撞檢測。改良龍書的碰撞檢測代碼如下:

std::vector<Transform> XM_CALLCONV Collision::FrustumCulling2(
	const std::vector<Transform>& transforms, const DirectX::BoundingBox& localBox, DirectX::FXMMATRIX View, DirectX::CXMMATRIX Proj)
{
	std::vector<Transform> acceptedData;

	BoundingFrustum frustum, localFrustum;
	BoundingFrustum::CreateFromMatrix(frustum, Proj);
	XMMATRIX InvView = XMMatrixInverse(nullptr, View);
	for (auto& t : transforms)
	{
		XMMATRIX W = t.GetLocalToWorldMatrixXM();
		XMMATRIX InvWorld = XMMatrixInverse(nullptr, W);

		// 將視錐體從觀察坐標系(或局部坐標系)變換到物體所在的局部坐標系中
		frustum.Transform(localFrustum, InvView * InvWorld);
		// 相交檢測
		if (localFrustum.Intersects(localBox))
			acceptedData.push_back(t);
	}

	return acceptedData;
}

方法3

這個方法理解起來也比較簡單,先將物體從局部坐標系搬移到世界坐標系,然后再用觀察矩陣將其搬移到視錐體自身的局部坐標系來與視錐體進行碰撞檢測。代碼如下:

std::vector<Transform> XM_CALLCONV Collision::FrustumCulling3(
	const std::vector<Transform>& transforms, const DirectX::BoundingBox& localBox, DirectX::FXMMATRIX View, DirectX::CXMMATRIX Proj)
{
	std::vector<Transform> acceptedData;

	BoundingFrustum frustum;
	BoundingFrustum::CreateFromMatrix(frustum, Proj);

	BoundingOrientedBox localOrientedBox, orientedBox;
	BoundingOrientedBox::CreateFromBoundingBox(localOrientedBox, localBox);
	for (auto& t : transforms)
	{
		XMMATRIX W = t.GetLocalToWorldMatrixXM();
		// 將有向包圍盒從局部坐標系變換到視錐體所在的局部坐標系(觀察坐標系)中
		localOrientedBox.Transform(orientedBox, W * View);
		// 相交檢測
		if (frustum.Intersects(orientedBox))
			acceptedData.push_back(t);
	}

	return acceptedData;
}

這三種方法的裁剪表現效果是一致的。

C++代碼實現

GameApp::CreateRandomTrees方法--創建大量隨機位置和方向的樹

該方法創建了樹的模型,並以隨機的方式在一個大范圍的圓形區域中生成了225棵樹,即225個實例的數據(世界矩陣)。其中該圓形區域被划分成16個扇形區域,每個扇形划分成4個面,距離中心越遠的扇面生成的樹越多。

void GameApp::CreateRandomTrees()
{
	srand((unsigned)time(nullptr));
	// 初始化樹
	m_ObjReader.Read(L"Model\\tree.mbo", L"Model\\tree.obj");
	m_Trees.SetModel(Model(m_pd3dDevice.Get(), m_ObjReader));
	XMMATRIX S = XMMatrixScaling(0.015f, 0.015f, 0.015f);
	
	BoundingBox treeBox = m_Trees.GetLocalBoundingBox();

	// 讓樹木底部緊貼地面位於y = -2的平面
	treeBox.Transform(treeBox, S);
	float Ty = -(treeBox.Center.y - treeBox.Extents.y + 2.0f);
	// 隨機生成256顆隨機朝向的樹
	m_InstancedData.resize(256);
	m_Trees.ResizeBuffer(m_pd3dDevice.Get(), 256);

	float theta = 0.0f;
	int pos = 0;
	for (int i = 0; i < 16; ++i)
	{
		// 取5-125的半徑放置隨機的樹
		for (int j = 0; j < 4; ++j)
		{
			// 距離越遠,樹木越多
			for (int k = 0; k < 2 * j + 1; ++k, ++pos)
			{
				float radius = (float)(rand() % 30 + 30 * j + 5);
				float randomRad = rand() % 256 / 256.0f * XM_2PI / 16;
				m_InstancedData[pos].SetScale(0.015f, 0.015f, 0.015f);
				m_InstancedData[pos].SetRotation(0.0f, rand() % 256 / 256.0f * XM_2PI, 0.0f);
				m_InstancedData[pos].SetPosition(radius * cosf(theta + randomRad), Ty, radius * sinf(theta + randomRad));
			}
		}
		theta += XM_2PI / 16;
	}

	
}

GameObject::ResizeBuffer方法--重新調整實例緩沖區的大小

若實例緩沖區的大小容不下當前增長的實例數據,則需要銷毀原來的實例緩沖區,並重新創建一個更大的,以確保剛好能容得下之前的大量實例數據。

void GameObject::ResizeBuffer(ComPtr<ID3D11Device> device, size_t count)
{
	// 設置實例緩沖區描述
	D3D11_BUFFER_DESC vbd;
	ZeroMemory(&vbd, sizeof(vbd));
	vbd.Usage = D3D11_USAGE_DYNAMIC;
	vbd.ByteWidth = (UINT)count * sizeof(InstancedData);
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	// 創建實例緩沖區
	HR(device->CreateBuffer(&vbd, nullptr, m_pInstancedBuffer.ReleaseAndGetAddressOf()));

	// 重新調整m_Capacity
	m_Capacity = count;
}

GameObject::DrawInstanced方法--繪制游戲對象的多個實例

該方法接受一個裝滿物體變換信息的數組,把數據裝填進實例緩沖區(若容量不夠則重新擴容),然后交給設備上下文進行實例的繪制。

但是要注意需要將世界矩陣和其逆的轉置矩陣都進行一次轉置。

void GameObject::DrawInstanced(ID3D11DeviceContext* deviceContext, BasicEffect& effect, const std::vector<Transform>& data)
{
	D3D11_MAPPED_SUBRESOURCE mappedData;
	UINT numInsts = (UINT)data.size();
	// 若傳入的數據比實例緩沖區還大,需要重新分配
	if (numInsts > m_Capacity)
	{
		ComPtr<ID3D11Device> device;
		deviceContext->GetDevice(device.GetAddressOf());
		ResizeBuffer(device.Get(), numInsts);
	}

	HR(deviceContext->Map(m_pInstancedBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));

	InstancedData* iter = reinterpret_cast<InstancedData*>(mappedData.pData);
	for (auto& t : data)
	{
		XMMATRIX W = t.GetLocalToWorldMatrixXM();
		iter->world = XMMatrixTranspose(W);
		iter->worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
		iter++;
	}

	deviceContext->Unmap(m_pInstancedBuffer.Get(), 0);

	UINT strides[2] = { m_Model.vertexStride, sizeof(InstancedData) };
	UINT offsets[2] = { 0, 0 };
	ID3D11Buffer* buffers[2] = { nullptr, m_pInstancedBuffer.Get() };
	for (auto& part : m_Model.modelParts)
	{
		buffers[0] = part.vertexBuffer.Get();

		// 設置頂點/索引緩沖區
		deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets);
		deviceContext->IASetIndexBuffer(part.indexBuffer.Get(), part.indexFormat, 0);

		// 更新數據並應用
		effect.SetTextureDiffuse(part.texDiffuse.Get());
		effect.SetMaterial(part.material);
		effect.Apply(deviceContext);

		deviceContext->DrawIndexedInstanced(part.indexCount, numInsts, 0, 0, 0);
	}
}

剩余的代碼都可以在GitHub項目中瀏覽。

效果展示

該項目展示了一個同時存在225棵樹的場景,用戶可以自行設置開啟/關閉視錐體裁剪或硬件實例化。若關閉硬件實例化,則是對每個對象單獨調用繪制命令。

注意:Debug模式下開啟視錐體裁剪的效率可能比關閉還低,因為std::vector在debug模式下的性能相比release模式的確會有明顯的損耗。

練習題

  1. 修改教程07,利用Geometry中的立方體網格數據自己構造出3個頂點緩沖區,同時綁定到3個輸入槽做驗證,看結果時候和原來的一致。

DirectX11 With Windows SDK完整目錄

Github項目源碼

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


免責聲明!

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



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