参考:
https://learnopenglcn.github.io/03%20Model%20Loading/03%20Model/
定义一个模型类
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模型到OpenGL
首先需要包含Assimp对应的头文件
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
首先需要调用的函数是loadModel,它会从构造器中直接调用.
在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中.
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
先声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数。
这个函数需要一个文件路径,它的第二个参数是一些后期处理(Post-processing)的选项
通过设定aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。
aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标
…
…
其它一些比较有用的选项有:
aiProcess_GenNormals:
如果模型不包含法向量的话,就为每个顶点创建法线
aiProcess_SplitLargeMeshes:
将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用
aiProcess_OptimizeMeshes:
它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化
完整的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)是所有的节点都被处理完毕。
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
从Assimp到网格
将一个aiMesh对象转化为我们自己的网格对象不是那么困难。我们要做的只是访问网格的相关属性并将它们储存到我们自己的对象中。processMesh函数的大体结构如下:
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector