DirectX11 With Windows SDK--19(Dev) 編譯Assimp並加載模型、新的Effects框架

前言

注意:這一章進行了重寫,對應教程Dev分支第19章的項目,在更新完後面的項目後會替換掉原來第19章的教程

在前面的章節中我們一直使用的是由代碼生成的幾何模型,但現在我們希望能夠導入模型設計師生成的各種格式的模型。然而,在DirectX中,將模型導入到內存後還需要我們進行處理,最終變成能讓管線使用的頂點緩衝區、索引緩衝區、各種常量等,這就意味着我們需要關注這些模型數據的細節了。

然而一個現實問題是,模型的格式有很多種,且每種格式內部的存儲結構又各不相同,不僅可以是文本形式,還可以是二進制形式。在這一章中,我們將學習使用Assimp模型加載庫,它支持很多種模型格式的導入,能夠處理成Assimp統一的存儲結構。

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

構建Assimp

Assimp頁面找到最新的Release然後下載它的源碼,我們需要使用CMake來生成VS項目。當然現在教程的代碼中直接包含了一份完整的Assimp源碼,位於Assimp文件夾內。

我們將介紹兩種生成和使用Assimp的方式

通過cmake-gui配置生成

運行cmake-gui.exe,填寫源碼位置和build binaries的位置,點擊Configure選擇你所擁有的Visual Studio版本後確定。等待CMake完成配置過程後,在中間可以看到大量新出現(紅顏色標記)的配置屬性。由於目前版本的Assimp需要使用ZLIB,而通常本機是沒有編譯過ZLIB的,我們需要在下圖中找到ASSIMP_BUILD_ZLIB項並將其勾選:

image

與此同時我們要找到CMAKE_INSTALL_PREFIX確定Assimp安裝的位置,默認爲C:/Program Files (x86)/Assimp,當然你也可以進行更改,但需要記住安裝的位置在後面教程項目配置的時候需要用到。

然後ASSIMP_BUILD_ASSIMP_TOOLS這個選項也推薦關掉

如果你想使用Assimp的模型查看器的話,勾選ASSIMP_BUILD_ASSIMP_VIEW,但前提是需要安裝Microsoft DirectX SDK

完成上述配置後點擊Generate就會生成Visual Studio項目。然後我們需要以管理員身份打開Visual Studio,並打開Assimp.sln

我們需要分別以Release x64Debug x64配置各生成一次,右鍵INSTALL生成就會自動編譯並將生成的靜態庫、動態庫以及頭文件複製到CMAKE_INSTALL_PREFIX確定的路徑中:

image

如果出現以下情況也不代表就是失敗了,實際上也已經完成了複製:

image

完成兩次生成後,應該去CMAKE_INSTALL_PREFIXC:/Program Files (x86)/Assimp)的路徑檢查文件情況,應該可以看到文件結構大致如下:

Assimp
|---bin
|   |---assimp-vc14*-mt.dll
|   |---assimp-vc14*-mtd.dll
|---include
|   |---assimp
|---lib
    |---cmake
    |---pkgconfig
    |---assimp-vc14*-mt.lib
    |---assimp-vc14*-mtd.lib
    |---zlibstatic.lib
    |---zlibstaticd.lib

對於你的項目而言,需要引入Assimp的頭文件、靜態庫和動態庫,具體過程如下。

在項目屬性頁中,選擇C/C++ → 常規 → 附加包含目錄,添加Assimp頭文件所在路徑:

image

選擇鏈接器 → 常規 → 附加庫目錄,添加Assimp庫目錄:

image

然後是動態庫。一般在提供他人程序的時候也要在exe路徑放所需的動態庫,但現在在學習的過程中我們可能會產生很多項目,但又不希望爲每個項目都複製dll過來,爲此我們可以讓程序在運行的時候去額外尋找指定路徑的dll。

選擇調試 → 環境,添加PATH=C:\Program Files (x86)\Assimp\bin,這樣程序運行的時候就會額外在該路徑尋找動態庫了,但每個使用Assimp的項目都需要進行這樣的設置,其屬性保存在*.vcxproj.user

image

這樣你編譯好的程序在運行的時候就會額外尋找該路徑下的dll了。

編寫cmake給你的項目引入assimp

由於assimp提供了cmake,如果你的項目是通過cmake來生成的話,自然就會想到是不是能夠用cmake去調assimp的cmake?答案是肯定的。

首先在你的CMakeLists.txt中添加這段,確保你的項目路徑中包含了assimp:

add_subdirectory("assimp")
target_link_libraries(TargetName assimp)

這樣相當於你在cmake-gui直接配置、生成,然後產生的那些子項目現在全部都包含到了你的解決方案裏。但是這裏面有很多的項目我們是不需要的,爲此我們得在cmake中設置各種選項,下面是一種個人推薦的做法:

set(ASSIMP_BUILD_ZLIB ON)
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF)
set(ASSIMP_BUILD_TESTS OFF)
set(ASSIMP_INSTALL OFF)
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF)

add_subdirectory("assimp")
target_link_libraries(TargetName assimp)

這時候你生成的解決方案中,有關於Assimp的項目就只剩下assimpUpdateAssimpLibsDebugSymbolsAndDLLszlibstatic了。但直接生成解決方案的話會找不到dll,爲此我們還要在cmake中能夠自動配置調試環境。完整的cmake代碼如下:

set(ASSIMP_BUILD_ZLIB ON)
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF)
set(ASSIMP_BUILD_TESTS OFF)
set(ASSIMP_INSTALL OFF)
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF)

add_subdirectory("assimp")
target_link_libraries(TargetName assimp)
set_target_properties(TargetName PROPERTIES VS_DEBUGGER_ENVIRONMENT "PATH=${ASSIMP_LIBRARY_OUTPUT_DIRECTORY}/$<IF:$<CONFIG:Debug>,Debug,Release>")

然後就可以看到你的項目已經包含了assimp,並且應該可以在項目屬性頁的調試、C/C++附加庫目錄、鏈接器附加依賴項都應該包含了assimp的路徑(更多的圖就不放了,自行檢查)。

image

現在你的項目應該就可以編譯完直接運行了。光是這短短8句話,就花費了我很長的時間去探尋。

如果要打包程序,記得去assimp/bin/Release把dll複製一份到你的exe旁。

Assimp的統一模型格式

當使用Assimp導入一個模型的時候,它通常會將整個模型加載進一個場景aiScene當中。下圖描述Assimp的存儲結構:

image

可以看到,在Assimp的場景中存儲了所有的模型和材質,並且是以數組的方式存儲的。然而有些模型文件是樹狀結構的,在這裏Assimp也支持樹狀結構,只不過存儲的網格模型是以索引的方式,需要用戶對。爲了方便,教程項目會將模型和材質數據進行數組方式的存儲和解析。

Assimp模型導入

Assimp導入模型的實現存放在ModelManager類中。現在我們需要使用Assimp的Importer來導入一個模型:

#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>

using namespace Assimp;
Importer importer;
const aiScene* pAssimpScene = importer.ReadFile(filename.data(), aiProcess_ConvertToLeftHanded |
        aiProcess_GenBoundingBoxes | aiProcess_Triangulate | aiProcess_ImproveCacheLocality);

由於讀取的模型有可能是左手系的,也有可能是右手系的,甚至有的模型三角形繞序也不一樣。對DirectX來說,常用的是左手系及順時針三角形繞序,我們可以使用aiProcess_ConvertToLeftHanded宏將其處理成上述所說的左手系和順時針繞序。

然後我們需要檢查是否成功讀取模型,且模型是否完整:

if (pAssimpScene && !(pAssimpScene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) && pAssimpScene->HasMeshes())
{
    // ...
}

接下來我們定義了MeshDataMaterial結構體,將Assimp的統一模型格式轉化成DirectX11所需的數據格式:

// 獲取ID
using XID = size_t;
inline XID StringToID(std::string_view str)
{
    static std::hash<std::string_view> hash;
    return hash(str);
}

template<class T, class V>
struct IsVariantMember;

