文章目录
整个 Cesium3DTileset 可以通过公开的 modelMatrix 移动,但 Cesium 没有提供稳定的“修改任意子瓦片 transform”业务接口。案例中的局部移动是对运行时瓦片树的实验性操作。
这套方法适合研究数据组织和验证交互,不适合直接作为长期生产接口。文章中的私有字段会用“实验代码”标明。
瓦片不是业务构件
3D Tiles 的瓦片首先是空间层级和 LOD 单元。一个瓦片可能包含整栋建筑,也可能只包含建筑的一部分;同一个建筑也可能分布在不同层级的多个瓦片中。
因此“选中塔体”实际包含两个独立问题:
- 如何识别哪些瓦片属于塔体。
- 如何只修改这些瓦片的运行时变换。
如果数据本身没有构件 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._roottile._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 版本,并建立升级回归测试。
更可靠的生产方案
按稳定性排序:
- 数据预处理时拆分需要移动的构件。
- 使用 glTF 节点、结构化元数据或 Feature ID 表达业务对象。
- 以多个 Tileset 组织独立变换单元,分别设置公开
modelMatrix。 - 最后才考虑运行时瓦片节点实验。
3D Tiles 局部移动的难点不在矩阵公式,而在数据语义和 LOD 生命周期。没有稳定构件标识时,瓦片中心选区只能证明交互思路,不能证明生产方案已经成立。