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

相关推荐
Htr_1 小时前
Vercel 使用指南:框架、工作流与基础设施一体化的现代 Web 部署平台
前端
一颗烂土豆1 小时前
ECharts 太平面?试试这款 Vue 3D 图表库
前端·vue.js·echarts
anyup1 小时前
迁移uni-app x,我是如何让 AI 把我一步步搞崩溃的...
前端·uni-app·trae
用户921080262861 小时前
从读框架到搭项目:基于 Ant Design X Vue 和 RICH 范式搭建 AI 前端工作台
前端
Hilaku1 小时前
为什么同一段代码在 Safari 上永远有 Bug?
前端·javascript·程序员
半仙er1 小时前
第二周06天 Vue3 + TypeScript 实战与本周复盘
前端
heyCHEEMS1 小时前
切页回来组件消失了?一个浏览器渲染机制引起的容器高度坍塌 bug
前端·浏览器
iaku1 小时前
Prompt 不是玄学:写给前端的 Prompt 工程指南
前端·人工智能
爱丶不疚2 小时前
在 dsh 仓库里扒到的宝藏工作流:详解 .agents/notes 决策沉淀系统
前端·agent·vibecoding