什么是 3D Tiles
3D Tiles 是 Cesium 提出的、面向大规模三维地理空间数据的流式加载规范,适合倾斜摄影、城市白模、BIM/CAD、点云、实例化模型等场景。
可以把它理解成"三维世界里的地图瓦片":
text
二维地图:把图片切成 z / x / y.png
3D Tiles:把大规模三维模型切成一棵 tile 树
普通 glTF 模型通常是一次性加载;3D Tiles 会根据相机位置、视锥范围和屏幕误差,按需加载当前视角需要的瓦片。
text
普通模型 = 一个文件,一次性加载
3D Tiles = 一棵瓦片树,按需流式加载
适合什么数据
| 数据类型 | 典型场景 | 说明 |
|---|---|---|
| 倾斜摄影 | 城市实景、园区实景 | 通常由摄影测量软件生成 |
| 城市白模 | 建筑群、城市规划 | 建筑几何 + 属性信息 |
| BIM / CAD | 建筑、工厂、桥梁 | 模型精细,可能需要分层和属性查询 |
| 点云 | 激光雷达、测绘点云 | 数据量大,常用于地形、道路、矿山 |
| 实例化模型 | 树木、路灯、设备 | 同一个模型重复出现很多次 |
文件结构
一个 3D Tiles 数据集的入口通常是 tileset.json。
text
3d-tiles/
├─ tileset.json # 入口文件,描述 tile 树
├─ 0/
│ ├─ 0.b3dm # 批量三维模型
│ ├─ 1.b3dm
│ └─ 2.b3dm
├─ points.pnts # 点云
├─ trees.i3dm # 实例化模型
└─ composite.cmpt # 复合瓦片
常见瓦片内容格式
| 格式 | 全称 | 用途 |
|---|---|---|
b3dm |
Batched 3D Model | 最常见,常用于倾斜摄影、建筑模型 |
i3dm |
Instanced 3D Model | 同一模型的多实例渲染,例如树、路灯 |
pnts |
Point Cloud | 点云数据 |
cmpt |
Composite | 把多个瓦片内容组合在一起 |
glb / glTF |
glTF Model | 3D Tiles 1.1 中更推荐直接使用 glTF |
早期 3D Tiles 常见的是
b3dm / i3dm / pnts。在 3D Tiles 1.1 之后,很多能力开始向 glTF 和扩展机制靠拢。
tileset.json
tileset.json 可以理解为三维瓦片目录树。它描述了有哪些 tile、每个 tile 的空间范围、LOD 关系、几何误差、子节点和内容地址。
一个简化示例如下:
json
{
"asset": {
"version": "1.1"
},
"geometricError": 500,
"root": {
"boundingVolume": {
"region": [
1.9,
0.6,
2.0,
0.7,
0,
300
]
},
"geometricError": 100,
"refine": "REPLACE",
"content": {
"uri": "root.glb"
},
"children": [
{
"boundingVolume": {
"box": [
0,
0,
0,
100,
0,
0,
0,
100,
0,
0,
0,
50
]
},
"geometricError": 20,
"content": {
"uri": "building.b3dm"
}
}
]
}
}
root / children
root 是整棵瓦片树的根节点,children 是它的子瓦片。
text
root 低精度、大范围
├─ child 中等精度、中等范围
│ └─ child 高精度、小范围
└─ child 中等精度、中等范围
通常越靠近根节点,模型越粗、范围越大;越往下,模型越精细、范围越小。
boundingVolume
boundingVolume 定义 tile 覆盖的空间范围,用于视锥裁剪、碰撞判断和 LOD 选择。
常见有三种:
| 类型 | 含义 | 适合 |
|---|---|---|
region |
经纬度矩形 + 最小/最大高度 | 地理范围明确的数据 |
box |
有向包围盒 | 建筑、倾斜摄影、局部模型 |
sphere |
包围球 | 简单粗略范围 |
如果 boundingVolume 设置不准,常见问题是模型提前消失、迟迟不加载,或者相机飞过去看不到数据。
geometricError
geometricError 表示当前 tile 的几何误差,单位通常可以理解为米。
- 值越大:表示这个 tile 越粗糙,可以在远处使用。
- 值越小:表示这个 tile 越精细,通常出现在子节点。
- 根节点的
geometricError一般最大,叶子节点逐渐接近 0。
Cesium 会把 geometricError 转成屏幕空间误差(SSE,Screen Space Error),再决定是否继续加载子节点。
text
距离越近 -> SSE 越大 -> 需要更精细的子 tile
距离越远 -> SSE 越小 -> 粗糙 tile 就够了
refine
refine 定义父子瓦片的细化方式。
| 值 | 含义 | 典型场景 |
|---|---|---|
REPLACE |
子瓦片加载后替换父瓦片 | 倾斜摄影、层级模型 |
ADD |
子瓦片叠加在父瓦片之上 | 城市白模逐级加细节、附加数据 |
REPLACE 更常见,意思是"远处看粗模,近处换精模"。ADD 是"父瓦片保留,子瓦片继续叠加细节"。
content.uri
content.uri 指向真正的瓦片内容文件,比如 b3dm、pnts、i3dm、glb,也可以指向另一个外部 tileset.json。
json
{
"content": {
"uri": "building.b3dm"
}
}
如果 uri 指向外部 tileset,可以把一个大场景拆成多个子 tileset,方便分块管理和懒加载。
transform
transform 是一个 4x4 变换矩阵,用来把 tile 的局部坐标变换到父节点或世界坐标中。
它通常负责三件事:
- 平移:决定模型放在哪里。
- 旋转:决定模型朝向。
- 缩放:决定模型大小。
常见用途是修正模型位置、高度、朝向,或者把局部坐标模型放到真实地理位置。
在 Cesium 中加载
3D Tiles 加到 scene.primitives,不是加到 viewer.entities。
js
const tileset = await Cesium.Cesium3DTileset.fromUrl('/tileset/tileset.json', {
maximumScreenSpaceError: 16,
cacheBytes: 512 * 1024 * 1024
})
viewer.scene.primitives.add(tileset)
viewer.flyTo(tileset)
常用配置:
| 配置 | 作用 |
|---|---|
maximumScreenSpaceError |
控制清晰度和性能,越小越清晰但越耗性能 |
cacheBytes |
tileset 缓存大小(字节) |
maximumCacheOverflowBytes |
允许临时超过缓存上限的大小 |
show |
显示 / 隐藏整个 tileset |
style |
Cesium3DTileStyle,按属性着色 / 过滤 |
modelMatrix |
整体位移、旋转、缩放 |
shadows |
阴影模式 |
lightColor |
模型光照颜色,倾斜摄影常用于"提亮" |
backFaceCulling |
是否做背面剔除,倾斜摄影经常需要关 |
dynamicScreenSpaceError |
远处自动降精度,性能优化 |
preloadWhenHidden |
隐藏时是否仍然预加载 |
preferLeaves |
优先加载叶子节点(更清晰) |
debugShowBoundingVolume |
显示包围盒,调试用 |
debugShowGeometricError |
显示 SSE 信息,调试用 |
maximumScreenSpaceError是最常调的参数。默认值通常能用,性能不够时调大,画质不够时调小。
从 Cesium Ion 加载
Cesium 官方托管的数据(OSM Buildings、Google Photorealistic 3D Tiles、自传 tileset 等)可以直接通过资产 id 加载:
js
Cesium.Ion.defaultAccessToken = 'YOUR_ION_TOKEN'
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(96188)
viewer.scene.primitives.add(tileset)
viewer.flyTo(tileset)
注意:
- 没有 token 时 Cesium 会用默认 token,频繁出现 401 / 速率限制。生产环境一定要换成自己的 token。
- 国内访问 Cesium Ion 可能不稳定,多数项目倾向于自己切片、自己托管数据。
加载流程
text
加载 tileset.json
↓
构建 tile 树
↓
根据相机位置遍历 tile 树
↓
视锥裁剪 Frustum Culling
↓
计算屏幕空间误差 SSE
↓
选择合适 LOD
↓
请求缺失 tile
↓
解析 b3dm / pnts / i3dm / glb
↓
创建 GPU Buffer
↓
生成 DrawCommand
↓
渲染
Cesium 每一帧都会根据相机状态重新判断哪些 tile 需要显示、加载或淘汰。
text
相机变化
↓
遍历 tile 树
↓
可见性测试
↓
SSE 计算
↓
LOD 选择
↓
请求缺失 tile
↓
淘汰旧 tile
↓
生成 DrawCommand
常见操作
调整高度
如果模型整体有高度偏差,可以通过修改 modelMatrix 做整体平移。
js
const cartographic = Cesium.Cartographic.fromCartesian(
tileset.boundingSphere.center
)
const surface = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
0
)
const offset = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
50
)
const translation = Cesium.Cartesian3.subtract(
offset,
surface,
new Cesium.Cartesian3()
)
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation)
拾取 3D Tiles
3D Tiles 的拾取结果是 Cesium3DTileFeature,不是 Entity,也不是普通 Primitive。
js
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.position)
if (!Cesium.defined(picked)) return
if (picked instanceof Cesium.Cesium3DTileFeature) {
console.log('属性 ids:', picked.getPropertyIds())
console.log('id:', picked.getProperty('id'))
console.log('高度:', picked.getProperty('height'))
console.log('tileset:', picked.tileset)
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
Cesium3DTileFeature 常用 API:
| API | 作用 |
|---|---|
getProperty(name) |
读属性 |
setProperty(name, value) |
改属性(仅内存,刷新会丢) |
getPropertyIds() |
列出所有属性名 |
tileset |
所属 tileset |
content |
所在 tile content |
color |
高亮颜色,直接改能做单体高亮 |
show |
是否显示,可以隐藏单个对象 |
单体高亮:
js
let lastPicked
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.position)
if (lastPicked) {
lastPicked.color = Cesium.Color.WHITE
}
if (picked instanceof Cesium.Cesium3DTileFeature) {
picked.color = Cesium.Color.YELLOW
lastPicked = picked
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
注意:
setProperty/color/show的修改都是内存里的临时状态,相机移走 tile 被卸载再加载就丢了。需要持久按规则呈现,用tileset.style(见下文)。- 不同切片工具导出的属性键名不一样(
id/name/BIN/Name等),上手前先getPropertyIds()看一眼。 - 如果同一像素位置叠了 Entity / Primitive / 3D Tiles,需要用
scene.drillPick拿到所有命中再做分支处理。
裁剪模型
局部项目中常用 clipping plane 裁剪地下、剖切建筑、显示局部区域。
js
tileset.clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [
new Cesium.ClippingPlane(
new Cesium.Cartesian3(0.0, 0.0, -1.0),
0.0
)
],
edgeWidth: 1.0
})
Cesium3DTileStyle 样式
tileset.style 是 3D Tiles 的样式系统,可以基于 feature 属性批量改颜色和显隐。它接受一种类似表达式的小语言。
整体染色:
js
tileset.style = new Cesium.Cesium3DTileStyle({
color: "color('red')"
})
按属性着色(条件按顺序匹配,第一个为真的胜出):
js
tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
['${height} > 100', "color('red')"],
['${height} > 50', "color('orange')"],
['true', "color('green')"]
]
}
})
按属性过滤(不满足条件的隐藏):
js
tileset.style = new Cesium.Cesium3DTileStyle({
show: "${category} === 'commercial'"
})
隐藏特定 id 的建筑(业务里非常常用):
js
tileset.style = new Cesium.Cesium3DTileStyle({
show: "${id} !== 'building-001'"
})
简单理解:
text
单个对象高亮 / 隐藏 -> feature.color / feature.show
按规则批量着色 / 过滤 -> tileset.style
注意 ${propName} 引用的是 feature 属性,键名要和 getPropertyIds() 看到的一致。
Tileset 事件
Tileset 在加载过程中会触发一些事件,业务里常用来做加载进度、按 tile 处理或加载完后再执行操作。
| 事件 | 触发时机 |
|---|---|
tileLoad |
单个 tile 加载完成 |
tileUnload |
单个 tile 被卸载 |
tileVisible |
每帧某个 tile 即将渲染前 |
tileFailed |
单个 tile 加载失败 |
initialTilesLoaded |
首批可见 tile 加载完 |
allTilesLoaded |
当前视角的全部 tile 都加载完 |
等首屏加载完再隐藏 loading:
js
tileset.initialTilesLoaded.addEventListener(() => {
console.log('首屏加载完成')
})
按 tile 批量改造 feature:
js
tileset.tileVisible.addEventListener((tile) => {
const content = tile.content
for (let i = 0; i < content.featuresLength; i++) {
const feature = content.getFeature(i)
if (feature.getProperty('floor') > 10) {
feature.color = Cesium.Color.RED
}
}
})
注意:tileVisible 每帧每个可见 tile 都会触发,回调里不要做重活。
倾斜摄影常用配置
倾斜摄影 / 实景三维数据常常需要一些视觉修正:
js
tileset.maximumScreenSpaceError = 16
tileset.lightColor = new Cesium.Cartesian3(2.0, 2.0, 2.0)
tileset.backFaceCulling = false
tileset.dynamicScreenSpaceError = true
tileset.dynamicScreenSpaceErrorDensity = 0.00278
tileset.dynamicScreenSpaceErrorFactor = 4.0
tileset.dynamicScreenSpaceErrorHeightFalloff = 0.25
常见症状和对应调整:
| 现象 | 调整 |
|---|---|
| 模型整体偏暗 | 调大 lightColor |
| 楼内看不到内部结构 | 关 backFaceCulling |
| 远处加载太慢 / 卡顿 | 开 dynamicScreenSpaceError |
| 远处太糊 | 调小 maximumScreenSpaceError |
| 镜头切换时缺块 | 开 preloadWhenHidden、preferLeaves |
点云专属配置
点云数据有自己的渲染参数,位于 pointCloudShading:
js
tileset.pointCloudShading.attenuation = true
tileset.pointCloudShading.geometricErrorScale = 1.0
tileset.pointCloudShading.maximumAttenuation = 4.0
tileset.pointCloudShading.eyeDomeLighting = true
| 参数 | 作用 |
|---|---|
attenuation |
启用基于距离的点大小衰减 |
geometricErrorScale |
几何误差缩放,影响点的视觉大小 |
maximumAttenuation |
最大点像素大小 |
eyeDomeLighting |
EDL 后处理,让点云轮廓更清晰 |
EDL 对点云可读性帮助很大,矿山、激光雷达、测绘点云建议开。
调试 3D Tiles
Cesium 自带一个调试面板 Cesium3DTilesInspector:
js
viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin)
viewer.cesium3DTilesInspector.viewModel.tileset = tileset
面板上可以直接:
- 显示 / 隐藏 包围盒、SSE
- 看 tile statistics(请求中、已加载、可见、缓存)
- 调整
maximumScreenSpaceError、dynamicScreenSpaceError
排查"为什么加载这么慢"、"为什么这块加载不出来"很好用。
代码里也可以直接打开几个 debug 参数:
js
tileset.debugShowBoundingVolume = true
tileset.debugShowGeometricError = true
tileset.debugShowRenderingStatistics = true
常见问题
-
模型加载不出来
优先检查
tileset.json地址、content.uri路径、跨域、Token、Network 请求状态,以及服务器是否正确返回.b3dm / .pnts / .glb文件。 -
模型位置偏移
常见原因是数据坐标系、ENU 局部坐标、WGS84 坐标转换或
transform矩阵处理不正确。国内数据还要注意 GCJ02 / BD09 和 WGS84 的偏移问题。 -
模型高度不对
可能是 HAE / ASL / AGL 高度基准不一致,也可能是生产工具导出时带了高度偏移。通常可以先用
modelMatrix做整体修正。 -
近处不清晰或加载太慢
可以调整
maximumScreenSpaceError。数值越小越清晰,但请求更多、显存和 CPU 压力更大。 -
显存占用太高
可以调低
cacheBytes,或者重新切片,减小单个 tile 的体积,合理控制纹理尺寸。 -
模型闪烁或穿插
可能是多个 tileset 重叠、地形和模型高度接近、深度冲突,或者
ADD / REPLACE设置不合理。 -
包围盒不准
会影响裁剪和加载判断。可以开启:
jstileset.debugShowBoundingVolume = true如果包围盒和模型明显不一致,通常需要回到数据生产或切片阶段修正。
-
拾取拿不到 feature 属性
常见原因是数据切片时没保留属性、属性键名不一致,或者
picked不是Cesium3DTileFeature。可以先getPropertyIds()看一下当前实际有哪些属性。 -
倾斜摄影发暗 / 表皮过黑
通常是光照强度问题,尝试调大
tileset.lightColor,或者根据时间 / 太阳位置调整viewer.scene.globe.enableLighting。 -
Cesium Ion 提示 401 / 速率限制
默认 token 有频次限制,生产必须替换成自己的 Ion token。如果国内访问不稳定,建议自传切片到自己的 CDN。
-
tileset 闪烁 / 切换 LOD 卡顿
可以开启
tileset.preloadWhenHidden、tileset.preferLeaves,或者适当调大cacheBytes减少卸载频率。 -
改了
feature.color离开再回来颜色没了Feature 的
color/show都是临时状态,tile 被卸载后会丢。需要持久的批量样式用tileset.style。