第十二课,assimp模型加载(数据加载篇)

大佬讲解入口

OpenGL学习脚印:模型加载初步-加载obj模型(load obj model) ,作者:The fool
OpenGL学习脚印-AssImp模型加载 ,作者:The fool

本人主要通过程序流程理解程序代码

创建对象

Model ourModel("文件路径(.obj文件)");

.obj文件存储模式参照最上方大佬文章。

Model类存储的public成员有

vector<Texture> textures_loaded;//存储到目前为止加载的所有纹理,优化以确保纹理不会加载超过一次。  
vector<Mesh>    meshes;
string directory;
bool gammaCorrection;

Model类的构造函数

Model(string const& path, bool gamma = false) : gammaCorrection(gamma)
{
    loadModel(path);
}

通过loadmodel检查并加载数据

void loadModel(string const& path)
{
    // read file via ASSIMP
    Assimp::Importer importer;//c++接口
    const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);//读取数据并做部分处理,返回根节点
    // check for errors
    if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // 检查读取是否正确
    {
        cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
        return;
    }
    // retrieve the directory path of the filepath
    directory = path.substr(0, path.find_last_of('/'));//保存根目录

    // process ASSIMP's root node recursively
    processNode(scene->mRootNode, scene);//处理结点
}

根据官方文档解释
Importer类为Open Asset Import Library的功能形成了一个c++接口。
首先创建 Assimp::Importer的示例
然后通过它的l Assimp::Importer::ReadFile()方法加载模型

const aiScene* Assimp::Importer::ReadFile(const char *pFile,unsigned int 	pFlags )	

pFlags为导入成功后执行的可选后处理步骤,是一些后期处理(Post-processing)的选项,可在官网找到具体的信息。
例如:

  • aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。
  • aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标
  • aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
  • aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
  • aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。

aiScene–这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。

if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)检查场景和其根节点不为null,并且检查了它的一个标记(Flag),来查看返回的数据是不是不完整的。

设置参数directory = path.substr(0, path.find_last_of('/'));保存根目录路径。

如果没有错误,则通过processNode(scene->mRootNode, scene);处理场景中的所有节点,所以我们将第一个节点(根节点)传入了递归的processNode函数。

processNode递归处理结点载入mesh类

// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).以递归方式处理节点。处理位于该节点上的每个网格,并在其子节点(如果有)上重复此过程。
void processNode(aiNode* node, const aiScene* scene)
{
   // process each mesh located at the current node处理当前结点的每个网络
   for (unsigned int i = 0; i < node->mNumMeshes; i++)
   {
       // the node object only contains indices to index the actual objects in the scene. 
       // the scene contains all the data, node is just to keep stuff organized (like relations between nodes).
       aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
       meshes.push_back(processMesh(mesh, scene));//递归扫描,将模型数据存储在vector<mesh>中
   }
   // after we've processed all of the meshes (if any) we then recursively process each of the children nodes处理当前结点的所有子节点
   for (unsigned int i = 0; i < node->mNumChildren; i++)
   {
       processNode(node->mChildren[i], scene);
   }

}

到这里可能已经懵了,让我们来看看assimp内部了类
在这里插入图片描述
在这里插入图片描述

这里对上述图作简要说明:

  • aiScene 作为AssImp加载模型的根数据结构,保存了从模型加载的顶点位置、法向量、纹理、光照等数据,例如它通过数组mMeshes保存Mesh数据,通过mMaterials保存材质数据。总之aiScene保存了加载的模型数据,其余类通过索引aiScene中存储的对象来获取对应的数据。

  • aiNode 模型通过层次结构存储,根节点mRootNode保存在aiScene中,根节点下面有0至多个子节点,每个节点通过aiNode类表达,aiNode中包含一个或者多个Mesh,注意这里的Mesh是对aiScene中Mesh数据的一个索引

  • aiMesh是上一节中所讲的Mesh对象,Mesh中包含顶点位置数据、法向量、纹理数据,每个Mesh可以包含一个或者多个Face。

  • aiFace是一个面,一般来讲在读取模型时通过后处理选项(post-process flag)将模型转换为三角形网格,那么这里的面主要是三角形面。后处理选项稍后介绍。通过三角形面,我们可以获取渲染模型需要的索引数据。

原文链接:https://blog.csdn.net/wangdingqiaoit/article/details/52014321

代码讲解:

  • void processNode(aiNode* node, const aiScene* scene)中node是我们需要加载的模型结点,但它只是个索引,真正的数据要到scene中去找。所以还需要常量指针const aiScene* scene指向具体数据。
  • aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene));
    将该结点内的每一个Mesh(对aiScene中Mesh数据的一个索引)进行processMesh(mesh, scene)操作,该操作会在下面详细解释。
  • processNode(node->mChildren[i], scene);循环遍历每一个子节点迭代。

因此该函数的意义便是自上(根节点)到下(子节点)导出所有结点的mesh对象(数据的一个索引),进行processMesh(mesh, scene)操作,并将返回值存储在meshes容器中。

processMesh(mesh, scene)处理mesh对象

    Mesh processMesh(aiMesh* mesh, const aiScene* scene)
    {
        // data to fill
        vector<Vertex> vertices;
        vector<unsigned int> indices;
        vector<Texture> textures;

        // 遍历网格的每个顶点
        for (unsigned int i = 0; i < mesh->mNumVertices; i++)//mNumVertices存储了顶点数量
        {
            Vertex vertex;//Vertex结构体在Mesh.h中有定义
            glm::vec3 vector; // 我们声明了一个占位符向量,因为assimp使用它自己的向量类,它不会直接转换到glm的vec3类,所以我们首先将数据转换到这个占位符glm::vec3。
            // positions
            vector.x = mesh->mVertices[i].x;
            vector.y = mesh->mVertices[i].y;
            vector.z = mesh->mVertices[i].z;
            vertex.Position = vector;//将该点数据存储在结构体中
            // normals
            if (mesh->HasNormals())//同理存储法线(如果有的话)
            {
                vector.x = mesh->mNormals[i].x;
                vector.y = mesh->mNormals[i].y;
                vector.z = mesh->mNormals[i].z;
                vertex.Normal = vector;
            }
            // texture coordinates
            if (mesh->mTextureCoords[0]) //加载纹理坐标
            {
                glm::vec2 vec;
                // 一个顶点最多可以包含8个不同的纹理坐标. 因此,我们假设我们不会
                // 使用顶点可以有多个纹理坐标的模型,所以我们总是取第一个集合(0)。
                vec.x = mesh->mTextureCoords[0][i].x;
                vec.y = mesh->mTextureCoords[0][i].y;
                vertex.TexCoords = vec;
                // tangent
                vector.x = mesh->mTangents[i].x;
                vector.y = mesh->mTangents[i].y;
                vector.z = mesh->mTangents[i].z;
                vertex.Tangent = vector;
                // bitangent
                vector.x = mesh->mBitangents[i].x;
                vector.y = mesh->mBitangents[i].y;
                vector.z = mesh->mBitangents[i].z;
                vertex.Bitangent = vector;
            }
            else
                vertex.TexCoords = glm::vec2(0.0f, 0.0f);

            vertices.push_back(vertex);
        }
        // 现在遍历网格的每个面(面是网格的三角形)并检索相应的顶点索引。
        for (unsigned int i = 0; i < mesh->mNumFaces; i++)
        {
            aiFace face = mesh->mFaces[i];
            // retrieve all indices of the face and store them in the indices vector
            for (unsigned int j = 0; j < face.mNumIndices; j++)//每个面都有顶点索引
                indices.push_back(face.mIndices[j]);//将索引存储在容器中,类似于VEO
        }
        // 处理材料
        aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
        // 我们假设在着色器中采样器名称有一个约定。每个漫反射纹理都应该被命名
        // 为'texture_diffuseN',其中N是一个从1到MAX_SAMPLER_NUMBER的连续数字。
        // 这同样适用于其他纹理,如下列表总结:
        // diffuse: texture_diffuseN
        // specular: texture_specularN
        // normal: texture_normalN

        // 1. diffuse maps
        vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
        textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
        // 2. specular maps
        vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
        textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
        // 3. normal maps
        std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
        textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
        // 4. height maps
        std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
        textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());

        // return a mesh object created from the extracted mesh data
        return Mesh(vertices, indices, textures);
    }

