站内搜索

输入关键词开始搜索

Cesium 3D Tiles 局部移动实验:选择瓦片并修改节点变换

文章目录

整个 Cesium3DTileset 可以通过公开的 modelMatrix 移动,但 Cesium 没有提供稳定的“修改任意子瓦片 transform”业务接口。案例中的局部移动是对运行时瓦片树的实验性操作。

3D Tiles 局部节点变换

这套方法适合研究数据组织和验证交互,不适合直接作为长期生产接口。文章中的私有字段会用“实验代码”标明。

瓦片不是业务构件

3D Tiles 的瓦片首先是空间层级和 LOD 单元。一个瓦片可能包含整栋建筑,也可能只包含建筑的一部分;同一个建筑也可能分布在不同层级的多个瓦片中。

因此“选中塔体”实际包含两个独立问题:

  1. 如何识别哪些瓦片属于塔体。
  2. 如何只修改这些瓦片的运行时变换。

如果数据本身没有构件 ID、结构化元数据或独立 Tileset,仅靠瓦片包围体无法得到稳定的语义选择。

保存整体变换和节点原始变换

整个 Tileset 的位置应使用公开 modelMatrix:

const anchor = Cesium.Cartesian3.fromDegrees(longitude, latitude, height)
const baseModelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(anchor)

Cesium.Matrix4.multiply(
  baseModelMatrix,
  Cesium.Matrix4.fromUniformScale(globalScale),
  baseModelMatrix,
)

tileset.modelMatrix = Cesium.Matrix4.clone(baseModelMatrix)

局部实验还要为每个目标节点保存原始矩阵。所有交互变化都从原始矩阵重新计算,不能在上一次结果上持续累乘:

interface TileNodeRef {
  node: Cesium.Cesium3DTile
  originalTransform: Cesium.Matrix4
  selected: boolean
  longitude: number
  latitude: number
  center: Cesium.Cartesian3
}

公开的 Cesium3DTile 可以读取 children、boundingSphere、content 和 computedTransform。但是用于写入的节点 transform、JSON _header 和 Tileset _root 不属于稳定公开接口。

实验性遍历运行时瓦片树

下面的结构用于说明案例做了什么,不是推荐的公共 API 用法:

function collectRuntimeTiles(runtimeTile: any, result: TileNodeRef[]) {
  const header = runtimeTile._header
  if (!header) return

  for (const child of runtimeTile.children ?? []) {
    collectRuntimeTiles(child, result)
  }

  if (!header.content?.uri) return

  const sphere = runtimeTile.boundingSphere
  const cartographic = Cesium.Cartographic.fromCartesian(sphere.center)

  result.push({
    node: runtimeTile,
    originalTransform: Cesium.Matrix4.clone(runtimeTile.transform),
    selected: false,
    longitude: Cesium.Math.toDegrees(cartographic.longitude),
    latitude: Cesium.Math.toDegrees(cartographic.latitude),
    center: Cesium.Cartesian3.clone(sphere.center),
  })
}

它读取了三个不稳定点:

  • tileset._root
  • tile._header
  • 可写的 tile.transform

Cesium 升级后字段名、初始化时机和变换计算方式都可能变化。

用包围球中心做区域选择

案例允许用户绘制一个多边形,再判断瓦片包围球中心是否位于区域内:

function pointInPolygon(
  longitude: number,
  latitude: number,
  polygon: Array<{ longitude: number; latitude: number }>,
) {
  let inside = false

  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const a = polygon[i]
    const b = polygon[j]
    const intersects =
      a.latitude > latitude !== b.latitude > latitude &&
      longitude <
        (b.longitude - a.longitude) *
          (latitude - a.latitude) /
          (b.latitude - a.latitude) +
        a.longitude

    if (intersects) inside = !inside
  }

  return inside
}

然后更新选择状态:

for (const tile of tileRefs) {
  tile.selected = pointInPolygon(
    tile.longitude,
    tile.latitude,
    selectionPolygon,
  )
}

这是中心点命中,不是瓦片与多边形相交测试。包围球中心在区域外但几何进入区域时会漏选;中心在区域内但瓦片跨度很大时会误选。

围绕瓦片局部中心做旋转和缩放

直接缩放节点矩阵会让几何向局部原点收缩。若希望瓦片围绕自己的包围体中心变化,需要先平移到中心、执行旋转或缩放、再移回去:

M' = M × T(center) × R × S × T(-center)

