载入模型:
使用 tinyobjloader 库来从 OBJ 文件加载顶点数据。tinyobjloader
库是一个简单易用的单文件 OBJ 加载器,我们只需要下载 tiny_obj_loader.h
文件,然后在代码中包含这一头文件就可以使用它了。
我们暂时不使用光照,只简单地将纹理贴在模型上。
在这里,我们加载的模型叫做 Chalet Hippolyte Chassande Baroz。我
们对它的大小和方向进行了调整:
• chalet.obj
• chalet.jpg
这一模型大概由 50 万面三角形构成。
大小为1.5x1.5x1.5。如果使用的模型大于这一尺寸,读者就需要对使用的视图矩阵进行修改。
示例:
#define GLM_FORCE_RADIANS//用来使 glm::rotate这些函数使用弧度作为参数的单位
/**
默认情况下,GLM 库的透视投影矩阵使用 OpenGL 的深度值范围 (-
1.0,1.0)。我们需要定义 GLM_FORCE_DEPTH_ZERO_TO_ONE 宏
来让它使用 Vulkan 的深度值范围 (0.0,1.0)。
*/
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#define GLM_ENABLE_EXPERIMENTAL//启用GLM 库的哈希函数
#include <glm/glm.hpp>//线性代数库
//为了使用glm::rotate之类的矩阵变换函数
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/hash.hpp>
#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>
#include <unordered_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);
}
};
}
const std::string MODEL_PATH=
"E:/workspace/Qt5.6/VulkanLearn/models/chalet.obj";
const std::string TEXTURE_PATH=
"E:/workspace/Qt5.6/VulkanLearn/models/chalet.jpg";
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
//绑定顶点缓冲到指令缓冲对象--第三个参数为索引数据的类型
vkCmdBindIndexBuffer(commandBuffers[i],indexBuffer,0,
VK_INDEX_TYPE_UINT32);
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(),
&texWidth,&texHeight,&texChannels,
STBI_rgb_alpha);
//载入模型文件,填充模型数据到 vertices 和 indices
void loadModel(){
/**
一个 OBJ 模型文件包含了模型的位置、法线、纹理坐标和表面数据。
表面数据包含了构成表面的多个顶点数据的索引。
attrib 变量来存储载入的位置、法线和纹理坐标数据。
shapes 变量存储独立的对象和它们的表面数据。
每个表面数据包含了一个顶点数组,顶点数组中的每个顶点数据包含了顶点的
位置索引、法线索引和纹理坐标索引。OBJ 模型文件格式允许为模型的每
个表面定义材质和纹理数据,但在这里,我们没有用到。
err 变量来存储载入模型文件时产生的错误和警告信息,
比如载入时没有找到引用的材质信息。
OBJ 模型文件中的表面数据可以包含任意数量的顶点数据,但我们的程序只能渲染三角形表面,
这就需要进行转换将 OBJ 模型文件中的表面数据都转换为三角形表面。
tinyobj::LoadObj 函数有一个可选的默认参数,可以设置在加载 OBJ 模型
数据时将表面数据转换为三角形表面。由于这一设置是默认的,所以,我们不需要自己设置它。
*/
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string warn;
std::string err;
if(!tinyobj::LoadObj(&attrib,&shapes,&materials,&err,&warn,
MODEL_PATH.c_str())){
throw std::runtime_error(warn+err);
}
/**
我们需要达到索引缓冲节约空间的目的。三角形表面的顶点是被多个三角形表面共用的,
而我们则是每个顶点都重新定义一次,vertices 向量包含了大量重复的顶点数据。
我们可以将完全相同的顶点数据只保留一个,来解决空间。
这一去重过程可以通过 STL 的 map 或unordered_map 来实现:
*/
std::unordered_map<Vertex, uint32_t> uniqueVertices;
//将加载的表面数据复制到我们的 vertices 和 indices 向量中
for(const auto& shape : shapes){
//载入的表面数据已经被三角形化,所以直接将它们复制到vertices 向量中
for(const auto& index: shape.mesh.indices){
Vertex vertex = {};
/**
为了简化 indices 数组的处理,我们这里假定每个顶点都是独一无
二的,可以直接使用 indices 数组的当前大小作为顶点索引数据。上面
代码中的 index 变量的类型为 tinyobj::index_t,这一类型的变量包含了
vertex_index、normal_index 和 texcoord_index 三个成员变量。
我们使用这三个成员变量来检索存储在 attrib 数组变量中的顶点数据:
attrib.vertices 是一个浮点数组,并非 glm::vec3 数组,我们需要在使
用索引检索顶点数据时首先要把索引值乘以 3 才能得到正确的顶点数据位
置。对于纹理坐标数据,则乘以 2 进行检索。对于顶点位置数据,偏移值
0 对应 X 坐标,偏移值 1 对应 Y 坐标,偏移值 2 对应 Z 坐标。对于纹理
坐标数据,偏移值 0 对应 U 坐标,偏移值 1 对应 V 坐标
*/
vertex.pos = {
attrib.vertices[3*index.vertex_index+0],
attrib.vertices[3*index.vertex_index+1],
attrib.vertices[3*index.vertex_index+2]
};
/**
Vulkan的纹理坐标的原点是左上角,而OBJ模型文件格式假设纹理坐标原点是左下角。
所以需要反转纹理的 Y 坐标使两个对应
*/
vertex.texCoord = {
attrib.texcoords[2*index.texcoord_index+0],
1.0f - attrib.texcoords[2*index.texcoord_index+1]
};
vertex.color = {1.0f,1.0f,1.0f};
//优化顶点结构
if (uniqueVertices.count(vertex) == 0) {
uniqueVertices[vertex] =
static_cast<uint32_t>(vertices.size());
vertices.push_back(vertex);
}
indices.push_back(uniqueVertices[vertex]);
}
}
}