OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)

写在前面

前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解。但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味。本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩。本节的示例代码均可以在我的github下载

加载模型可以使用比较好的库,例如obj模型加载的库Assimp加载库。本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个简单的obj模型加载类,加载一个简单的立方体模型。

不要太急于看到漂亮的3D模型,下一节我们会使用Assimp库会加载一个酷炫的3d模型,但是本节还是注重多感受下模型加载的基础,否则下一节学习起来会吃力。

通过本节可以了解到

  • Mesh的概念
  • Obj模型数据格式
  • Obj模型简单的加载类和加载实验

模型的表达

在3d图形处理中,一个模型(model)通常由一个或者多个Mesh(网格)组成,一个Mesh是可绘制的独立实体。例如复杂的人物模型,可以分别划分为头部,四肢,服饰,武器等各个部分来建模,这些Mesh组合在一起最终形成人物模型。

Mesh由顶点、边、面Faces组成的,它包含绘制所需的数据,例如顶点位置、纹理坐标、法向量,材质属性等内容,它是OpenGL用来绘制的最小实体。Mesh的概念示意如下图所示(来自:What is a mesh in OpenGL?):

Mesh可以包含多个Face,一个Face是Mesh中一个可绘制的基本图元,例如三角形,多边形,点。要想模型更加逼真,一般需要增加更多图元使Mesh更加精细,当然这也会受到硬件处理能力的限制,例如PC游戏的处理能力要强于移动设备。由于多边形都可以划分为三角形,而三角形是图形处理器中都支持的基本图元,因此使用得较多的就是三角形网格来建模。例如下面的图(来自:What is a mesh in OpenGL?)表达了使用越来越复杂的Mesh建模一只兔子的过程:

随着增加三角形个数,兔子模型变得越来越真实。

目前模型存储的格式很丰富,比较常用的,例如Wavefront .obj fileCOLLADA等,要了解各个格式的特点,可以参考wiki 3D graphics file formats。在众多的格式中以obj格式比较通用,它内部是以文本形式表达的,接下来我们通过熟悉下obj格式,了解模型是如何定义的,以及如何加载到OpenGL中来渲染模型。

Obj模型数据格式

obj模型内部以文本存储,例如从Model loading处获取的一个立方体模型cube.obj的数据如下:

# Blender3D v249 OBJ File: untitled.blend
# www.blender3d.org
mtllib cube.mtl
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
vt 0.999455 0.750380
vt 0.250471 0.500702
vt 0.249682 0.749677
vt 0.001085 0.750380
vt 0.001517 0.499994
vt 0.499422 0.500239
vt 0.500149 0.750166
vt 0.748355 0.998230
vt 0.500193 0.998728
vt 0.498993 0.250415
vt 0.748953 0.250920
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn -0.000001 0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn 0.000000 1.000000 -0.000000
vn -0.000000 -1.000000 0.000000
usemtl Material_ray.png
s off
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
f 3/5/2 8/7/2 4/8/2
f 2/9/3 6/10/3 3/5/3
f 6/10/4 7/6/4 3/5/4
f 1/2/5 5/1/5 2/9/5
f 5/1/6 6/10/6 2/9/6
f 5/1/7 8/11/7 6/10/7
f 8/11/7 7/12/7 6/10/7
f 1/2/8 2/9/8 3/13/8
f 1/2/8 3/13/8 4/14/8

对这个文本格式做一个简要说明:

  • 以#开始的行为注释行
  • usemtl和mtllib表示的纹理相关数据,解析纹理数据稍微繁琐,本节我们只是为了说明加载模型的原理,不做讨论。
  • o 引入一个新的object
  • v 表示顶点位置
  • vt 表示顶点纹理坐标
  • vn 表示顶点法向量
  • f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0

模型一般通过3d建模软件,例如Blender, 3DS Max 或者 Maya等工具建模,导出时的数据格式变化较大,我们导入模型到OpenGL的任务就是:将一种模型数据文件表示的模型,转换为OpenGL可以利用的数据。例如上面的Obj文件中,我们需要解析顶点位置,纹理坐标等数据,构成OpenGL可以渲染的Mesh对象。

