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

  • 相关阅读:
    洛谷 1850 NOIP2016提高组 换教室
    2018牛客多校第三场 C.Shuffle Cards
    2018牛客多校第一场 B.Symmetric Matrix
    2018牛客多校第一场 A.Monotonic Matrix
    2018牛客多校第一场 D.Two Graphs
    2018宁夏邀请赛L Continuous Intervals
    2018宁夏邀请赛K Vertex Covers
    BZOJ
    HDU
    ACM International Collegiate Programming Contest, Egyptian Collegiate Programming Contest (ECPC 2015)
  • 原文地址:https://www.cnblogs.com/mikewolf2002/p/2879067.html
Copyright © 2011-2022 走看看