操作系统: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制作并授权的。我调整了模型的大小和方向,将其用作当前几何体的替代品:

它有50W个三角形,所以它是我们的应用程序的一个很好的基准。在这里随意使用自己的模型文件,但是要确保它们是由一种材质构成的,尺寸约为1.5 x 1.5 x 1.5 单位。如果大于此值,则必须修改视图矩阵。将模型文件放在 shaderstextures 同级的新模型目录中 models,并将纹理贴图放在 textures 目录中。

添加两个新的配置变量到程序中,用于定义模型和贴图的路径:

const int WIDTH = ;
const int HEIGHT = ; 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, , 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[ * index.vertex_index + ],
attrib.vertices[ * index.vertex_index + ],
attrib.vertices[ * index.vertex_index + ]
}; vertex.texCoord = {
attrib.texcoords[ * index.texcoord_index + ],
attrib.texcoords[ * index.texcoord_index + ]
}; 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[ * index.texcoord_index + ],
1.0f - attrib.texcoords[ * index.texcoord_index + ]
};

再次运行程序看到如下正确结果:

所有这些艰苦的工作终于开始通过这样的演示得到回报!

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) == ) {
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) << )) >> ) ^
(hash<glm::vec2>()(vertex.texCoord) << );
}
};
}

该代码应该放置在 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的更多相关文章

  1. [译]Vulkan教程(29)组合的Image采样器

    [译]Vulkan教程(29)组合的Image采样器 Combined image sampler 组合的image采样器 Introduction 入门 We looked at descripto ...

  2. Vulkan Tutorial 11 Shader modules

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 与之前的图像API不同,Vulkan中的着色器代码必须以二进制字节码的格式使用,而不 ...

  3. Vulkan Tutorial 13 Render passes

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Setup 在我们完成管线的创建工作,我们接下来需要告诉Vulkan渲染时候使用的f ...

  4. Vulkan Tutorial 25 Images

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 到目前为止,几何图形使用每个顶点颜色进行着色处理,这是一个 ...

  5. Vulkan Tutorial 开发环境搭建之Windows

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 相信很多人在开始学习Vulkan开发的起始阶段都会在开发环境的配置上下一些功夫,那么 ...

  6. Vulkan Tutorial 02 编写Vulkan应用程序框架原型

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 General structure 在上一节中,我们创建了一个正确配置.可运行的的V ...

  7. Vulkan Tutorial 05 物理设备与队列簇

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Selecting a physical device 通过VkInstance初始 ...

  8. Vulkan Tutorial 05 逻辑设备与队列

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在选择要使用的物理设备之后,我们需要设置一个逻辑设备用于交 ...

  9. Vulkan Tutorial 07 Window surface

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 到目前为止,我们了解到Vulkan是一个与平台特性无关联的API集合.它不能直接与窗 ...

随机推荐

  1. OpenGL教程(2)——第一个窗口

    OpenGL环境终于配置好了,现在我们可以开始学习OpenGL了. 首先,创建一个.cpp文件,然后打上几行#include指令: #include <iostream> using st ...

  2. Eclipse debug 调试快捷键

    F3  跳到光标所在的类或方法(按Ctrl+鼠标左键同样可以实现这一功能) F5  进到函数的内部 F6  单步调试 F7  从函数中退出 F8  调到下一个断点(不能使用时应该是和有道词典的快捷键冲 ...

  3. 【小练习06】HTML+CSS--电影公告

    要求实现如下效果图: 代码演示 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"&g ...

  4. selinux导致docker启动失败

    1. 问题描述:一向运行正常的一群容器,突然有一天挂掉了,再也起不来,报错如下 Error response from daemon: devmapper: Error mounting '/dev/ ...

  5. Floyd判圈算法

    Floyd判圈算法 leetcode 上 编号为202 的happy number 问题,有点意思.happy number 的定义为: A happy number is a number defi ...

  6. Hadoop SSH+IP、SSH+别名 免密登录配置

    1.为什么要进行 SSH 无密码验证配置? Hadoop运行过程中需要管理远端Hadoop守护进程,在Hadoop启动以后,NameNode是通过SSH(Secure Shell)来启动和停止各个Da ...

  7. Asp.Net页面传值的方法简单总结【原创】

    1.QueryString 当页面上form按照get的方式向页面发送请求数据的时候,web server会将请求数据放入 一个QEURY_STRING的环境变量中,然后通过QeueryString方 ...

  8. 设计模式的征途—8.桥接(Bridge)模式

    在现实生活中,我们常常会用到两种或多种类型的笔,比如毛笔和蜡笔.假设我们需要大.中.小三种类型的画笔来绘制12中不同的颜色,如果我们使用蜡笔,需要准备3*12=36支.但如果使用毛笔的话,只需要提供3 ...

  9. Spring事务隔离级别

    REQUIRED:业务方法需要在一个容器里运行.如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务. NOT_SUPPORTED:声明方法不需要事务.如果方法没有关联到一 ...

  10. jquery.qrcode.min.js(支持中文转化二维码)

    详情请看:http://www.ncloud.hk/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/jqueryqrcodeminjs/ 今天还是要讲一下关于二维码的知识,前 ...