欢迎光临散文网 会员登陆 & 注册

opengl模型(导入3D模型)

2023-06-14 20:13 作者:我梦见珍妮  | 我要投稿

OpenGL

模型

导入3D模型

优化

使用3D模型

使用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轴都是反的,所系这个后期处理选项可以修复该问题。

 其他有用的选项还有: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 = 0; i < mesh->mNumVertices; i++)

    {

        Vertex vertex;

        // 处理顶点位置、法线和纹理坐标

        ...

        vertices.push_back(vertex);

    }

    // 处理索引

    ...

    // 处理材质

    if(mesh->mMaterialIndex >= 0)

    {

        ...

    }


    return Mesh(vertices, indices, textures);

}

处理网格的过程主要有三部分:获取所有的顶点数据,获取它们的网格索引,并获取相关的材质数据。处理后的数据将会储存在三个vector当中,我们会利用它们构建一个Mesh对象,并返回它到函数的调用者那里。

 1。获取顶点数据:定义了一个Vertex结构体,将在每个迭代之后将它加入到vertices数组中。会遍历网格中的所有顶点——使用mesh->mNumVertices来获取。每个迭代中,使用所有的相关数据填充这个结构体,顶点的位置如下:


glm::vec3 vector; 

vector.x = mesh->mVertices[i].x;

vector.y = mesh->mVertices[i].y;

vector.z = mesh->mVertices[i].z; 

vertex.Position = vector;

使用了vec3的临时变量,是因为Assimp对向量,矩阵,字符串等都有自己的一套数据类型,并不能完美地转换到GLM的数据类型中。

 处理法线的过程类似:


vector.x = mesh->mNormals[i].x;

vector.y = mesh->mNormals[i].y;

vector.z = mesh->mNormals[i].z;

vertex.Normal = vector;

纹理坐标的处理也大体相似,但Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,我们不会用到那么多,我们只关心第一组纹理坐标。我们同样也想检查网格是否真的包含了纹理坐标:


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);

vertex结构体现在已经填充好了需要的顶点属性,我们会在迭代的最后将它压入vertices这个vector的尾部。这个过程会对每个网格的顶点都重复一遍。


Assimp的接口定义了每个网格都有一个面(Face)数组,每个面代表了一个图元,在例子中(由于使用了aiProcess_Triangulate选项)它总是三角形。一个面包含了多个索引,它们定义了在每个图元中,我们应该绘制哪个顶点,并以什么顺序绘制,所以如果我们遍历了所有的面,并储存了面的索引到indices这个vector中就可以了。


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]);

}

到目前为止,有了一系列的顶点和索引数据,可以通过glDrawElements函数来绘制网格。为了提供一些细节,还需要处理网格的材质。

 一个网格只包含了一个指向材质对象的索引。如果要获取网格真正的材质,还需要索引场景的mMaterials数组。网格材质索引位于其mMaterialIndex属性,同样可以用它来检测一个网格是否包含有材质:


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());

}

我们首先从场景的mMaterials数组中获取aiMaterial对象。接下来我们希望加载网格的漫反射和/或镜面光贴图。一个材质对象的内部对每种纹理类型都存储了一个纹理位置数组。不同的纹理类型都以aiTextureType_为前缀。我们使用一个叫做loadMaterialTextures的工具函数来从材质中获取纹理。这个函数将会返回一个Texture结构体的vector,我们将在模型的textures的尾部之后存储它。


loadMaterialTextures函数遍历了给定纹理类型的所有纹理位置,获取了纹理的文件位置,并加载并和生成了纹理,将信息储存在了一个Vertex结构体中。loadMaterialTextures函数它看起来会像这样:


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;

}

我们首先通过GetTextureCount函数检查储存在材质中纹理的数量,这个函数需要一个纹理类型。我们会使用GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString中。我们接下来使用另外一个叫做TextureFromFile的工具函数,它将会(用stb_image.h)加载一个纹理并返回该纹理的ID。第二个参数是模型的文件路径。


注意:我们假设了模型文件中纹理文件的路径是相对于模型文件的本地(Local)路径,比如说与模型文件处于同一目录下。我们可以将纹理位置字符串拼接到之前获取的目录字符串上(TextureFromFile),来获取完整的纹理路径(这也是为什么GetTexture函数也需要一个目录字符串)。



在网络上找到的某些模型会对纹理位置使用绝对(Absolute)路径,这就不能在每台机器上都工作了。在这种情况下,你可能会需要手动修改这个文件,来让它对纹理使用本地路径(如果可能的话)。


综上,是使用Assimp导入模型的全部。


优化

优化不是必须的,但是可以提高加载过程。

 大多数场景都会在多个网络中 重用部分纹理。比如:一个纹理不仅可以用到人身上,也能用到物体身上。当然就是用同一个纹理进行加载。但是同样的纹理已经被加载过了很多遍,对每个网格仍会加载并生成一个新的纹理。很快就会变成模型加载实现的性能瓶颈。

 可以被模型的代码进行调整,将所有加载过的纹理全局存储。每当要加载一个纹理的时候,首先去检查是否被加载过,如果有的话,直接使用那个纹理,并跳过整个加载流程。为了能够比较纹理,还需要存储它们的路径:


struct Texture {

    unsigned int id;

    string type;

    aiString path;  // 我们储存纹理的路径用于与其它纹理进行比较

};

接下来我们将所有加载过的纹理储存在另一个vector中,在模型类的顶部声明为一个私有变量:


vector<Texture> textures_loaded;


在loadMaterialTextures函数中,我们希望将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。如果是的话,则跳过纹理加载/生成的部分,直接使用定位到的纹理结构体为网格的纹理。更新后的函数如下:


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);

        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; 

                break;

            }

        }

        if(!skip)

        {   // 如果纹理还没有被加载,则加载它

            Texture texture;

            texture.id = TextureFromFile(str.C_Str(), directory);

            texture.type = typeName;

            texture.path = str.C_Str();

            textures.push_back(texture);

            textures_loaded.push_back(texture); // 添加到已加载的纹理中

        }

    }

    return textures;

}

所以现在我们不仅有了个灵活的模型加载系统,我们也获得了一个加载对象很快的优化版本。


综上,完整代码如下:


#ifndef MODEL_H

#define MODEL_H


#include <glad/glad.h> 


#include <glm/glm.hpp>

#include <glm/gtc/matrix_transform.hpp>

#include <stb_image.h>

#include <assimp/Importer.hpp>

#include <assimp/scene.h>

#include <assimp/postprocess.h>


#include <learnopengl/mesh.h>

#include <learnopengl/shader.h>


#include <string>

#include <fstream>

#include <sstream>

#include <iostream>

#include <map>

#include <vector>

using namespace std;


unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false);


class Model 

{

public:

    // model data 

    vector<Texture> textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.

    vector<Mesh>    meshes;

    string directory;

    bool gammaCorrection;


    // constructor, expects a filepath to a 3D model.

    Model(string const &path, bool gamma = false) : gammaCorrection(gamma)

    {

        loadModel(path);

    }


    // draws the model, and thus all its meshes

    void Draw(Shader &shader)

    {

        for(unsigned int i = 0; i < meshes.size(); i++)

            meshes[i].Draw(shader);

    }

    

private:

    // loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.

    void loadModel(string const &path)

    {

        // read file via ASSIMP

        Assimp::Importer importer;

        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) // if is Not Zero

        {

            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);

    }


    // 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));

        }

        // 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);

        }


    }


    Mesh processMesh(aiMesh *mesh, const aiScene *scene)

    {

        // data to fill

        vector<Vertex> vertices;

        vector<unsigned int> indices;

        vector<Texture> textures;


        // walk through each of the mesh's vertices

        for(unsigned int i = 0; i < mesh->mNumVertices; i++)

        {

            Vertex vertex;

            glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.

            // 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]) // does the mesh contain texture coordinates?

            {

                glm::vec2 vec;

                // a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't 

                // use models where a vertex can have multiple texture coordinates so we always take the first set (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);

        }

        // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.

        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]);        

        }

        // process materials

        aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];    

        // we assume a convention for sampler names in the shaders. Each diffuse texture should be named

        // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER. 

        // Same applies to other texture as the following list summarizes:

        // 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);

    }


    // 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);

            // 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);

                texture.type = typeName;

                texture.path = str.C_Str();

                textures.push_back(texture);

                textures_loaded.push_back(texture);  // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures.

            }

        }

        return textures;

    }

};



unsigned int TextureFromFile(const char *path, const string &directory, bool gamma)

{

    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;

}

#endif

使用3D模型

加载一个3D模型,这个模型被输出为一个.obj文件和一个.mtl文件,.mtl文件包含了模型的漫反射,镜面光,法线贴图。

注意:所有的纹理和模型文件应该位于同一个目录下,以供加载纹理。

 在代码中,声明一个Model对象,将模型的文件位置传入。接下来模型会自动加载并在渲染循环中使用它的Draw函数来绘制物体。不再需要缓冲分配、属性指针和渲染指令,只需要一行代码就可以了。


opengl模型(导入3D模型)的评论 (共 条)

分享到微博请遵守国家法律