1. 程式人生 > >DirectX11 With Windows SDK--19 模型載入:obj格式的讀取及使用二進位制檔案提升讀取效率

DirectX11 With Windows SDK--19 模型載入:obj格式的讀取及使用二進位制檔案提升讀取效率

前言

一個模型通常是由三個部分組成:網格、紋理、材質。在一開始的時候,我們是通過Geometry類來生成簡單幾何體的網格。但現在我們需要尋找合適的方式去表述一個複雜的網格,而且包含網格的檔案型別多種多樣,對應的描述方式也存在著差異。這一章我們主要研究obj格式檔案的讀取。

.obj格式

.obj格式是Alias|Wavefront公司推出的一種模型檔案格式,通常以文字形式進行描述,因此你可以按記事本來開啟檢視裡面的內容。通過市面上的一些常見的建模軟體如3dsMax,Maya等都可以匯出.obj檔案。一些遊戲引擎如Unity3d也支援匯入.obj格式的模型。該檔案可以直接描述多邊形、法向量、紋理座標等等資訊。

.obj檔案結構簡述

.obj檔案內部的每一行具體含義取決於開頭以空格、製表符分隔的關鍵字是什麼。這裡只根據當前專案需要的部分來描述關鍵字

關鍵字 含義
# 這一行是一條註釋

頂點資料:

關鍵字 含義
v 這是一個3D頂點座標
vt 這是一個紋理座標
vn 這是一個3D法向量

元素:

關鍵字 含義
f 這是一個面,這裡我們只支援三角形構成的面

組合:

關鍵字 含義
g 這是一個組,後面接著的內容是組的名稱
o 這是一個物件,後面接著的內容是物件的名稱

材質:

關鍵字 含義
mtllib 需要載入.mtl材質檔案,後面接著的內容是檔名
usemtl 使用載入的.mtl材質檔案中的某一材質,後面接著的內容是材質名

.mtl檔案結構簡述

.mtl檔案內部描述方式和.obj檔案一樣,但裡面使用的關鍵字有所不同

關鍵字 含義
# 這一行是一條註釋
newmtl 這是一個新的材質,後面接著的內容是材質名稱

材質描述:

關鍵字 含義
Ka 環境光反射顏色
Kd 漫射光反射顏色
Ks 鏡面反射光反射顏色
d 不透明度,即Alpha值
Tr 透明度,即1.0 - Alpha值
map_Ka 環境光反射指定的紋理檔案
map_Kd 漫射光反射指定的紋理檔案

簡單示例

現在要通過.obj檔案來描述一個平面正方形草叢。ground.obj檔案如下:

mtllib ground.mtl

v -10.0 -1.0 -10.0
v -10.0 -1.0 10.0
v 10.0 -1.0 10.0
v 10.0 -1.0 -10.0

vn 0.0 0.0 -1.0

vt 0.0 0.0
vt 0.0 5.0
vt 5.0 5.0
vt 5.0 0.0

g Square
usemtl TestMat
f 1/1/1 2/2/1 3/3/1
f 3/3/1 4/4/1 1/1/1
# 2 faces

其中根據v的先後出現順序,對應的索引為1到4。若索引值為3,則對應第3行v對應的頂點

注意: 索引的初始值在.obj中為1,而不是0!

而諸如1/1/1這樣的三索引對應的含義為:頂點座標索引/紋理座標索引/法向量索引

若寫成1//1,則表明不使用紋理座標,但現在在我們的專案中不允許缺少上面任何一種索引

這樣在一個f裡面出現頂點座標索引/紋理座標索引/法向量索引的次數說明了該面的頂點數目,目前我們也僅考慮支援三角形面

一個模型最少需要包含一個組或一個物件

注意:.obj紋理座標是基於笛卡爾座標系的,即(0.3, 0.7)對應的是實際的紋理座標(0.3, 0.3),即需要做(x, 1.0 - y)的變換

而.mtl檔案的描述如下

newmtl TestMat
    d 1.0000
    Ns 10.0000
    Ka 0.8000 0.8000 0.8000
    Kd 0.3000 0.3000 0.3000
    Ks 0.0000 0.0000 0.0000
    map_Ka grass.dds
    map_Kd grass.dds

