Vulkan教程 - 23 加載模型

現在你的程序已經爲有貼圖的3D網格渲染做好準備了,但是現在的頂點和索引數組都是比較無聊的。本章我們擴展該程序來從真實的模型文件加載頂點和索引數據,以讓顯卡做點真正的工作。

許多圖形API教程讓讀者自己寫OBJ加載器,這樣做的問題是,稍微有點意思的3D程序很快會要求本格式不支持的特性,比如骨骼動畫。我們會從OBJ模型加載數據,但是我們主要關注集成網格數據,而不是如何從文件加載。

我們會使用tinyobjloader庫來從OBJ文件加載頂點和麪。它比較快,易於集成,因爲它就是一個單個的庫文件,就和stb_image一樣。到:

https://github.com/syoyo/tinyobjloader

下載最新的tiny_obj_loader.h文件,放在自己的庫目錄中。確保下載的是master分支的,因爲最新的官方發行版已經過時了。

將其添加到VS的附加包含目錄中。

本章我們不會啓用光照,所以用已經有光照烘焙貼圖的模型是不錯的選擇。可以在:

https://sketchfab.com/

找到這樣的模型,且許多都是OBJ格式,許可也比較自由。這裏選擇Escadrone的Chalet Hippolyte Chassande Baroz模型:

https://sketchfab.com/3d-models/chalet-hippolyte-chassande-baroz-e925320e1d5744d9ae661aeff61e7aef

我調整了其大小和方向來作爲當前幾何體的替換:

https://vulkan-tutorial.com/resources/chalet.obj.zip

https://vulkan-tutorial.com/resources/chalet.jpg

它有五十萬三角形,對我們程序來說十個不錯的基準測試。可以用自己的模型,但是要保證它僅有一個材質,且它的大小大約爲1.5*1.5*1.5單位。如果比這個值大,那你要改變視圖矩陣了。在shaders同級目錄建立models目錄,把obj文件放進去。然後將貼圖文件放在textures目錄。

寫兩個新的配置變量定義模型和貼圖路徑:

const int WIDTH = 800;
const int HEIGHT = 600;

const std::string MODEL_PATH = "models/chalet.obj";
const std::string TEXTURE_PATH = "textures/chalet.jpg";

並修改createTextureImage方法來使用該路徑:

stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(),
    &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);

我們打算從模型加載頂點和索引了,所以你應該去除全局vertices和indices數組了。使用不是常量的容器替換它們作爲類成員:

std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;

你應該將索引類型從uint16_t改爲uint32_t,因爲頂點數量會遠超65535。記住還要修改vkCmdBindIndexBuffer參數:

vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT32);

包含tinyobjloader庫和STB一樣,要確保定義TINYOBJLOADER_IMPLEMENTATION來包含方法體以免鏈接報錯:

#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

現在我們要寫一個loadModel方法,使用該庫從網格中抽出數據放到vertices和indices容器中。它應該在初始化Vulkan方法中的頂點和索引緩衝創建之前調用:

loadModel();
createVertexBuffer();
createIndexBuffer();

通過調用tinyobj::LoadObj將模型導入到庫的數據結構:

void loadModel() {
    tinyobj::attrib_t attrib;
    std::vector<tinyobj::shape_t> shapes;
    std::vector<tinyobj::material_t> materials;
    std::string warn, err;

    if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) {
        throw std::runtime_error(warn + err);
    }
}

OBJ文件由點、法線、貼圖座標和麪組成。面由任意數量頂點組成,每個頂點通過索引引用了一個位置,法線及貼圖座標。這使得重用整個頂點成爲可能,且也能重用各個屬性。

attrib容器保存所有點、法線和貼圖座標在attrib.vertices,attrib.normals和attrib.texcoords向量中。shapes容器包含所有獨立對象和它們的面。每個面由一組頂點組成,每個頂點包含了點的索引,法線和貼圖座標屬性。OBJ模型也能定義每個面的材質和貼圖,但是這裏就忽略了。

err字符串包含錯誤信息,warn字符串包含了加載文件時的警告信息,比如丟失材質定義等。只有LoadObj返回false的時候纔是真的失敗。OBJ文件的面實際上可以包含任意數量的頂點,然而我們的程序只能渲染三角形。幸運的是,LoadObj能將這些面轉換爲三角形,這也是默認啓用的功能。

我們會將所有面結合起來,作爲一個模型,所以就遍歷所有形狀即可:

for (const auto& shape : shapes) {

}

三角化特性已經保證每個面有三個點,所以我們現在可以直接迭代整個頂點然後直接導出到我們的vertices向量中:

for (const auto& shape : shapes) {
    for (const auto& index : shape.mesh.indices) {
        Vertex vertex = {};

        vertices.push_back(vertex);
        indices.push_back(indices.size());
    }
}

爲了簡單起見,我們認爲每個頂點是獨一無二的,也就可以用自增索引了。index遍歷是tinyobj::index_t 類型的,包含了vertex_index,normal_index和texcoord_index成員。我們要使用這些索引來查找真正的頂點屬性:

vertex.pos = {
    attrib.vertices[3 * index.vertex_index + 0],
    attrib.vertices[3 * index.vertex_index + 1],
    attrib.vertices[3 * index.vertex_index + 2]
};

vertex.texCoord = {
    attrib.texcoords[2 * index.texcoord_index + 0],
    attrib.texcoords[2 * index.texcoord_index + 1]
};

vertex.color = { 1.0f, 1.0f, 1.0f };

不幸的是,attrib.vertices數組是float類型的而不是glm::vec3這樣的類型,所以你要把索引乘以3。類似地,每個入口都有兩個貼圖座標組件。偏置0,1和2用於訪問X,Y和Z組件,或者貼圖座標中的U和V組件。

選擇Release模式運行程序,否則加載模型會很慢。你會看到這樣的效果:

很好,幾何看着沒問題,但是貼圖是怎麼回事?OBJ格式認爲的座標系統垂直座標爲0的時候表示圖形底部,但是我們將圖像上傳到Vulkan是從上到下的,所以0表示的是圖像頂部。解決該問題的方法就是翻轉垂直座標:

vertex.texCoord = {
    attrib.texcoords[2 * index.texcoord_index + 0],
    1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
};

現在運行程序如下:

我們目前還未用索引緩衝呢。vertices向量包含許多重複的頂點數據,因爲許多頂點是被包含在很多三角形中的。我們應該保存獨一無二的頂點並使用索引緩衝來重用它們。有個比較直白的方法就是使用map或者unordered_map來保存這些獨立頂點和各自的索引:

#include <unordered_map>
...
std::unordered_map<Vertex, uint32_t> uniqueVertices = {};

for (const auto& shape : shapes) {
    for (const auto& index : shape.mesh.indices) {
        Vertex vertex = {};

        ...

        if (uniqueVertices.count(vertex) == 0) {
            uniqueVertices[vertex] = static_cast<uint32_t>(vertices.size());
            vertices.push_back(vertex);
        }

        indices.push_back(uniqueVertices[vertex]);
    }
}

每次我們從OBJ文件讀取一個頂點,我們就檢查是否已經看到過一樣位置一樣貼圖座標的頂點。如果沒有就添加到vertices並存儲其索引到uniqueVertices容器。之後我們添加新頂點的索引到indices。如果我們看到過一樣的頂點,我們就找到它在uniqueVertices中的索引並存儲到indices。

現在程序還無法運行,因爲我們使用用戶自定義類型的結構體作爲哈希表的key需要實現兩個方法:相等性測試和哈希計算。前者容易實現,就是在Vertex結構體中重寫==運算符:

bool operator==(const Vertex& other) const {
    return pos == other.pos && color == other.color && texCoord == other.texCoord;
}

Vertex的哈希方法需要指定std::hash<T>模板明細來實現。哈希方法是個複雜的話題,推薦用下面的方法創建較好質量的哈希方法:

namespace std {
    template<> struct hash<Vertex> {
        size_t operator()(Vertex const& vertex) const {
            return ((hash<glm::vec3>()(vertex.pos) ^
                (hash<glm::vec3>()(vertex.color) << 1)) >> 1) ^
                (hash<glm::vec2>()(vertex.texCoord) << 1);
        }
    };
}

這段代碼放在Vertex之外,哈希方法使用要包含頭文件:

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/hash.hpp>

哈希方法是在gtx目錄定義的,意味着它是實驗性的,因此要定義GLM_ENABLE_EXPERIMENTAL來使用。

現在可以成功運行程序了,如果你檢查了vertices大小,會發現它從一百五十萬降低到了26萬。這意味着每個頂點是被大約6個三角形重用的,這極大減少了GPU內存消耗。

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