倾斜摄影「穿模」了?Cesium 里做一次 3D Tiles × 地形交叉检测与压平
面向 CesiumJS / 实景三维开发者的一篇实战分享。
效果展示
原始3dtiles加载会被地形遮挡

处理后效果

只压平突出区域

动态效果

写在前面
做三维地球时,几乎都会遇到同一类画面:
- 倾斜摄影(3D Tiles)叠在地形上;
- 有的地方建筑「沉」进山里;
- 有的地方山体从屋顶「冒」出来;
- 相机一转,穿模更明显。
很多人第一反应是:把模型整体抬高/压低几米 。
这在小范围、高程误差均匀时够用;一旦地形起伏大、模型底面也不平,整体平移就治不好局部穿模。
更稳的思路是:
- 先找出「地形相对模型底部突出」的那些区域;
- 只把这些区域的地形,压到模型底附近;
- 边缘做过渡,避免出现刀切一样的台阶。
本文分享的就是这条链路:
交叉检测 → 生成压平面 → GPU 地形压平。
一句话理解问题
把地形想成一张弹性橡胶垫,把 3D Tiles 想成放在垫子上的积木。
穿模 = 垫子某处鼓得太高,把积木「顶穿」了 。
我们要做的不是把整张垫子压扁,而是:
找到鼓包位置,只把鼓包按下去,按到积木底面附近,边缘再缓缓过渡回原地形。
整体流程(先看地图)
text
加载 3D Tiles + 地形
│
▼
① 得到模型水平覆盖(脚印 / footprint)
│
▼
② 在覆盖范围内,逐格比对:
地形高程 vs 模型表面/底部高程
│
▼
③ 地形高出模型超过阈值 → 记为「突出区」
│
▼
④ 把突出区轮廓交给地形着色器
│
▼
⑤ GPU 把对应地形高度压到目标高程(带过渡)
可以记成两步 API:
ts
await flatten.detect(); // 找出要挖的地方
await flatten.apply(); // 真正压平
// 或一键
await flatten.detectAndApply();
为什么不能只靠「模型整体抬高」
| 做法 | 优点 | 缺点 |
|---|---|---|
改 modelMatrix 整体抬高 |
实现简单 | 全局一刀切,起伏地形仍会局部穿模 |
| 改地形源数据(离线重切) | 一劳永逸 | 周期长、多模型难复用 |
| 运行时检测 + GPU 压平 | 按区域处理、可调参、可撤销 | 要算网格、吃一点性能 |
| 如果产品里经常换模型、换区域,运行时方案更适合调试和交付。 |
检测:怎么知道「哪里地形太高」
核心不是拍脑袋选一块多边形,而是用数据说话。
1)先拿到模型覆盖范围
对每个水平采样点,判断它是否落在 3D Tiles 覆盖下。常见两条路径:
快速路径(优先)
解析 tileset 根/子节点的 b3dm / glb 网格,在 Worker 里做水平投影栅格化。
优点:不依赖主场景把所有瓦片刷完,速度快。
兜底路径
快速路径失败时(复杂结构、压缩格式等),用离屏正交相机渲染模型,从像素/深度里读覆盖与高程。
工程经验:很多 CesiumLab 导出的模型,根节点没有 mesh,真正内容在子
NoLod_*.glb里。只解析 root content 会误判「没模型」------要递归收集子 tile。
2)再和地形比高低
在覆盖网格上:
- 读该格的模型高程(更贴近「底面/表面」);
- 用
sampleTerrainMostDetailed读该格的地形高程; - 若
地形高 − 模型高 > 阈值,判定为突出。
阈值(heightThresholdMeters)可以理解为「允许的误差带」。
设成0更严格,设成0.1~0.5可以过滤噪声。
3)内缩一下,少误伤边缘
模型外轮廓附近,采样噪声更大,也更容易把「正常坡地」算进去。
所以通常会做 boundaryInset(检测内缩) :从覆盖边缘往里缩几米再比对。
直观效果:
少挖一点边缘,结果更稳。
4)输出什么?
检测结果不是一张截图,而是一组可用于压平的几何:
- 突出区多边形(footprints);
- 每块对应的目标高程(可以不同块不同高度);
- 挖深、面积等统计信息(方便 GUI / 日志)。
有了这些,压平阶段就不用再猜「挖哪里」。
压平:为什么放在 GPU 做
如果用 Entity 挖洞、或者每帧 CPU 改地形 mesh,成本和闪烁都难控。
更合适的做法是:
- 把 footprint 做成一张/一组 mask(带边缘过渡);
- 在地形材质/着色器里注入:
最终高度 = mix(原地形高, 目标高, mask); - 过渡带用距离场渐变,避免硬边。
用户看到的是:
模型底下的山被「按」下去了,周围还连着原来的地形。
并且可以:
- 多块同时压;
- 每块目标高不同;
remove()一键撤销。