glTF 与 3D Tiles

glTF 与 3D Tiles

到目前为止,我们已经介绍了如何利用 Cesium 加载影像数据、地形高程数据、矢量数据,以及空间可视化方面的几何数据(Entity),另外在三维数据方面,Cesium 还支持模型文件 glTF,以及三维瓦片数据 3D Tiles,下面我们就先从 glTF 开始看起

什么是 glTF

glTF 全称是 Graphics Language Transmission Format(图形语言传输格式),是一种针对 GLWebGLOpenGL ES 以及 OpenGL)接口的运行时资产传递格式,由澳大利亚的 Khronons 集团进行维护,并于 2017 年在 GitHub 上公布了 glTF 2.0 的规范,这里我们也是针对于 2.0 版本进行介绍的

glTF 通过提供高效、可扩展、可互操作的格式来传输和加载三维内容,填补了 3D 建模工具与现代图形应用程序之间的空白,它已成为了 Web 上的 3D 对象标准(Web 导出的通用标准),可以说 glTF3D 模型的 JPEG 格式,几乎每个 3D Web 框架都支持 glTF,随着 glTF 的不断发展,glTF 形成了自己庞大的生态系统,同时收到了各行业的大力支持

glTF 的生态系统如下图所示

glTF 行业支持如下图所示

同时 Cesium 官方 GitHub 中也提供了 objglTF 的源码库,可以见 obj2gltf,目前 glTF 3D 模型格式有两种

  • *.gltf - 基于 JSON 的文本文件,可使用文本编辑器编辑,通常会引用外部文件,例如纹理贴图、二进制网格数据等
  • *.glb - 是二进制格式,通常文件较小且自包含所有资源,但不容易编辑

要获取 glb 文件,可以直接从 3D 建模程序中导出它们,也可以使用工具将 gltf 转换为 glb,在线转换工具推荐 MakeGLB,当然如果使用的是 VS Code 编辑器,建议安装 glTF Tools 这个扩展工具,它能够非常方便的查看 glTF 的数据结构、glTFglb 互转等

glTF 场景描述结构

glTF 的核心是一个 JSON 文件,另外还支持外部数据,具体而言,一个 glTF 模型可包括以下三部分内容

  • JSON 格式的文件(.gltf),其中包含完整的场景描述,并通过场景结点引用网格进行定义,主要包括
    • 节点层次结构、材质(定义了 3D 对象的外观)
    • 相机(定义义了渲染程序的视锥体设置 )
    • mesh(网格)
    • 动画(定义了 3D 对象的变换操作,比如选择、平移操作)
    • 蒙皮(定义了 3D 对象如何进行骨骼变换)
  • .bin 包含几何和动画数据以及其他基于缓冲区的数据的二进制文件
  • 图像文件(.jpg.png)的纹理

如下图所示

以其他格式定义的文件(例如图像文件)可以存储在通过 URI 引用的外部文件中,并排存储在 GLB 容器中,或使用数据 URI 直接嵌入到 JSON 中,一个有效的 glTF 模型必须指定其版本

glTF 的 JSON 结构

通常而言,场景对象是以数组的形式存储在 JSON 文件中,我们可以使用数组中各个对象的索引来访问它们,就像下面这样

1
2
3
4
5
'meshes': [
{ ... }
{ ... }
...
],

这些索引还用于定义对象之间的关系,上面的示例定义了多个网格对象,并且一个节点可以使用网格索引引用上面定义的其中一个网格对象

1
2
3
4
5
'nodes': [
{ 'mesh': 0, ... },
{ 'mesh': 5, ... },
...
}

下图概述了 glTFJSON 部分的顶级元素

  • scene - glTF 格式的场景结构描述条目,它通过引用 node 来定义场景图
  • node - 场景图层次中的一个节点,它可以包含一个变换(比如旋转或平移),并且可以引用其他(子)节点 ,此外它可以引用网格和相机,以及描述网格变换的蒙皮
  • camera - 定义了用于渲染场景的视锥体配置
  • mesh - 描述了出现在场景中几何对象实际的几何数据,它是指 accessor 用于访问实际几何数据 material 的对象,并且是指在渲染对象时定义其外观的
  • skin - 定义了用于蒙皮的参数,参数的值通过一个 accessor 对象获得
  • animation - 描述了一些结点如何随时间进行变换(比如旋转或平移)
  • accessor - 一个访问任意数据的抽象数据源,被 meshskinanimation 元素使用来提供几何数据、蒙皮参数和基于时间的动画值,它通过引用一个 bufferView 对象,来引用实际的二进制数据
  • material - 包含了定义 3D 对象外观的参数,它通常引用了用于 3D 对象渲染的 texture 对象
  • texture - 定义了一个 sampler 对象和一个 image 对象,sampler 对象定义了 image 对象在 3D 对象上的张贴方式

更多详情可以查阅 glTF 2.0 规范glTF 官方教程,我们来对照示例进行了解,比如下面这个就是一个最小巧的 glTF 格式文件的内容,它描述了一个简单的三角形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"scenes": [
{
"nodes": [0]
}
],
"nodes": [
{
"mesh": 0
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 1
},
"indices": 0
}
]
}
],
"buffers": [
{
"uri": "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
"byteLength": 44
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 6,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 8,
"byteLength": 36,
"target": 34962
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5123,
"count": 3,
"type": "SCALAR",
"max": [2],
"min": [0]
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 3,
"type": "VEC3",
"max": [1.0, 1.0, 0.0],
"min": [0.0, 0.0, 0.0]
}
],
"asset": {
"version": "2.0"
}
}

效果是下面这样的

Cesium 加载 glTF 模型

Cesium 提供了两种方式加载 glTF 模型,分别是通过 Entity APIPrimitive API 两个 API 实现的,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var position = Cesium.Cartesian3.fromDegrees(-120.05, 44, 0)
var heading = Cesium.Math.toRadians(45)
var pitch = 0
var roll = 0
var hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll)
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)

var model_entity = viewer.entities.add({
name: 'gltf模型',
position: position,
orientation: orientation, // 默认情况下,模型是直立的并面向东,可以通过 Quaternion 为 Entity.orientation 属性指定值来控制模型的方向,控制模型的航向,俯仰和横滚
model: {
show: true,
uri: './data/models/DracoCompressed/CesiumMilkTruck.gltf',
scale: 1.0, // 缩放比例
minimumPixelSize: 128, // 最小像素大小
maximumScale: 20000, // 模型的最大比例尺大小,minimumPixelSize 的上限
incrementallyLoadTextures: true, // 加载模型后纹理是否可以继续流入
runAnimations: true, // 是否应启动模型中指定的glTF动画
clampAnimations: true, // 指定 glTF 动画是否应在没有关键帧的持续时间内保持最后一个姿势
shadows: Cesium.ShadowMode.ENABLED, // 指定模型是否投射或接收来自光源的阴影
heightReference: Cesium.HeightReference.NONE,
},
})

// viewer.trackedEntity = entity // 相机保持在实体上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var origin = Cesium.Cartesian3.fromDegrees(-120, 44.0, 0)

// 创建一个本地的东北向上坐标系,其原点为经度 -120 度,纬度 44.0 度
// 可以随时更改模型的 modelMatrix 属性以移动或旋转模型
var modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin)
var model = viewer.scene.primitives.add(
Cesium.Model.fromGltf({
url: './data/models/DracoCompressed/CesiumMilkTruck.gltf',
modelMatrix: modelMatrix,
minimumPixelSize: 128,
maximumScale: 20000,
})
)

model.readyPromise.then(function (model) {
// Play all animations when the model is ready to render
model.activeAnimations.addAll()
})

效果如下所示

这里我们简单提及一下 modelMatrix 这个属性,该属性类型是 Matrix4,即 4x4 转换矩阵,用于将模型坐标转换为世界坐标,也就是为模型创建一个局部坐标系,正如示例中的代码那样,为模型创建了一个本地的东北向上坐标系,其原点为经度 -120 度,纬度 44.0 度,在 Cesium 调试器面板中勾选显示参考框架,能够很清晰的看到该模型对应的 xyz 轴以及原点

3D Tiles

3D Tiles 是目前 Cesium 在加载海量三维模型数据方面必须采用的一种数据格式,其实简单来说,3D Tiles 就是在 glTF 的基础上,加入了分层 LOD 的概念(可以把 3D Tiles 简单地理解为带有 LODglTF),专门为流式传输和渲染海量 3D 地理空间数据而设计的,例如倾斜摄影、3D 建筑、BIM/CAD、实例化要素集和点云

它定义了一种数据分层结构和一组切片格式,用于渲染数据内容,3D Tiles 没有为数据的可视化定义明确的规则,客户可以按照自己合适的方式来可视化 3D 空间数据,同时 3D Tiles 也是 OGC 标准规范成员之一,可用于在台式机、Web 端和移动应用程序中实现与海量异构 3D 地理空间数据的共享、可视化、融合以及交互功能,下图的动画则是加入了 LOD 的效果

3D Tiles 中,一个瓦片集(Tileset)是由一组瓦片(Tile)按照空间数据结构(树状结构)组织而成的,它至少包含一个用于描述瓦片集的 JSON 文件(包含瓦片集的元数据和瓦片对象),其中每一个瓦片对象可以引用下面的其中一种格式,用于渲染瓦片内容

格式 用途
批处理 3D 模型(Batched 3D Model(b3dm) 异构 3D 模型,例如带纹理的地形和表面,3D 建筑外部和内部,大型模型
实例化 3D 模型(Instanced 3D Model(i3dm) 3D 模型实例,例如树木,风车,螺栓
点云(Point Cloud(pnts) 大量的点
复合(Composite(cmpt) 以上不同格式的切片组合到一个切片中

瓦片的内容(瓦片格式的一个单独实例)是一个二进制 blob,具有特定于格式的组件,包括要素表(Feature Table)和批处理表(Batch Table

瓦片内容参考多种要素集特征,例如表示建筑物或树木的 3D 模型或点云中的点,每个要素的位置和外观属性都存储在瓦片要素表中,其他应用于特定程序的属性则存储在批处理表中,客户端可选择在运行时选择要素,并检索其属性以进行可视化或分析

上面表格中的 b3dmi3dm 格式是基于 glTF 构建的,它们的瓦片内容在二进制体中嵌入了 glTF 资源,包含模型的几何和纹理信息,而 pnts 格式却没有嵌入 glTF 资源

瓦片中的树状组织结合了层次细节模型(Hierarchical Level of Detail,简称 HLOD)的概念,以便最佳地渲染空间数据,在树状结构中,每个瓦片都有一个边界范围框属性,该边界范围框在空间中能够完全包围该瓦片和孩子节点的数据,下图为一种 3D Tiles 边界范围框所形成的层次体系示例

瓦片集可以使用类似于 2D 空间的栅格和矢量瓦片方案(例如 Web 地图切片服务 WMTSXYZ 方案),其在若干细节级别(或缩放级别)处提供预定义的瓦片,但是由于瓦片集的内容通常是不一致的,或者可能很难仅在二维上组织,因此树可以是具有空间一致性的任何空间数据结构,包括 k-d 树,四叉树,八叉树和网格

3D Tiles 的样式是可选的,可以将其应用于 Tileset,样式是由可计算的表达式所定义,用于修改每个要素的显示方式,这里我们暂时就介绍到这里,更多关于 3D Tiles 的信息可查看 3d-tilesOGC 规范 了解更多

下面我们主要来简单介绍一下最为核心的两个概念,即 TilesTileset,我们先从一个简单的 3D Tiles 数据示例说起,下面代码为一个 3D Tiles 的主瓦片集 JSON 文件(tileset.json)的一部分,也是调用 3D Tiles 数据的入口文件,为了尽可能少占篇幅,children 部分已省略,详细文件可见官方当中的 tileset.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"asset": {
"version": "1.0",
"tilesetVersion": "e575c6f1-a45b-420a-b172-6449fa6e0a59",
},
"properties": {
"Height": {
"minimum": 1,
"maximum": 241.6
}
},
"geometricError": 494.50961650991815,
"root": {
"boundingVolume": {
"region": [
-0.0005682966577418737,
0.8987233516605286,
0.00011646582098558159,
0.8990603398325034,
0,
241.6
]
},
"geometricError": 268.37878244706053,
"refine": "ADD",
"content": {
"uri": "0/0/0.b3dm",
"boundingVolume": {
"region": [
-0.0004001690908972599,
0.8988700116775743,
0.00010096729722787196,
0.8989625664878067,
0,
241.6
]
}
},
"children": []
}
}

上面代码中 root 下面的内容,就是一个 Tile,即一个瓦片

瓦片(Tiles)

瓦片包含用于确定是否渲染瓦片的元数据、对渲染内容的引用以及任何子瓦片的数组,切片实际上也是一个 JSON 对象,它由以下属性组成

  • boundingVolumes(边界范围框)

定义了瓦片的最小边界范围,用于确定在运行时渲染哪个瓦片,有 regionboxsphere 三种形式

  • geometricError(几何误差)

是一个非负数,以米为单位定义了不同瓦片层级的几何误差,通过几何误差来计算以像素为单位的屏幕误差(SSE),从而确定不同缩放级别下应该调用哪个层级的瓦片,简单来说 Tile 的几何误差是用来确定瓦片切换层级的,即控制 LOD

  • refine(细化方式)

确定瓦片从低级别(LOD)切换为高级别(LOD)的呈现过程,简单来说就是瓦片是如何切换的,其中包括替换(REPLACE)和添加(ADD)两种方式,替换就是直接把父级的瓦片替换掉,添加则是在父级瓦片的基础增加细节部分,下图说明了具体的切换方式

理论上来说,ADD 方式是一种非常好的方式,是一种增量的 LOD 策略,能够减少数据的传输,这里强调一下,refine 属性在根节点的 Tile 中是必须定义的,子节点中是可选的,如果子节点没有定义,则继承父节点的该属性

  • content(内容)

content 属性指定了瓦片实际渲染的内容,content.uri 属性可以是一个指定二进制块(b3dmi3dmpntscmpt)的位置,也可以是指向另一个外部的 tileset.json

content.boundingVolume 属性定义了类似 Tile 属性 boundingVolume 的边界范围框,但是 content.boundingVolume 是一个紧密贴合的边界范围框,仅包含切片的内容,该属性可以用来做视锥体裁剪,只渲染视图范围内的内容,如果该属性没定义,系统也会自动计算

下图是关于 Tile.boundingVolumescontent.boundingVolumes 的比较,其中红色是 Tile的boundingVolumes,包围了 Tileset 的整个区域,而蓝色则是 content的boundingVolumes,仅包围切片中的渲染模型

  • children(孩子节点)

这个很容易理解,因为 3D Tiles 是分级别的,所以每个 Tile 还会有子 Tile、子子 Tile 等,分的越多,层级划分的越精细,和下面将要介绍到的 Tileset 瓦片集的 root.children 是同一个概念

  • viewerRequestVolume(可选,观察者请求体)

定义了一个边界范围,使用与 boundingVolumes 相同的模式,只有当观察者处于其定义的范围内时,Tile 才显示,从而精细控制了个别瓦片的显示与否,如下图所示,只有相机拉近到某一个距离时,才显示屋内的球

  • transform(可选,位置变换矩阵)

定义了一个 4x4 的变换矩阵,通过此属性 Tile 的坐标就可以是自己的局部坐标系内的坐标,最后通过自己的 transform 矩阵变换到父节点的坐标系中,它会对 TilecontentboudingVolumeviewerRequestVolume 进行转换,详情可查看 3D Tiles规范文档

瓦片集(Tileset)

通常,一个 3D Tiles 数据会使用一个主 tileset JSON 文件作为定义 tileset 的入口点,一般是以 tileset.json 文件命名(当然该文件名称是可以修改的),从上面示例代码可以看出,tileset JSON 有四个顶级属性 assetpropertiesgeometricErrorroot

  • asset

asset 包含整个 tileSet 的元数据对象,asset.Version 属性,用于定义 3D Tiles 版本,该版本指定 tilesetJSON 模式和基本的 tileset 格式,tileVersion 属性可选,用于定义特定的应用程序的 tileset

  • properties

properties 是一个对象,包含 tileset 中每个 feature 属性的对象,上面的例子是一个建筑物的 3D Tiles,因此每个瓦片都含有三维建筑物模型,每个三维建筑物模型都有高度属性,所以上面的例子中就定义了 Height 属性,属性中每个对象的名称与每个要素属性的名称相对应(如例子中的 Height 对应高度),并且包含该属性的最大值和最小值,这些值用于创建样式的颜色渐变非常有用

  • geometricError

geometricError 是一个非负数,是通过这个几何误差的值来计算屏幕误差,确定 Tileset 是否渲染,如果在渲染的过程中,当前屏幕误差大于这里定义的屏幕误差,这个 Tileset 就不渲染,即根据屏幕误差来控制 Tileset 中的 root 是否渲染

  • root

root 是一个 JSON 对象,定义了最根级的 Tile,它存储的是真正的 Tile,也就是说 root 的数据组织方式与 Tile 的数据组织方式是一样的

不过需要注意的是,root.geometricErrortileset 的顶级 geometricError 不同,tileSetgeometricError 是根据屏幕误差来控制 tileSet 中的 root 是否渲染,而 roottile)中的 geometricError 则是用来控制 tile 中的 children 是否渲染

root.children 是一个定义子 Tile 的对象数组,每个 Tile 还会有其 children,这样就形成了一种递归定义的树状结构,每个子 Tile 的内容完全由其父 TileboundingVolume 包围,并且通常是其 geometricError 小于其父 TilegeometricError,因为越接近叶子节点,模型越精细,与原模型的几何误差就越小,对于叶子节点的 Tile,其数组的长度为零,或者是未定义 children

当然,为了创建树状结构,tilecontent.uri 也可以指向外部的 tileset(另一个 tilesetJSON 文件),这样做的一个好处就是,不同的 tileset 可以分开存储,例如我国的每个城市可单独存储成一个 tileset,然后再定义一个包含所有 tileset 的全局 tileset

Cesium 加载 3D Tiles

Cesium 虽然也支持两种方式(EntityPrimitive)加载 3D Tiles 数据,但因为多数情况下 3D Tiles 数据都是成片区的数据,数据量比较大,所以为了保证性能,建议使用 Primitive 方式

Cesium 中 3D Tiles 相关类

我们可以在 Cesium API官方文档 中可以找到如下与 3Dtile 相关的 API

这里我们主要介绍几个平常会经常使用到的,也就是图中红框所标注的部分

  • Cesium3Dtileset - 用于流式传输大量的异构 3D 地理空间数据集
  • Cesium3DTileStyle - 瓦片集样式
  • Cesium3DTile - 数据集中的一个瓦片
  • Cesium3DTileContent - 瓦片内容
  • Cesium3DTileFeature - 瓦片集要素,用于访问 Tile 中批量表中的属性数据
    • 可通过 scene.pick 方法来获取一个 BATCH,即三维要素
    • 可使用 Cesium3DTileFeature.getPropertyNames() 方法可以获取批量表中所有属性名
    • 可使用 Cesium3DTileFeature.getProperty(string Name) 方法可以获取对应属性名的属性值

加载 3D Tiles

1
2
3
4
5
6
7
8
9
10
var viewer = new Cesium.Viewer('cesiumContainer')

// 添加3D Tiles
var tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: './data/Cesium3DTiles/Tilesets/Tileset/tileset.json',
// maximumScreenSpaceError: 2, // 最大的屏幕空间误差
// maximumNumberOfLoadedTiles: 1000, // 最大加载瓦片个数
})
)