从Obj到OpenGL可以理解的Mesh

上面说明了Obj的数据格式,那么在OpenGL中我们怎么表达Mesh呢?首先定义顶点属性数据如下所示:

 // 表示一个顶点属性
struct Vertex
{
    glm::vec3 position;  // 顶点位置
    glm::vec2 texCoords; // 纹理坐标
    glm::vec3 normal;  // 法向量
};

Mesh中包含顶点属性,纹理对象等信息,本节我们定义Mesh数据结构如下所示:


// 表示一个OpenGL渲染的最小实体
class Mesh
{
public:
    void draw(Shader& shader) // 绘制Mesh
    Mesh(const std::vector<Vertex>& vertData,
        GLint textureId) // 构造一个Mesh
private:
    std::vector<Vertex> vertData;// 顶点数据
    GLuint VAOId, VBOId; // 缓存对象
    GLint textureId; // 纹理对象id
    void setupMesh();  // 建立VAO,VBO等缓冲区
};

载入obj模型的过程,就是读取obj文件,并转换为上面Mesh对象的过程。这个过程的思路大致是这样的,读取文件的每一行,根据行首部的指示,确定数据类型,然后加载到mesh的vertData里面去,这个框架是这样:

std::ifstream file(objFilePath);
while (getline(file, line))
{
    if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据
    {
        // 解析顶点纹理数据
    }
    else if (line.substr(0, 2) == "vn") // 顶点法向量数据
    {
        // 解析法向量数据
    }
    else if (line.substr(0, 1) == "v") // 顶点位置数据
    {
        // 解析顶点位置数据
    }
    else if (line.substr(0, 1) == "f") // 面数据
    {
        // 解析面数据
    }
    else if (line[0] == ‘#‘) // 注释忽略
    { }
    else
    {
        // 其余内容 暂时不处理
    }
}

上面提供了一个读取obj文件格式的框架,例如解析纹理数据如下:

if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据
{
    std::istringstream s(line.substr(2));
    glm::vec2 v;
    s >> v.x;
    s >> v.y;
    v.y = -v.y;  // 注意这里加载的dds纹理 要对y进行反转
    temp_textCoords.push_back(v);
}

其余的也类似处理。读取到数据后,在Mesh对象里面需要向前面绘制物体时一样建立缓冲数据,如下:

void setupMesh()  // 建立VAO,VBO等缓冲区
{
    glGenVertexArrays(1, &this->VAOId);
    glGenBuffers(1, &this->VBOId);

    glBindVertexArray(this->VAOId);
    glBindBuffer(GL_ARRAY_BUFFER, this->VBOId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)* this->vertData.size(),
        &this->vertData[0], GL_STATIC_DRAW);
    // 顶点位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // 顶点纹理坐标
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)(3 * sizeof(GL_FLOAT)));
    glEnableVertexAttribArray(1);
    // 顶点法向量属性
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,
        sizeof(Vertex), (GLvoid*)(5 * sizeof(GL_FLOAT)));
    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

建立缓冲区的同时,本节我们使用的立方体模型cube.dds纹理如下图所示:

这与以前使用的png纹理不一样,这里我用C++重新改编了Model loading处的加载dds纹理的函数,加载纹理不是本节的重点,具体可以查看github代码。加载纹理后,可以渲染这个obj表达的立方体模型,整个过程如下:

//Section1 从obj文件加载数据
std::vector<Vertex> vertData;
ObjLoader::loadFromFile("cube.obj", vertData)

// Section2 准备纹理
GLint textureId = TextureHelper::loadDDS("cube.dds");

// Section3 建立Mesh对象
Mesh mesh(vertData, textureId);

// Section4 准备着色器程序
Shader shader("cube.vertex", "cube.frag");

// 在游戏主循环中渲染立方体

