cesium学习(三)-3d tiles

25 阅读10分钟

什么是 3D Tiles

3D Tiles 是 Cesium 提出的、面向大规模三维地理空间数据的流式加载规范,适合倾斜摄影、城市白模、BIM/CAD、点云、实例化模型等场景。

可以把它理解成“三维世界里的地图瓦片”:

二维地图:把图片切成 z / x / y.png
3D Tiles:把大规模三维模型切成一棵 tile 树

普通 glTF 模型通常是一次性加载;3D Tiles 会根据相机位置、视锥范围和屏幕误差,按需加载当前视角需要的瓦片。

普通模型 = 一个文件,一次性加载
3D Tiles = 一棵瓦片树,按需流式加载

适合什么数据

数据类型典型场景说明
倾斜摄影城市实景、园区实景通常由摄影测量软件生成
城市白模建筑群、城市规划建筑几何 + 属性信息
BIM / CAD建筑、工厂、桥梁模型精细,可能需要分层和属性查询
点云激光雷达、测绘点云数据量大,常用于地形、道路、矿山
实例化模型树木、路灯、设备同一个模型重复出现很多次

文件结构

一个 3D Tiles 数据集的入口通常是 tileset.json

3d-tiles/
 ├─ tileset.json        # 入口文件,描述 tile 树
 ├─ 0/
 │  ├─ 0.b3dm           # 批量三维模型
 │  ├─ 1.b3dm
 │  └─ 2.b3dm
 ├─ points.pnts         # 点云
 ├─ trees.i3dm          # 实例化模型
 └─ composite.cmpt      # 复合瓦片

常见瓦片内容格式

格式全称用途
b3dmBatched 3D Model最常见,常用于倾斜摄影、建筑模型
i3dmInstanced 3D Model同一模型的多实例渲染,例如树、路灯
pntsPoint Cloud点云数据
cmptComposite把多个瓦片内容组合在一起
glb / glTFglTF Model3D Tiles 1.1 中更推荐直接使用 glTF

早期 3D Tiles 常见的是 b3dm / i3dm / pnts。在 3D Tiles 1.1 之后,很多能力开始向 glTF 和扩展机制靠拢。

tileset.json

tileset.json 可以理解为三维瓦片目录树。它描述了有哪些 tile、每个 tile 的空间范围、LOD 关系、几何误差、子节点和内容地址。

一个简化示例如下:

{
  "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 是它的子瓦片。

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),再决定是否继续加载子节点。

距离越近 -> SSE 越大 -> 需要更精细的子 tile
距离越远 -> SSE 越小 -> 粗糙 tile 就够了

refine

refine 定义父子瓦片的细化方式。

含义典型场景
REPLACE子瓦片加载后替换父瓦片倾斜摄影、层级模型
ADD子瓦片叠加在父瓦片之上城市白模逐级加细节、附加数据

REPLACE 更常见,意思是“远处看粗模,近处换精模”。ADD 是“父瓦片保留,子瓦片继续叠加细节”。

content.uri

content.uri 指向真正的瓦片内容文件,比如 b3dmpntsi3dmglb,也可以指向另一个外部 tileset.json

{
  "content": {
    "uri": "building.b3dm"
  }
}

如果 uri 指向外部 tileset,可以把一个大场景拆成多个子 tileset,方便分块管理和懒加载。

transform

transform 是一个 4x4 变换矩阵,用来把 tile 的局部坐标变换到父节点或世界坐标中。

它通常负责三件事:

  • 平移:决定模型放在哪里。
  • 旋转:决定模型朝向。
  • 缩放:决定模型大小。

常见用途是修正模型位置、高度、朝向,或者把局部坐标模型放到真实地理位置。

在 Cesium 中加载

3D Tiles 加到 scene.primitives,不是加到 viewer.entities

const tileset = await Cesium.Cesium3DTileset.fromUrl('/tileset/tileset.json', {
  maximumScreenSpaceError: 16,
  cacheBytes: 512 * 1024 * 1024
})

viewer.scene.primitives.add(tileset)
viewer.flyTo(tileset)

常用配置:

配置作用
maximumScreenSpaceError控制清晰度和性能,越小越清晰但越耗性能
cacheBytestileset 缓存大小(字节)
maximumCacheOverflowBytes允许临时超过缓存上限的大小
show显示 / 隐藏整个 tileset
styleCesium3DTileStyle,按属性着色 / 过滤
modelMatrix整体位移、旋转、缩放
shadows阴影模式
lightColor模型光照颜色,倾斜摄影常用于"提亮"
backFaceCulling是否做背面剔除,倾斜摄影经常需要关
dynamicScreenSpaceError远处自动降精度,性能优化
preloadWhenHidden隐藏时是否仍然预加载
preferLeaves优先加载叶子节点(更清晰)
debugShowBoundingVolume显示包围盒,调试用
debugShowGeometricError显示 SSE 信息,调试用

maximumScreenSpaceError 是最常调的参数。默认值通常能用,性能不够时调大,画质不够时调小。

从 Cesium Ion 加载

Cesium 官方托管的数据(OSM Buildings、Google Photorealistic 3D Tiles、自传 tileset 等)可以直接通过资产 id 加载:

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 可能不稳定,多数项目倾向于自己切片、自己托管数据。

加载流程

加载 tileset.json
      ↓
构建 tile 树
      ↓
根据相机位置遍历 tile 树
      ↓
视锥裁剪 Frustum Culling
      ↓
计算屏幕空间误差 SSE
      ↓
选择合适 LOD
      ↓
请求缺失 tile
      ↓
