OpenGL学习笔记(七)

OpenGL中级篇(三)

Assimp模型加载库

一个很流行的模型加载库,叫做Assimp,全称为Open Asset Import Library。Assimp可以导入几十种不同格式的模型文件(同样也可以导出部分模型格式)。只要Assimp加载完了模型文件,就可以从Assimp上获取所有需要的模型数据。Assimp把不同的模型文件都转换为一个统一的数据结构,所有无论导入何种格式的模型文件,都可以用同一个方式去访问需要的模型数据。
在这里插入图片描述

  • 所有的模型、场景数据都包含在scene对象中,如所有的材质和Mesh。同样,场景的根节点引用也包含在这个scene对象中。
    在这里插入图片描述
  • 场景的根节点可能也会包含很多子节点和一个指向保存模型点云数据mMeshes[]的索引集合。根节点上的mMeshes[]里储存了真正的Mesh对象,而每个子节点上的mMesshes[]都只是指向根节点中的mMeshes[]的一个引用
  • 一个Mesh对象本身包含渲染所需的所有相关数据,比如顶点位置、法线向量、纹理坐标、面片及物体的材质
  • 一个Mesh包含多个面片。一个面片表示渲染中的一个最基本的形状单位,即图元(基本图元有点、线、三角面片、矩形面片)。一个面片记录了一个图元的顶点索引,通过这个索引,可以在mMeshes[]中寻找到对应的顶点位置数据。顶点数据和索引分开存放,可以便于使用缓存 (VBO、ABO、EBO) 来高速渲染物体。
  • 一个Mesh还会包含一个Material(材质)对象用于指定物体的一些材质属性。如颜色、纹理贴图(漫反射贴图、高光贴图等)

首先要做的是加载一个模型文件为scene对象,然后获取每个节点对应的Mesh对象(需要递归搜索每个节点的子节点来获取所有的节点),并处理每个Mesh对象对应的顶点数据、索引以及它的材质属性。最终的结果是一系列的网格数据,将它们包含在一个Model对象中。

网格(Mesh)

当构建一个模型时,例如一个人形模型,通常会把头、四肢、衣服、武器这些组件都分别构建出来,然后在把所有的组件拼合在一起,形成最终的完整模型。
一个网格(包含顶点、索引和材质属性)是在OpenGL中绘制物体的最小单位
一个模型通常有多个网格组成。
在这里插入图片描述
使用Assimp可以把多种不同格式的模型加载到程序中,但是一旦载入,它们就都被储存为Assimp自己的数据结构。
最终的目的是把这些数据转变为OpenGL可读的数据,才能用OpenGL来渲染物体。
一个网格应该至少需要一组顶点,每个顶点包含一个位置向量,一个法线向量,一个纹理坐标向量。一个网格也应该包含一个索引绘制用的索引,以纹理(diffuse / specular map)形式表现的材质数据。
定义一个Vertex的结构体,它被用来索引每个顶点属性。
在这里插入图片描述
定义一个Texture结构体储存纹理数据,包括纹理的id和它的类型,比如diffuse纹理或者specular纹理。
在这里插入图片描述
清楚顶点和纹理的实际表达后,可以开始定义网格类的结构
在这里插入图片描述
构造方法里初始化网格所有必须数据。
setupMesh() 里初始化缓冲。最后通过Draw()绘制网格。
在这里插入图片描述

初始化

