模型
使用Assimp并创建实际的加载和转换代码。Model类结构如下:
class Model
{
public:
/* 函数 */
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
/* 模型数据 */
vector<Mesh> meshes;
string directory;
/* 函数 */
void loadModel(string path);
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
string typeName);
};
Model类包含一个Mesh对象的vector,构造器参数需要一个文件路径。
构造器通过loadModel来加载文件。私有函数将会处理Assimp导入过程中的一部分,私有函数还存储了 文件路径的目录,加载纹理时会用到。
Draw函数的作用:遍历所有网格,调用网格 各自的Draw函数:
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
导入3D模型
导入一个模型,并将其转换到自己的数据结构中。则首先需要包含Assimp对应的头文件:
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
首先调用函数loadModel,直接从构造器中调用。在该函数汇总,使用Assimp加载模型到Assimp的一个叫做scene的数据结构中。这个是场景对象,通过它可以访问到加载后的模型中所有需要的数据。
Assimp抽象了加载不同文件格式的所有技术细节,只需要一行代码即可:
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
代码解读:声明了Assimp命名空间内的一个Importer,之后调用ReadFile函数。该函数需要一个文件路径,第二个参数是后期处理的选项。除了加载文件外,Assimp允许设定一些选项来强制它对导入的数据做一些额外的计算。
通过设定aiProcess_Triangulate ,能告诉Assimp,如果模型不是全部由三角形组成,那么需要将模型的所有图元转换成三角形。
aiProcess_FlipUVs,将在处理的时候翻转y轴的纹理坐标,因为在OpenGL中大部分的图像的y轴都是反的,所系这个后期处理选项可以修复该问题。
其他有用的选项还有:(https://assimp.sourceforge.net/lib_html/postprocess_8h.html)
- aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
- aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
- aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
可以看出使用Assimp加载模型是非常容易的。难的是之后使用返回的场景对象将加载的数据转换到一个Mesh对象的数组。
完整的loadModel函数如下:
void loadModel(string path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));
processNode(scene->mRootNode, scene);
}
在我们加载了模型之后,我们会检查场景和其根节点不为null,并且检查了它的一个标记(Flag),来查看返回的数据是不是不完整的。如果遇到了任何错误,我们都会通过导入器的GetErrorString函数来报告错误并返回。我们也获取了文件路径的目录路径。
如果什么错误都没有发生,我们希望处理场景中的所有节点,所以我们将第一个节点(根节点)传入了递归的processNode函数。因为每个节点(可能)包含有多个子节点,我们希望首先处理参数中的节点,再继续处理该节点所有的子节点,以此类推。这正符合一个递归结构,所以我们将定义一个递归函数。递归函数在做一些处理之后,使用不同的参数递归调用这个函数自身,直到某个条件被满足停止递归。在我们的例子中退出条件(Exit Condition)是所有的节点都被处理完毕。
Assimp结构中,每个节点包含一系列网格索引,每个索引指向场景对象中的那个特定网格。接下来需要去获取这些网格索引,获取每个网格,处理每个网格,接着对每个节点的子节点重复这个过程,则processNode函数如下:
void processNode(aiNode *node, const aiScene *scene)
{
// 处理节点所有的网格(如果有的话)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下来对它的子节点重复这一过程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
我们首先检查每个节点的网格索引,并索引场景的mMeshes数组来获取对应的网格。返回的网格将会传递到processMesh函数中,它会返回一个Mesh对象,我们可以将它存储在meshes列表/vector。
所有网格都被处理之后,我们会遍历节点的所有子节点,并对它们调用相同的processMesh函数。当一个节点不再有任何子节点之后,这个函数将会停止执行。
下一步是将Assimp的数据解析到Mesh类中。就是将一根aiMesh对象转化为自己的网格对象。只需要访问网格的相关属性并将它们存储到自己的对象中。processMesh函数如下:
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(unsigned int i