到目前为止,我们已经介绍了如何利用 Cesium
加载影像数据、地形高程数据、矢量数据,以及空间可视化方面的几何数据(Entity
),另外在三维数据方面,Cesium
还支持模型文件 glTF
,以及三维瓦片数据 3D Tiles
,下面我们就先从 glTF
开始看起
什么是 glTF
glTF
全称是 Graphics Language Transmission Format
(图形语言传输格式),是一种针对 GL
(WebGL
,OpenGL ES
以及 OpenGL
)接口的运行时资产传递格式,由澳大利亚的 Khronons
集团进行维护,并于 2017
年在 GitHub
上公布了 glTF 2.0 的规范,这里我们也是针对于 2.0
版本进行介绍的
glTF
通过提供高效、可扩展、可互操作的格式来传输和加载三维内容,填补了 3D
建模工具与现代图形应用程序之间的空白,它已成为了 Web
上的 3D
对象标准(Web
导出的通用标准),可以说 glTF
是 3D
模型的 JPEG
格式,几乎每个 3D Web
框架都支持 glTF
,随着 glTF
的不断发展,glTF
形成了自己庞大的生态系统,同时收到了各行业的大力支持
glTF
的生态系统如下图所示
glTF
行业支持如下图所示
同时 Cesium
官方 GitHub
中也提供了 obj
转 glTF
的源码库,可以见 obj2gltf,目前 glTF 3D
模型格式有两种
*.gltf
- 基于JSON
的文本文件,可使用文本编辑器编辑,通常会引用外部文件,例如纹理贴图、二进制网格数据等*.glb
- 是二进制格式,通常文件较小且自包含所有资源,但不容易编辑
要获取 glb
文件,可以直接从 3D
建模程序中导出它们,也可以使用工具将 gltf
转换为 glb
,在线转换工具推荐 MakeGLB,当然如果使用的是 VS Code
编辑器,建议安装 glTF Tools
这个扩展工具,它能够非常方便的查看 glTF
的数据结构、glTF
和 glb
互转等
glTF 场景描述结构
glTF
的核心是一个 JSON
文件,另外还支持外部数据,具体而言,一个 glTF
模型可包括以下三部分内容
JSON
格式的文件(.gltf
),其中包含完整的场景描述,并通过场景结点引用网格进行定义,主要包括- 节点层次结构、材质(定义了
3D
对象的外观) - 相机(定义义了渲染程序的视锥体设置 )
mesh
(网格)- 动画(定义了
3D
对象的变换操作,比如选择、平移操作) - 蒙皮(定义了
3D
对象如何进行骨骼变换)
- 节点层次结构、材质(定义了
.bin
包含几何和动画数据以及其他基于缓冲区的数据的二进制文件- 图像文件(
.jpg
,.png
)的纹理
如下图所示
以其他格式定义的文件(例如图像文件)可以存储在通过 URI
引用的外部文件中,并排存储在 GLB
容器中,或使用数据 URI
直接嵌入到 JSON
中,一个有效的 glTF
模型必须指定其版本
glTF 的 JSON 结构
通常而言,场景对象是以数组的形式存储在 JSON
文件中,我们可以使用数组中各个对象的索引来访问它们,就像下面这样
1 | 'meshes': [ |
这些索引还用于定义对象之间的关系,上面的示例定义了多个网格对象,并且一个节点可以使用网格索引引用上面定义的其中一个网格对象
1 | 'nodes': [ |
下图概述了 glTF
的 JSON
部分的顶级元素
scene
-glTF
格式的场景结构描述条目,它通过引用node
来定义场景图node
- 场景图层次中的一个节点,它可以包含一个变换(比如旋转或平移),并且可以引用其他(子)节点 ,此外它可以引用网格和相机,以及描述网格变换的蒙皮camera
- 定义了用于渲染场景的视锥体配置mesh
- 描述了出现在场景中几何对象实际的几何数据,它是指accessor
用于访问实际几何数据material
的对象,并且是指在渲染对象时定义其外观的skin
- 定义了用于蒙皮的参数,参数的值通过一个accessor
对象获得animation
- 描述了一些结点如何随时间进行变换(比如旋转或平移)accessor
- 一个访问任意数据的抽象数据源,被mesh
、skin
和animation
元素使用来提供几何数据、蒙皮参数和基于时间的动画值,它通过引用一个bufferView
对象,来引用实际的二进制数据material
- 包含了定义3D
对象外观的参数,它通常引用了用于3D
对象渲染的texture
对象texture
- 定义了一个sampler
对象和一个image
对象,sampler
对象定义了image
对象在3D
对象上的张贴方式
更多详情可以查阅 glTF 2.0 规范 和 glTF 官方教程,我们来对照示例进行了解,比如下面这个就是一个最小巧的 glTF
格式文件的内容,它描述了一个简单的三角形
1 | { |
效果是下面这样的
Cesium 加载 glTF 模型
Cesium
提供了两种方式加载 glTF
模型,分别是通过 Entity API
和 Primitive API
两个 API
实现的,核心代码如下
1 | var position = Cesium.Cartesian3.fromDegrees(-120.05, 44, 0) |
1 | var origin = Cesium.Cartesian3.fromDegrees(-120, 44.0, 0) |
效果如下所示
这里我们简单提及一下 modelMatrix
这个属性,该属性类型是 Matrix4
,即 4x4
转换矩阵,用于将模型坐标转换为世界坐标,也就是为模型创建一个局部坐标系,正如示例中的代码那样,为模型创建了一个本地的东北向上坐标系,其原点为经度 -120
度,纬度 44.0
度,在 Cesium
调试器面板中勾选显示参考框架,能够很清晰的看到该模型对应的 x
、y
、z
轴以及原点
3D Tiles
3D Tiles
是目前 Cesium
在加载海量三维模型数据方面必须采用的一种数据格式,其实简单来说,3D Tiles
就是在 glTF
的基础上,加入了分层 LOD
的概念(可以把 3D Tiles
简单地理解为带有 LOD
的 glTF
),专门为流式传输和渲染海量 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
模型或点云中的点,每个要素的位置和外观属性都存储在瓦片要素表中,其他应用于特定程序的属性则存储在批处理表中,客户端可选择在运行时选择要素,并检索其属性以进行可视化或分析
上面表格中的 b3dm
和 i3dm
格式是基于 glTF
构建的,它们的瓦片内容在二进制体中嵌入了 glTF
资源,包含模型的几何和纹理信息,而 pnts
格式却没有嵌入 glTF
资源
瓦片中的树状组织结合了层次细节模型(Hierarchical Level of Detail
,简称 HLOD
)的概念,以便最佳地渲染空间数据,在树状结构中,每个瓦片都有一个边界范围框属性,该边界范围框在空间中能够完全包围该瓦片和孩子节点的数据,下图为一种 3D Tiles
边界范围框所形成的层次体系示例
瓦片集可以使用类似于
2D
空间的栅格和矢量瓦片方案(例如Web
地图切片服务WMTS
或XYZ
方案),其在若干细节级别(或缩放级别)处提供预定义的瓦片,但是由于瓦片集的内容通常是不一致的,或者可能很难仅在二维上组织,因此树可以是具有空间一致性的任何空间数据结构,包括k-d
树,四叉树,八叉树和网格
3D Tiles
的样式是可选的,可以将其应用于 Tileset
,样式是由可计算的表达式所定义,用于修改每个要素的显示方式,这里我们暂时就介绍到这里,更多关于 3D Tiles
的信息可查看 3d-tiles 和 OGC 规范 了解更多
下面我们主要来简单介绍一下最为核心的两个概念,即 Tiles
和 Tileset
,我们先从一个简单的 3D Tiles
数据示例说起,下面代码为一个 3D Tiles
的主瓦片集 JSON
文件(tileset.json
)的一部分,也是调用 3D Tiles
数据的入口文件,为了尽可能少占篇幅,children
部分已省略,详细文件可见官方当中的 tileset.json
1 | { |
上面代码中 root
下面的内容,就是一个 Tile
,即一个瓦片
瓦片(Tiles)
瓦片包含用于确定是否渲染瓦片的元数据、对渲染内容的引用以及任何子瓦片的数组,切片实际上也是一个 JSON
对象,它由以下属性组成
boundingVolumes
(边界范围框)
定义了瓦片的最小边界范围,用于确定在运行时渲染哪个瓦片,有 region
、box
、sphere
三种形式
geometricError
(几何误差)
是一个非负数,以米为单位定义了不同瓦片层级的几何误差,通过几何误差来计算以像素为单位的屏幕误差(SSE
),从而确定不同缩放级别下应该调用哪个层级的瓦片,简单来说 Tile
的几何误差是用来确定瓦片切换层级的,即控制 LOD
的
refine
(细化方式)
确定瓦片从低级别(LOD
)切换为高级别(LOD
)的呈现过程,简单来说就是瓦片是如何切换的,其中包括替换(REPLACE
)和添加(ADD
)两种方式,替换就是直接把父级的瓦片替换掉,添加则是在父级瓦片的基础增加细节部分,下图说明了具体的切换方式
理论上来说,ADD
方式是一种非常好的方式,是一种增量的 LOD
策略,能够减少数据的传输,这里强调一下,refine
属性在根节点的 Tile
中是必须定义的,子节点中是可选的,如果子节点没有定义,则继承父节点的该属性
content
(内容)
content
属性指定了瓦片实际渲染的内容,content.uri
属性可以是一个指定二进制块(b3dm
、i3dm
、pnts
、cmpt
)的位置,也可以是指向另一个外部的 tileset.json
content.boundingVolume
属性定义了类似 Tile
属性 boundingVolume
的边界范围框,但是 content.boundingVolume
是一个紧密贴合的边界范围框,仅包含切片的内容,该属性可以用来做视锥体裁剪,只渲染视图范围内的内容,如果该属性没定义,系统也会自动计算
下图是关于 Tile.boundingVolumes
和 content.boundingVolumes
的比较,其中红色是 Tile的boundingVolumes
,包围了 Tileset
的整个区域,而蓝色则是 content的boundingVolumes
,仅包围切片中的渲染模型
children
(孩子节点)
这个很容易理解,因为 3D Tiles
是分级别的,所以每个 Tile
还会有子 Tile
、子子 Tile
等,分的越多,层级划分的越精细,和下面将要介绍到的 Tileset
瓦片集的 root.children
是同一个概念
viewerRequestVolume
(可选,观察者请求体)
定义了一个边界范围,使用与 boundingVolumes
相同的模式,只有当观察者处于其定义的范围内时,Tile
才显示,从而精细控制了个别瓦片的显示与否,如下图所示,只有相机拉近到某一个距离时,才显示屋内的球
transform
(可选,位置变换矩阵)
定义了一个 4x4
的变换矩阵,通过此属性 Tile
的坐标就可以是自己的局部坐标系内的坐标,最后通过自己的 transform
矩阵变换到父节点的坐标系中,它会对 Tile
的 content
、boudingVolume
、viewerRequestVolume
进行转换,详情可查看 3D Tiles
的 规范文档
瓦片集(Tileset)
通常,一个 3D Tiles
数据会使用一个主 tileset JSON
文件作为定义 tileset
的入口点,一般是以 tileset.json
文件命名(当然该文件名称是可以修改的),从上面示例代码可以看出,tileset JSON
有四个顶级属性 asset
、properties
、geometricError
和 root
asset
asset
包含整个 tileSet
的元数据对象,asset.Version
属性,用于定义 3D Tiles
版本,该版本指定 tileset
的 JSON
模式和基本的 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.geometricError
与 tileset
的顶级 geometricError
不同,tileSet
的 geometricError
是根据屏幕误差来控制 tileSet
中的 root
是否渲染,而 root
(tile
)中的 geometricError
则是用来控制 tile
中的 children
是否渲染
root.children
是一个定义子 Tile
的对象数组,每个 Tile
还会有其 children
,这样就形成了一种递归定义的树状结构,每个子 Tile
的内容完全由其父 Tile
的 boundingVolume
包围,并且通常是其 geometricError
小于其父 Tile
的 geometricError
,因为越接近叶子节点,模型越精细,与原模型的几何误差就越小,对于叶子节点的 Tile
,其数组的长度为零,或者是未定义 children
当然,为了创建树状结构,tile
的 content.uri
也可以指向外部的 tileset
(另一个 tileset
的 JSON
文件),这样做的一个好处就是,不同的 tileset
可以分开存储,例如我国的每个城市可单独存储成一个 tileset
,然后再定义一个包含所有 tileset
的全局 tileset
Cesium 加载 3D Tiles
Cesium
虽然也支持两种方式(Entity
和 Primitive
)加载 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 | var viewer = new Cesium.Viewer('cesiumContainer') |
设置样式
1 | var properties = tileset.properties |
位置调整
1 | var cartographic = Cesium.Cartographic.fromCartesian(tileset.boundingSphere.center) |
拾取要素
1 | var handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas) |
小结
至此,我们介绍了 Cesium
是如何加载各种数据的,从基础的影像数据、地形图数据,到矢量数据、空间可视化实体数据,以及三维方面的模型数据和瓦片数据,并简单介绍了各种类型数据的基本概念和特点
可以说,到目前为止,一个三维系统所需要展示的各种数据基本上已经完成了,剩下的就是如何对数据进行操作了,比如符号渲染、单击获取属性、控制显隐以及其他与实际业务的结合等功能,而这部分内容我们也会在接下来的章节中逐一来进行介绍