cesium学习(三)-3d tiles

什么是 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 指向真正的瓦片内容文件,比如 b3dmpntsi3dmglb,也可以指向另一个外部 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
镜头切换时缺块 preloadWhenHiddenpreferLeaves

点云专属配置

点云数据有自己的渲染参数,位于 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(请求中、已加载、可见、缓存)
  • 调整 maximumScreenSpaceErrordynamicScreenSpaceError

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

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

js 复制代码
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. 包围盒不准

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

    js 复制代码
    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

相关推荐
前端那点事1 小时前
Vue3自定义Hooks保姆级教程!从原理到企业级实战,告别混乱代码
前端·vue.js
前端那点事1 小时前
别再乱用Vue3响应式!ref、reactive、toRef、toRefs完整区别+企业级落地实战
前端·vue.js
yingyima1 小时前
Base64 编码解码实战:业务场景下的高效应用
前端
悠哉摸鱼大王1 小时前
cesium学习(五)-Primitive
前端·cesium
悟空瞎说1 小时前
Git Worktree 实战:多 AI 编码代理并行开发,彻底解决分支切换冲突痛点
前端·git
悠哉摸鱼大王1 小时前
cesium学习(四)-相机
前端·cesium
zeqinjie2 小时前
Skills-Flutter 内测泄漏审核
前端·flutter·app
村上小树3 小时前
非常简单地学习一下shareDB的原理
前端·javascript
认真的薛薛3 小时前
阿里云: A记录 & CNAME
服务器·前端·阿里云