搞定!OpenGL实战笔记第十八章:用Assimp导入自定义模型(Model类)-步骤详解
1.1 头文件
经过上节网格类的设置以后,我们现在开始正式接触assimp(上图为其简化结构),加载自己的模型,首先我们要使用assimp,就需要它的头文件:
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
1.2 Model类
其次我们要定义一个Model类,包含一个构造函数用于读取数据,一个Draw函数用于渲染,一个上节定义的Mesh类数组用于存储读取的Mesh数据,一个directory用于存储目录,剩下的loadModel,processNode,processMesh和loadMaterialTextures函数都是用于具体处理数据的,后面我们会提到:
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);
};
1.3 Scene的读取(loadModel)
loadModel函数就是根据构造函数传入的模型路径来读取和加载数据,首先利用Assimp中Importer的ReadFile方法获取Scene结构体,后面的参数:
aiProcess_Triangulate指如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形
aiProcess_FlipUVs指在处理的时候翻转y轴的纹理坐标(之前读取纹理的时候我们做过垂直翻转的处理,这里只用设置一个参数就直接翻转了)
还有其他参数:
aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
void loadModel(string path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
接下来检查场景和其根节点不为null,并且检查了它的一个标记(Flag),来查看返回的数据是不是不完整的。如果遇到了任何错误,我们都会通过导入器的GetErrorString函数来报告错误并返回,我们也获取了文件路径的目录路径(不包含文件名)。
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('/'));
接下来我们就从Scene的根节点中开始处理每个节点中的mesh:
processNode(scene->mRootNode, scene);
}
1.4 Mesh的读取(processNode)
进入到每个节点中,我们首先要做的就是读取每个节点的mesh,而根据之前的结构来看,节点中存储的mMeshes只是一个索引,所以我们还要配合传入的scene来读取保存每个节点中的mesh:
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));
}
该节点的mesh存储完以后,我们再对它的子节点进行递归处理:
// 接下来对它的子节点重复这一过程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
1.5 Mesh的处理(processMesh)
读取了Mesh结构体数据,我们现在要把它放到上一节中定义的Mesh类里,而上一步的processNode中,有这么一句:
meshes.push_back(processMesh(mesh, scene));
这里的processMesh函数就是将读取到的数据存储为Mesh类返回到meshes数组中,我们来看看它具体是怎么实现的:
1.5.1 获取Mesh中的顶点位置、法线和纹理坐标
这一步非常简单,我们定义了一个Vertex结构体,我们将在每个迭代之后将它加到vertices数组中。我们会遍历网格中的所有顶点(使用mesh->mNumVertices来获取)。
(纹理坐标比较特殊,Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,我们不会用到那么多,我们只关心第一组纹理坐标。我们同样也想检查网格是否真的包含了纹理坐标)
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
//顶点位置
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
//法线
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
//纹理坐标
if(mesh->mTextureCoords[0]) // 网格是否有纹理坐标?
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);
}
1.5.2 获取Mesh中的索引
读取了顶点数据之后,自然要读取顶点的索引,我们也利用同样的原理从mesh的mFaces中读取Face,并从中提取索引
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for(unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
1.5.3 获取Mesh中的材质
我们可以利用Mesh的mMaterialIndex变量(材质序号)进到Scene中利用工具函数loadMaterialTextures读取mMaterials(获取漫反射和/或镜面光贴图),存储在之前Mesh类中定义的Texture结构体中:
(我们这里同样也可以先判断是否有材质存在)
if(mesh->mMaterialIndex >= 0)
{
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
vector<Texture> diffuseMaps = loadMaterialTextures(material,
aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
vector<Texture> specularMaps = loadMaterialTextures(material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
1.5.3.1 loadMaterialTextures
这个函数首先通过GetTextureCount函数检查储存在材质中某类纹理的数量,这个函数需要一个纹理类型。我们会使用GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString中。我们接下来使用另外一个叫做TextureFromFile的工具函数,它将会(用stb_image.h)加载一个纹理并返回该纹理的ID。
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
1.5.3.2 TextureFromFile
这一步就是利用传入的路径去读取、创建纹理,最后返回生成的纹理ID,其他基本与之前介绍的一致,就不赘述了
unsigned int TextureFromFile(const char* path, const string& directory)
{
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
自此,加载模型的流程已全部完毕
上一篇: python opengl load 3d model
下一篇: assimp.net