glTF文件格式
撰写时间:2023-12-31
修订时间:2026-06-20
在本章中,我们初步学习glTF的几个重要属性。
glTF需以.gltf
作为文件的扩展名。我们以最简单的TriangleWithoutIndices.gltf作为例子进行逐步讲解。
Scenes
场景是渲染一个glTF文件最开始的入口。属性scene可用于声明一个在加载glTF文件时需渲染的场景。
属性scene的值为0。这个值是索引值,表示是某个数组中的第0个元素。或者说,该属性值指向了某个数组的第0个元素。
可以有0或多个场景。专门用于存储各个场景的属性名为scenes,下面,声明了一个这样的数组:
属性scenes是位于层级最顶端的根部元素,定义了一个由各个scene所组成的数组。在上面的例子中,只有一个scene。
结合scene及scenes的关系来来看,很明显,scene指向了scenes的第0个元素。
若从代码的角度,则如下所示:
Nodes
上面的scenes的第0个元素有一个nodes属性,其值为[0],用数组表示可能有多个nodes,而数组中仅有一个元素,表示这里仅一个node。
同样,该nodes的属性值也是指针,指向某个数组中的第0个元素。因此,我们需要继续在glTF文件的根部声明nodes属性:
可见,glTF的各个属性,本质上是相互引用的关系,如下图所示:

nodes表示场景中可渲染的元素,如Skin, Mesh, Camera等等。glTF将所有这些可渲染的元素都统一放置于根部的nodes属性中,然后,继续用nodes属性值中各个子元素的相应属性名值来指明该node的类型与索引值。
上面的mesh属性说明,该node下面有一个mesh,指向了一个meshes数组的第0个元素。
Meshes
因此,继续编写meshes属性,以供上面的mesh引用。
primitives属性指定了该Mesh的基本图元的情况。该属性的值用于GPU渲染命令中。在其attributes属性中,POSITION属性名表示该属性值将用于顶点着色器中存储顶点位置数据的顶点属性,如我们之前所用的aPosition。
上面的POSITION属性值引用了下面要谈到的数组属性accessors(访问器)中的索引值。
二进制数据的处理
为何需要二进制数据
glTF的主要目的是在网络上高效传输3D数据。而像顶点位置、顶点索引值这样的数据,数据量可能较大。例如:
为高效传输,需将这些数据转换为二进制数块(binary blob)。对于计算机来讲,二进制数块无须进行转换,即可由计算机直接处理,因此其速度是最快的。当然,有时候为节省空间,事先可能先压缩这些二进制数块数据,此时先进行解压缩即可。
存储二进制数据的媒介
glTF处理二进制共有2种方式:一种是通过Data URIs进行内嵌的基于base64编码的二进制数据,另一种是外部二进制文件。
Base64编码需要额外的操作进行解码,且文件大小也因此增加。
为有效解决文件大小及额外处理的问题,引入了一种称为GLB(GL Binaray)的文件格式。
| 文件类型 | 扩展名 | 媒介类型 |
|---|---|---|
| JSON glTF | .gltf | model/gltf+json |
| GLB容器中的glTF文件 | .glb | model/gltf-binary |
| 代表二进制缓冲区的文件 | .bin | application/octet-stream |
| .bin, .glbin, .glbuf | application/gltf-buffer | |
| PNG图像文件 | .png | image/png |
| JPEG图像文件 | .jpeg, .jpg | image/jpeg |
基于Base64的编码与解码
URL
URL(Uniform Resource Locator,统一资源定位系统),是因特网上引用资源的方式。例如,我们常见的网址表现形式:
就是一种URL,浏览器根据此URL就能在因特网上找到相应的资源并打开该网页。上面的URL有特定的格式,如传输协议为https
,服务器名称为www.sarkuya.com
,路径为index.php
等等。
其他常见的URL还有ftp, mailto, telnet等等。它们都自己特定的格式。
HTML网页中的URL
在HTML网页中,URL随处可见。如:
上面link的href属性、script及img的src属性,其值都属于URL,它们的特点是,都用于查找并加载外部的文件资源。
Data URL
但有时,对于一些尺寸较小的数据,也可以使用Data URL的方式,直接内嵌到网页文件中,以加快网络传输速度。
下面的代码,使用Data URL的格式,加载了一张图像。
Data URL格式为:
对比上面img的src值不难看出,该Data URL的媒介类型为image/gif
,使用base64编码,后面全部都是图像的内容了。
其效果如下:
(摘自rtc2397的例子)
图像文件原本是二进制的。我们如果使用文本编辑程序打开,则满眼都是乱码,因为文件的内容都是由二进制的0与1组成的,无法安全地转换为可打印出来的文本字符。尽管人类看不懂其内容,但计算机的图像编辑软件却可识别,它根据文件扩展名,按特定图像文件的格式进行解析后,就可将其转换为屏幕上的像素数据并最终显示出来。
Base64编码
上面的这段这么难看的字符串是怎么来的?我们可以事先使用base64编码机制,将二进制的图像文件内容转换为使用常见的ASCII字符来表示的六十四进制的字符串。这就是上面这一长串看不懂的字符串的由来。
因为它是常见的ASCII字符串,因此可安全地在网页中存储。又因为它表示的是六十四进制的数据,可方便地直接转换为二进制数据。因此,base64编码机制是二进制数据与可打印的文本之间的一个桥梁。
目前,Firefox允许转换后的字符大小为32MB,Chromium限制为512MB,Safari限制为2048MB。
32 MB = 32 * 1024 = 32,768 KB = 32,768 * 1024 = 33,554,432 bytes = 33,554,432 chars
上面转换后的字符串(不包括6个断行符)长度为364个字符。
Base64编码机制看起来比较抽象,但实际一操作,就很容易理解。
在Mac OSX系统中,打开一个终端,输入:
屏幕输出:
与文本字符串hello
对应的base64编码的字符串为aGVsbG8=
。
下面,我将一个名为三筒.png的图像文件的内容转换为base64编码的字符串,并存储进一个名为base64-string.txt的文件中:
然后,打开base64-string.txt文件,复制其全部内容,替换下面代码
中的<paste_here>
部分。下面就显示出了这张横空出世
的图像:
一个自制的Data URL图片源
经转换后的base64字符串较长,共有10964个字符,这里就不展示出来了。
Unicode应知细节
Unicode标准对每个字符都指定了一个数值(code point,码位)。因此,每个码位成为每个Unicode字符的身份标识。
Unicode标准包含了1,114,112个码位用于字符编码。 前65,536个码位用于全世界主要语言的常用字符,也称为BMP(Basic Multilingual Plane, 基本多语言平面)。BMP保留了6,400个码位用以为将来扩充使用。在BMP之外,还保留了131,068个这样的码位作为后备扩充。
Unicode标准不仅支持英语等基于字母的字符,也支持如中文等象形文字的字符。同时支持各种货币符号、标点符号、数学符号、科技符号、几何图形、印刷符号、表情符号等。
Scripts是可用于一或多种语言的书写系统中的字母及其他书写符号的组合,它们都是从历史承袭下来且有共同的书写习惯。而一种语言可能同时用到多个Scripts。如日文,就用到了汉字、平假名、片假名及拉丁字母的书写系统。对于这些通用书写系统,Unicode标准含有149,186个字符以供使用。
汉字书写系统有97,058个象形字符以供全世界各国,包括中国、日本、朝鲜、越南、新加坡等国家使用。
对于ASCII字符,其码位就是ASCII值。
对于常用汉字,绝大部分也均位于BMP范围内。
可以直接使用十六进制的码位来表示特定字符。
上面,十六进制的2270等于十进制的8816,也等于二进制的0010 0010 0111 0000。
Unicode字符有3种编码方式:UTF-32, UTF-16, 及UTF-8。JavaScript的字符串使用UTF-16编码。这是一种可变长的编码方式。大多数常用字符落在[U+0000, U+FFFF]的范围内,只需使用一个16位的双字节就可以存储。这个16位的双字节就称为编码单元(code unit)。而落在[U+10000, U+10FFFF]的范围内(也称为补充平面,supplementary planes)的字符,须使用一对被称为surrogate pairs(代理对)的16位的编码单元来表示。
代理对由高位代理(high-surrogate,或称leading surrogate)的编码单元及低位代理(low-surrogate,或称trailing surrogate)的编码单元组成。
但代理对加起来共计32位,不符合以16位来存储一个字符的要求,因此,需要有一种算法从代理对中取出相应的部分以构成一个16位的存储空间。
String的codePointAt方法,如果参数index的值正好在高位代理的编码单元时,可以返回正确的码位;而如果参数index的值在低位代理的编码单元时,仅返回低位代理编码单元的值,但这不是特定字符的码位。 因此,不能简单地使用字符串中字符的索引值来遍历字符串,而应使用for...of语句。该语句能自动遍历每个码位, 不管特定字符有无代理对。
当我们需要对任意的Unicode文本进行base64编码时,需注意此细节。
File类的UTF-8编码
现代的浏览器中均内在支持base64编码的解码。
结合到WebGL编程的需求来讲,我们需要将:
的数据先用base64编码后,存储到特定的位置,然后再用base64解码。
Blob
最初的思路是通过Blob:
Blob类并没有一个直接转换为Data URL的方法,意味着我们不能通过fetch函数来访问该资源。
FileReader
我们可以尝试使用FileReader类。
先以blob作为参数调用reader的readAsDataURL方法,在reader将blob的内容读取并解析为Data URL后,将触发reader的loadend事件,且reader的result属性将含有Data URL的数据。
而在loadend事件的处理代码中,调用fetch函数来加载dataURL,最后再将结果转换回Uint8Array。
File
可以使用File类来替换Blob。这是因为File的prototype是Blob,且其name属性即文件名。
注意File的构造函数。第一个参数类型为iterable object,即可遍历对象,如数组。因此,需先使用[]
来表示这么一个可遍历对象。然后,在可遍历对象中,再放置诸如ArrayBuffers, TypedArrays, DataViews, Blobs, 字符串,或上述这些类型的组合。这些内容均将存储为文件的内容。第二个参数为文件名,第三个参数为MIME type。因此,就出现了:
这样稍显奇怪的语句。
而如果不小心写成:
则程序也能运行,但所得到的结果却还是ASCII值。这是需要特别注意的地方。
注意到变量dataURL是一个base64编码,上面打印了其编码后的值CwwN,而response.arrayBuffer方法进行了正确的解码。
Promise
FileReader的readAsDataURL方法采用的是传统的事件触发机制, 我们可以将其改写为Promise的方式。
这样改写后,配合async与await关键字,在init函数内,代码执行顺序得以从上到下依序执行。
Accessors
回到glTF文件内容上面。上面谈到,POSITION属性引用了accessors属性值。
现在,我们准备与二进制数据打交道了。这里存在一个层级为4级的引用关系。
因这里存在多层级、多对多的引用关系,因此,本节中我们将先根据实际需求,绘制出完整的引用关系,然后再逐步给出相应的JSON结构。这样可帮我们更好地理解glTF文件内容。4级引用关系图示如下:
Buffers
首先,buffer0为一个每个元素字长为1字节的数组,以字节的方式用以存储数据类型包括整数值、浮点数值、或图像数据等的任意数据。具体到WebGL应用来讲,可用buffer0来存储诸如顶点位置数据、颜色数据、纹理坐标值,甚至是图像数据。当然,还允许存在buffer1, buffer2等其他多个缓冲区。当然最好是分类存储,例如顶点位置数据可存储于buffer0中,纹理坐标值存储于buffer1中等等。这由各个开发人员自行决定。但我们这里暂不关心其存储的是什么数据,而是关心其内部数据结构。
图中第一行,buffer0共有10个字节的数据。对应于JSON内容:
BufferViews
图中第二行,buffer0被bufferView0及bufferView1这两个视图进行了分割。它们的buffer属性值指向同一个buffer0。
其中,bufferView0所分割的视图从buffer0的第0个字节开始,共分割了8个字节,target值为34962;bufferView1所分割的视图从buffer0的第8个字节开始,共分割了2个字节,target值为34963。
下表列出了bufferView的target的值及其含义。
| 十进制数值 | 含义 |
|---|---|
| 34962 | ARRAY_BUFFER |
| 34963 | ELEMENT_ARRAY_BUFFER |
可使用WebGLSpecUtils来方便地查看各个枚举数值的名称。
说明bufferView0的数据将用于绑定ARRAY_BUFFER目标,bufferView1的数据将用于绑定ELEMENT_ARRAY_BUFFER目标。
图中的[sub buffer data]
并非bufferView0真实存在的属性,图中添加此行的目的,在于清晰地绘制第3行的箭头指向。
因此,图中第二行的内容对应于以下的JSON内容:
Accessors
图中第三行,bufferView0的数据被accessor0及accessor1再次进行分割。
其中,accessor0从bufferView0的第0个字节开始,componentType属性值为5120;accessor1从bufferView0的第0个字节开始,componentType属性值为5120。
下表列出了componentType的值域及对应的数据类型与类型化数组。
| 十进制数值 | 数据类型 | 类型化数组 |
|---|---|---|
| 5120 | BYTE | Int8Array |
| 5121 | UNSIGNED_BYTE | Uint8Array |
| 5122 | SHORT | Int16Array |
| 5123 | UNSIGNED_SHORT | Uint16Array |
| 5125 | UNSIGNED_INT | Uint32Array |
| 5126 | FLOAT | Float32Array |
下表列出了type的值域:
| 字符串值 | 含义 |
|---|---|
SCALAR | 标量 |
VEC2 | 2维向量 |
VEC3 | 3维向量 |
VEC4 | 4维向量 |
MAT2 | 2阶矩阵 |
MAT3 | 3阶矩阵 |
MAT4 | 4阶矩阵 |
注意,accessor没有byteLength属性,因为其字节长度需通过以下公式来计算:
因此,accessor0的字节长度为:
accessor1的字节长度也依此计算。
因此,图中第三行的内容对应于以下的JSON内容:
最后,POSITION指向了accessor0。
构建什么样的基本图元,是LINES还是TRIANGLES,还是其他的基本图元?我们可以用mode来指定:
其值与所代表所类型如下表所示:
| 值 | 类型 | 是否默认值 |
|---|---|---|
| 0 | POINTS | |
| 1 | LINES | |
| 2 | LINE_LOOP | |
| 3 | LINE_STRIP | |
| 4 | TRIANGLES | ✓ |
| 5 | TRIANGLE_STRIP | |
| 6 | TRIANGLE_FAN |
mesh.primitive.mode的属性是可选的,若未指定,则默认值为4
,对应基本图元的类型为TRIANGLES。
至此,我们已经获得足够的数据,足以渲染出这个glTF文件的内容了。
POSITION的accessor必须定义min与max属性。其值列出了各个组件中各个元素的最小值与最大值。
Asset
每个glTF文件必须具备asset属性,且使用version来指定版本。
完整的glTF文件内容
下面列出了TriangleWitoutIndices.gltf文件的完整内容:
glTFLoader V1.0
根据上面的理解,编写glTFLoader如下。
因为GLTFLoader类先后有两次调用异步函数fetch函数,而各方法之间又有相互依赖关系,因此除loadFile方法之外,其他方法均使用await操作符来同步运行结果。
glTF的各个关键元素的处理都分离为相应的方法,且暂未能处理的属性均有意地抛出异常。
当加载完一个glTF后,各个属性数量应不在少数,因此可构建一个树形结构以分类存储在returnObj类属性中。目前只有一个vertices属性。
要从缓冲区中正确地取出数据,glTF的buffer, bufferView, accessor等属性中与字节偏移处byteOffset、字节长度byteLength相关的属性都需特别注意,均已使用数组的slice方法予以处理。
客户端代码如下:
运行应用。
我们加载了TriangleWithoutIndices.gltf文件,并如实地渲染了出来。
参考资源
- glTF home
- glTF 2.0 Specification
- glTF Sample Viewer
- glTF Sample Assets
- glTF Sample models on Gitee
- glTF Sample Viewer Release
- The Official Khronos KTX Software Repository
- glTF-Tutorials
- The "data" URL scheme
- gltf editor
- HTML中Data的数据类型
- Unicode Specification
- Unicode® Standard Annex #24 - UNICODE SCRIPT PROPERTY
- Unicode:Surrogate Pairs UTF-16中用于扩展字符