漫反射和環境光反射都將使用同一種紋理。

使用自定義二進位制資料格式提升讀取效率

使用文字型別的.obj格式檔案進行讀取的話必然要面臨一個比較嚴重的問題:模型網格的面數較多會導致文字量極大,直接讀取.obj的效率會非常低下。通常推薦在第一次讀取.obj檔案匯入到程式後,再將讀取好的頂點等資訊以二進位制檔案的形式合理安排內容佈局並儲存,然後下次執行的時候讀取該二進位制檔案來獲取模型資訊,可以大幅度加快讀取速度,並且節省了一定的記憶體空間。

現在來說明下當前專案下自定義二進位制格式.mbo的位元組佈局:

// [Part數目] 4位元組
// [AABB盒頂點vMax] 12位元組
// [AABB盒頂點vMin] 12位元組
// [Part
//   [環境光材質檔名]520位元組
//   [漫射光材質檔名]520位元組
//   [材質]64位元組
//   [頂點數]4位元組
//   [索引數]4位元組
//   [頂點]32*頂點數 位元組
//   [索引]2(或4)*索引數 位元組,取決於頂點數是否不超過65535
// ]
// ...

這裡將.obj中的一個組或一個物件定義為.mbo格式中的一個模型部分,然後頂點使用的是VertexPosNormalTex型別,大小為32位元組。索引使用WORDDWORD型別,若當前Part不同的頂點數超過65535,則必須使用DWORD型別來儲存索引。

環境光/漫射光材質檔名使用的是wchar_t[MAX_PATH]的陣列,大小為2*260位元組。

但要注意一開始從.obj匯出的頂點陣列是沒有經過處理(包含重複頂點),需要通過一定的操作分離出頂點陣列和索引陣列才能傳遞給.mbo格式。

ObjReader--讀取.obj/.mbo格式模型

ObjReader.h中包含了ObjReader類和MtlReader類:

#ifndef OBJREADER_H
#define OBJREADER_H

#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <unordered_map>
#include <map>
#include <algorithm>
#include <locale>
#include <filesystem>
#include "Vertex.h"
#include "LightHelper.h"


class MtlReader;

class ObjReader
{
public:
    struct ObjPart
    {
        Material material;                          // 材質
        std::vector<VertexPosNormalTex> vertices;   // 頂點集合
        std::vector<WORD> indices16;                // 頂點數不超過65535時使用
        std::vector<DWORD> indices32;               // 頂點數超過65535時使用
        std::wstring texStrA;                       // 環境光紋理檔名,需為相對路徑,且在mbo必須佔260位元組
        std::wstring texStrD;                       // 漫射光紋理檔名,需為相對路徑,在mbo必須佔260位元組
    };

    // 指定.mbo檔案的情況下,若.mbo檔案存在,優先讀取該檔案
    // 否則會讀取.obj檔案
    // 若.obj檔案被讀取,且提供了.mbo檔案的路徑,則會根據已經讀取的資料建立.mbo檔案
    bool Read(const wchar_t* mboFileName, const wchar_t* objFileName);
    
    bool ReadObj(const wchar_t* objFileName);
    bool ReadMbo(const wchar_t* mboFileName);
    bool WriteMbo(const wchar_t* mboFileName);
public:
    std::vector<ObjPart> objParts;
    DirectX::XMFLOAT3 vMin, vMax;                   // AABB盒雙頂點
private:
    void AddVertex(const VertexPosNormalTex& vertex, DWORD vpi, DWORD vti, DWORD vni);

    // 快取有v/vt/vn字串資訊
    std::unordered_map<std::wstring, DWORD> vertexCache;
};

class MtlReader
{
public:
    bool ReadMtl(const wchar_t* mtlFileName);


public:
    std::map<std::wstring, Material> materials;
    std::map<std::wstring, std::wstring> mapKaStrs;
    std::map<std::wstring, std::wstring> mapKdStrs;
};


#endif

ObjReader.cpp定義如下:

#include "ObjReader.h"