实验代码:

function updateSelectedTile(tile: TileNodeRef, state: TransformState) {
  if (!tile.selected) return

  const matrix = Cesium.Matrix4.clone(tile.originalTransform)

  Cesium.Matrix4.multiply(
    matrix,
    Cesium.Matrix4.fromTranslation(tile.center),
    matrix,
  )

  const rotation = Cesium.Matrix3.fromRotationZ(
    Cesium.Math.toRadians(state.rotationZ),
  )
  Cesium.Matrix4.multiply(
    matrix,
    Cesium.Matrix4.fromRotationTranslation(rotation),
    matrix,
  )
  Cesium.Matrix4.multiply(
    matrix,
    Cesium.Matrix4.fromUniformScale(state.scale),
    matrix,
  )

  const inverseCenter = Cesium.Cartesian3.negate(
    tile.center,
    new Cesium.Cartesian3(),
  )
  Cesium.Matrix4.multiply(
    matrix,
    Cesium.Matrix4.fromTranslation(inverseCenter),
    matrix,
  )

  ;(tile.node as any).transform = matrix
}

实际数据中的包围体中心可能位于瓦片局部坐标,也可能需要结合父节点变换。不能把世界坐标包围球中心直接塞进局部矩阵。案例读取的是 tileset JSON 中 boundingVolume.box 的局部中心,用它构造 T(center)。

整体缩放会影响局部平移单位

如果整个 Tileset 已经放大十倍,节点局部空间中的一米最终会显示为十米。希望 UI 中的偏移保持世界米,需要反向补偿:

const localScale = 1 / globalScale

const localOffset = new Cesium.Cartesian3(
  worldOffsetX * localScale,
  worldOffsetY * localScale,
  worldOffsetZ * localScale,
)

这只适用于整体是均匀缩放的情况。存在非均匀缩放或多级父节点旋转时,应通过完整逆矩阵把世界向量转换到节点局部空间。

缩放到接近零不是隐藏 API

案例把节点缩小到 0.0001 来模拟隐藏:

const matrix = Cesium.Matrix4.clone(tile.originalTransform)
Cesium.Matrix4.multiply(
  matrix,
  Cesium.Matrix4.fromUniformScale(0.0001),
  matrix,
)
;(tile.node as any).transform = matrix

这仍然会让瓦片进入选择、更新和资源管理流程,只是几何变得几乎不可见。它不是渲染层面的真正隐藏,也可能改变包围体与剔除关系。

生产环境更适合:

  • 在数据生产阶段把可独立控制的构件拆成独立 Tileset。
  • 为要素写入结构化元数据,通过 Cesium3DTileFeature#show 控制要素。
  • 使用 Cesium3DTileStyle 按属性控制显示。

LOD 会让节点身份发生变化

3D Tiles 根据相机位置加载和卸载内容。即使运行时树节点仍在,当前可见几何也可能从父瓦片切换到子瓦片。

局部改动要面对这些状态:

  • 目标内容还没有加载。
  • 同一语义对象在不同 LOD 层级重复出现。
  • 瓦片卸载后,运行时修改是否需要恢复。
  • 精细层级加载后,新节点没有继承旧节点选择状态。

公开 API 提供 tileVisible 和 tileUnload 事件用于观察瓦片生命周期,但没有承诺可写入每个瓦片变换。依赖运行时字段的方案需要锁定 Cesium 版本,并建立升级回归测试。

更可靠的生产方案

按稳定性排序:

  1. 数据预处理时拆分需要移动的构件。
  2. 使用 glTF 节点、结构化元数据或 Feature ID 表达业务对象。
  3. 以多个 Tileset 组织独立变换单元,分别设置公开 modelMatrix。
  4. 最后才考虑运行时瓦片节点实验。

3D Tiles 局部移动的难点不在矩阵公式,而在数据语义和 LOD 生命周期。没有稳定构件标识时,瓦片中心选区只能证明交互思路,不能证明生产方案已经成立。

参考资料

相关文章

Cesium 加载与调整 3D Tiles:ENU 变换、模型矩阵和 LOD

从 Cesium3DTileset.fromUrl 开始,完成 3D Tiles 定位、旋转、缩放、垂直偏移和精度控制

Cesium 北斗三维网格:在城市 3D Tiles 上做 GeoSOT-3D 剖分

介绍区域体素生成、建筑表面采样、结构碰撞检测、Worker 几何构建和网格编码查询