loadMaterialTextures函数如下:

// checks all material textures of a given type and loads the textures if they're not loaded yet.
    // the required info is returned as a Texture struct.
    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);//获取每个纹理的文件位置,它会将结果储存在一个aiString中
            // check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
            bool skip = false;
            for (unsigned int j = 0; j < textures_loaded.size(); j++)
            {
                if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
                {
                    textures.push_back(textures_loaded[j]);
                    skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
                    break;
                }
            }
            if (!skip)
            {   // if texture hasn't been loaded already, load it
                Texture texture;
                texture.id = TextureFromFile(str.C_Str(), this->directory);//将会(用stb_image.h)加载一个纹理并返回该纹理的ID。
                texture.type = typeName;
                texture.path = str.C_Str();
                textures.push_back(texture);//在该容器中存储的只有id,类型和路径,对应特定的网络mesh
                textures_loaded.push_back(texture);  // 将其存储为整个模型加载的纹理,以确保我们不会不必要地加载重复的纹理。  
            }
        }
        return textures;
    }
};

TextureFromFile函数及使用stb_image.h加载纹理的函数,在之前已有介绍。

补充(2020.10.5):
return Mesh(…,…,…,);这里new了一个Mesh类,从这里进入了Mesh类的内部构造。

// constructor
    Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
    {
        this->vertices = vertices;//存储了顶点,法线,纹理坐标等
        this->indices = indices;//EBO
        this->textures = textures;//纹理

        // now that we have all the required data, set the vertex buffers and its attribute pointers.
        setupMesh();
    }

关键在于三个容器存储的数据类型。

void setupMesh()
    {
        // create buffers/arrays
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);

        glBindVertexArray(VAO);
        // 将数据加载入VAO
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        // 关于结构一个伟大的事情是,对于它的所有项,它们的内存布局是连序的。
        // 其效果是,我们可以简单地传递一个指向结构体的指针,它会完美地转换为glm::vec3/2数组,再转换为3/2浮点数,转换为字节数组。
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

        // set the vertex attribute pointers
        // vertex Positions
        glEnableVertexAttribArray(0);//顶点
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        // vertex normals
        glEnableVertexAttribArray(1);//法线
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
        // vertex texture coords
        glEnableVertexAttribArray(2);//纹理坐标
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
        // vertex tangent
        glEnableVertexAttribArray(3);//Tangent
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
        // vertex bitangent
        glEnableVertexAttribArray(4);//Bitangent
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
        // ids
        glEnableVertexAttribArray(5);
        glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));

        // weights
        glEnableVertexAttribArray(6);
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));
        glBindVertexArray(0);
    }

Model类构建的核心内容

Model ourModel("文件路径(.obj文件)"){//载入obj文件
	loadModel(path){//
		检查场景是否完整;
		记录地址string directory;
		processNode(scene->mRootNode, scene){//遍历所有的结点
			meshes.push_back(processMesh(mesh, scene)){//载入每个结点的每个网格
				将	vector<Vertex> vertices;//顶点,法向量,纹理坐标
        			vector<unsigned int> indices;//顶点索引
        			vector<Texture> textures{//纹理数据
						loadMaterialTextures{
							获取纹理地址;
							是否已经加载?已经加载则{
								texture.id = TextureFromFile(str.C_Str(), this->directory);//从该路径下加载该纹理
                				texture.type = typeName;
                				texture.path = str.C_Str();
							}
						}
					}
        		构造Mesh并返回给Mesh类容器存储数据。
			}
		}
	}
}

未知内容

  • assimp是如何将obj文件加载为该种存储形式的
  • importer.ReadFile中的第二个参数对模型的改变是如何进行的

assimp内部数据处理也是今后学习要点。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片