1. 程式人生 > >Vulkan Tutorial 29 Loading models

Vulkan Tutorial 29 Loading models

tps 渲染 視圖矩陣 opera 一個 visual 所有 找到 探索

操作系統:Windows8.1

顯卡:Nivida GTX965M

開發工具:Visual Studio 2017


Introduction

應用程序現在已經可以渲染紋理3D模型,但是 vertices 頂點和 indices 索引數組中的幾何體不是很有趣。在本章節我們擴展程序,從實際的模型文件沖加載頂點和索引數據,並使圖形卡實際做一些工作。

許多圖形API系列教程中讓讀者在這樣的章節中編寫自己的OBJ加載程序。這樣做的問題是任何有趣的3D應用程序很快需要某種功能,但是該文件格式不支持,比如骨骼動畫 skeletal animation。我們將在本章加載 OBJ 模型文件的網格數據,但是我們更多關註在網格數據與程序本身進行整合,而不是從文件中加載它們的細節。

Library


我們將使用 tinyobjloader 庫來從OBJ文件中加載vertices和faces數據。它很快速,容易集成,因為它是一個單獨的文件庫,如stb_image。轉到上面鏈接的庫地址,並將 tiny_obj_loader.h 頭文件下載到庫目錄中的文件夾中。

Visual Studio

因為之前已經在VS中設置了引用的庫目錄在解決方案的根目錄下,所以我們直接在庫目錄新建 tinyobjloader目錄 存放 tiny_obj_loader.h 頭文件即可使用。下圖示例:

技術分享技術分享

Sample mesh


在本章中我們不會涉及光照,所以它有助於使用具有烘培到紋理中的光照的樣本模型。找到這樣的模型的簡單方法是直接在 Sketchfab

上查找。該網站上的許多模型都具有OBJ格式,並且都有 lisence 授權許可。

在本教程中我們決定使用 Chalet Hippolyte Chassande Baroz 模型,它是由Escadrone制作並授權的。我調整了模型的大小和方向,將其用作當前幾何體的替代品:

  • chalet.obj
  • chalet.jpg

它有50W個三角形,所以它是我們的應用程序的一個很好的基準。在這裏隨意使用自己的模型文件,但是要確保它們是由一種材質構成的,尺寸約為1.5 x 1.5 x 1.5 單位。如果大於此值,則必須修改視圖矩陣。將模型文件放在 shaderstextures 同級的新模型目錄中 models

,並將紋理貼圖放在 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);

Loading vertices and indices


現在我們將要從模型文件中加載頂點和索引數據,所以應該移除全局的 verticesindices 數組。將它們作為類成員替換為非常量容器:

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

在這裏應該修改索引數據類型 uint16_tuint32_t 。因為將會有超過65535個或者更多的頂點。還需要更改 vkCmdBindIndexBuffer 參數:

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

tinyobjloader庫與STB庫一樣。包括 tiny_obj_loader.h 文件,並確保在一個源文件中定義 TINYOBJLOADER_IMPLEMENTATION 以包含函數體,並避免鏈接錯誤:

#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

我們現在編寫一個 loadModel 函數,該函數使用這個庫來填充頂點 vertices 和索引 indices 容器,其中包含網格中的頂點數據。在創建頂點和索引緩沖區之前應該調用它:

void initVulkan() {
    ...
    loadModel();
    createVertexBuffer();
    createIndexBuffer();
    ...
}

...

void loadModel() {

}

模型加載後被封裝到庫的數據結構中,通過調用 tinyobj::LoadObj 函數完成。

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

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

OBJ文件由positions, normals, texture uvs 和 faces組成,其中每個頂點指向一個位置,通過索引指向 法線或者紋理坐標。這使得不僅可以重復使用整個頂點,還可以具有單獨的屬性。

attrib 容器持有所有的 positions, normals 和 texture uvs 在它的 attrib.vertices, attrib.normalsattrib.texcoords 容器中。 shapes 容器包含所有單獨的對象和面。每個面由一組頂點組成,每個頂點包含 positions,normals 和 texture uvs 對應的 indices。OBJ模型也可以定義每個面的材質和紋理,但是我們忽略它們。

err 字符串包含了加載文件過程中產生的錯誤和警告信息,比如缺少材質的定義。如果 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_indextexcoord_index 成員。我們需要使用這些索引從 attrib 數組中 查找實際的頂點屬性:

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 。相似的,每個條目有兩個紋理坐標分量。 012的偏移用於訪問X,Y和Z分量,或者在紋理坐標的情況下訪問U和V分量。

運行程序,啟動優化(例如Visual studio中的 Relase,以及GCC的 -O3 編譯器標誌),這是必要的,否則加載模型會很慢,你會看到如下內容:

技術分享

很好,看起來幾何圖形是正確的,但是紋理貼圖發生了什麽?這個問題是由於Vulkan的紋理坐標的起點是左上角,而OBJ格式則是左下角。通過反轉紋理坐標的垂直分量來解決這個問題:

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

再次運行程序看到如下正確結果:

技術分享

所有這些艱苦的工作終於開始通過這樣的演示得到回報!

Vertex deduplication


遺憾的是我們並沒有真正利用索引緩沖區的優勢。 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 容器中。之後,我們將新的頂點的索引添加到索引容器中。如果我們已經看到完全相同的頂點,那麽我們在 uniqueVertices 中查找其索引,並將該索引存儲在 indices 中。

程序將會編譯錯誤,因為使用類似我們的 Vertex 結構體,它是自定義類型作為哈希表中的鍵,因為需要實現兩個功能:燈飾測試和散列值計算。前者通過覆蓋 Vertex 結構中的 == 運算符很容易實現:

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

通過為 std::hash<T> 指定模版專門來實現 Vertex 的哈希函數。散列函數是一個復雜的主題,但 cppreference.com 建議采用以下方法組合結構體的字段來創建質量比較高的散列函數:

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 結構體之外。需要使用以下頭文件來包含GLM類型的哈希函數:

#include <glm/gtx/hash.hpp>

現在應該能夠成功編譯和運行程序。如果檢查 vertices 頂點數量,會發現它已經從 1,500,000 縮小到 265,645!這意味著每個頂點以平均被 大約6個三角形重新使用。這絕對會為我們節省很多GPU內存。

Conclusion


到目前位置,已經做了很多工作,但是現在你終於有了一個很好的基礎。現在擁有的Vulkan的基本原理的只是應該足以探索更多的更能,諸如:

  • Push constants
  • Instanced rendering
  • Dynamic uniforms
  • Separate images and sampler descriptors
  • Pipeline cache
  • Multi-threaded command buffer generation
  • Multi subpasses
  • Compute shaders

現在的程序有很多方式進行擴展,比如添加 Blinn-Phong lighting,post-processing效果和陰影映射。你應該能夠了解這些效果如何從其他的API來完成,盡管因為Vulkan的明確性,但是許多概念是相同的。

項目代碼 GitHub 地址。

Vulkan Tutorial 29 Loading models