template<class T, class... ALL_V>
struct IsVariantMember<T, std::variant<ALL_V...>> : public std::disjunction<std::is_same<T, ALL_V>...> {};

using Property = std::variant<
    int, uint32_t, float, DirectX::XMFLOAT4, DirectX::XMFLOAT4X4, 
    std::vector<float>, std::vector<DirectX::XMFLOAT4>, std::vector<DirectX::XMFLOAT4X4>,
    std::string>;

class Material
{
public:
    Material() = default;

    void Clear()
    {
        m_Properties.clear();
    }

    template<class T>
    void Set(std::string_view name, const T& value)
    {
        static_assert(IsVariantMember<T, Property>::value, "Type T isn't one of the Property types!");
        m_Properties[StringToID(name)] = value;
    }

    template<class T>
    const T& Get(std::string_view name) const
    {
        auto it = m_Properties.find(StringToID(name));
        return std::get<T>(it->second);
    }

    template<class T>
    bool Has(std::string_view name) const
    {
        auto it = m_Properties.find(StringToID(name));
        if (it == m_Properties.end() || !std::holds_alternative<T>(it->second))
            return false;
        return true;
    }

    bool HasProperty(std::string_view name) const
    {
        return m_Properties.find(StringToID(name)) != m_Properties.end();
    }

private:

    std::unordered_map<XID, Property> m_Properties;
};

而網格的頂點在這裏我們按每個屬性單獨生成一個Buffer,因爲Assimp對於頂點也是按不同的屬性分開存儲的。

struct MeshData
{
    // 使用模板別名(C++11)簡化類型名
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    ComPtr<ID3D11Buffer> m_pVertices;
    ComPtr<ID3D11Buffer> m_pNormals;
    std::vector<ComPtr<ID3D11Buffer>> m_pTexcoordArrays;
    ComPtr<ID3D11Buffer> m_pTangents;
    ComPtr<ID3D11Buffer> m_pColors;

    ComPtr<ID3D11Buffer> m_pIndices;
    uint32_t m_VertexCount = 0;
    uint32_t m_IndexCount = 0;
    uint32_t m_MaterialIndex = 0;

    DirectX::BoundingBox m_BoundingBox;
    bool m_InFrustum = true;
};

模型本身我們也遵循Assimp的順序存儲結構:

struct Model
{
    std::vector<Material> materials;
    std::vector<MeshData> meshdatas;
    DirectX::BoundingBox boundingbox;
};

接下來我們先處理模型相關的屬性:

Model model;