这里我们可以看到,与以往在程序中通过数值指定立方体模型相比,我们的代码更简洁,后面介绍使用Assimp加载库后,可以加载更多丰富的模型,当然要比这个立方体好看。但是本节还是看一下最终立方体的效果吧,如下:

最后的说明

在使用dds纹理的时候,要注意纹理的y轴相对于OpenGL是进行反转的,因此需要使用( coord.u, 1.0-coord.v) 来访问,这可以在加载obj时做,也可以在着色器里面做。没有使用反转的v坐标将导致,无法正常渲染,这也是困住我的一个地方。后来使用数据比对格式发现了这个错误,如下图,左边是反转了的数据,右边是未反转的数据:

在使用blender软件导出模型时,即使勾选了includ UVs,输出时仍然没有纹理坐标,这是因为除了勾选这些选项外,还需要一个uv map操作,关于这一点也是容易产生错误的,详细可以参考Add UV Mapped texture coordinates to OBJ file?。uv mappring这个操作的过程比较繁琐,就不再这里介绍了,感兴趣地可以参考UV Mapping a Mesh

最后本节的加载obj程序只是一个示例,并没有解析纹理mtl部分,不使用纹理数据绘制经典的Suzanne 模型如下图所示:

这里缺少了纹理和光照,所以模型看起来不真实,下一节介绍使用Assimp加载库时将会改善这一点。

参考资料

  1. https://www.quora.com/What-is-a-mesh-in-OpenGL
  2. http://gamedev.stackexchange.com/questions/38412/whats-the-difference-between-mesh-and-a-model
  3. http://www.opengl-tutorial.org/beginners-tutorials/tutorial-7-model-loading/
  4. https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Load_OBJ
  5. http://www.opengl-tutorial.org/beginners-tutorials/tutorial-5-a-textured-cube/
时间: 2024-08-09 12:08:51

OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)的相关文章

OpenGL学习脚印: 坐标变换过程(vertex transformation)

写在前面 前面几节分别介绍了模型变换,视变换,以及给出了投影矩阵和视口变换矩阵的推导,本节从全局把握一遍OpenGL坐标转换的过程,从整体上认识坐标变换过程.相关矩阵的数学推导过程请参考前面几节对应的内容. 通过本节可以了解到 坐标变换的各个阶段 利用GLM数学库实现坐标变换 坐标变换的全局图 OpenGL中的坐标处理过程包括模型变换.视变换.投影变换.视口变换等过程,如下图所示: 在上面的图中,注意,OpenGL只定义了裁剪坐标系.规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系).世界

OpenGL学习脚印:光源类型和使用多个光源(Light source and multiple lights)

写在前面 上一节光照中使用材质和lighting maps介绍了使用材质属性和lighting maps使物体的光照效果能反映物体的材料特性,看起来更逼真.在前面的章节中使用的实际上都是一个点光源,本节将学习其他几种光源类型,以及在场景中使用多个光源.本节代码均可以在我的github下载. 本节内容整理自: 1.www.learnopengl.com light casters 2.www.learnopengl.com Multiple lights 通过本节可以了解到 方向光源 点光源 聚光

OpenGL学习脚印: 帧缓冲对象(Frame Buffer Object)

写在前面 一直以来,我们在使用OpenGL渲染时,最终的目的地是默认的帧缓冲区,实际上OpenGL也允许我们创建自定义的帧缓冲区.使用自定义的帧缓冲区,可以实现镜面,离屏渲染,以及很酷的后处理效果.本节将学习帧缓存的使用,文中示例代码均可以在我的github下载. 本节内容整理自 1.OpenGL Frame Buffer Object (FBO) 2.www.learnopengl.com Framebuffers FBO概念 在OpenGL中,渲染管线中的顶点.纹理等经过一系列处理后,最终显

OpenGL学习脚印:立方体纹理和天空包围盒(Cubemaps And Skybox)