using namespace DirectX;
using namespace std::experimental;
bool ObjReader::Read(const wchar_t * mboFileName, const wchar_t * objFileName)
{
    if (mboFileName && filesystem::exists(mboFileName))
    {
        return ReadMbo(mboFileName);
    }
    else if (objFileName && filesystem::exists(objFileName))
    {
        bool status = ReadObj(objFileName);
        if (status && mboFileName)
            return WriteMbo(mboFileName);
        return status;
    }

    return false;
}

bool ObjReader::ReadObj(const wchar_t * objFileName)
{
    objParts.clear();
    vertexCache.clear();

    MtlReader mtlReader;

    std::vector<XMFLOAT3>   positions;
    std::vector<XMFLOAT3>   normals;
    std::vector<XMFLOAT2>   texCoords;

    XMVECTOR vecMin = g_XMInfinity, vecMax = g_XMNegInfinity;

    std::wifstream wfin(objFileName);
    // 切換中文
    std::locale china("chs");
    wfin.imbue(china);
    for (;;)
    {
        std::wstring wstr;
        if (!(wfin >> wstr))
            break;

        if (wstr[0] == '#')
        {
            //
            // 忽略註釋所在行
            //
            while (!wfin.eof() && wfin.get() != '\n')
                continue;
        }
        else if (wstr == L"o" || wstr == L"g")
        {
            // 
            // 物件名(組名)
            //
            objParts.emplace_back(ObjPart());
            // 提供預設材質
            objParts.back().material.Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
            objParts.back().material.Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
            objParts.back().material.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);

            vertexCache.clear();
        }
        else if (wstr == L"v")
        {
            //
            // 頂點位置
            //
            XMFLOAT3 pos;
            wfin >> pos.x >> pos.y >> pos.z;
            positions.push_back(pos);
            XMVECTOR vecPos = XMLoadFloat3(&pos);
            vecMax = XMVectorMax(vecMax, vecPos);
            vecMin = XMVectorMin(vecMin, vecPos);
        }
        else if (wstr == L"vt")
        {
            //
            // 頂點紋理座標
            //

            // 注意obj使用的是笛卡爾座標系,而不是紋理座標系
            float u, v;
            wfin >> u >> v;
            v = 1.0f - v;
            texCoords.emplace_back(XMFLOAT2(u, v));
        }
        else if (wstr == L"vn")
        {
            //
            // 頂點法向量
            //
            float x, y, z;
            wfin >> x >> y >> z;
            normals.emplace_back(XMFLOAT3(x, y, z));
        }
        else if (wstr == L"mtllib")
        {
            //
            // 指定某一檔案的材質
            //
            std::wstring mtlFile;
            wfin >> mtlFile;
            // 去掉前後空格
            size_t beg = 0, ed = mtlFile.size();
            while (iswspace(mtlFile[beg]))
                beg++;
            while (ed > beg && iswspace(mtlFile[ed - 1]))
                ed--;
            mtlFile = mtlFile.substr(beg, ed - beg);
            // 獲取路徑
            std::wstring dir = objFileName;
            size_t pos;
            if ((pos = dir.find_last_of('/')) == std::wstring::npos &&
                (pos = dir.find_last_of('\\')) == std::wstring::npos)
            {
                pos = 0;
            }
            else
            {
                pos += 1;
            }
                

            mtlReader.ReadMtl((dir.erase(pos) + mtlFile).c_str());
        }
        else if (wstr == L"usemtl")
        {
            //
            // 使用之前指定檔案內部的某一材質
            //
            std::wstring mtlName;
            std::getline(wfin, mtlName);
            // 去掉前後空格
            size_t beg = 0, ed = mtlName.size();
            while (iswspace(mtlName[beg]))
                beg++;
            while (ed > beg && iswspace(mtlName[ed - 1]))
                ed--;
            mtlName = mtlName.substr(beg, ed - beg);

            objParts.back().material = mtlReader.materials[mtlName];
            objParts.back().texStrA = mtlReader.mapKaStrs[mtlName];
            objParts.back().texStrD = mtlReader.mapKdStrs[mtlName];
        }
        else if (wstr == L"f")
        {
            //
            // 幾何面
            //
            VertexPosNormalTex vertex;
            DWORD vpi, vni, vti;
            wchar_t ignore;

            // 確定
            // 頂點位置索引/紋理座標索引/法向量索引
            wfin >> vpi >> ignore >> vti >> ignore >> vni;
            vertex.pos = positions[vpi - 1];
            vertex.normal = normals[vni - 1];
            vertex.tex = texCoords[vti - 1];
            AddVertex(vertex, vpi, vti, vni);

            wfin >> vpi >> ignore >> vti >> ignore >> vni;
            vertex.pos = positions[vpi - 1];
            vertex.normal = normals[vni - 1];
            vertex.tex = texCoords[vti - 1];
            AddVertex(vertex, vpi, vti, vni);

            wfin >> vpi >> ignore >> vti >> ignore >> vni;
            vertex.pos = positions[vpi - 1];
            vertex.normal = normals[vni - 1];
            vertex.tex = texCoords[vti - 1];
            AddVertex(vertex, vpi, vti, vni);

            while (iswblank(wfin.peek()))
                wfin.get();
            // 幾何面頂點數可能超過了3,不支援該格式
            if (wfin.peek() != '\n')
                return false;
        }
    }

    // 頂點數不超過WORD的最大值的話就使用16位WORD儲存
    for (auto& part : objParts)
    {
        if (part.vertices.size() < 65535)
        {
            for (auto& i : part.indices32)
            {
                part.indices16.push_back((WORD)i);
            }
            part.indices32.clear();
        }
    }

    XMStoreFloat3(&vMax, vecMax);
    XMStoreFloat3(&vMin, vecMin);

    return true;
}

