章节31

载入模型

介绍

本章节我们将会渲染一个带有纹理的三维模型。

我们使用tinyobjloader库来从OBJ文件加载顶点数据。tinyobjloader库是一个简单易用的单文件OBJ加载器,我们只需要下载tiny_obj_loader.h文件,然后在代码中包含这一头文件就可以使用它了。

Visual Studio

将tiny_obj_loader.h加入Additional Include Directories paths中:

图31.1

Makefile

将tiny_obj_loader.h所在目录加入编译器的包含目录中:

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++11 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) -I$(TINYOBJ_INCLUDE_PATH)

网格样例

在本章节,我们暂时不使用光照,只简单地将纹理贴在模型上。读者可以从Sketchfab找到自己喜欢的OBJ模型来加载。

在这里,我们加载的模型叫做Chalet Hippolyte Chassande Baroz。我们对它的大小和方向进行了调整:

  • chalet.obj
  • chalet.jpg

这一模型大概由50万面三角形构成。读者也可以使用自己的OBJ模型,但需要确保使用的模型给只包含了一个材质,并且模型的大小为1.5x1.5x1.5。如果使用的模型大于这一尺寸,读者就需要对使用的视图矩阵进行修改。我们新建一个和shaders和textures文件夹同级的models文件夹,用于存放模型文件。

添加两个常量定义我们使用的模型文件路径和纹理文件路径:

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;

由于我们使用的模型包含的顶点个数远远大于65535,所以不能使用uint16_t作为索引的数据类型,而应该使用uint32_t作为索引的数据类型。更改索引数据类型后,还要修改我们调用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。我们在顶点缓冲和索引缓冲创建之前调用它来载入模型数据:

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模型文件包含了模型的位置、法线、纹理坐标和表面数据。表面数据包含了构成表面的多个顶点数据的索引。

我们在loadModel函数中使用attrib变量来存储载入的位置、法线和纹理坐标数据。使用shapes变量存储独立的对象和它们的表面数据。每个表面数据包含了一个顶点数组,顶点数组中的每个顶点数据包含了顶点的位置索引、法线索引和纹理坐标索引。OBJ模型文件格式允许为模型的每个表面定义材质和纹理数据,但在这里,我们没有用到。

我们使用err变量来存储载入模型文件时产生的错误和警告信息,比如载入时没有找到引用的材质信息。如果载入模型文件失败,那么tinyobj::LoadObj函数就会返回false。之前提到,OBJ模型文件中的表面数据可以包含任意数量的顶点数据,但我们的程序只能渲染三角形表面,这就需要进行转换将OBJ模型文件中的表面数据都转换为三角形表面。tinyobj::LoadObj函数有一个可选的默认参数,可以设置在加载OBJ模型数据时将表面数据转换为三角形表面。由于这一设置是默认的,所以,我们不需要自己设置它。

接着,我们将加载的表面数据复制到我们的vertices和indices向量中,这只需要遍历shapes向量即可:

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());}
}

为了简化indices数组的处理,我们这里假定每个顶点都是独一无二的,可以直接使用indices数组的当前大小作为顶点索引数据。上面代码中的index变量的类型为tinyobj::index_t,这一类型的变量包含了vertex_index、normal_index和texcoord_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才能得到正确的顶点数据位置。对于纹理坐标数据,则乘以2进行检索。对于顶点位置数据,偏移值0对应X坐标,偏移值1对应Y坐标,偏移值2对应Z坐标。对于纹理坐标数据,偏移值0对应U坐标,偏移值1对应V坐标。

现在使用优化模式编译我们的程序(使用Visual Studio的Release模式或GCC的-O3编译选项)。这样做可以提高我们的模型加载速度。运行程序,应该可以看到下面这样的画面:

图31.2

看起来,模型的几何形状是正确的,但纹理映射不对。这是因为Vulkan的纹理坐标的原点是左上角,而OBJ模型文件格式假设纹理坐标原点是左下角。我们可以通过反转纹理的Y坐标解决这一问题:

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

现在再次编译运行程序,就可以看到被正确映射纹理的模型了:

图31.3

顶点去重

按照之前的处理,我们没有达到索引缓冲节约空间的目的。三角形表面的顶点是被多个三角形表面共用的,而我们则是每个顶点都重新定义一次,vertices向量包含了大量重复的顶点数据。我们可以将完全相同的顶点数据只保留一个,来解决空间。这一去重过程可以通过STL的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向量,将已经加载的顶点数据的索引存储到indices向量中。如果不同,将其加入vertices向量,并存储它对应的索引值到uniqueVertices容器中。然后将其索引存储在indices向量中。

我们需要实现两个函数来让Vertex结构体可以作为map变量的键值来检索map变量,首先是==函数:

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

然后是对Vertex结构体进行哈希的函数:

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>

GLM库的哈希函数目前还是一个试验性的扩展,被定义在了GLM库的gtx目录下。所以需要我们定义GLM_ENABLE_EXPERIMENTAL宏来启用它。作为试验性扩展意味着在未来版本的GLM库有可能发生变化,但一般而言,我们可以认为变化不会太大。