model.meshdatas.resize(pAssimpScene->mNumMeshes);
model.materials.resize(pAssimpScene->mNumMaterials);
for (uint32_t i = 0; i < pAssimpScene->mNumMeshes; ++i)
{
    auto& mesh = model.meshdatas[i];

    auto pAiMesh = pAssimpScene->mMeshes[i];
    uint32_t numVertices = pAiMesh->mNumVertices;

    CD3D11_BUFFER_DESC bufferDesc(0, D3D11_BIND_VERTEX_BUFFER);
    D3D11_SUBRESOURCE_DATA initData{ nullptr, 0, 0 };
    // 位置
    if (pAiMesh->mNumVertices > 0)
    {
        initData.pSysMem = pAiMesh->mVertices;
        bufferDesc.ByteWidth = numVertices * sizeof(XMFLOAT3);
        m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pVertices.GetAddressOf());

        BoundingBox::CreateFromPoints(mesh.m_BoundingBox, numVertices,
                                      (const XMFLOAT3*)pAiMesh->mVertices, sizeof(XMFLOAT3));
        if (i == 0)
            model.boundingbox = mesh.m_BoundingBox;
        else
            model.boundingbox.CreateMerged(model.boundingbox, model.boundingbox, mesh.m_BoundingBox);
    }

    // 法線
    if (pAiMesh->HasNormals())
    {
        initData.pSysMem = pAiMesh->mNormals;
        bufferDesc.ByteWidth = numVertices * sizeof(XMFLOAT3);
        m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pNormals.GetAddressOf());
    }

    // 切線和副切線
    if (pAiMesh->HasTangentsAndBitangents())
    {
        std::vector<XMFLOAT4> tangents(numVertices, XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f));
        for (uint32_t i = 0; i < pAiMesh->mNumVertices; ++i)
        {
            memcpy_s(&tangents[i], sizeof(XMFLOAT3),
                     pAiMesh->mTangents + i, sizeof(XMFLOAT3));
        }

        initData.pSysMem = tangents.data();
        bufferDesc.ByteWidth = pAiMesh->mNumVertices * sizeof(XMFLOAT4);
        m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pTangents.GetAddressOf());

        for (uint32_t i = 0; i < pAiMesh->mNumVertices; ++i)
        {
            memcpy_s(&tangents[i], sizeof(XMFLOAT3),
                     pAiMesh->mBitangents + i, sizeof(XMFLOAT3));
        }
        m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pBitangents.GetAddressOf());
    }

    // 紋理座標
    uint32_t numUVs = 8;
    while (numUVs && !pAiMesh->HasTextureCoords(numUVs - 1))
        numUVs--;

    if (numUVs > 0)
    {
        mesh.m_pTexcoordArrays.resize(numUVs);
        for (uint32_t i = 0; i < numUVs; ++i)
        {
            std::vector<XMFLOAT2> uvs(numVertices);
            for (uint32_t j = 0; j < numVertices; ++j)
            {
                memcpy_s(&uvs[j], sizeof(XMFLOAT2),
                         pAiMesh->mTextureCoords[i] + j, sizeof(XMFLOAT2));
            }
            initData.pSysMem = uvs.data();
            bufferDesc.ByteWidth = numVertices * sizeof(XMFLOAT2);
            m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pTexcoordArrays[i].GetAddressOf());
        }
    }

    // 索引
    uint32_t numFaces = pAiMesh->mNumFaces;
    uint32_t numIndices = numFaces * 3;
    if (numFaces > 0)
    {
        mesh.m_IndexCount = numIndices;
        if (numIndices < 65535)
        {
            std::vector<uint16_t> indices(numIndices);
            for (size_t i = 0; i < numFaces; ++i)
            {
                indices[i * 3] = static_cast<uint16_t>(pAiMesh->mFaces[i].mIndices[0]);
                indices[i * 3 + 1] = static_cast<uint16_t>(pAiMesh->mFaces[i].mIndices[1]);
                indices[i * 3 + 2] = static_cast<uint16_t>(pAiMesh->mFaces[i].mIndices[2]);
            }
            bufferDesc = CD3D11_BUFFER_DESC(numIndices * sizeof(uint16_t), D3D11_BIND_INDEX_BUFFER);
            initData.pSysMem = indices.data();
            m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pIndices.GetAddressOf());
        }
        else
        {
            std::vector<uint32_t> indices(numIndices);
            for (size_t i = 0; i < numFaces; ++i)
            {
                memcpy_s(indices.data() + i * 3, sizeof(uint32_t) * 3,
                         pAiMesh->mFaces[i].mIndices, sizeof(uint32_t) * 3);
            }
            bufferDesc = CD3D11_BUFFER_DESC(numIndices * sizeof(uint32_t), D3D11_BIND_INDEX_BUFFER);
            initData.pSysMem = indices.data();
            m_pDevice->CreateBuffer(&bufferDesc, &initData, mesh.m_pIndices.GetAddressOf());
        }
    }

    // 材質索引
    mesh.m_MaterialIndex = pAiMesh->mMaterialIndex;
}

這裏規定如果索引數小於等於65535,則每個索引使用2字節存儲,否則使用4字節存儲。

然後是材質相關的屬性,主要可以分爲值和紋理。對於值,我們可以使用Get()方法,通過AI_MATKEY嘗試獲取;對於紋理,我們可以使用GetTexture()方法,通過aiTextureType嘗試獲取:

namespace fs = std::filesystem;
for (uint32_t i = 0; i < pAssimpScene->mNumMaterials; ++i)
{
    auto& material = model.materials[i];

    auto pAiMaterial = pAssimpScene->mMaterials[i];
    XMFLOAT4 vec{};
    float value{};
    uint32_t boolean{};
    uint32_t num = 3;

    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_AMBIENT, (float*)&vec, &num))
        material.Set("$AmbientColor", vec);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, (float*)&vec, &num))
        material.Set("$DiffuseColor", vec);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_SPECULAR, (float*)&vec, &num))
        material.Set("$SpecularColor", vec);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_SPECULAR_FACTOR, value))
        material.Set("$SpecularFactor", value);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, (float*)&vec, &num))
        material.Set("$EmissiveColor", vec);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_TRANSPARENT, (float*)&vec, &num))
        material.Set("$TransparentColor", vec);
    if (aiReturn_SUCCESS == pAiMaterial->Get(AI_MATKEY_COLOR_REFLECTIVE, (float*)&vec, &num))
        material.Set("$ReflectiveColor", vec);
    if (pAiMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0)
    {
        aiString aiPath;
        pAiMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &aiPath);
        fs::path tex_filename = filename;
        tex_filename = tex_filename.parent_path() / aiPath.C_Str();
        TextureManager::Get().CreateTexture(tex_filename.string(), true, true);
        material.Set("$Diffuse", tex_filename.string());
    }
    if (pAiMaterial->GetTextureCount(aiTextureType_NORMALS) > 0)
    {
        aiString aiPath;
        pAiMaterial->GetTexture(aiTextureType_NORMALS, 0, &aiPath);
        fs::path tex_filename = filename;
        tex_filename = tex_filename.parent_path() / aiPath.C_Str();
        TextureManager::Get().CreateTexture(tex_filename.string());
        material.Set("$Normal", tex_filename.string());
    }
}

後續隨着項目的複雜,這裏需要讀取判斷的內容也會變多。

多頂點緩衝區輸入

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

現在假定着色器需要使用的頂點結構爲:

索引 頂點位置 頂點法向量 頂點UV
0 P1 N1 T0
1 P2 N2 T1
2 P3 N3 T2

如今我們有兩種方式輸入頂點的方式,見下圖:

image

左邊的是我們之前常用的方式,在一個頂點緩衝區內按頂點、法線、UV交替存放,然後設置正確的stride和offset來讀取對應區域的數據來組成頂點。而現在我們拿到的數據是頂點數組、法線數組和UV數組,並分別創建各自的頂點緩衝區,然後在輸入裝配階段綁定多個頂點緩衝區,此時我們只需要讓各自的offset爲0,只需要設置正確的stride即可。

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

 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
TEXCOORD 0 R32G32_FLOAT 2 0

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

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

然後,輸入裝配器就會根據輸入佈局,以及索引值來抽取對應數據,最終構造出來的頂點數據流和一開始給出的表格數據是一致的。當然,我們也可以將這些數據拼成單個頂點緩衝區,但這樣就無法應對頂點結構複雜多變的shader了。

然後在輸入裝配階段,傳入這些頂點緩衝區,並設置好各自的步幅和偏移即可:

ID3D11Buffer* pVBs[] = { pPosBuffer, pNormalBuffer, pTexBuffer };
uint32_t strides[] = { 12, 12, 8 };
uint32_t offsets[] = { 0, 0, 0 };
m_pd3dImmediateContext->IASetVertexBuffers(0, ARRAYSIZE(pVBs), pVBs, strides, offsets);

新的EffectHelper

本章開始的代碼引入了新的EffectHelper來管理着色器所需的資源,並且由於它能夠支持對着色器反射,我們可以在C++獲取到着色器的變量名,用戶可以直接通過這些變量名來預先設置管線所需的資源。此外,我們可以無需操心常量緩衝區的創建和設置了。這裏我們不會對EffectHelper的內部實現做解析,讀者只需要知道如何使用即可。