bool ObjReader::ReadMbo(const wchar_t * mboFileName)
{
    // [Part數目] 4位元組
    // [AABB盒頂點vMax] 12位元組
    // [AABB盒頂點vMin] 12位元組
    // [Part
    //   [環境光材質檔名]520位元組
    //   [漫射光材質檔名]520位元組
    //   [材質]64位元組
    //   [頂點數]4位元組
    //   [索引數]4位元組
    //   [頂點]32*頂點數 位元組
    //   [索引]2(或4)*索引數 位元組,取決於頂點數是否不超過65535
    // ]
    // ...
    std::ifstream fin(mboFileName, std::ios::in | std::ios::binary);
    if (!fin.is_open())
        return false;

    UINT parts = (UINT)objParts.size();
    // [Part數目] 4位元組
    fin.read(reinterpret_cast<char*>(&parts), sizeof(UINT));
    objParts.resize(parts);

    // [AABB盒頂點vMax] 12位元組
    fin.read(reinterpret_cast<char*>(&vMax), sizeof(XMFLOAT3));
    // [AABB盒頂點vMin] 12位元組
    fin.read(reinterpret_cast<char*>(&vMin), sizeof(XMFLOAT3));


    for (UINT i = 0; i < parts; ++i)
    {
        wchar_t filePath[MAX_PATH];
        // [環境光材質檔名]520位元組
        fin.read(reinterpret_cast<char*>(filePath), MAX_PATH * sizeof(wchar_t));
        objParts[i].texStrA = filePath;
        // [漫射光材質檔名]520位元組
        fin.read(reinterpret_cast<char*>(filePath), MAX_PATH * sizeof(wchar_t));
        objParts[i].texStrD = filePath;
        // [材質]64位元組
        fin.read(reinterpret_cast<char*>(&objParts[i].material), sizeof(Material));
        UINT vertexCount, indexCount;
        // [頂點數]4位元組
        fin.read(reinterpret_cast<char*>(&vertexCount), sizeof(UINT));
        // [索引數]4位元組
        fin.read(reinterpret_cast<char*>(&indexCount), sizeof(UINT));
        // [頂點]32*頂點數 位元組
        objParts[i].vertices.resize(vertexCount);
        fin.read(reinterpret_cast<char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));

        if (vertexCount > 65535)
        {
            // [索引]4*索引數 位元組
            objParts[i].indices32.resize(indexCount);
            fin.read(reinterpret_cast<char*>(objParts[i].indices32.data()), indexCount * sizeof(DWORD));
        }
        else
        {
            // [索引]2*索引數 位元組
            objParts[i].indices16.resize(indexCount);
            fin.read(reinterpret_cast<char*>(objParts[i].indices16.data()), indexCount * sizeof(WORD));
        }
    }

    fin.close();

    return true;
}