现在重新编译运行程序,查看vertices向量的大小,可以发现vertices向量的大小从1,500,000下降到了265,645。这也说明对于我们的模型数据,每个顶点数据平均被6个三角形表面使用。

threejs加载obj模型_Vulkan编程指南(章节31-载入模型)相关推荐

  1. threejs加载obj文件

    threejs加载并展示obj文件: 研究了一段时间,总结下,废话少说,直接上代码:<!DOCTYPE html> <html lang="en">< ...

  2. ThreeJS 加载 obj/3ds/fbx/gltf/等等

    1. 下载ThreeJS https://github.com/mrdoob/three.js/archive/master.ziphttps://github.com/mrdoob/three.js ...

  3. threejs加载obj模型_倾斜摄影三维模型几种常见的格式,你能说出哪些?

    本文首发于公众号Wish3D,原文链接:倾斜摄影三维模型几种常见的格式,你能说出哪些? 无人机航拍的影像经过建模软件处理产出之时,有很多成果的数据需要我们去选择输出,对于新手而言,如何选择数据格式呢? ...

  4. three.js加载obj模型和材质

    1.Vue中安装three.js和加载用的包 安装three.js使用npm install three --save 安装加载obj和mtl文件的包npm install three-obj-mtl ...

  5. 超图桌面版加载obj 3D模型 - 2

    在 https://blog.csdn.net/bcbobo21cn/article/details/109041525 里,加载obj格式模型没有出来效果: 下面来看一下其他方法:当前用的版本是10 ...

  6. 超图桌面版加载obj 3D模型

    根据目前看到的资料帮助,使用超图桌面版加载obj 3D模型的步骤如下: 新建场景,图层管理器---普通图层右键,新建KML----开启KML可编辑,右键添加模型. 下面来操作一下: 下一个obj模型: ...

  7. wpf加载obj格式的3D模型图解

    网上下一个obj格式的3D模型:再下一个obj文件查看器,看一下模型,效果如下: 使用SharpDevelop,新建一个WPF工程: 加载obj模型需要WavefrontObjLoader.cs文件: ...

  8. threejs加载服务器文件,如何使用Three.js加载obj和mtl文件

    OBJ和MTL是3D模型的几何模型文件和材料文件. 在最新的three.js版本(r78)中,以前的OBJMTLLoader类已废弃. 现在要加载OBJ和MTL文件,需要结合OBJLoader和MTL ...

  9. Panda3D如何加载obj格式的3D模型文件

    ​ 在上文中,讲了如何将max.obj.mb.fbx等主流格式的3D模型文件转换为egg.gltf格式,Panda3D可以加载的格式.在上文末尾,博主临时看到了一种方式,可以让Panda3d直接加载o ...

最新文章

  1. 树莓派电压过低 串口数据错误增多
  2. mysql多数据源_egg-mysql配置多数据源
  3. ActiveMQ学习总结(8)——消息队列设计精要
  4. Flutter功能 中ListView和GridView嵌套报错?【教你一步搞定】
  5. 苹果 CEO 库克“喜当爹”,被女子索赔31.6亿分手费!
  6. [JSOI2008] 最小生成树计数
  7. 跟随我在oracle学习php(42)
  8. html 判断IE版本并加载对应的css,js
  9. sql分页查询与offset的使用
  10. iframe用法详解
  11. win系统下非系统盘msdia.dll文件怎么处理
  12. Debian和Ubuntu版本比较
  13. 信息检索与搜索引擎:Simhash算法优化
  14. 蓝桥杯单片机(八)DS18B20温度测量(整数显示)
  15. 去中心化通信简易方案
  16. Micro Surface Pro 鼠标一直闪烁的问题
  17. Qt之Windows下禁用中文输入法
  18. 计算机专业杭州申请公租房有,2018年第四批公租房配租方案及参加摇号名单出炉!报名状况查询方式在此!...
  19. Java大牛呕心沥血经历——技术面试与HR谈薪资技巧...
  20. Codeforces Round #532 (Div. 2) F. Ivan and Burgers(可持久化异或线性基+双指针)

热门文章

  1. 快速记忆python函数-python入门(误)速记
  2. 用python画关系网络图-python networkx 包绘制复杂网络关系图
  3. python编程课程上课有用吗-Python培训网络课堂|Python编程软件有哪些功能?
  4. c与python的区别-对比平台--C和Python之间的区别
  5. python安装第三方库-安装第三方模块
  6. Ubuntu(Linux)上安装ROS缓慢,解决方法:添加ros的国内镜像源,以及ROS安装过程使用rosdep update出现错误:‘The read operation timed out‘
  7. 2_Python实现基于人脸特征的美颜算法(20181224)
  8. UVa12583 - Memory Overflow
  9. 【7.2】__getattr__、__getattribute__魔法函数
  10. 20年研发管理经验谈(十六)