写在前面 之前学习了2D纹理映射,实际上还有其他类型的纹理有待我们进一步学习,本节将要学习的立方体纹理(cubemaps),是一种将多个纹理图片复合到一个立方体表面的技术.在游戏中应用得较多的天空包围盒可以使用cubemap实现.本节示例程序均可以在我的github下载. 本节内容整理自: 1.Tutorial 25:SkyBox 2.www.learnopengl.com Cubemaps 创建Cubemap cubemap是使用6张2D纹理绑定到GL_TEXTURE_CUBE_MAP目标而创

OpenGL学习脚印: 绘制一个三角形

写在前面 接着上一节内容,开发环境搭建好后,我们当然想立即编写3D应用程序了.不过我们还需要些耐心,因为OpenGL是一套底层的API,因而我们要掌握的基本知识稍微多一点,在开始绘制3D图形之前,本节我们将通过绘制一个三角形的程序来熟悉现代OpenGL的概念和流程. 通过本节可以了解到: 缓存对象VAO和VBO GLSL着色器程序的编译.链接和使用方法 OpenGL绘图的基本流程 绘图流水线简要了解 与使用高级绘图API(例如java里swing绘图,MFC里的绘图)不同,使用OpenGL绘制图

OpenGL学习脚印: 视变换(view transformation)

写在前面 OpenGL中的坐标处理过程包括模型变换.视变换.投影变换.视口变换等内容,这个主题的内容有些多,因此分节学习,主题将分为5节内容来学习.上一节模型变换,本节学习模型变换的下一阶段--视变换.到目前位置,主要在2D下编写程序,学习了视变换后,我们可以看到3D应用的效果了.本节示例程序均可在我的github下载. 通过本节可以了解到 视变换的概念 索引绘制立方体 LookAt矩阵的推导(对数学不感兴趣,可以跳过) 相机位置随时间改变的应用程序 坐标处理的全局过程(了解,另文详述) Ope

OpenGL学习脚印: 模型变换(model transformation)

写在前面 前面为本节内容准备了向量和矩阵.线性变换等内容,本节开始学习OpenGL中的坐标处理.OpenGL中的坐标处理过程包括模型变换.视变换.投影变换.视口变换等内容,这个主题的内容有些多,因此分节学习,主题将分为5节内容来学习.本节主要学习模型变换.本节示例代码均可在我的github处下载. 通过本节可以了解到 模型变换的作用 模型变换的类型和计算方法 坐标处理的全局过程(了解,另文详述) OpenGL中的坐标处理包括模型变换.视变换.投影变换.视口变换等内容,具体过程如下图1所示: 每一

OpenGL学习脚印: 向量和矩阵要点(math-vector and matrices)

写在前面 前面几节内容环境搭建,绘制三角形,以及使用索引绘制,让我们对现代OpenGL中绘图做了简单了解.要继续后面的部分,需要熟悉OpenGL中涉及的数学知识.因此本节开始介绍OpenGL中的基本数学. 介绍这部分内容的主旨在于对OpenGL涉及的数学有个整体把握,重点把握一些概念在OpenGL中的应用.内容尽量以例子形式说明,仅在必要时会给出数学证明.一个主题往往涉及过多内容,对于文中省略的部分,请参考相应的教材. 通过本节可以了解到 向量基本概念和操作 矩阵的基本概念和操作 GLM数学库

解析OBJ模型并将其加载到Unity3D场景中

??各位朋友,大家好,欢迎大家关注我的博客,我是秦元培,我的博客地址是http://qinyuanpei.com.今天想和大家交流的是解析obj模型并将其加载到Unity3D场景中,虽然我们知道Unity3D是可以直接导入OBJ模型的,可是有时候我们并不能保证我们目标客户知道如何使用Unity3D的这套制作流程,可能对方最终提供给我们的就是一个模型文件而已,所以这个在这里做这个尝试想想还是蛮有趣的呢,既然如此,我们就选择在所有3D模型格式中最为简单的OBJ模型来一起探讨这个问题吧! 关于OBJ模