EffectHelper內部負責管理着色器需要用到的各種資源,如常量緩衝區、紋理輸入、採樣器等。而一個IEffectPass代表一次繪製所需要用到的各個着色器、光柵化狀態、深度/模板狀態和混合狀態。通過EffectHelper可以獲取到全局的常量緩衝區變量,而IEffectPass可以獲取到和着色器相關的uniform變量。在完成這些設置後,調用IEffectPass::Apply就會將EffectHelper緩存的各項資源綁定到渲染管線上。基本順序爲:

  • 添加各種shader,讓其進行着色器反射,但需要注意不同的shader中,同一個register對應的變量名和類型都應該相同,cbuffer佈局也應該相同。若有不同則應該歸類於不同的EffectHelper
  • 根據已經添加的shader,創建一系列需要用到EffectPass,每個EffectPass需要指定所需的着色器和渲染狀態
  • 根據名稱去設置着色器常量、採樣器狀態、着色器資源等
  • 要使用某個Pass,從EffectHelper獲取並進行Apply,這樣就會將shader、常量緩衝區、採樣器、着色器資源等綁定到渲染管線上

image

此外,爲了簡化調用過程,這裏爲每個EffectHelper對象配備一個具體的Effect單例類,負責設置好EffectHelper所需的東西。這些Effect需要繼承IEffect接口類,除此之外,目前還引入了IEffectTransformIEffectMaterialIEffectMeshData接口類來統一變換的設置、材質和模型的讀取和解析,根據需要來繼承。

IEffect.h內我們定義了下述結構體:

struct MeshDataInput
{
    std::vector<ID3D11Buffer*> pVertexBuffers;
    ID3D11Buffer* pIndexBuffer = nullptr;
    std::vector<uint32_t> strides;
    std::vector<uint32_t> offsets;
    uint32_t indexCount = 0;
};

class IEffect
{
public:
    IEffect() = default;
    virtual ~IEffect() = default;
    // 不允許拷貝,允許移動
    IEffect(const IEffect&) = delete;
    IEffect& operator=(const IEffect&) = delete;
    IEffect(IEffect&&) = default;
    IEffect& operator=(IEffect&&) = default;

    // 更新並綁定常量緩衝區
    virtual void Apply(ID3D11DeviceContext * deviceContext) = 0;
};

class IEffectTransform
{
public:
    virtual void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W) = 0;
    virtual void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V) = 0;
    virtual void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P) = 0;
};

class IEffectMaterial
{
public:
    virtual void SetMaterial(const Material& material) = 0;
};

class IEffectMeshData
{
public:
    virtual MeshDataInput GetInputData(const MeshData& meshData) = 0;
};

這裏我們繼續使用之前BasicEffect的名字,接口部分也和前面的相差不大:

class BasicEffect : public IEffect, public IEffectTransform,
    public IEffectMaterial, public IEffectMeshData
{
public:
    BasicEffect();
    virtual ~BasicEffect() override;

    BasicEffect(BasicEffect&& moveFrom) noexcept;
    BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;

    // 獲取單例
    static BasicEffect& Get();

    // 初始化所需資源
    bool InitAll(ID3D11Device* device);

    //
    // IEffectTransform
    //

    void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W) override;
    void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V) override;
    void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P) override;

    //
    // IEffectMaterial
    //

    void SetMaterial(const Material& material) override;

    //
    // IEffectMeshData
    //

    MeshDataInput GetInputData(const MeshData& meshData) override;


    //
    // BasicEffect
    //

    // 默認狀態來繪製
    void SetRenderDefault(ID3D11DeviceContext* deviceContext);
    
    // 各種類型燈光允許的最大數目
    static const int maxLights = 5;

    void SetDirLight(uint32_t pos, const DirectionalLight& dirLight);
    void SetPointLight(uint32_t pos, const PointLight& pointLight);
    void SetSpotLight(uint32_t pos, const SpotLight& spotLight);

    void SetEyePos(const DirectX::XMFLOAT3& eyePos);

    // 應用常量緩衝區和紋理資源的變更
    void Apply(ID3D11DeviceContext* deviceContext) override;

private:
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

在初始化階段,我們需要爲EffectHelper添加shader,創建頂點佈局和EffectPasses

template<size_t numElements>
using D3D11_INPUT_ELEMENT_DESC_ARRAY = const D3D11_INPUT_ELEMENT_DESC(&)[numElements];

struct VertexPosNormalTex
{
    // ...
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT2 tex;