bool ObjReader::WriteMbo(const wchar_t * mboFileName)
{
    // [Part數目] 4位元組
    // [AABB盒頂點vMax] 12位元組
    // [AABB盒頂點vMin] 12位元組
    // [Part
    //   [環境光材質檔名]520位元組
    //   [漫射光材質檔名]520位元組
    //   [材質]64位元組
    //   [頂點數]4位元組
    //   [索引數]4位元組
    //   [頂點]32*頂點數 位元組
    //   [索引]2(或4)*索引數 位元組,取決於頂點數是否不超過65535
    // ]
    // ...
    std::ofstream fout(mboFileName, std::ios::out | std::ios::binary);
    UINT parts = (UINT)objParts.size();
    // [Part數目] 4位元組
    fout.write(reinterpret_cast<const char*>(&parts), sizeof(UINT));

    // [AABB盒頂點vMax] 12位元組
    fout.write(reinterpret_cast<const char*>(&vMax), sizeof(XMFLOAT3));
    // [AABB盒頂點vMin] 12位元組
    fout.write(reinterpret_cast<const char*>(&vMin), sizeof(XMFLOAT3));

    // [Part
    for (UINT i = 0; i < parts; ++i)
    {
        wchar_t filePath[MAX_PATH];
        wcscpy_s(filePath, objParts[i].texStrA.c_str());
        // [環境光材質檔名]520位元組
        fout.write(reinterpret_cast<const char*>(filePath), MAX_PATH * sizeof(wchar_t));
        wcscpy_s(filePath, objParts[i].texStrD.c_str());
        // [漫射光材質檔名]520位元組
        fout.write(reinterpret_cast<const char*>(filePath), MAX_PATH * sizeof(wchar_t));
        // [材質]64位元組
        fout.write(reinterpret_cast<const char*>(&objParts[i].material), sizeof(Material));
        UINT vertexCount = (UINT)objParts[i].vertices.size();
        // [頂點數]4位元組
        fout.write(reinterpret_cast<const char*>(&vertexCount), sizeof(UINT));

        UINT indexCount;
        if (vertexCount > 65535)
        {
            indexCount = (UINT)objParts[i].indices32.size();
            // [索引數]4位元組
            fout.write(reinterpret_cast<const char*>(&indexCount), sizeof(UINT));
            // [頂點]32*頂點數 位元組
            fout.write(reinterpret_cast<const char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));
            // [索引]2(或4)*索引數 位元組,取決於頂點數是否不超過65535
            fout.write(reinterpret_cast<const char*>(objParts[i].indices32.data()), indexCount * sizeof(DWORD));
        }
        else
        {
            indexCount = (UINT)objParts[i].indices16.size();
            // [索引數]4位元組
            fout.write(reinterpret_cast<const char*>(&indexCount), sizeof(UINT));
            // [頂點]32*頂點數 位元組
            fout.write(reinterpret_cast<const char*>(objParts[i].vertices.data()), vertexCount * sizeof(VertexPosNormalTex));
            //   [索引]2(或4)*索引數 位元組,取決於頂點數是否不超過65535
            fout.write(reinterpret_cast<const char*>(objParts[i].indices16.data()), indexCount * sizeof(WORD));
        }
    }
    // ]
    fout.close();

    return true;
}

void ObjReader::AddVertex(const VertexPosNormalTex& vertex, DWORD vpi, DWORD vti, DWORD vni)
{
    std::wstring idxStr = std::to_wstring(vpi) + L"/" + std::to_wstring(vti) + L"/" + std::to_wstring(vni);

    // 尋找是否有重複頂點
    auto it = vertexCache.find(idxStr);
    if (it != vertexCache.end())
    {
        objParts.back().indices32.push_back(it->second);
    }
    else
    {
        objParts.back().vertices.push_back(vertex);
        DWORD pos = objParts.back().vertices.size() - 1;
        vertexCache[idxStr] = pos;
        objParts.back().indices32.push_back(pos);
    }
}