设置样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var properties = tileset.properties

if (Cesium.defined(properties) && Cesium.defined(properties.Height)) {
tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
['${Height} >= 83', "color('purple', 0.5)"],
['${Height} >= 80', "color('red')"],
['${Height} >= 70', "color('orange')"],
['${Height} >= 12', "color('yellow')"],
['${Height} >= 7', "color('lime')"],
['${Height} >= 1', "color('cyan')"],
['true', "color('blue')"],
],
},
})
}

位置调整

1
2
3
4
5
6
7
8
9
var cartographic = Cesium.Cartographic.fromCartesian(tileset.boundingSphere.center)

var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0)

var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height)

var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3())

tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation)

拾取要素

1
2
3
4
5
6
7
8
9
10
11
12
13
var handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)

handler.setInputAction(function (movement) {
var feature = viewer.scene.pick(movement.position)
if (Cesium.defined(feature) && feature instanceof Cesium.Cesium3DTileFeature) {
var propertyNames = feature.getPropertyNames()
var length = propertyNames.length
for (var i = 0; i < length; ++i) {
var propertyName = propertyNames[i]
console.log(propertyName + ': ' + feature.getProperty(propertyName))
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)

小结

至此,我们介绍了 Cesium 是如何加载各种数据的,从基础的影像数据、地形图数据,到矢量数据、空间可视化实体数据,以及三维方面的模型数据和瓦片数据,并简单介绍了各种类型数据的基本概念和特点

可以说,到目前为止,一个三维系统所需要展示的各种数据基本上已经完成了,剩下的就是如何对数据进行操作了,比如符号渲染、单击获取属性、控制显隐以及其他与实际业务的结合等功能,而这部分内容我们也会在接下来的章节中逐一来进行介绍

# GIS

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×