加载 3D 模型
渲染三角形和参数化网格很好,但是引擎加载在专用程序中制作的 3D 模型。为此,我们将实现基本的 OBJ 格式加载。
OBJ 格式是一种非常简单的格式,几乎所有处理 3D 模型的软件都能理解。我们将使用库 tiny_obj_loader 加载 Blender 猴子网格(在 assets 文件夹中)并渲染它。
我们现在拥有的代码可以渲染任何任意网格,只要顶点数组被填充,并且我们可以使用推送常量矩阵在 3D 空间中移动该网格。
我们将首先向 VulkanEngine 类添加一个新的 Mesh 对象,以保存新加载的猴子网格。
class VulkanEngine {
public:
//other code ....
Mesh _monkeyMesh;
}
接下来,我们将向 Mesh 对象添加一个函数,以从 obj 文件初始化它。
struct Mesh {
// other code .....
bool load_from_obj(const char* filename);
};
//make sure that you are including the library
#include <tiny_obj_loader.h>
#include <iostream>
bool Mesh::load_from_obj(const char* filename)
{
return false;
}
OBJ 格式
在 OBJ 文件中,顶点不是存储在一起的。相反,它保存了位置、法线、UV 和颜色的单独数组,然后是一个指向这些数组的面数组。给定的 obj 文件也有多个形状,因为它可以容纳多个对象,每个对象都有单独的材质。在本教程中,我们将单个 obj 文件加载到单个网格中,并且所有 obj 形状都将被合并。
让我们继续填充加载函数
bool Mesh::load_from_obj(const char* filename)
{
//attrib will contain the vertex arrays of the file
tinyobj::attrib_t attrib;
//shapes contains the info for each separate object in the file
std::vector<tinyobj::shape_t> shapes;
//materials contains the information about the material of each shape, but we won't use it.
std::vector<tinyobj::material_t> materials;
//error and warning output from the load function
std::string warn;
std::string err;
//load the OBJ file
tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, filename, nullptr);
//make sure to output the warnings to the console, in case there are issues with the file
if (!warn.empty()) {
std::cout << "WARN: " << warn << std::endl;
}
//if we have any error, print it to the console, and break the mesh loading.
//This happens if the file can't be found or is malformed
if (!err.empty()) {
std::cerr << err << std::endl;
return false;
}
}
通过这段代码,我们使用库将 obj 文件加载到我们可以用来转换为网格格式的结构中。我们需要声明 LoadObj 函数使用的一些结构,然后我们进行错误检查。
继续加载函数,将文件中的网格放入我们的顶点缓冲区
// Loop over shapes
for (size_t s = 0; s < shapes.size(); s++) {
// Loop over faces(polygon)
size_t index_offset = 0;
for (size_t f = 0; f < shapes[s].mesh.num_face_vertices.size(); f++) {
//hardcode loading to triangles
int fv = 3;
// Loop over vertices in the face.
for (size_t v = 0; v < fv; v++) {
// access to vertex
tinyobj::index_t idx = shapes[s].mesh.indices[index_offset + v];
//vertex position
tinyobj::real_t vx = attrib.vertices[3 * idx.vertex_index + 0];
tinyobj::real_t vy = attrib.vertices[3 * idx.vertex_index + 1];
tinyobj::real_t vz = attrib.vertices[3 * idx.vertex_index + 2];
//vertex normal
tinyobj::real_t nx = attrib.normals[3 * idx.normal_index + 0];
tinyobj::real_t ny = attrib.normals[3 * idx.normal_index + 1];
tinyobj::real_t nz = attrib.normals[3 * idx.normal_index + 2];
//copy it into our vertex
Vertex new_vert;
new_vert.position.x = vx;
new_vert.position.y = vy;
new_vert.position.z = vz;
new_vert.normal.x = nx;
new_vert.normal.y = ny;
new_vert.normal.z = nz;
//we are setting the vertex color as the vertex normal. This is just for display purposes
new_vert.color = new_vert.normal;
_vertices.push_back(new_vert);
}
index_offset += fv;
}
}
return true;
TinyOBJ 转换循环可能很难正确实现。这个是从他们的示例代码派生出来的,并进行了一些简化。您可以在以下位置查看原始代码:https://github.com/tinyobjloader/tinyobjloader README 页面。在这里,我们将每个面的顶点数硬编码为 3。如果您将此代码与尚未三角化的模型一起使用,则会出现问题。加载具有 4 个或更多顶点的面的模型会更复杂,因此我们将其留到以后再说。
添加代码后,我们现在可以将 obj 加载到我们的 Mesh 结构中,所以让我们将猴子网格加载到我们的三角形网格中,看看会发生什么。
加载网格
在 VulkanEngine 的 load_meshes 函数中,我们将加载猴子网格以及三角形
void VulkanEngine::load_meshes()
{
_triangleMesh._vertices.resize(3);
_triangleMesh._vertices[0].position = { 1.f,1.f, 0.5f };
_triangleMesh._vertices[1].position = { -1.f,1.f, 0.5f };
_triangleMesh._vertices[2].position = { 0.f,-1.f, 0.5f };
_triangleMesh._vertices[0].color = { 0.f,1.f, 0.0f }; //pure green
_triangleMesh._vertices[1].color = { 0.f,1.f, 0.0f }; //pure green
_triangleMesh._vertices[2].color = { 0.f,1.f, 0.0f }; //pure green
//load the monkey
_monkeyMesh.load_from_obj("../../assets/monkey_smooth.obj");
//make sure both meshes are sent to the GPU
upload_mesh(_triangleMesh);
upload_mesh(_monkeyMesh);
}
猴子网格现在已加载,所以我们可以在我们的绘制循环中使用它来显示它。它与三角形相同,但我们现在使用猴子而不是 triangleMesh
//bind the mesh vertex buffer with offset 0
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &_monkeyMesh._vertexBuffer._buffer, &offset);
//we can now draw the mesh
vkCmdDraw(cmd, _monkeyMesh._vertices.size(), 1, 0, 0);
您应该看到一个旋转的猴子头。但是有一个小故障,有些面会互相重叠绘制。这是由于我们现在缺少深度缓冲区造成的,所以让我们在下一篇文章中修复它。
下一步:设置深度缓冲区