zoukankan      html  css  js  c++  java
  • opengl 教程(22) 用开源库装入模型

    原帖地址:http://ogldev.atspace.co.uk/www/tutorial22/tutorial22.html

          前面的教程中,我们都是使用手工指定三维模型,渲染一些简单的物体,比如,正方体、四面体金字塔等等。如果要渲染复杂的物体,该物体包含很多的顶点,每个顶点除了位置,还有很多的属性,比如一张人脸,那么通过在程序中指定顶点缓冲来渲染的话,几乎是不可能的事情,因为模型太复杂了。通常在三维游戏或者一些商业三维应用中,都是艺术家通过一些专用的建模软件,比如Blender, Maya 或者 3ds Max来进行物体建模,模型完成后,然后导出一定的模型文件格式,最后游戏引擎或者别的应用程序,可以读取这些模型文件,产生顶点缓冲、索引缓冲以及一些其它的设置,从而完成复杂模型渲染。本篇教程中,我们将学习如何解析模型文件,并在我们的程序中使用。

          几乎每种游戏引擎或者建模软件都有自己的模型格式,开发一个自己的解析器,来兼容大部分的模型格式,是件费力费时的工作。本篇教程中,我们使用一个第三方开源库Open Asset Import Library来导入模型文件,Assimp开源库能处理很多模型文件格式,比如D3D的x文件,静态的obj文件等等,而且Assimp库是用c++写的,很容易集成到我们的程序里。

          本教程中,我们不会详细介绍Assimp库的原理,感兴趣的朋友可以去它的网站看看,里面有很多介绍,或者你也可以研究它内部的代码,看它是如何解析模型文件的,本文中,只是介绍了如何在我们的程序中通过Assimp库装入三维模型。

    (注意:开始编写程序前,你要确保安装了Assimp库,可以从上面给出的链接处下载)

    主要代码:

    mesh.h

    class Mesh
    {
    public:
    Mesh();
    ~Mesh();
    bool LoadMesh(const std::string& Filename);
    void Render();
    private:
    bool InitFromScene(const aiScene* pScene, const std::string& Filename);
    void InitMesh(unsigned int Index, const aiMesh* paiMesh);
    bool InitMaterials(const aiScene* pScene, const std::string& Filename);
    void Clear();
    #define INVALID_MATERIAL 0xFFFFFFFF
    struct MeshEntry {
    MeshEntry();
    ~MeshEntry();
    bool Init(const std::vector& Vertices,
    const std::vector& Indices);
    GLuint VB;
    GLuint IB;
    unsigned int NumIndices;
    unsigned int MaterialIndex;
    };
    std::vector m_Entries;
    std::vector m_Textures;
    };

          Mesh类是Assimp库和我们OpenGL程序的接口, 该类会通过LoadMesh函数从一个模型文件中装入数据,用来产生顶点缓冲,索引缓冲,纹理对象等等。为了渲染三维模型,我们也在该类中增加了Render函数。Mesh类的内部数据结构是和Assimp库装入模型的方式相匹配的, Assimp库用了一个aiScene对象来表示装入的模型,aiScene包含了各种各样模型数据的mesh结构。在aiScene对象中,至少会有一种mesh结构,复杂的模型中,可能包含多种mesh结构。m_Entries是一个MeshEntry类型的向量,每个MeshEntry都对应aiScene对象中的一个mesh结构,这些mesh结构包含顶点缓冲,索引缓冲,纹理索引等等。 现在我们的材质只是一个简单的纹理,因为MeshEntries之间可能会共享纹理,所以我们的Mesh类的包含一个单独的向量m_Texures, MeshEntry::MaterialIndex会指向该MeshEntry在m_Textures中对应的纹理。

    mesh.cpp

    bool Mesh::LoadMesh(const std::string& Filename)
    {
    // 释放掉以前装入的模型数据

    Clear();
    bool Ret = false;
    Assimp::Importer Importer;
    const aiScene* pScene = Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs);
    if (pScene) {
    Ret = InitFromScene(pScene, Filename);
    }
    else {
    printf("Error parsing '%s': '%s'\n", Filename.c_str(), Importer.GetErrorString());
    }
    return Ret;
    }

          我们在LoadMesh函数中装入模型文件。首先,我们会创建一个Assimp::Importer类实例,并调用它的成员函数 ReadFile来装入模型文件,该函数的参数有2个,第一个是要装入模型文件的全路径名称,第二个是模型数据后处理选项 。Assimp在装入模型时候,可以进行很多有用的操作,比如,如果模型缺少法向数据,我们可以指定后处理选项让 Assimp 为Mesh自动计算法向,Assimp还可以执行一些优化操作以便改进性能,等等诸如此类的操作。我们通过下面的链接去产看所有的后处理选项, 点击这儿

          在本篇教程中,我们用了三个选项: aiProcess_Triangulate, 表示会把模型三角形话,如果模型是多面体数据,Assimp会替我们把这些多边形顶点转化为三角形mesh顶点数据,例如一个四边形可能会被转化为2个三角形。第二个选项  aiProcess_GenSmoothNormals表示,如果原始顶点没有法向数据,Assimp会为顶点产生法向数据。最后一个选项aiProcess_FlipUVsv表示,沿着y方向翻转纹理坐标,这在渲染quake模型时候是必须的[注意:我们这些后处理选项是可以通过或操作叠加的] 。模型装入成功后,我们会得到一个指向aiScene 对象的指针,该对象中会包含以aiMesh结构分类的所有模型数据。最后,我们会调用InitFromScene函数,初始化mesh对象。

    mesh.cpp

    bool Mesh::InitFromScene(const aiScene* pScene, const std::string& Filename)
    {
    m_Entries.resize(pScene->mNumMeshes);
    m_Textures.resize(pScene->mNumMaterials);
    //逐个初始化场景中的mesh对象

    for (unsigned int i = 0 ; i < m_Entries.size() ; i++) {
    const aiMesh* paiMesh = pScene->mMeshes[i];
    InitMesh(i, paiMesh);
    }
    return InitMaterials(pScene, Filename);
    }

         在初始化三维渲染场景函数中,我们首先为mesh entries和texture vectors两个成员变量分配空间,它们的大小分别为aiScene对象中的mesh和材质数量。接着,我们会遍历aiScene对象中的mesh数组,来逐个初始化mesh entries成员变量。

    void Mesh::InitMesh(unsigned int Index, const aiMesh* paiMesh)
    {
        m_Entries[Index].MaterialIndex = paiMesh->mMaterialIndex;
        std::vector Vertices;
        std::vector Indices;
        ...

          在初始化mesh时候,我们首先会保存材质索引,在渲染过程中,该值用来绑定正确的纹理,接下来,我们会创建2个STL向量,用来存储顶点缓冲和索引缓冲。STL向量通常会被数据存在连续的缓冲中,而且使用方便,我们很容易把向量中的数据装入到opengl buffer中去[通过glBufferData函数]。

        const aiVector3D Zero3D(0.0f, 0.0f, 0.0f);
        for (unsigned int i = 0 ; i < paiMesh->mNumVertices ; i++) {
            const aiVector3D* pPos = &(paiMesh->mVertices[i]);
            const aiVector3D* pNormal = &(paiMesh->mNormals[i]) : &Zero3D;
            const aiVector3D* pTexCoord = paiMesh->HasTextureCoords(0) ? &(paiMesh->mTextureCoords[0][i]) : &Zero3D;
            Vertex v(Vector3f(pPos->x, pPos->y, pPos->z),
                    Vector2f(pTexCoord->x, pTexCoord->y),
                    Vector3f(pNormal->x, pNormal->y, pNormal->z));
            Vertices.push_back(v);
        }
        ...

    在上面的代码中,我们生成顶点缓冲的数据(放在Vertices向量中)。

    我们使用了aiMesh类的下列属性:

    1. mNumVertices - 顶点数量
    2. mVertices - 顶点位置向量mNumVertices
    3. mNormals - 顶点法向向量 mNormals
    4. mTextureCoords - 顶点纹理坐标向量 mTextureCoords ,注意一个顶点可能包含多个纹理坐标,所以该变量是一个二维数组

          我们把mesh的顶点,法向,纹理分别放在三个数组中,最终我们会用这三个数组构建顶点属性结构,并把顶点属性结构变量v保存到顶点缓冲变量Vertices中。注意:一些模型可能没有纹理,也不存在纹理坐标,所以我们从aiMesh对象中取纹理时候,要先调用HasTextureCoords(0)函数进行判断,另外一个顶点可能有多个纹理坐标,但在本教程中,我们只用了一个纹理坐标,所以使用paiMesh->mTextureCoords[0][i],0表示第一个纹理坐标,当不在纹理坐标时候,我们只是简单的把纹理坐标负值为0。

        for (unsigned int i = 0 ; i < paiMesh->mNumFaces ; i++) {
            const aiFace& Face = paiMesh->mFaces[i];
            assert(Face.mNumIndices == 3);
            Indices.push_back(Face.mIndices[0]);
            Indices.push_back(Face.mIndices[1]);
            Indices.push_back(Face.mIndices[2]);
        }
        ...

          上面的代码中,我们生成索引缓冲:aiMesh类的成员变量mNumFaces指定了每个mesh中包含多少个多边形(三角形),mFaces成员变量包含具体的索引数据。我们首先会判断每个多边形的顶点数是否为3,不为3的话会产生异常(前面装入模型时候,我们已经旋转了三角形化),接着我们会把三角形的索引数据保存到Indices向量中去。

        m_Entries[Index].Init(Vertices, Indices);
    }

          最后,我们会用顶点和索引向量初始化MeshEntry变量。在Init函数中,会用glGenBuffer(), glBindBuffer() and glBufferData()几个函数产生顶点和索引缓冲。

    bool Mesh::InitMaterials(const aiScene* pScene, const std::string& Filename)
    {
        for (unsigned int i = 0 ; i < pScene->mNumMaterials ; i++) {
            const aiMaterial* pMaterial = pScene->mMaterials[i];
           ...

          该函数会装入模型所用的所有纹理。aiScene对象的成员变量mNumMaterials中有材质的数量,mMaterials则是一个指向aiMaterials结构的数组。aiMaterial是一个很庞大,复杂的类,通常材质被组织成纹理栈的形式,在两个连续的纹理之间,我们需要配置blend和strength函数,blend函数用来决定2个纹理颜色如何相加操作,而strength函数决定两个纹理颜色如何相乘操作,这两个函数都是aiMaterial的一部分。在本教程中,为了和前面的光照shader一致,我们将忽略这两个函数。

            m_Textures[i] = NULL;
            if (pMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
                aiString Path;
                if (pMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS) {
                    std::string FullPath = Dir + "/" + Path.data;
                    m_Textures[i] = new Texture(GL_TEXTURE_2D, FullPath.c_str());
                    if (!m_Textures[i]->Load()) {
                        printf("Error loading texture '%s'\n", FullPath.c_str());
                        delete m_Textures[i];
                        m_Textures[i] = NULL;
                        Ret = false;
                    }
                }
            }
            ...

            一个材质可能包含多个纹理,并不是其中的每个纹理都有颜色,比如有的纹理表示高度图,有的纹理表示法向图,偏移图等等。我们光照模型现在只用了一个单纹理来对应所有的光照类型,所以我们只关注漫反射光材质,因此,我们会aiMaterial::GetTextureCount() 函数检测有多少个材质存在,这个函数用纹理类型作为参数,返回值该指定类型纹理的数量。该函数第一个参数即为纹理类型,第二个参数是索引,我们总是指定为0,第三个参数指定纹理文件名字,后面的5个参数是各种各样的纹理配置,比如blend因子,map模式,纹理操作等等,这些参数是可选的,在我们程序中,总是被指定为NULL。我们会把纹理文件名字和目录名字连接起来,我们会假设模型文件和纹理文件在同一个目录。

           if (!m_Textures[i]) {
              m_Textures[i] = new Texture(GL_TEXTURE_2D, "./white.png");
              Ret = m_Textures[i]->Load();
           }
        }
        return Ret;
    }

          有时候,在模型目录,纹理文件并不存在,此时渲染的结果可能是一片漆黑,所以我们会增加上面的一段代码,当在模型目录找不到纹理时候,我们会装入一个默认的纹理文件,该文件是一副白色的png图片。

    void Mesh::Render()
    {
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glEnableVertexAttribArray(2);
        for (unsigned int i = 0 ; i < m_Entries.size() ; i++) {
            glBindBuffer(GL_ARRAY_BUFFER, m_Entries[i].VB);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
            glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)12);
            glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)20);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Entries[i].IB);
            const unsigned int MaterialIndex = m_Entries[i].MaterialIndex;
            if (MaterialIndex < m_Textures.size() && m_Textures[MaterialIndex]) {
                m_Textures[MaterialIndex]->Bind(GL_TEXTURE0);
            }
            glDrawElements(GL_TRIANGLES, m_Entries[i].NumIndices, GL_UNSIGNED_INT, 0);
        }
        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glDisableVertexAttribArray(2);
    }

        在前面教程中,我们都把渲染函数放在主cpp中,本篇教程代码中,我们会把Render函数分离出来。我们会遍历m_Entries,指定顶点缓冲,索引缓冲,以及材质,最后调用draw函数进行gpu渲染操作,这样我们就可以在场景中渲染多个物体了。

    glut_backend.cpp

    glEnable(GL_DEPTH_TEST);

    最后我们在程序初始化开启深度测试,以保证前后遮挡的物体渲染正确。开启深度测试的代码在GLUTBackendRun函数中。

    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH);

          我们还要初始化深度缓冲,通常深度缓冲初始化时,每个像素深度值都是1.0,和颜色缓冲相似,所有像素在深度缓冲中都有一个对应的单元。

    tutorial22.cpp

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

          在每帧渲染前,我们都要清除深度缓冲和颜色缓冲,如果不做这个操作,可能深度缓冲和颜色缓冲中的值还是上一帧的结果,这可能会使得渲染结果不正确。

    程序执行后界面如下:

    clipboard

  • 相关阅读:
    别闹了,这些都不是数字化转型
    对不起,“下一代ERP”仍旧是现在的ERP
    这世界真小
    SAP S4HANA 2020 Fully-Activated Appliance 虚拟机版分享
    花费巨资参加SAP培训真的有用吗?
    剑指 Offer 07. 重建二叉树
    剑指 Offer 06. 从尾到头打印链表
    剑指 Offer 05. 替换空格
    剑指 Offer 04.二维数组中的查找
    剑指 Offer 03. 数组中重复的数字
  • 原文地址:https://www.cnblogs.com/mikewolf2002/p/2879067.html
Copyright © 2011-2022 走看看