    static D3D11_INPUT_ELEMENT_DESC_ARRAY<3> GetInputLayout()
    {
        static const D3D11_INPUT_ELEMENT_DESC inputLayout[3] = {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 1, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 2, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }
        };
        return inputLayout;
    }
};

//
// BasicEffect::Impl 需要先於BasicEffect的定義
//

class BasicEffect::Impl
{
public:
    // 必須顯式指定
    Impl() {}
    ~Impl() = default;

public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    std::unique_ptr<EffectHelper> m_pEffectHelper;

    std::shared_ptr<IEffectPass> m_pCurrEffectPass;

    ComPtr<ID3D11InputLayout> m_pVertexPosNormalTexLayout;

    XMFLOAT4X4 m_World{}, m_View{}, m_Proj{};
};

bool BasicEffect::InitAll(ID3D11Device* device)
{
    if (!device)
        return false;

    if (!RenderStates::IsInit())
        throw std::exception("RenderStates need to be initialized first!");

    pImpl->m_pEffectHelper = std::make_unique<EffectHelper>();

    Microsoft::WRL::ComPtr<ID3DBlob> blob;
    // 創建頂點着色器
    pImpl->m_pEffectHelper->CreateShaderFromFile("BasicVS", L"Shaders/Basic_VS.cso", device,
        "VS", "vs_5_0", nullptr, blob.GetAddressOf());
    // 創建頂點佈局
    HR(device->CreateInputLayout(VertexPosNormalTex::GetInputLayout(), ARRAYSIZE(VertexPosNormalTex::GetInputLayout()),
        blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexPosNormalTexLayout.GetAddressOf()));

    // 創建像素着色器
    pImpl->m_pEffectHelper->CreateShaderFromFile("BasicPS", L"Shaders/Basic_PS.cso", device,
        "ForwardPS", "ps_5_0");

    
    // 創建通道
    EffectPassDesc passDesc;
    passDesc.nameVS = "BasicVS";
    passDesc.namePS = "BasicPS";
    pImpl->m_pEffectHelper->AddEffectPass("Basic", device, &passDesc);

    pImpl->m_pEffectHelper->SetSamplerStateByName("g_Sam", RenderStates::SSLinearWrap.Get());


    return true;
}

然後是一些設置和獲取方法的使用示例:

void XM_CALLCONV BasicEffect::SetWorldMatrix(DirectX::FXMMATRIX W)
{
    XMStoreFloat4x4(&pImpl->m_World, W);
}

void XM_CALLCONV BasicEffect::SetViewMatrix(DirectX::FXMMATRIX V)
{
    XMStoreFloat4x4(&pImpl->m_View, V);
}

void XM_CALLCONV BasicEffect::SetProjMatrix(DirectX::FXMMATRIX P)
{
    XMStoreFloat4x4(&pImpl->m_Proj, P);
}

void BasicEffect::SetMaterial(const Material& material)
{
    TextureManager& tm = TextureManager::Get();

    PhongMaterial phongMat{};
    phongMat.ambient = material.Get<XMFLOAT4>("$AmbientColor");
    phongMat.diffuse = material.Get<XMFLOAT4>("$DiffuseColor");
    phongMat.specular = material.Get<XMFLOAT4>("$SpecularColor");
    phongMat.specular.w = material.Has<float>("$SpecularFactor") ? material.Get<float>("$SpecularFactor") : 1.0f;
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_Material")->SetRaw(&phongMat);

    const auto& str = material.Get<std::string>("$Diffuse");
    pImpl->m_pEffectHelper->SetShaderResourceByName("g_DiffuseMap", tm.GetTexture(str));
}

MeshDataInput BasicEffect::GetInputData(const MeshData& meshData)
{
    MeshDataInput input;
    input.pVertexBuffers = {
        meshData.m_pVertices.Get(),
        meshData.m_pNormals.Get(),
        meshData.m_pTexcoordArrays.empty() ? nullptr : meshData.m_pTexcoordArrays[0].Get()
    };
    input.strides = { 12, 12, 8 };
    input.offsets = { 0, 0, 0 };

    input.pIndexBuffer = meshData.m_pIndices.Get();
    input.indexCount = meshData.m_IndexCount;

    return input;
}

