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

前言

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

在此以前須要額外瞭解的章節以下:git

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

DirectX11 With Windows SDK完整目錄github

Github項目源碼數組

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。app

硬件實例化(Hardware Instancing)

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

要可以實現上面的這種操做,還須要圖形庫底層API自己可以支持按對象繪製。對於每一個對象,咱們必須設置它們各自的材質、世界矩陣等,而後纔是調用繪製命令。儘管在Direct3D 10和後續的版本已經將本來Direct3D 9的一些API從新設計以儘量最小化性能上的開銷,部分多餘的開銷仍然存在。所以,Direct3D提供了一種機制,不須要經過API上的額外性能開銷來實現實例化,咱們稱之爲硬件實例化。ide

爲何要擔心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<XMMATRIX> XM_CALLCONV Collision::FrustumCulling(
    const std::vector<XMMATRIX>& Matrices,const BoundingBox& localBox, FXMMATRIX View, CXMMATRIX Proj)
{
    std::vector<DirectX::XMMATRIX> acceptedData;

    BoundingFrustum frustum;
    BoundingFrustum::CreateFromMatrix(frustum, Proj);
    XMMATRIX InvView = XMMatrixInverse(nullptr, View);
    // 將視錐體從局部座標系變換到世界座標系中
    frustum.Transform(frustum, InvView);

    BoundingOrientedBox localOrientedBox, orientedBox;
    BoundingOrientedBox::CreateFromBoundingBox(localOrientedBox, localBox);
    for (auto& mat : Matrices)
    {
        // 將有向包圍盒從局部座標系變換到世界座標系中
        localOrientedBox.Transform(orientedBox, mat);
        // 相交檢測
        if (frustum.Intersects(orientedBox))
            acceptedData.push_back(mat);
    }

    return acceptedData;
}

方法2

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

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

    BoundingFrustum frustum, localFrustum;
    BoundingFrustum::CreateFromMatrix(frustum, Proj);
    XMMATRIX InvView = XMMatrixInverse(nullptr, View);
    for (auto& mat : Matrices)
    {
        XMMATRIX InvWorld = XMMatrixInverse(nullptr, mat);

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

    return acceptedData;
}

方法3

這個方法理解起來也比較簡單,直接將物體先用世界變換矩陣從物體自身座標系搬移到世界座標系,而後用觀察矩陣將其搬移到視錐體自身的局部座標系來與視錐體進行碰撞檢測。代碼以下:

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

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

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

    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();
    // 獲取樹包圍盒頂點
    m_TreeBoxData = Collision::CreateBoundingBox(treeBox, XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f));
    // 讓樹木底部緊貼地面位於y = -2的平面
    treeBox.Transform(treeBox, S);
    XMMATRIX T0 = XMMatrixTranslation(0.0f, -(treeBox.Center.y - treeBox.Extents.y + 2.0f), 0.0f);
    // 隨機生成256顆隨機朝向的樹
    float theta = 0.0f;
    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)
            {
                float radius = (float)(rand() % 30 + 30 * j + 5);
                float randomRad = rand() % 256 / 256.0f * XM_2PI / 16;
                XMMATRIX T1 = XMMatrixTranslation(radius * cosf(theta + randomRad), 0.0f, radius * sinf(theta + randomRad));
                XMMATRIX R = XMMatrixRotationY(rand() % 256 / 256.0f * XM_2PI);
                XMMATRIX World = S * R * T0 * T1;
                m_InstancedData.push_back(World);
            }
        }
        theta += XM_2PI / 16;
    }

    m_Trees.ResizeBuffer(m_pd3dDevice.Get(), 256);
}

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<DirectX::XMMATRIX>& 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& mat : data)
    {
        iter->world = XMMatrixTranspose(mat);
        iter->worldInvTranspose = XMMatrixInverse(nullptr, mat);    // 兩次轉置抵消
        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棵樹的場景,用戶能夠自行設置開啓/關閉視錐體裁剪或硬件實例化。若關閉硬件實例化,則是對每一個對象單獨調用繪製命令。

練習題

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

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索