bool MtlReader::ReadMtl(const wchar_t * mtlFileName)
{
    materials.clear();
    mapKaStrs.clear();
    mapKdStrs.clear();


    std::wifstream wfin(mtlFileName);
    std::locale china("chs");
    wfin.imbue(china);


    if (!wfin.is_open())
        return false;

    std::wstring wstr;
    std::wstring currMtl;
    for (;;)
    {
        if (!(wfin >> wstr))
            break;

        if (wstr[0] == '#')
        {
            //
            // 忽略註釋所在行
            //
            while (wfin.get() != '\n')
                continue;
        }
        else if (wstr == L"newmtl")
        {
            //
            // 新材質
            //

            std::getline(wfin, currMtl);
            // 去掉前後空格
            size_t beg = 0, ed = currMtl.size();
            while (iswspace(currMtl[beg]))
                beg++;
            while (ed > beg && iswspace(currMtl[ed - 1]))
                ed--;
            currMtl = currMtl.substr(beg, ed - beg);
        }
        else if (wstr == L"Ka")
        {
            //
            // 環境光反射顏色
            //
            XMFLOAT4& ambient = materials[currMtl].Ambient;
            wfin >> ambient.x >> ambient.y >> ambient.z;
            if (ambient.w == 0.0f)
                ambient.w = 1.0f;
        }
        else if (wstr == L"Kd")
        {
            //
            // 漫射光反射顏色
            //
            XMFLOAT4& diffuse = materials[currMtl].Diffuse;
            wfin >> diffuse.x >> diffuse.y >> diffuse.z;
            if (diffuse.w == 0.0f)
                diffuse.w = 1.0f;
        }
        else if (wstr == L"Ks")
        {
            //
            // 鏡面光反射顏色
            //
            XMFLOAT4& specular = materials[currMtl].Specular;
            wfin >> specular.x >> specular.y >> specular.z;
        }
        else if (wstr == L"Ns")
        {
            //
            // 鏡面係數
            //
            wfin >> materials[currMtl].Specular.w;
        }
        else if (wstr == L"d" || wstr == L"Tr")
        {
            //
            // d為不透明度 Tr為透明度
            //
            float alpha;
            wfin >> alpha;
            if (wstr == L"Tr")
                alpha = 1.0f - alpha;
            materials[currMtl].Ambient.w = alpha;
            materials[currMtl].Diffuse.w = alpha;
        }
        else if (wstr == L"map_Ka" || wstr == L"map_Kd")
        {
            //
            // map_Ka為環境光反射使用的紋理,map_Kd為漫反射使用的紋理
            //
            std::wstring fileName;
            std::getline(wfin, fileName);
            // 去掉前後空格
            size_t beg = 0, ed = fileName.size();
            while (iswspace(fileName[beg]))
                beg++;
            while (ed > beg && iswspace(fileName[ed - 1]))
                ed--;
            fileName = fileName.substr(beg, ed - beg);

            // 追加路徑
            std::wstring dir = mtlFileName;
            size_t pos;
            if ((pos = dir.find_last_of('/')) == std::wstring::npos &&
                (pos = dir.find_last_of('\\')) == std::wstring::npos)
                pos = 0;
            else
                pos += 1;

            if (wstr == L"map_Ka")
                mapKaStrs[currMtl] = dir.erase(pos) + fileName;
            else
                mapKdStrs[currMtl] = dir.erase(pos) + fileName;
        }
    }

    return true;
}

其中AddVertex方法用於去除重複的頂點,並構建索引陣列。

在改為讀取.mbo檔案後,原本讀取.obj需要耗時3s,現在可以降到2ms以內,大幅提升了讀取效率。其關鍵點就在於要構造連續性的二進位制資料以減少讀取次數,並剔除掉原本讀取.obj時的各種詞法分析部分(在該部分也浪費了大量的時間)。