由于有了构造器,现在有一大列的网格数据用于渲染。
setupMesh函数
在这里插入图片描述
C++的结构体有一个重要的属性,那就是在内存中它们是连续的。如果用结构体表示一列数据,这个结构体只包含结构体的连续的变量,它就会直接转变为一个float数组,就能用于一个数组缓冲(array buffer)中了。比如,如果声明一个Vertex结构体对象,它在内存中的排布等于:
在这里插入图片描述
在这里插入图片描述
结构体的另外一个很好的用途是它的预处理指令offsetof(s, m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。
这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。这正好可以用在定义glVertexAttribPointer函数中的偏移参数
在这里插入图片描述

渲染

需要为Mesh类定义最后一个Draw函数。在真正渲染前需要绑定合适的纹理,然后调用glDrawElements。
为解决这个问题,需要假设一个特定的名称惯例:

  • 每个漫反射纹理(diffuse)被命名为texture_diffuseN,
  • 每个镜面光纹理(specular)被命名为texture_specularN。
    其中N的范围是1到纹理采样器最大允许的数字。比如说某一个网格有3个漫反射纹理,2个镜面光纹理,它们的纹理采样器定义如下:
    在这里插入图片描述
    在这里插入图片描述
    Mesh类是对前面学习内容的简洁抽象,接下来要创建一个模型,作为多个网格对象的容器,真正的实现Assimp的加载接口。

模型

现在需要着手启用Assimp,并开始创建实际的加载和转换代码。
创建Model类,可以表达模型(Model)的全部。更确切的说,一个模型包含多个网格(Mesh),一个网格可能带有多个对象。
Model类的结构定义如下:
在这里插入图片描述
Model类包含一个Mesh对象的向量,我们需要在构造函数中给出文件的位置。通过loadModel() 加载文件。
私有成员部分被设计为处理一部分的Assimp导入的常规操作。同样,我们储存文件路径的目录,这样稍后加载纹理的时候会用到。
Draw() 没有什么特别之处,基本上是循环每个网格,调用各自的Draw()。
在这里插入图片描述
导入3D模型到OpenGL
在这里插入图片描述
声明了Assimp命名空间内的一个Importer,然后调用ReadFile()。第一个参数是文件路径,第二个参数是后处理(post - processing)选项。
除了可以加载文件外,可以定义几个选项来强制Assimp对导入数据做额外的计算或操作。

  • 设置aiProcess_Triangulate,如果模型不是(全部)由三角形组成,应该转换所有的模型的原始几何形状为三角形。
  • aiProcess_FlipUVs基于y轴翻转纹理坐标,在处理时是必须的。
    ReadFile() 的其他常用后处理选项
  • aiProcess_GenNormals : 如果模型没有包含法线向量,就为每个顶点创建法线。
  • aiProcess_SplitLargeMeshes : 将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
  • aiProcess_OptimizeMeshes : 和上个选项相反,它把几个网格结合为一个更大的网格。以减少绘制函数调用的次数的方式来优化。
    更多后期处理命令

通过Assimp加载一个模型很简单。困难的是使用返回的场景对象把加载的数据变换到一个Mesh对象的数组。
在这里插入图片描述
在加载了模型之后,会检查场景和其根节点不为null,并且检查了它的一个标记 (Flag),来查看返回的数据是不是不完整的。如果遇到了任何错误,都会通过ImporterGetErrorString函数来报告错误并返回,同时也获取了文件路径的目录路径。
在这里插入图片描述
在这里插入图片描述
使用不同的参数递归调用这个函数自身,直到某个条件被满足停止递归。
在processNode函数中退出条件(Exit Condition)是所有的节点都被处理完毕。
Assimp的结构,每个节点包含一个网格集合的索引,每个索引指向一个在场景对象中特定的网格位置。希望获取这些网格索引,获取每个网格,处理每个网格,然后对其他的节点的子节点做同样的处理。
在这里插入图片描述
首先利用场景的mMeshes数组来检查每个节点的网格索引以获取相应的网格。被返回的网格被传递给processMesh函数,它返回一个网格对象,可以把它储存在meshes的list或vector中。
一旦所有的网格都被处理,遍历所有子节点,同样调用processNode函数。一旦一个节点不再拥有任何子节点,函数就会停止执行。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
下一步是使用创建的Mesh类开始真正处理Assimp的数据。
把一个aiMesh对象转换为一个定义的网格对象并不难。要做的只是访问网格的相关属性并将它们储存到对象中。

从Assimp到网格

处理网格的过程由三部分组成:

  • 获取所有顶点数据
  • 获取顶点的网格索引
  • 获取相关材质数据

处理过的数据将被储存在3个Vector中,利用它们构建一个Mesh对象,并返回它到函数的调用者那里。
在这里插入图片描述
获取顶点数据:定义一个Vertex结构体,在每次遍历后把这个结构体添加到Vertices数组。会遍历网格中的所有顶点(使用mesh->mNumVertices来获取)。在每个迭代中,希望使用所有的相关数据填充这个结构体。顶点的位置是这样处理的:
在这里插入图片描述
注意:为了传输Assimp的数据,定义了一个vec3的临时变量。使用这样一个临时变量的原因是Assimp对向量、矩阵、字符串等都有自己的一套数据类型,它们并不能完美地转换到GLM的数据类型中。
法线坐标的处理步骤大致相同:
在这里插入图片描述
纹理坐标也基本一样,但是Assimp允许一个模型的每个顶点有8个不同的纹理坐标,可能用不到,所以只关心第一组纹理坐标,也希望检查网格是否真的包含纹理坐标:
在这里插入图片描述

索引

Assimp的接口定义每个网格有一个以面(Face)为单位的数组,每个面代表一个单独的图元,在例子中(由于aiProcess_Triangulate选项)总是三角形,一个面包含索引,这些索引定义需要绘制的顶点以怎样的顺序提供给每个图元,所以如果遍历所有面,把所有面的索引储存到indices向量,需要这么做:
在这里插入图片描述

材质

如同节点,一个网格只有一个指向材质对象的索引,获取网格实际的材质,需要索引场景的mMaterials数组。网格的材质索引被设置在mMaterialIndex属性中,通过这个属性同样能够检验一个网格是否包含一个材质:
在这里插入图片描述

函数解析

先从场景的mMaterials数组获取aimaterial对象,然后加载网格的diffuse或 / 和specular纹理。
一个材质储存了一个数组,这个数组为每个纹理类型提供纹理位置。不同的纹理类型都以aiTextureType_ 为前缀。我们使用一个帮助函数:loadMaterialTextures来从材质获取纹理。该函数返回一个Texture结构体的vector,储存在模型的textures 坐标的后面。
loadMaterialTextures() 遍历所有给定纹理类型的纹理位置,获取纹理的文件位置,然后加载生成纹理,把信息储存到Vertex结构体:
在这里插入图片描述
先通过GetTextureCount() 检验材质中储存的纹理,以期得到我们希望的纹理类型。然后通过GetTexture() 获取每个纹理的文件位置,这个位置以aiString类型储存。然后使用帮助函数:TextureFromFile() 加载一个纹理,返回纹理的ID。
在这里插入图片描述
注意:假设纹理文件与模型在相同的目录里。可以简单的链接纹理位置字符串和之前获取的目录字符串(在loadModel()中得到的)来获得完整的纹理路径(这就是为什么GetTexture()同样需要目录字符串)。
若模型使用绝对路径,它们的纹理位置就不会在每台机器上都有效了。

优化

加载纹理需要不少操作,当前的实现中一个新的纹理被加载和生成,来为每个网格使用,即使同样的纹理之前已经被加载了好几次,这会很快转变为你的模型加载实现的瓶颈。
会对模型的代码进行调整,将所有加载过的纹理全局储存,每当想加载一个纹理的时候,首先去检查它有没有被加载过。如果有的话,会直接使用那个纹理,并跳过整个加载流程,省下很多处理能力。为了能够比较纹理,还需要储存它们的路径:在这里插入图片描述
然后把所有加载过的纹理储存到另一个向量中,它是作为一个私有变量声明在模型类的顶部:
在这里插入图片描述
之后,在loadMaterialTextures函数中,希望将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。如果是的话,则跳过纹理加载 / 生成的部分,直接使用定位到的纹理结构体为网格的纹理。更新后的函数如下:
在这里插入图片描述

和箱子模型告别

现在不仅有了一个通用模型加载系统,同时也得到了一个能使加载对象更快的优化版本。
声明一个Model对象,把模型的文件位置传递给它。模型自动加载在游戏循环中使用Draw()绘制这个对象。没有更多的缓冲配置,属性指针和渲染命令,仅仅简单的一行。如果创建几个简单的着色器,像素着色器只输出对象的diffuse纹理颜色,结果看上去会有点像这样:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值