操作系统: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. Mac OS 的命令行 总结

    du 命令 查看目录下所有文件的大小: du -sh * ls 命令 直接显示当前目录下的所有的非隐藏文件: ls // 怎么在文章中显示不出来?? 显示当前目录下的所有的文件(包括隐藏的): ls ...

  2. windows7 64bit下mvn命令后提示‘cmd’不是内部或外部命令,也不是可执行程序或批处理文件

    首先,开命令提示符,输入如下命令试试echo %M2_HOME% 回车如果显示的路径和安装路径一致说明配置没问题; 那么出现这个问题的原因可能就是路径问题,可能是你安装了某个软件更改了系统映射路径导致 ...

  3. 用JMX远程监控Tomcat

    要通过JMX远程监控Tomcat,首先需要进行Tomcat的JMX远程配置. 注意:此配置添加在catalina.bat文件开头的注释行(rem)后面即可. 不需鉴权的配置: 先修改Tomcat的启动 ...

  4. Angular JS从入门基础 mvc三层架构 常用指令

    Angular JS从入门基础  mvc模型 常用指令 ★ 最近一直在复习AngularJS,它是一款优秀的前端JS框架,已经被用于Google的多款产品当中.AngularJS有着诸多特性,最为核心 ...

  5. LeetCode 206 单链表翻转

    https://leetcode.com/problems/reverse-linked-list/ 思路很简单,分别设置三个结点,之后依次调整结点1和结点2的指向关系. Before: pre -& ...

  6. Winform使用的一些常识

    ? 防止数据绑定的列动态添加 需要设置DataGridView的AutoGenerateColumns属性为False ---------------------------------------- ...

  7. Cordova各个插件使用介绍系列(四)—canvas2ImagePlugin保存二维码到手机本地

    详情链接地址:http://www.ncloud.hk/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/cordova-4-canvas2imageplugin/ 在前面几篇 ...

  8. v9更换域名

    网站在发展的过程中,很可能多次的修改域名.那么在PHPCMS V9中我们要怎么进行设置呢? 请进行以下步骤的修改: 修改/caches/configs/system.php里面所有和域名有关的,把以前 ...

  9. SICP-2.2-数据的抽象

    数据的抽象 生活中有许多的事物具有复合结构,例如地理位置所用的经纬度,便是通过一个复合结构来代表位置,在我们的程序当中,我们设法将经度纬度组合成一对,我们既可以把他们当做一个整体单元来进行操作,而且也 ...

  10. php利用gd实现图片的边框

    <?php //实现两张图片合并 并内图片有一定的边框 $file = 'image/qr_1047.png'; $logo = 'image/logo_1047.jpg'; $code = ' ...