携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情。
本文主要是对 Cesium 官网 关于Batched 3D Model 讲解的英文原文的翻译,同时加入个人理解,如标注“【注】”的地方,添加文字和思维导图的理解,旨在更加深入理解b3dm的文件格式,方便未来实现对 b3dm 文件的解析及相关处理操作。随着理解的不断深入,还会对此文章进一步修正,同时也欢迎大家批评与探讨。
一、引入
在3D Tiles中,tileset 是一组以空间数据结构(树)组织起来的瓦片(tiles)集合。一个 tileset 由至少一个tileset JSON 文件描述,该文件包含 tileset 元数据和瓦片对象树,每个瓦片对象可以引用以下格式之一的可渲染内容:
Batched 3D Model (b3dm) | 多样化的 3d 模型。例如,纹理地形和表面,3D建筑外部和内部,大量的模型。 |
|---|---|
Instanced 3D Model (i3dm) | 三维模型实例。例如树木、风车、螺栓。 |
Point Cloud (pnts) | 大量的点。 |
Composite (cmpt) | 将不同格式的瓦片连接到一个瓦片中。 |
【注】由于 b3dm 是一种很重要的 3d 模型,可独立存在,也可嵌入到 3d tiles中,被组织起来,下面就来一起看一下 Batched 3D Model 究竟长什么样子吧~
二、概述
批量3D模型允许离线批量处理由很多种类组成的3D模型,例如城市中的不同建筑,以便高效地流式传播(streaming)到 web 客户端进行渲染和交互。
效率来自于在一次请求中传输多个模型,并在最少的 WebGL 绘制调用(draw call)中渲染它们。使用核心的3D Tiles 规范语言,每个模型都是一个 feature 。
每个模型的属性(properties),例如 IDs,可以在运行时识别和更新单个模型,例如,显示/隐藏,突出显示颜色,等等。例如,属性可以用于查询 web 服务来访问元数据,例如通过传递建筑物的 ID 来获取其地址。或者可以动态地(on the fly)引用一个属性来改变模型的外观,例如,根据属性值改变高亮颜色。
一个批处理的3D模型瓦片是一个小端序的二进制blob。
三、布局
一个 tile 由两个部分组成: 一个头部(header)紧跟着一个主体(body)。下图显示了 Batched 3D Model 的布局 (破折号表示可选字段):
一个 tile 的 byteLength 必须对齐到 8 字节的边界。所包含的 Feature Table(特性表)和 Batch Table (批处理表)必须符合各自的填充要求。
二进制的 glTF 必须在8字节边界上开始和结束,以便满足 glTF 的字节对齐保证。这可以通过填充 Feature Table 或 Batch Table 来完成,如果它们存在的话。
【注】 直观来看下,就是一个 tile 里面的可以放一个 b3dm,这个 b3dm 具有以下的结构。
graph LR
a[A tile] -->b[b3dm]
b --> c["Header(28-byte)"]
c --> d["magic(4)"]
c --> e["version(4)"]
c --> f["byteLength (4)"]
c --> g["featureTableJSONByteLength (4)"]
c --> h["featureTableBinaryByteLength (4)"]
c --> i["batchTableJSONByteLength (4)"]
c --> j["batchTableBinaryByteLength (4) "]
b --> k["body"]
k --> l["Feature Table"]
k --> m["Batch Table"]
k --> n["Binary glTF"]
3.1. Header
28字节的 header 包含以下字段:
| 字段名 | 数据类型 | 描述 |
|---|---|---|
magic | 4-byte ANSI string | "b3dm". 这可以用来将内容标识为一个批处理的 3D 模型瓦片。 |
version | uint32 (4-byte) | 批量 3D 模型格式的版本。目前是1。 |
byteLength | uint32 | 包括 header 在内的整个 tile 的长度,以字节为单位。 |
featureTableJSONByteLength | uint32 | Feature Table 中 JSON 部分的长度,以字节为单位。 |
featureTableBinaryByteLength | uint32 | Feature Table 中二进制部分的长度,以字节为单位。 |
batchTableJSONByteLength | uint32 | Batch Table 中 JSON 段的长度,以字节为单位。0 表示没有 Batch Table。 |
batchTableBinaryByteLength | uint32 | Batch Table 二进制部分的长度,以字节为单位。如果' batchTableJSONByteLength '为零,这个也将为零。 |
body 部分紧跟着 header 部分,由三个字段组成: Feature Table、Batch Table和二进制的 glTF。
【注】 其中 byteLength 的计算如下所示(以下长度均按照字节计算):
byteLength = 28 + body的长度
body的长度 = featureTableJSONByteLength + featureTableBinaryByteLength + batchTableJSONByteLength + batchTableBinaryByteLength + 二进制的 glTF 的长度
3.2. Feature Table
包含b3dm语义的值。更多信息可在 Feature Table 说明文档。 b3dm的 Feature Table 结构可以参考 Property reference 。完整的 JSON 结构可以 在 b3dm.featureTable.schema.json 中找到。
3.2.1. 语义(Semantics)
-
特征语义(Feature semantics)
目前还没有每个特性的语义。
-
全局语义(Global semantics)
这些语义为所有特性定义了全局属性。 语义 | 数据类型 | 描述 | 是否必须 | | -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | |
BATCH_LENGTH|uint32| 批处理(batch)中可区分的模型的数量,也称为特征(features)。如果二进制 glTF 没有batchId属性,该字段必须为0。 | ✅ Yes. | |RTC_CENTER|float32[3]| 当位置是相对于中心定义时,使用三分量数组定义中心位置(参见 Coordinate system)。 | 🔴 No.
3.2.2. Feature Table 说明文档
上述提到的 Feature Table 说明文档的内容,我们在这里一起来看一下。
(1)概述
Feature Table 是一个 tile 二进制主体(body)的一个组成部分, 它描述了用来渲染每一个 tile 特征的顶点和外表属性。另一方面,Batch Table 包含每个特征的专用属性,这些属性不一定用于渲染。
每个被瓦片使用的 Feature Table 形成了像 Batched 3D Model (b3dm) 这个样的格式,b3dm 这里每个模型都是一个 feature ,点云(pnts),每个点都是一个特征。
每个特征属性使用 tile 格式特定的语义定义,这些语义在每个 tile 格式规范说明书中被定义。例如,对于实例化的3D模型(Instanced 3D Model),SCALE_NON_UNIFORM 定义了应用于每个3D模型实例的非均匀缩放。
(2)布局
一个 Feature Table 由两部分组成:一个 JSON 头和一个可选的小端二进制主体。JSON 属性名称是特定于 tile 格式的语义,它们的值可以直接以 JSON 的格式定义,也可以引用二进制主体中的部分。在二进制主体中存储长数字数组更高效。Feature Table 的布局如下图所示:
当一个 tile 格式包含一个 Feature Table 时,Feature Table 会紧跟在 tile 的 header 后面。header 还将包含 featureTableJSONByteLength 和featureTableBinaryByteLength uint32 字段,它们可以用来提取 Feature Table 的每个部分。
(3)填充
JSON 的 header 必须在包含 tile 二进制的8字节边界上结束。JSON header 必须用尾随的空格字符(0x20)填充,以满足这一要求。
二进制主体必须在包含 tile 二进制的8字节边界上开始和结束。二进制主体必须用任何值的额外字节填充,以满足这一需求。
二进制属性必须以一个字节偏移量开始,其字节偏移量是属性的隐式组件类型的字节大小的倍数。例如,具有隐式组件类型 FLOAT 的属性每个元素有 4 个字节,因此,偏移量必须是 4 的倍数。前面的二进制属性必须用任何值的额外字节填充,以满足此需求。
【注】下面具体来看一下 Feature Table 组成部分的样貌
graph LR
a["Feature Table"] -->b["JSON header"]-->d["BinaryBodyReference"]
d-->g["byteOffset ✅"]
d-->h["componentType 🔴"]
b-->f["Property"]
f-->i["可以是JSON数组"]
f-->j["或者是索引到 BinaryBodyReference 索引"]
a-->c["Binary body"]-->e["其长度在b3dm的header中的
featureTableBinaryByteLength 字段中可以找到"]
(4)JSON header
Feature Table 的值可以用两种不同的方式在 JSON header 中表示:
- 单个值或对象,e.g.,
"INSTANCES_LENGTH" : 4- 这用于全局语义,如
“INSTANCES_LENGTH”,它定义了一个实例化的 3D 模型tile 中的模型实例的数量。
- 这用于全局语义,如
- 对二进制主体中数据的引用,由带有 byteOffset 属性的对象表示,e.g.,
"SCALE" : { "byteOffset" : 24}.byteOffset指定了一个相对于二进制主体开头的从零开始的偏移量。byteOffset的值必须是该属性的隐式组件类型的字节大小的倍数,例如,“POSITION”属性的组件类型为FLOAT(4字节),因此byteOffset的值必须是4的倍数。- 语义定义了允许的数据类型,例如,所引导二进制主体中的实例化 3D 模型中的
“POSITION”,组件类型为FLOAT,组件数量为3。 - 有些语义允许重写隐式组件类型。这些情况在每个 tile 格式中指定,例如,
"BATCH_ID": {"byteOffset": 24, "componentType": "UNSIGNED_BYTE"}。JSON header 中唯一有效的属性是由 tile 格式和可选的extras属性和extensions属性所定义的语义。专用的特定数据应该存储在 Batch Table 中。
(5)Binary body
当 JSON header 包含对二进制数据的引用时,将使用提供的 byteOffset 来索引数据。下图显示了对 Feature Table 二进制体的索引:
值可以使用特性的数目(featuresLength);所需的特征 id (featureId);以及特征语义的数据类型(组件类型和组件数量) 被重新取回。
3.2.3. 包含的信息
一组批处理的3D模型语义,包含 关于tile 中特性的附加信息。
- 属性
| 类型 | 描述 | 是否必须 | |
|---|---|---|---|
| extensions | object | 带有扩展特定对象的字典对象 | No |
| extras | any | 专用的数据元素 | No |
| BATCH_LENGTH | object, number [1], number | 一个为所有特性定义整数属性的GlobalPropertyInteger对象。参见语义中相应的属性 Semantics | ✅ Yes |
| RTC_CENTER | object, number [3] | 一个GlobalPropertyCartesian3 对象,为所有特性定义一个由 3个组件组成的数值属性。参见Semantics中相应的属性semantic。 | No |
【注】
需要记录瓦片中模型的个数,对应属性:BATCH_LENGTH
可选地,还可以记录当前瓦片的中心坐标,对应属性:RTC_CENTER,以便 gltf 使用相对坐标,压缩顶点坐标数字的数据量。
下面直观的看一下结构
graph LR
a["Batched 3D Model Feature Table"]
a-->c["BinaryBodyReference"]
c-->g["byteOffset ✅"]
c-->l["componentType 🔴"]
a-->d["GlobalPropertyCartesian3 "]
a-->e["GlobalPropertyInteger"]
a-->f["Property"]
f-->h["extensions🔴"]
f-->i["extras🔴"]
f-->j["BATCH_LENGTH ✅"]
f-->k["RTC_CENTE🔴"]
【注】 读到大佬的文章,以及自己的实践体验,对于每个模型(一个模型就是一个BATCH,一个feature)各自独立的数据,都会存放在batchTable中,具体每种属性的个数,就是在featureTable中提到的batchLength的值。b3dm 瓦片与渲染相关的数据都在 glb 中了,对于单体来说没有batchTable,因为只有一个模型,不存在什么独立的数据,即不存在feature属性。这是我目前的理解,可能还有不到位的地方需要再还需要在深刻地体会一下!
3.3. Batch Table
Batch Table 包含每个模型专用的属性,可以通过 batchId 进行索引,这些属性可以用于声明样式和特定于应用程序的用例,比如填充 UI 或发出 REST API 请求。在二进制 glTF 部分中,每个顶点都有一个数值的 batchId 属性,其范围为整数[0,批处理中的模型数量- 1]。batchId 表示顶点所属的模型。这允许将模型批处理在一起,并且仍然可以识别。
有关更多信息,请参阅批 Batch Table 参考资料。 下面具体来看一下,上面参考资料中提到的 Batch Table 的特性:
(1)概述
一个 Batch Table 是一个 tile 的二进制主体的一个组件,它包含了一个 tile 中每个特性特定于应用程序的属性。这些属性在运行时被查询,用于声明式样式化和任何特定于应用程序的用例,比如填充UI或发出REST API请求。Batch Table 的一些示例属性包括建筑物高度、地理坐标和数据库主键。 一个批处理表是由以下瓦片格式使用:
- Batched 3D Model (b3dm)
- Instanced 3D Model (i3dm)
- Point Cloud (pnts)
(2)布局
Batch Table 由两部分组成: 一个 JSON header 和一个可选的小端二进制主体。JSON 描述了属性,其值可以直接在JSON中定义为数组,也可以引用二进制主体中的部分。在二进制体中存储长数字数组更有效。批处理表布局如下图所示:
当一个 tile 格式包含一个 Batch Table 时,Batch Table 会紧跟在 tile 的 Feature Table 后面。header 还将包含 batchTableJSONByteLength 和 batchTableBinaryByteLength uint32 fields, 字段,它们可以用来提取 Batch Table 的每个部分。
(3)填充
JSON header 必须在包含 tile 二进制的 8 字节边界上结束。JSON header 必须用尾随的空格字符(0x20)填充,以满足这一要求。
二进制主体必须在包含 tile 二进制的 8 字节边界上开始和结束。二进制主体必须用任何值的额外字节填充,以满足这一需求。
二进制属性必须在属性 componentType 的字节大小的倍数的字节偏移量开始。例如,具有 FLOAT 组件类型的属性每个元素有 4 个字节,因此,偏移量必须是4的倍数。前面的二进制属性必须用任何值的额外字节填充,以满足此需求。
(4)JSON header
Batch Table 的值可以用两种不同的方式在 JSON 头中表示:
- 值数组,e.g.,
"name" : ['name1', 'name2', 'name3']或者"height" : [10.0, 20.0, 15.0].- 数组元素可以是任何有效的 JSON 数据类型,包括对象和数组。元素可以为空。
- 每个数组的长度等于 batchLength,它在每个tile格式中被指定。这是tile中特性的数量。例如,batchLength 可以是 b3dm tile 中的模型数量,也可以是 i3dm tile 中的实例数量,或者 pnts tile 中的点的数(或对象数量)。
- 二进制体中对数据的引用,由带有
byteOffset,componentType, 和type的属性对象表示,eg.,"height" : { "byteOffset" : 24, "componentType" : "FLOAT", "type" : "SCALAR"}.- byteOffset 指定相对于二进制 body 开头的从零开始的偏移量。byteOffset 的值必须是该属性componentType 类型的字节大小的倍数,例如,组件类型为 FLOAT 的属性的 byteOffset 的值必须是4的倍数。
- componentType 是属性中组件的数据类型。允许的值是"BYTE", "UNSIGNED_BYTE", "SHORT", "UNSIGNED_SHORT", "INT", "UNSIGNED_INT", "FLOAT",和"DOUBLE"。
- type 指定属性是标量还是向量。取值为“SCALAR”,“VEC2”,“VEC3”,“VEC4”。
Batch Table JSON 是一个 UTF-8 格式的包含 JSON 的字符串。
【注】下面具体来看一下 Feature Table 组成部分的样貌
graph LR
a["Batch Table"] -->b["JSON header"]-->d["可以是值数组形式"]
b-->f["也可以是二进制体中对数据的引用"]
a-->c["Binary body"]-->e["如果有的话"]-->g["byteOffset"]
e-->h["componentType"]
e-->i["type"]
batchId 用于访问每个数组中的元素并提取相应的属性。例如,下面的 Batch Table 有一个含有两个特征的 batch 属性:
{
"id" : ["unique id", "another unique id"],
"displayName" : ["Building name", "Another building name"],
"yearBuilt" : [1999, 2015],
"address" : [{"street" : "Main Street", "houseNumber" : "1"}, {"street" : "Main Street", "houseNumber" : "2"}]
}
batchId = 0 的特征对应的属性是:
id[0] = 'unique id';
displayName[0] = 'Building name';
yearBuilt[0] = 1999;
address[0] = {street : 'Main Street', houseNumber : '1'};
batchId = 1 的特征对应的属性是:
id[1] = 'another unique id';
displayName[1] = 'Another building name';
yearBuilt[1] = 2015;
address[1] = {street : 'Main Street', houseNumber : '2'};
(5)Binary body
当 JSON header 包含对二进制数据的引用时,将使用提供的 byteOffset 来索引数据。如下所示:
值可以使用特征的数量:batchLength;对应的 batch 的 id;以及JSON header 文件中定义的componentType和type 来获取。
下列表格可用于计算属性的字节大小。
componentType | 字节长度 |
|---|---|
"BYTE" | 1 |
"UNSIGNED_BYTE" | 1 |
"SHORT" | 2 |
"UNSIGNED_SHORT" | 2 |
"INT" | 4 |
"UNSIGNED_INT" | 4 |
"FLOAT" | 4 |
"DOUBLE" | 8 |
type | Number of components |
|---|---|
"SCALAR" | 1 |
"VEC2" | 2 |
"VEC3" | 3 |
"VEC4" | 4 |
【注】 Batch Table 就是所谓的模型属性表,一般情况下, Batch Table 中每个属性数组的个数,就等于模型的个数,因为有多少个模型就对应多少个属性。
3.4. Binary glTF
Batch 3D Model 嵌入了包含模型几何(geometry)和纹理(texture)信息的 glTF 2.0。
二进制 glTF 紧跟在Feature Table 和 Batch Table 之后。它可以嵌入所有的几何(geometry)、纹理(texture)和动画(animations),也可以引用外部数据源的部分或所有这些数据。
如上所述,每个顶点都有一个 batchId 属性,表示它所属的模型。例如,一个包含三个模型的 batch 的顶点可能看起来像这样:
顶点不需要按 batchId 排序,所以下面也是OK的:
注意,一个顶点不能属于一个以上的模型; 在这种情况下,顶点需要被复制,以便 batchId 可以被分配。
在 glTF 网格基元(primitive)中的 batchId 参数,是通过提供 _BATCHID 属性语义以及 batchId 访问器(accessor) 的索引来指定的。例如,
"primitives": [
{
"attributes": {
"_BATCHID": 0
}
}
]
{
"accessors": [
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 4860,
"max": [2],
"min": [0],
"type": "SCALAR"
}
]
}
accessor 的 type 属性值必须为 “SCALAR” 。所有其他属性必须符合 glTF 纲要(schema),但没有其他要求。
当 Batch Table 存在或 BATCH_LENGTH 属性大于 0 时,_BATCHID 属性是必需的;否则就不是。
3.4.1. Coordinate system
默认情况下,嵌入的 gltf 使用 y 轴向上的右手坐标系。为了与 3D Tiles 的 z-up 坐标系保持一致, glTFs 必须在运行时进行转换。 参见 glTF transforms 转换了解更多细节。
对于高精度渲染,顶点位置可以相对于中心定义,请参见 Precisions, Precisions。如果定义了,RTC_CENTER 将指定坐标系统转换后以及 glTF 节点应用层级结构转换后所有顶点位置相对的中心位置。
四、总结
1 Header(28)
1.1 magic(4) b3dm
1.2 version(4)1
1.3 byteLength(4)总长度
1.4 featureTableJSONByteLength(4)
1.5 featureTableBinaryByteLength(4)
1.6 batchTableJSONByteLength(4) 0 表示没有 Batch Table
1.7 batchTableBinaryByteLength batchTableJSONByteLength(4) '为零,这个也将为零
2 Feature Table
3 Batch Table
4 Binary glTF (glb)
graph LR
a[A tile] -->b[b3dm]
b --> c["Header(28-byte)"]
c --> d["magic(4)"]
c --> e["version(4)"]
c --> f["byteLength (4)"]
c --> g["featureTableJSONByteLength (4)"]
c --> h["featureTableBinaryByteLength (4)"]
c --> i["batchTableJSONByteLength (4)"]
c --> j["batchTableBinaryByteLength (4) "]
b --> k["body"]
k --> l["Feature Table"]
l--> o["featureTableJSON"]
o--> s["BATCH_LENGTH"]
l--> p["featureTableBinary"]
k --> m["Batch Table"]
m--> q["batchTableJSON"]
m--> r["batchTableBinary"]
k --> n["Binary glTF"]