什么是glTF?
在3D开发领域,存储模型是一个基本需求,对于前端也不例外。就像一般网页需要使用jpg、png、webp等格式渲染图片一样,3d页面/软件/游戏的开发者,也需要把角色、场景、动画等等信息,按照某种格式存储下来,使用时解析并渲染。
通常来说,3d模型的数据由一些固定的元素构成,但存储格式却种类繁多,web常用的格式有obj、stl等等,不同平台和渲染引擎也会设计自己的私有格式,它们之间通常互不兼容,导致跨平台交换模型十分困难。为了解决这个痛点,glTF应运而生。
glTF是由khronos制定推广的一套开源存储标准,致力于成为3D领域的jpg,它的全称是GL Transmission Format,对GL图形api十分友好,比传统的obj、stl更便捷,目前已迭代到2.0版本,并得到许多建模软件和渲染引擎的支持,Maya、3dmax、unity、blender等都可以导入导出glTF模型,threejs,babylonjs等web渲染引擎都提供动态加载器。
如果你有跨平台需要交换3d模型的需求,不妨考虑使用它。
glTF的设计思想
glTF的核心设计思路是数据和结构的分离,通过json文件存储模型的层级和索引信息,通过二进制文件存储扁平的数据体。 这样做的优点是方便数据的读写,比起传统的使用二进制数据 标记位的方式,省略了很多索引和字节判断的逻辑,使代码更加简明易懂,同时只需要一次遍历即可解析全部数据,读取效率也更高。
但这样做的缺点是json文件复杂度增加,需要设置专门的索引构造,用来指明读取二进制文件的方式。
下面来看glTF的文件结构。
glTF文件结构概览
一个典型的glTF格式的json文件,由以下结构组成
每个结构都是单元数组,结构之间通过数组下标互相索引。
因为索引表达了树状结构,所以json中的属性,都是扁平的一维数组。
根据实际用途,我们把上述数据划分为“存储属性”和“几何属性”两类。存储属性是glTF专有的,用来指明二进制文件的读取方式,几何属性则用来表达模型的实际信息。
存储结构
包括buffers,bufferViews,accessors三部分
buffers数组中的某个单元,指向某个二进制文件。
bufferViews数组中的某个单元,指向某个buffer,并规定了读取文件的长度和偏移值,这些数据可以非常直观地转换成二进制阅读器的代码,将读出的数据写进WebGL或其他渲染api。
accessors指明了如何通过bufferViews来获取一组数据,并且规定了该数据的类型和范围。它是最终被几何属性引用的单位。几何结构里的坐标,索引等等,都会对应accessors里的一个下标值。
几何属性
除去以上三个结构与数据存取相关外,其余的结构都用来标识模型的几何信息,这些信息一定程度上是通用的,只是不同文件格式会设计不同的方式存储它们。
scenes、nodes
scenes是场景的根节点,包含了若干个nodes,nodes本身也是树状结构,可以包含若干个子节点,共同组成一个场景。
注意,虽然是树状结构,但子元素通过children数组间接引用,所以存储本身是一个一维的数组,避免了文件层级过深。
nodes中的一个单元可以是多种类型——如摄像机(camera),变换(matrix)和网格(mesh)
摄像机:
即是场景中camera的相关配置(如果有的话),不再赘述。
变换: 在三维模型里,骨骼就等同于变换,所以如果使用glTF存储角色模型,通常把就骨骼信息存储为node节点,存储的方式可以是矩阵,也可以是rotate、scale、transform三个三元数组,二者表达的信息是等价的,但矩阵更便于计算。
网格:
骨骼末梢的节点通常是网格(mesh)节点,它们是真正参与绘制的单元(这意味着如果输出一个没有mesh的glTF,是不会渲染出任何内容的)。
nodes中的mesh节点只是一个索引,引用了meshes数组里的一个元素,网格的几何信息都定义在meshes里。
meshes
包含了网格的基础几何信息,如顶点坐标,顶点索引,法线,切线等等,以及它对应的材质下标。
几何信息的部分这里不再赘述,但需要额外关注targets属性。它声明了该网格的形变动画信息。
形变动画,morph targets,在不同的软件中又命名为blend shape,shape keys,其本质是一种定义网格动画的方式。形变动画原理上和骨骼动画不同,并非通过骨骼来带动网格运动,而是通过将若干个网格顶点聚合为一个通道(target),并通过定义每个顶点的position和normal,“捏出”该通道形变后的状态。每个状态即是一个关键帧,在两帧极值之间,通过取0和1之间的权重数据进行插值形成。
当前的通道权重信息,就存储在weights属性里,extra中的targetName属性,指明了每个通道的名称。 插值计算本身的计算效率很高,但存储关键帧的数据量庞大,是一种用空间换时间的策略。应用领域很广泛,比如用于角色捏脸。
形变动画的存储并没有统一标准,不同文件格式会设计不同的机制。glTF选择存储在mesh中,这样设计的好处是省去了一级索引,targets中的下标即对应顶点数组,但坏处是如果一个网格中只有少量顶点被通道包含,那么会存在大量冗余的位被设置为0。
skins
骨骼蒙皮信息,定义了mesh中的每个顶点受骨骼影响的权重信息。将其和meshes,nodes的信息结合,即可表达骨骼动画的基本结构。每个图元类型的node,可以持有一个mesh和一个skin索引,skin的joints里存储了node的下标,表示该图元受到哪些骨骼的影响,以及每块骨骼的逆矩阵。
逆矩阵是一个重要的信息,用于计算骨骼动画时,把节点的变化从全局坐标变回局部坐标。如果不这样做,我们算出来的节点位置就是叠加了骨骼矩阵本身的双重变化,从而出现错误的结果。
代码语言:javascript复制jointMatrix[j] =
inverse(globalTransform) *
globalJointTransform[j] *
inverseBindMatrix[j];
结合skin和nodes的信息,使用上面的公式,我们可以算出每个骨骼的变化矩阵。
同时,我们在受到骨骼影响的meshes内部,定义了JOINTS和WEIGHTS数组,储存每个顶点受骨骼影响的权重信息。 注意,glTF格式也使用了普遍的假定——每个顶点最多受到四块骨骼影响,所以JOINTS和WEIGHTS数组的长度通常是顶点数组的4倍。
三者之间的引用关系以下图表示。
material、textures、images、samplers
上述四种结构定义了材质和纹理信息。 material材质支持设置PBR(Physically Based Rendering,基于物理属性的渲染)属性,渲染时方便转化为PBR渲染中的各项参数,默认使用Metallic-Roughness-Model,具体的参数这里不再赘述。
textures储存了纹理资源,可以引用某一个图片images,也可以直接写入文件的二进制数据。纹理可以被几何单元(mesh)引用,也可以被材质(material)引用,纹理坐标则由一个accessor获得。
纹理资源的采样器,可以使用sampler来定义,其中的参数都可以直接交给基于gl api的渲染引擎使用。
animations
用于存储动画信息,静态模型可以忽略此结构。
总结
glTF作为一种通用模型交换格式,可以容纳常见的3d模型所必须的信息,由于采用json结构表达,也非常适合前端同学入门学习。在blender等建模软件、unity等游戏引擎,threejs等动态运行时库中,均获得了比较好的支持。
参考资料
glTF 2.0 Quick Reference Guide(https://www.khronos.org/files/gltf20-reference-guide.pdf)
https://github.com/KhronosGroup/glTF