Loading models

目录

Introduction

Library

Sample mesh

Loading vertices and indices

Vertex deduplication


Introduction

您的程序现在已准备好渲染纹理化的3D网格,但顶点和索引阵列中的当前几何体还不是很有趣。在本章中,我们将扩展程序,从实际模型文件加载顶点和索引,以使图形卡实际执行某些工作。

许多图形API教程都让读者在这样的章节中编写自己的OBJ加载器。这样做的问题是,任何一个非常有趣的3D应用程序很快就会需要这种文件格式不支持的功能,比如骨骼动画。在本章中,我们将从OBJ模型加载网格数据,但我们将更多地关注网格数据与程序本身的集成,而不是从文件加载网格数据的细节。

Library

我们将使用tinyobjloader库从OBJ文件加载顶点和面。它速度快,易于集成,因为它是一个像stb_image这样的单一文件库。转到上面链接的存储库,将tiny_obj_loader.h文件下载到库目录中的文件夹中。

Visual Studio

将其中包含tiny_obj_loader.h的目录添加到Additional Include Directories路径。

Makefile

将tiny_obj_loader.h目录添加到GCC的include目录中:

VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64
STB_INCLUDE_PATH = /home/user/libraries/stb
TINYOBJ_INCLUDE_PATH = /home/user/libraries/tinyobjloader

...

CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) -I$(TINYOBJ_INCLUDE_PATH)

Sample mesh

在本章中,我们还没有启用照明,因此使用将照明烘焙到纹理中的示例模型会有所帮助。找到此类模型的一个简单方法是在Sketchfab上查找3D扫描。该网站上的许多模型都以OBJ格式提供,并具有许可证。

在本教程中,我决定使用nigelgohViking房间模型(CC by 4.0)。我调整了模型的大小和方向,将其作为当前几何图形的替代品:

请随意使用您自己的模型,但请确保它仅由一种材料组成,即尺寸约为1.5 x 1.5 x 1.5单位。如果它大于此值,则必须更改视图矩阵。将模型文件放在着色器和纹理旁边的新模型目录中,然后将纹理图像放在纹理目录中。

在程序中添加两个新的配置变量以定义模型和纹理路径:

const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;

const std::string MODEL_PATH = "models/viking_room.obj";
const std::string TEXTURE_PATH = "textures/viking_room.png";

并更新createTextureImage以使用此路径变量:

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

Loading vertices and indices

我们现在要从模型文件加载顶点和索引,所以现在应该删除全局顶点和索引数组。将它们替换为作为类成员的非常量容器:

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

您应该将索引的类型从uint16_t更改为uint32_t,因为将有比65535多得多的顶点。还记得更改vkCmdBindIndexBuffer参数:

vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);

tinyobjloader库的包含方式与STB库相同。包括tiny_obj_loader.h文件,并确保在一个源文件中定义TINYOBJLOADER_IMPLEMENTATION,以包含函数体并避免链接器错误:

#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

现在,我们将编写一个loadModel函数,该函数使用该库用网格中的顶点数据填充顶点和索引容器。应该在创建顶点和索引缓冲区之前的某个位置调用它:

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 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向量中保存所有位置、法线和纹理坐标。形状容器包含所有单独的对象及其面。每个面由一组顶点组成,每个顶点包含位置、法线和纹理坐标属性的索引。OBJ模型也可以定义每个面的材质和纹理,但我们将忽略这些。

错误字符串包含错误,警告字符串包含加载文件时出现的警告,例如缺少材料定义。只有当LoadObj函数返回false时,加载才会真正失败。如上所述,OBJ文件中的面实际上可以包含任意数量的顶点,而我们的应用程序只能渲染三角形。幸运的是,LoadObj有一个可选参数来自动三角化这些面,这在默认情况下是启用的。

我们将把文件中的所有面组合成一个模型,所以只需遍历所有形状:

for (const auto& shape : shapes) {

}

三角剖分功能已经确保每个面有三个顶点,因此我们现在可以直接迭代顶点并将它们直接转储到顶点向量中:

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

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

为了简单起见,我们将假设每个顶点现在都是唯一的,因此是简单的自动递增索引。索引变量的类型为tinyobj::index_t,它包含vertex_index、normal_index和texcord_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数组是一个浮点值数组,而不是glm::vec3,因此需要将索引乘以3。类似地,每个条目有两个纹理坐标组件。偏移量0、1和2用于访问X、Y和Z分量,或在纹理坐标的情况下访问U和V分量。

现在在启用优化的情况下运行程序(例如,Visual Studio中的Release模式和GCC的-O3编译器标志)。这是必要的,因为否则加载模型将非常缓慢。您应该看到以下内容:

很好,几何体看起来是正确的,但纹理是怎么回事?OBJ格式假设一个坐标系,其中垂直坐标为0表示图像的底部,但是我们已将图像以从上到下的方向上传到Vulkan,其中0表示图像顶部。通过翻转纹理坐标的垂直分量来解决此问题:

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

再次运行程序时,现在应该会看到正确的结果:

所有这些辛苦的工作终于开始有了这样的演示!

当模型旋转时,你可能会注意到后面(墙的背面)看起来有点滑稽。这是正常的,只是因为模型的设计并不是为了从那个角度看。

Vertex deduplication

不幸的是,我们还没有真正利用索引缓冲区。顶点向量包含大量重复的顶点数据,因为许多顶点包含在多个三角形中。我们应该只保留唯一的顶点,并使用索引缓冲区在它们出现时重用它们。实现这一点的一种简单方法是使用map或unrdered_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文件中读取顶点时,我们都会检查之前是否已经看到了位置和纹理坐标完全相同的顶点。如果没有,我们将其添加到顶点并将其索引存储在uniqueVertices容器中。之后,我们将新顶点的索引添加到索引中。如果我们以前见过完全相同的顶点,那么我们在uniqueVertices中查找其索引,并将该索引存储在索引中。

该程序现在将无法编译,因为在哈希表中使用像Vertex结构这样的用户定义类型作为键需要我们实现两个函数:相等测试和哈希计算。前者很容易通过重写Vertex结构中的==运算符来实现:

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

Vertex的哈希函数是通过为std::hash<T>指定模板专用化来实现的。哈希函数是一个复杂的主题,但cpreference.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类型的哈希函数:

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

哈希函数在gtx文件夹中定义,这意味着它在技术上仍然是GLM的实验性扩展。因此,您需要定义GLM_ENABLE_EXPERIMENTAL才能使用它。这意味着API将来可能会随着GLM的新版本而改变,但实际上API非常稳定。

现在您应该能够成功编译并运行程序。如果你检查顶点的大小,你会发现它已经从1500000缩小到265645!这意味着每个顶点在平均约6个三角形中重复使用。这无疑为我们节省了大量GPU内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值