由於ObjReader類對.obj格式的檔案要求比較嚴格,如果出現不能正確載入的現象,請檢查是否出現下面這些情況,否則需要自行修改.obj/.mtl檔案,或者給ObjReader實現更多的功能:

  1. 使用了/將下一行的內容連線在一起表示一行
  2. 存在索引為負數
  3. 使用了類似1//2這樣的頂點(即不包含紋理座標的頂點)
  4. 使用了絕對路徑的檔案引用
  5. 相對路徑使用了.和..兩種路徑格式
  6. 若.mtl材質檔案不存在,則內部會使用預設材質值
  7. 若.mtl內部沒有指定紋理檔案引用,需要另外自行載入紋理
  8. 網格只能以三角形構造,即一個f的頂點數只能為3

GameObject類的改進

因為下一章還會講到硬體例項化,所以GameObject類在後期還會有所改動,現在只放出聲明部分:

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

    struct GameObjectPart
    {
        Material material;
        ComPtr<ID3D11ShaderResourceView> texA;
        ComPtr<ID3D11ShaderResourceView> texD;
        ComPtr<ID3D11Buffer> vertexBuffer;
        ComPtr<ID3D11Buffer> indexBuffer;
        UINT vertexCount;
        UINT indexCount;
        DXGI_FORMAT indexFormat;
    };

    GameObject();

    // 獲取位置
    DirectX::XMFLOAT3 GetPosition() const;
    // 獲取子模型
    const GameObjectPart& GetPart(size_t pos) const;
    // 獲取包圍盒
    void GetBoundingBox(DirectX::BoundingBox& box) const;
    void GetBoundingBox(DirectX::BoundingBox& box, DirectX::FXMMATRIX worldMatrix) const;
    //
    // 設定模型
    //
    
    void SetModel(ComPtr<ID3D11Device> device, const ObjReader& model);
    
    //
    // 設定網格
    //

    void SetMesh(ComPtr<ID3D11Device> device, const Geometry::MeshData& meshData);
    void SetMesh(ComPtr<ID3D11Device> device, const std::vector<VertexPosNormalTex>& vertices, const std::vector<WORD> indices);
    void SetMesh(ComPtr<ID3D11Device> device, const std::vector<VertexPosNormalTex>& vertices, const std::vector<DWORD> indices);
    
    //
    // 設定紋理
    //

    void SetTexture(ComPtr<ID3D11ShaderResourceView> texture);
    void SetTexture(ComPtr<ID3D11ShaderResourceView> texA, ComPtr<ID3D11ShaderResourceView> texD);
    void SetTexture(size_t partIndex, ComPtr<ID3D11ShaderResourceView> texture);
    void SetTexture(size_t partIndex, ComPtr<ID3D11ShaderResourceView> texA, ComPtr<ID3D11ShaderResourceView> texD);
    
    //
    // 設定材質
    //

    void SetMaterial(const Material& material);
    void SetMaterial(size_t partIndex, const Material& material);
    
    //
    // 設定矩陣
    //

    void SetWorldMatrix(const DirectX::XMFLOAT4X4& world);
    void SetWorldMatrix(DirectX::FXMMATRIX world);
    void SetTexTransformMatrix(const DirectX::XMFLOAT4X4& texTransform);
    void SetTexTransformMatrix(DirectX::FXMMATRIX texTransform);


    // 繪製物件
    void Draw(ComPtr<ID3D11DeviceContext> deviceContext);

private:
    void SetMesh(ComPtr<ID3D11Device> device, const VertexPosNormalTex* vertices, UINT vertexCount,
        const void * indices, UINT indexCount, DXGI_FORMAT indexFormat);

private:
    DirectX::XMFLOAT4X4 mWorldMatrix;                           // 世界矩陣
    DirectX::XMFLOAT4X4 mTexTransform;                          // 紋理變換矩陣
    std::vector<GameObjectPart> mParts;                         // 模型的各個部分
    DirectX::BoundingBox mBoundingBox;                          // 模型的AABB盒
};

剩餘一些不是很重大的變動就不放出來了,比如BasicObjectFX類和對應的hlsl的變動,可以檢視原始碼(文首文末都有)。

模型載入演示

這裡我選用了之前合作專案時設計師完成的房屋模型,經過ObjReader載入後實裝到GameObject以進行繪製。效果如下: