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

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

程序执行后界面如下:

时间: 2024-10-27 16:57:06

opengl 教程(22) 用开源库装入模型的相关文章

开源库Magicodes.ECharts使用教程

  目录 1    概要    2 2    Magicodes.ECharts工作原理    3 2.1    架构说明    3 2.1.1    Axis    4 2.1.2    CommonDefinitions    4 2.1.3    Components    4 2.1.4    JsonConverter    4 2.2    Series    6 2.3    ValueTypes    6 2.4    EChartsOptions    7 2.5    Tim

站在巨人的肩膀上,C++开源库大全

程序员要站在巨人的肩膀上,C++拥有丰富的开源库,这里包括:标准库.Web应用框架.人工智能.数据库.图片处理.机器学习.日志.代码分析等.   标准库 C++ Standard Library:是一系列类和函数的集合,使用核心语言编写,也是C++ISO自身标准的一部分. Standard Template Library:标准模板库 C POSIX library : POSIX系统的C标准库规范 ISO C++ Standards Committee :C++标准委员会 框架 C++通用框架

[译] 当发布安卓开源库时我希望知道的东西

本文讲的是[译] 当发布安卓开源库时我希望知道的东西, 原文地址:Things I wish I knew when I started building Android SDK/Libraries 原文作者:本文已获作者 Nishant Srivastava 授权 译文出自:掘金翻译计划 译者:jifaxu 校对者:BoilerYao, gaozp 当发布安卓开源库时我希望知道的东西 一切要从安卓开发者开发自己的"超酷炫应用"开始说起,他们中的大多数会在这个过程中遇到一系列问题,而他

Android开发之常用开源库直接拿来用

1.from  代码家 整理比较好的源码连接 *************************************************************************************************************************************************************************** http://blog.zhan-dui.com/?page_id=60 感谢 "代码家"整理 一.

NeHe的OpenGL教程6(Bang翻译Delphi版)-如何用图片进行纹理映射

NeHe的OpenGL教程6(Bang翻译Delphi版)-如何用图片进行纹理映射 在这一课里,我将教会你如何把纹理映射到立方体的六个面,如下图: 将下图放在应用程序data目录下,起名NeHe.bmp program lesson6a; {    OpenGL DelphiXE    出处:根据NeHe代码翻译而来(http://nehe.gamedev.net/)    作者:帅宏军 shuaihj@163.com     注:本单元用到了glaux.dll和glaux.pas,下载地址为:

NeHe的OpenGL教程1(Bang翻译Delphi版)-如何绘制OpenGL窗口

NeHe的OpenGL教程1(Bang翻译Delphi版)-如何绘制OpenGL窗口 在这个教程里,我将教你在Windows环境中创建OpenGL程序.它将显示一个空的OpenGL窗口,可以在窗口和全屏模式下切换,按ESC退出.它是我们以后应用程序的框架.如下图: program lesson1a; {    OpenGL DelphiXE    出处:根据NeHe代码翻译而来(http://nehe.gamedev.net/)    作者:帅宏军 shuaihj@163.com} uses 

NeHe的OpenGL教程2(Bang翻译Delphi版)-如何绘制平面图形

NeHe的OpenGL教程2(Bang翻译Delphi版)-如何绘制平面图形 这一课中,我将教您如何创建三角形和四边形.如下图: program lesson2a; {    OpenGL DelphiXE    出处:根据NeHe代码翻译而来(http://nehe.gamedev.net/)    作者:帅宏军 shuaihj@163.com} uses  Windows,  Messages,  OpenGL; // 全局变量var  h_Rc: HGLRC;               

NeHe的OpenGL教程5(Bang翻译Delphi版)-如何绘制立方体

NeHe的OpenGL教程5(Bang翻译Delphi版)-如何绘制立方体 在这一课里,我们把三角形变为立体的金子塔形状,把四边形变为立方体,如下图: program lesson5a; {    OpenGL DelphiXE    出处:根据NeHe代码翻译而来(http://nehe.gamedev.net/)    作者:帅宏军 shuaihj@163.com} uses  Windows,  Messages,  OpenGL; // 全局变量var  h_Rc: HGLRC;    

NeHe的OpenGL教程4(Bang翻译Delphi版)-如何让图形旋转

NeHe的OpenGL教程4(Bang翻译Delphi版)-如何让图形旋转 在这一课里,我将教会你如何旋转三角形和四边形.左图中的三角形沿Y轴旋转,四边形沿着X轴旋转.如下图 program lesson4a; {    OpenGL DelphiXE    出处:根据NeHe代码翻译而来(http://nehe.gamedev.net/)    作者:帅宏军 shuaihj@163.com} uses  Windows,  Messages,  OpenGL; // 全局变量var  h_Rc