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() 一键撤销。

相关推荐
lpfasd1237 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
Data analyse45610 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai10 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
云飞云共享云桌面11 小时前
设计师共享单台图形服务器,解决 SolidWorks大装配卡顿难题
运维·服务器·3d·自动化·电脑·制造
环境栈笔记11 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁11 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu11 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!12 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格12 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人12 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js