void BasicEffect::SetDirLight(uint32_t pos, const DirectionalLight& dirLight)
{
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_DirLight")->SetRaw(&dirLight, (sizeof dirLight) * pos, sizeof dirLight);
}

void BasicEffect::SetPointLight(uint32_t pos, const PointLight& pointLight)
{
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_PointLight")->SetRaw(&pointLight, (sizeof pointLight) * pos, sizeof pointLight);
}

void BasicEffect::SetSpotLight(uint32_t pos, const SpotLight& spotLight)
{
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_SpotLight")->SetRaw(&spotLight, (sizeof spotLight) * pos, sizeof spotLight);
}

void BasicEffect::SetEyePos(const DirectX::XMFLOAT3& eyePos)
{
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_EyePosW")->SetFloatVector(3, reinterpret_cast<const float*>(&eyePos));
}

void BasicEffect::SetRenderDefault(ID3D11DeviceContext* deviceContext)
{
    deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalTexLayout.Get());
    pImpl->m_pCurrEffectPass = pImpl->m_pEffectHelper->GetEffectPass("Basic");
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
}

最後Apply方法會調用當前IEffectPassApply

void BasicEffect::Apply(ID3D11DeviceContext* deviceContext)
{
    XMMATRIX W = XMLoadFloat4x4(&pImpl->m_World);
    XMMATRIX V = XMLoadFloat4x4(&pImpl->m_View);
    XMMATRIX P = XMLoadFloat4x4(&pImpl->m_Proj);

    XMMATRIX VP = V * P;
    XMMATRIX WInvT = XMath::InverseTranspose(W);

    W = XMMatrixTranspose(W);
    VP = XMMatrixTranspose(VP);
    WInvT = XMMatrixTranspose(WInvT);

    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_WorldInvTranspose")->SetFloatMatrix(4, 4, (FLOAT*)&WInvT);
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_ViewProj")->SetFloatMatrix(4, 4, (FLOAT*)&VP);
    pImpl->m_pEffectHelper->GetConstantBufferVariable("g_World")->SetFloatMatrix(4, 4, (FLOAT*)&W);

    if (pImpl->m_pCurrEffectPass)
        pImpl->m_pCurrEffectPass->Apply(deviceContext);
}

GameObject類的變化

現在GameObject類主要存有ModelTransform的數據,目前主要關注的是GameObject::Draw方法(有所刪減):

void GameObject::Draw(ID3D11DeviceContext * deviceContext, IEffect& effect)
{
    if (/* ... */!deviceContext)
        return;
    size_t sz = m_pModel->meshdatas.size();
    for (size_t i = 0; i < sz; ++i)
    {
        IEffectMeshData* pEffectMeshData = dynamic_cast<IEffectMeshData*>(&effect);
        if (!pEffectMeshData)
            continue;

        IEffectMaterial* pEffectMaterial = dynamic_cast<IEffectMaterial*>(&effect);
        if (pEffectMaterial)
            pEffectMaterial->SetMaterial(m_pModel->materials[m_pModel->meshdatas[i].m_MaterialIndex]);

        IEffectTransform* pEffectTransform = dynamic_cast<IEffectTransform*>(&effect);
        if (pEffectTransform)
            pEffectTransform->SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM());

        effect.Apply(deviceContext);

        MeshDataInput input = pEffectMeshData->GetInputData(m_pModel->meshdatas[i]);
        {
            deviceContext->IASetVertexBuffers(0, (uint32_t)input.pVertexBuffers.size(), 
                input.pVertexBuffers.data(), input.strides.data(), input.offsets.data());
            deviceContext->IASetIndexBuffer(input.pIndexBuffer, input.indexCount > 65535 ? DXGI_FORMAT_R32_UINT : DXGI_FORMAT_R16_UINT, 0);

            deviceContext->DrawIndexed(input.indexCount, 0, 0);
        }
        
    }
}

模型加載演示

這裏我選用了之前合作項目時設計師完成的房屋模型,經過Assimp加載後進行繪製。效果如下:

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章