解析 b3dm / pnts / i3dm / glb
      ↓
创建 GPU Buffer
      ↓
生成 DrawCommand
      ↓
渲染

Cesium 每一帧都会根据相机状态重新判断哪些 tile 需要显示、加载或淘汰。

相机变化
    ↓
遍历 tile 树
    ↓
可见性测试
    ↓
SSE 计算
    ↓
LOD 选择
    ↓
请求缺失 tile
    ↓
淘汰旧 tile
    ↓
生成 DrawCommand

常见操作

调整高度

如果模型整体有高度偏差,可以通过修改 modelMatrix 做整体平移。

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。

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是否显示,可以隐藏单个对象

单体高亮:

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 裁剪地下、剖切建筑、显示局部区域。

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 属性批量改颜色和显隐。它接受一种类似表达式的小语言。

整体染色:

tileset.style = new Cesium.Cesium3DTileStyle({
  color: "color('red')"
})

按属性着色(条件按顺序匹配,第一个为真的胜出):

tileset.style = new Cesium.Cesium3DTileStyle({
  color: {
    conditions: [
      ['${height} > 100', "color('red')"],
      ['${height} > 50',  "color('orange')"],
      ['true',            "color('green')"]
    ]
  }
})

按属性过滤(不满足条件的隐藏):

tileset.style = new Cesium.Cesium3DTileStyle({
  show: "${category} === 'commercial'"
})

隐藏特定 id 的建筑(业务里非常常用):

tileset.style = new Cesium.Cesium3DTileStyle({
  show: "${id} !== 'building-001'"
})

简单理解:

单个对象高亮 / 隐藏        -> 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:

tileset.initialTilesLoaded.addEventListener(() => {
  console.log('首屏加载完成')
})

按 tile 批量改造 feature:

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 都会触发,回调里不要做重活。

倾斜摄影常用配置

倾斜摄影 / 实景三维数据常常需要一些视觉修正:

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
镜头切换时缺块preloadWhenHiddenpreferLeaves

点云专属配置

点云数据有自己的渲染参数,位于 pointCloudShading

tileset.pointCloudShading.attenuation = true
tileset.pointCloudShading.geometricErrorScale = 1.0
tileset.pointCloudShading.maximumAttenuation = 4.0
tileset.pointCloudShading.eyeDomeLighting = true
参数作用
attenuation启用基于距离的点大小衰减
geometricErrorScale几何误差缩放,影响点的视觉大小
maximumAttenuation最大点像素大小
eyeDomeLightingEDL 后处理,让点云轮廓更清晰

EDL 对点云可读性帮助很大,矿山、激光雷达、测绘点云建议开。

调试 3D Tiles

Cesium 自带一个调试面板 Cesium3DTilesInspector

viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin)

viewer.cesium3DTilesInspector.viewModel.tileset = tileset

面板上可以直接:

  • 显示 / 隐藏 包围盒、SSE
  • 看 tile statistics(请求中、已加载、可见、缓存)
  • 调整 maximumScreenSpaceErrordynamicScreenSpaceError

排查"为什么加载这么慢"、"为什么这块加载不出来"很好用。

代码里也可以直接打开几个 debug 参数:

tileset.debugShowBoundingVolume = true
tileset.debugShowGeometricError = true
tileset.debugShowRenderingStatistics = true

常见问题

  1. 模型加载不出来

    优先检查 tileset.json 地址、content.uri 路径、跨域、Token、Network 请求状态,以及服务器是否正确返回 .b3dm / .pnts / .glb 文件。

  2. 模型位置偏移

    常见原因是数据坐标系、ENU 局部坐标、WGS84 坐标转换或 transform 矩阵处理不正确。国内数据还要注意 GCJ02 / BD09 和 WGS84 的偏移问题。

  3. 模型高度不对

    可能是 HAE / ASL / AGL 高度基准不一致,也可能是生产工具导出时带了高度偏移。通常可以先用 modelMatrix 做整体修正。

  4. 近处不清晰或加载太慢

    可以调整 maximumScreenSpaceError。数值越小越清晰,但请求更多、显存和 CPU 压力更大。

  5. 显存占用太高

    可以调低 cacheBytes,或者重新切片,减小单个 tile 的体积,合理控制纹理尺寸。

  6. 模型闪烁或穿插

    可能是多个 tileset 重叠、地形和模型高度接近、深度冲突,或者 ADD / REPLACE 设置不合理。

  7. 包围盒不准

    会影响裁剪和加载判断。可以开启:

    tileset.debugShowBoundingVolume = true
    

    如果包围盒和模型明显不一致,通常需要回到数据生产或切片阶段修正。

  8. 拾取拿不到 feature 属性

    常见原因是数据切片时没保留属性、属性键名不一致,或者 picked 不是 Cesium3DTileFeature。可以先 getPropertyIds() 看一下当前实际有哪些属性。

  9. 倾斜摄影发暗 / 表皮过黑

    通常是光照强度问题,尝试调大 tileset.lightColor,或者根据时间 / 太阳位置调整 viewer.scene.globe.enableLighting

  10. Cesium Ion 提示 401 / 速率限制

    默认 token 有频次限制,生产必须替换成自己的 Ion token。如果国内访问不稳定,建议自传切片到自己的 CDN。

  11. tileset 闪烁 / 切换 LOD 卡顿

    可以开启 tileset.preloadWhenHiddentileset.preferLeaves,或者适当调大 cacheBytes 减少卸载频率。

  12. 改了 feature.color 离开再回来颜色没了

    Feature 的 color / show 都是临时状态,tile 被卸载后会丢。需要持久的批量样式用 tileset.style