3dtiles加载时被地形遮挡问题研究及处理思路

倾斜摄影「穿模」了?Cesium 里做一次 3D Tiles × 地形交叉检测与压平

面向 CesiumJS / 实景三维开发者的一篇实战分享。


效果展示

原始3dtiles加载会被地形遮挡

处理后效果

只压平突出区域

动态效果

写在前面

做三维地球时,几乎都会遇到同一类画面:

  • 倾斜摄影(3D Tiles)叠在地形上;
  • 有的地方建筑「沉」进山里;
  • 有的地方山体从屋顶「冒」出来;
  • 相机一转,穿模更明显。
    很多人第一反应是:把模型整体抬高/压低几米 。
    这在小范围、高程误差均匀时够用;一旦地形起伏大、模型底面也不平,整体平移就治不好局部穿模。
    更稳的思路是:
  1. 先找出「地形相对模型底部突出」的那些区域;
  2. 只把这些区域的地形,压到模型底附近;
  3. 边缘做过渡,避免出现刀切一样的台阶。
    本文分享的就是这条链路:
    交叉检测 → 生成压平面 → 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,成本和闪烁都难控。

更合适的做法是:

  1. 把 footprint 做成一张/一组 mask(带边缘过渡);
  2. 在地形材质/着色器里注入:
    最终高度 = mix(原地形高, 目标高, mask);
  3. 过渡带用距离场渐变,避免硬边。
    用户看到的是:
    模型底下的山被「按」下去了,周围还连着原来的地形。
    并且可以:
  • 多块同时压;
  • 每块目标高不同;
  • remove() 一键撤销。

相关推荐
IT_陈寒38 分钟前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
默_笙38 分钟前
🚓 分诊台与拆题术:让 RAG 学会判断和规划
前端·javascript
CopyCode38 分钟前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
去伪存真38 分钟前
Electron 自动化发布指南:GitHub Actions 跨平台打包全纪录
前端·electron
计算机魔术师38 分钟前
OpenAI智能体失控闯进美国政府网站,53张用户图片外泄背后
前端
颜进强38 分钟前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan39 分钟前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫39 分钟前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh39 分钟前
MCP 报错分不清?先看响应里是 result 还是 error
前端
呃呃呃呃ex39 分钟前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript