尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cesium高程数据全解析:从DEM到地形加载与采样实践

Cesium高程数据全解析:从DEM到地形加载与采样实践 做 Cesium 项目做到第三个月我被一块地形绊倒了。那是一个智慧园区项目楼层、道路、植被、管线都摆得整整齐齐但领导把视角往地面一压所有车辆都陷在柏油路面下楼层地板全部裸露在绿化带上画面惨不忍睹。问题很简单——项目一开始根本没有加载任何高程数据整个场景是贴在一个光滑的参照椭球面上的。从那以后我养成了个习惯只要有 Cesium 项目第一件事一定是先把高程数据链路跑通再谈模型、图标、业务功能。这篇就把 Cesium 高程数据从选型、加载到取高程值的完整链路按我的实操顺序从头讲一遍适合刚开始接触地形数据的 Cesium 开发也适合那些已经加载了地形却被各种古怪问题折磨的同行。1. 没有高程数据Cesium 只是张糊了贴图的皮球1.1 高程数据在 Cesium 里到底管什么高程数据在 GIS 领域的正式叫法是 DEMDigital Elevation Model它本质上就是一组记录地面格网点海拔高度的数据。别小看这层高度信息Cesium 的地形渲染、贴地摆放、通视分析、淹没分析、航线规划全部建立在它之上。Cesium 拿到高程数据后会把它构造成带细节层次LOD的三角网地形再叠加影像瓦片作为纹理最终才呈现出我们肉眼看到的起伏山川。这么说可能有点抽象换个角度Cesium 默认情况下如果不配置任何地形整个地球就是基于 WGS84 椭球体数学表面生成的光滑球面。这个椭球面不是真实地面它只是描述地球形状的一个数学近似。没有高程数据时你在上面摆放建筑、车辆、管线这些物体全都贴在了一个“虚拟光滑地面”上而不是实际的坡面、沟谷、山脊上。一旦视角压低穿模、浮空、陷地的效果就会全部暴露。日常开发中高程数据至少影响四类事情地表渲染出来的起伏效果决定场景真实感CLAMP_TO_GROUND贴地实体的落点位置例如一辆车在地形坡面上必须跟着倾斜相机飞行、跟踪目标时离地面的相对高度航线会不会撞山就看它的光照阴影、可视域、淹没分析等业务计算没有真实地表这些全是空中楼阁。1.2 地形数据在渲染管线里的位置Cesium 的渲染体系里高程数据通过TerrainProvider接口接入最常用的是CesiumTerrainProvider。它负责把高程瓦片变成三角形网格再把网格交到 Globe 的地形四叉树QuadtreePrimitive里统一管理。地形网格加载完成之后影像图层会像贴纸一样覆盖在网格表面灯光、雾效、阴影再叠加在网格上。这里有个关键点地形网格是不分图层的它和影像图层的加载是两条并行管线。影像只需要一张 PNG地形却需要精确的高度字段所以地形瓦片格式和普通影像格式完全不同。Cesium 的量化网格地形会自动根据相机距离决定加载哪一层 LOD近处加载高分辨率网格远处加载低分辨率网格这就是地形四叉树的核心逻辑。理解这个机制之后你再去调地形参数就知道改的是什么东西而不是瞎试。1.3 地形缺失时最常见的三种翻车现场我先列出自己实际遇到过的三种情况把你可能踩的坑提前摆出来第一模型集体陷地或浮空。园区里所有建筑底部都陷进山体半截或者整个楼悬在半空。原因是模型的高度是用某一固定高程写死的但地表起伏让它实际交点的位置偏了。这类问题最迷惑人因为从正视角看不出毛病一旦斜视角拉近就原形毕露。第二相机视角穿山。用户点击一个远处目标flyTo 过去后镜头直接钻进了山体内部屏幕一片黑。没有高程数据的场景下椭球面上飞行永远畅通无阻加载真实地形后这条路径可能正好从山肚子里穿过去。第三业务分析数据完全失准。比如做雷达遮蔽分析时两个点之间明明有山体挡住系统却说通视做淹没分析时水淹到哪里根本算不出来。这些本质上都是因为高程基准缺失所有三维位置都在一个假表面上互相“错位”。这三个翻车现场背后是同一个根因没有把地理空间数据的第三维——高程——纳入项目的基础设施。所以接下来我们看看怎么选一个靠谱的高程数据源。2. 选对数据源和格式后面能省一半时间2.1 去哪拿高程数据高程数据的来源五花八门但生产项目里用下来主流其实就是下面几类SRTMNASA 测绘的全球数据分辨率有 1 弧秒约 30 米和 3 弧秒约 90 米两档覆盖全球大部分陆地区域免费可商用适合做区域级场景。ASTER GDEM也是全球覆盖分辨率 30 米但局部区域噪声略明显用它做城市级精细场景会有点吃力。ALOS AW3D30日本 JAXA 发布的全球 30 米数据在山地区域的细节表现通常比 SRTM 更稳免费开放是很多国内外项目的主力数据。地方高分 DEM国内很多城市有 1 米到 5 米分辨率的商业 DEM 或 DSM这类数据通常来自测绘部门精度高但获取和授权流程复杂适合城市级、园区级的正式交付项目。我的建议是Demo 演示、验证功能用全球免费数据就够了但如果是给政企客户做交付千万要用经过当地测绘部门检验的高分辨率数据全球免费数据在关键区域出现几米到几十米的偏差在验收时很难交代。实际选数据时要同时考虑覆盖范围、分辨率、时效性和授权这四个维度缺一个都会在后面出问题。授权这点尤其容易被忽略——很多朋友从某个网盘随手下了个 GeoTIFF 就开始做做到一半发现没法上线公网非常被动。2.2 栅格高程和量化网格两种格式的取舍拿到以上数据源之后原始文件通常是 GeoTIFF、HGT、DTED、IMG 这类栅格高程格式。它们的特点是每个像素记录一个高度值适合做分析但 Cesium 前端不能直接流式加载这种大栅格。Cesium 真正推荐的是 quantized-mesh量化网格格式的地形切片——一组带 LOD 的三角形网格四叉树切分、按需加载。这两种格式的取舍可以从下面这张表看出端倪格式典型来源特点Cesium 直接支持情况GeoTIFFSRTM、ASTER GDEM、商业 DEM栅格高度场分析友好文件大需转切片HGTSRTM 官网按经纬度分块无压缩需转切片DTED测绘军事数据分级标准Level 0/1/2需转切片quantized-meshCesium 地形切片三角网 四叉树 LOD流式加载通过 CesiumTerrainProvider 直接加载cesium terrainCesium ion 生成含法线、水纹等增强数据直接加载从栅格到量化网格的转换会顺手完成几件事一是把连续高度场离散成自适应三角网平地少三角形、山地多三角形二是按四叉树切瓦片前端可以按需请求三是生成法线贴图和水体蒙版等增强数据。所以哪怕你手里只有一张 GeoTIFF也要有把它变成地形瓦片再加载的思路而不是直接丢给浏览器。2.3 分辨率怎么选30米、10米还是1米分辨率代表每个采样点的间距间距越小地形细节越丰富。全球免费数据大多是 30 米做省市级宏观场景完全够用但你要是做单栋建筑级别的精细园区30 米的数据连楼顶的轮廓都表达不出来必须换 1 米级别的 DSM 或 DEM。还有一种很容易混淆的情况DSM 和 DEM 的区别。DEM 是纯地面高程把建筑和树都削平了DSM 是地表以上所有东西的高程包含屋顶、树冠。可视化场景如果追求“楼是楼、地是地”的效果DSM 反而会把建筑和地形混在一起造成穿模。所以做建筑贴合、道路铺装时优先选 DEM做树冠遮挡、无人机航线规划时 DSM 更真实。别拿到一份数据就开干先问清楚它到底是哪个。2.4 我对数据源的推荐组合如果是快速原型验证直接用 Cesium ion 的全球地形World Terrain一行代码就能加载50 米甚至更细的细节都能拉起来省心省事。如果是国内正式项目又想控制成本我一般这样搭配区域大背景用 ALOS AW3D30 转好的地形切片免费且覆盖广重点城市或园区中心单独采购或复用当地 1 米到 5 米的 DEM 做局部高精度地形多个精度的地形切片通过 Cesium 的多个 TerrainProvider 切换或拼接实现“宏观能看到全貌微观能看到细节”的效果。这套组合最大的好处是花钱花在刀刃上全球 30 米免费数据铺底重点区域用高精度商用数据替换视觉和预算都兼顾。3. 把高程数据挂到 Cesium 里的几种接法3.1 最省事Cesium ion 全球地形如果你只是想在项目里快速看到起伏地形Cesium ion 的全球地形是最快路径。代码极短const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1), });这里的 asset id 1 就是 Cesium World Terrain它包含了全球范围内的量化网格地形还附带法线和水纹数据显示效果很惊艳。Cesium ion 免费额度基本够开发测试用但正式上线如果请求量大还是建议评估一下成本或者干脆把地形瓦片同步到自己的服务器上。这里提醒一句Cesium 新版本里CesiumTerrainProvider的构造函数已经改为异步工厂方法fromUrl()/fromIonAssetId()老版本里new Cesium.CesiumTerrainProvider({ url })的写法在新版本里会直接报错。如果你从网上抄了一段老代码发现地形一直加载不了先检查是不是这个原因。3.2 本地化GeoTIFF 转地形切片的完整链路国内生产环境最稳妥的做法还是把高程数据转成 terrain 格式放在自己的对象存储或 CDN 上。转换工具链我实测下来比较顺的是这条先把 GeoTIFF 等原始数据用 GDAL 重投影到 WGS84 / EPSG:4326统一坐标基准再使用支持 quantized-mesh 输出的工具生成地形瓦片。常见的工具包括 Cesium ion 的上传转换、开源社区的 Cesium Terrain Builder、以及一些商业 GIS 软件的地形切片模块把生成的tileset.json和瓦片目录部署到任意静态服务器或 OSS前端通过CesiumTerrainProvider.fromUrl()加载。const terrainProvider await Cesium.CesiumTerrainProvider.fromUrl( https://your-cdn.example.com/terrain/, { requestVertexNormals: true, requestWaterMask: true, } ); const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: terrainProvider, });requestVertexNormals这个参数很多人忽略但如果你需要地形受光照影响产生明暗面或者做地形压平和动态光照效果必须开启它。requestWaterMask同理想要海岛海岸线或者模拟水面效果就必须开。这两个参数会额外请求法线瓦片和水纹瓦片体积和请求数会增加但视觉提升很明显值得。3.3 数据服务型ArcGIS 地形服务和其他远程地形除了自建瓦片和 Cesium ion生产环境中也经常对接现成的地形服务。ArcGIS 的 ImageServer 里很多高程服务可以通过ArcGISTiledElevationTerrainProvider直接接入。这类方式的优点是不用自己处理转换缺点是对服务端性能依赖很大瓦片请求不稳定时地形加载会一卡一卡的。还有一个不能忽视的方向是 Cesium for Unreal、Cesium for Unity 这类引擎插件。虽然它们的 API 和 Web 版本不完全一样但高程数据的源头和瓦片切分机制是共通的。你在 Unreal 里看到的山脉和 Web 端其实是同一套地形瓦片理解了 Web 端的加载逻辑再去看引擎插件的 CesiumTerrainProvider 配置就不会发怵。3.4 地形 Provider 切换的细节换地形最让人抓狂的一个问题是把viewer.terrainProvider赋了新值之后画面并不会立刻刷新重绘尤其是你开了requestRenderMode: true时场景会停在那里纹丝不动。这个不算 bug而是 Cesium 的渲染循环被优化了没有事件驱动就不会重绘。解决方法是手动触发一次渲染viewer.scene.requestRender();另外切换地形 Provider 时原本使用CLAMP_TO_GROUND的实体位置会在一瞬间重新计算表现就是模型先轻微跳动再落定。如果你的业务对位置敏感建议在切换完成前先把相关实体隐藏等terrainProvider的首个瓦片加载完成后再显示。判断首个瓦片是否就绪可以通过检查viewer.scene.globe.tilesLoaded或者监听地形 provider 的 error 事件来实现。4. 取高程值的 API 用了不少真正好用的就这几个4.1 sampleTerrainMostDetailed批量采样地形很多业务场景并不是要渲染地形而是要在某个经纬度上拿到地形高度。比如放置设备、无人机航线规划、土方计算都需要批量采样高程。Cesium 官方 API 里最顺手的函数就是Cesium.sampleTerrainMostDetailed。const positions Cesium.Cartographic.fromDegreesArray([ 116.391, 39.907, 121.473, 31.230, ]); const updated await Cesium.sampleTerrainMostDetailed(terrainProvider, positions); updated.forEach((p, i) { console.log(第${i 1}个点地形高度${p.height.toFixed(2)} 米); });这个方法会自动请求当前区域能拿到的最高细节层级返回的Cartographic对象里height字段就是该点地形高程。注意它是一批一批异步抓瓦片如果一次采样的点数非常多网络请求会瞬间拉满。我一般会把几千个点切成小批次每批 50~100 个并发避免把地形瓦片服务打崩。还有一个旧版 APICesium.sampleTerrain需要手动指定 level 参数。 level 太小采样不准 level 太大请求瓦片数量爆炸坑不少。新项目直接放弃它用sampleTerrainMostDetailed就好。4.2 clampToHeight把模型精准“钉”在地上另一类高频需求是给模型找一个精确接地的高度。最简单的方式是设置heightReference: Cesium.HeightReference.CLAMP_TO_GROUND让 Cesium 自动计算但它实际上会占用额外的渲染进程实体一多性能就会掉。生产项目里我更推荐手动算好高度再摆放模型。const carto Cesium.Cartographic.fromDegrees(116.391, 39.907); const height await viewer.scene.clampToHeight(carto, [], 1); if (Cesium.defined(height)) { // 用这个 height 创建实体 viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.391, 39.907, height), model: { uri: device.glb }, }); }clampToHeight的第二个参数是objectsToExclude第三个参数是采样宽度默认是 1 像素。这里有一个细节如果场景里同时有 3D Tiles 建筑物clampToHeight 默认会连建筑几何体一起采样导致模型被“钉”在楼顶而不是地面。这种情况下你必须在第二个参数里把建筑 tileset 传进去排掉。const height await viewer.scene.clampToHeight(carto, [buildingTileset], 1);踩过这个坑的人应该不少——摆了半天的设备全部跑到了屋顶上排查到半夜才发现是这个参数的问题。4.3 scene.pickPosition 和 camera.pickEllipsoid鼠标拾取隐藏的高程鼠标点击场景获取地表位置是所有交互功能的地基。viewer.scene.pickPosition(movement.position)可以把屏幕像素反算成世界坐标如果该点恰好落在地形网格上就能拿到包含高程的准确坐标。但它有个适用前提必须开启viewer.scene.globe.depthTestAgainstTerrain true否则 Cesium 不知道你要拾取的是地形还是背景拾取结果经常是一个莫名其妙的椭球面点。如果你只是想要一个“鼠标点一下告诉我这块地海拔多高”的简单功能还有一种更稳的做法先用camera.pickEllipsoid拿到椭球面上的经纬度再用sampleTerrainMostDetailed取高程。这样不依赖深度缓冲也不会受建筑遮挡影响缺点是稍慢一点但在交互平移到地形加载完成后误差很小。这两条路线的取舍很简单场景交互要实时、要准用pickPosition后台计算做分析用sampleTerrain或clampToHeight。混着用容易把自己绕晕建议一个项目里统一好。4.4 这些 API 的适用场景对照表API返回值适用场景注意事项sampleTerrainMostDetailed地形高程数组批量采样、航线规划、后台分析大批量要分批请求clampToHeight贴地后的高度放置模型、获取贴地位置注意排除 3D Tiles 建筑viewer.scene.pickPosition世界坐标鼠标拾取、编辑交互需要开启深度测试camera.pickEllipsoid椭球面坐标快速求经纬度不含真实地形高度HeightReference.CLAMP_TO_GROUND自动贴地少量展示性实体实体过多时性能下降5. 高程数据落地时我踩过的那些坐标系和精度坑5.1 椭球高、正高、海拔三个“高度”不是一回事这个坑坑了很多人因为它藏得很深。我们常说的“海拔”是基于大地水准面的正高GPS 设备返回的高度则是相对于 WGS84 椭球面的椭球高两者之间差一个大地水准面差距geoid undulation。在中国区域这个差距大概在负十米到正三十米之间浮动不同地方差异还不一样。也就是说如果你手里有一份高程数据标注着“海拔 500 米”但 Cesium 地形内部用的是 WGS84 椭球高那两者直接在高度上就差了十几米到几十米。模型摆上去后整体偏高或偏低就是数据基准没对齐。怎么判断你的数据到底用的哪个基准最直接的办法是取一个已知控制点做比对。比如找个 GPS 实测点把它的椭球高和这份高程数据在该点的值相减如果差值稳定在一个常数附近就把它作为整体偏移修正值加进去。Cesium 也提供了geoidHeight相关的支持但从我的经验看项目中更常用的是先确认数据定义再做统一偏移逻辑更简单可控。5.2 中国区域内 CGCS2000 和 WGS84 的细微差异中国目前的法定测绘基准是 CGCS2000它和 WGS84 在大多数应用场景下可以近似等同但严格来说两个椭球的定义参数并不完全一致。在实际坐标转换中平面位置差一般是厘米级小比例尺地图根本看不出来但在城市级高精度项目中这个厘米级差异可能会反应在建筑边缘和地形对齐的微小偏差上。更麻烦的是高程基准。CGCS2000 框架下的高程通常采用 1985 国家高程基准它的零点定义和全球大地水准面也不是完全一致在西部局部地区差异会更明显。对 Cesium 这种国际上以 WGS84 为默认基准的引擎来说国内数据直接叠加时要做严密的基准核查最保险的方式还是拿着项目区域的控制点矩阵做一次全面的高程匹配测试别凭感觉给一个固定偏移值。5.3 地形加载不刷新、模型浮空、地形闪烁的排查路径我把开发中最容易遇到的三个问题集中排查一下因为它们的表象非常相似经常被混为一谈。地形加载后画面不刷新通常就是渲染循环没有被触发。排查步骤很固定先看是否开了requestRenderMode开了就先scene.requestRender()手动触发再看是否用了异步创建 terrainProvider但赋值之前没等它返回最后看是不是每次切换 Provider 后旧瓦片和新瓦片没有清理干净。模型整体浮空多半是高程基准问题也就是上一小节说的椭球高和正高的差异。如果整体浮空高度在所有地方都差不多那基本可以确定是固定偏移如果高差随地形起伏变化说明数据本身基准就分叉了需要做区域级别的校正。地形闪烁则优先怀疑深度测试和 LOD 切换导致的 z-fighting。打开深度测试后闪烁反而更明显说明是相邻 LOD 层级之间的几何误差造成了表面重叠。这类问题可以从两个方向解决一是调整相机的maximumScreenSpaceError让 LOD 切换更平滑二是检查是否存在多个贴地实体和地形完全共面给实体加一个微小的高度偏移例如 0.05 米即可消除闪烁。5.4 大范围场景下的高程精度与性能平衡全球级场景直接加载最高细节地形是灾难性的浏览器可能瞬间崩掉瓦片请求堆积如山。这里有个热词我经常在交流群里看到Cesium 3D 地球滚动出现崩溃。很多崩溃并不是 Cesium 本身的问题而是地形瓦片请求量瞬间爆发或者 3D Tiles 和地形瓦片同时抢带宽把服务器和浏览器一起拖垮。解决办法不外乎几板斧限制地形最大 LOD 层级不要无限请求高层级瓦片调大maximumScreenSpaceError让远处用更粗糙的网格合理设置viewer.scene.globe.tileCacheSize避免内存无限增长必要时预加载重点区域的地形瓦片手动控制请求节奏。性能优化的本质是告诉 Cesium“哪里重要多花资源哪里不重要省着点”这和业务需求强相关——你来之不易的 1 米地形不要在整个地球范围内全量加载只在你关心的园区边界内做高精度其他区域用 30 米全球数据兜底这是性价比最高的布置。6. 别忘了这些业务场景才是高程数据的真正出口6.1 沿地形飞行的航线规划航拍、巡检、路径巡航这些功能都要让相机贴着地形往前走不能撞山也不能飞得太高。实现思路很朴素先在路径上均匀插值一串经纬度点用sampleTerrainMostDetailed批量取高度给每个点加上一个离地偏移量再把结果喂给SampledPositionProperty驱动相机。这样相机就能沿着地表起伏走高度始终保持在地面上方一个固定值。不过有个细节新手容易忽略取到的地形高度序列直接驱动相机时相机高度会在每个采样点之间“咔咔”跳因为采样点是离散的高度差会形成台阶。解决办法是对高度序列做滑动平均平滑或者用 Catmull-Rom 样条插值让相机高度曲线连续可导。实际飞行效果是否顺滑和这一步的处理精细度直接相关。6.2 通视分析和雷达遮蔽计算做雷达、通信、监控布点这类项目时通视分析是核心算法。算法本身不复杂在两点之间线性插值 N 个采样点每个点算一条视线内插高度再和该点的地形高度比较一旦视线高度低于地形高度就说明视觉被遮挡了。这里最关键的细节是插值高度和地形高度必须在同一高程基准下比较否则整个判定全是错的。另外采样点数量要足够山地地形插值太稀会漏掉关键遮挡点平原可以适当稀疏。这些参数都需要根据具体区域的地形起伏反复调没有统一标准。对于 Cesium 项目还可以把 Ray 和地形做真正的三维求交比插值更精确但实现成本高一些。折中方案是先用 Cesium 的scene.pickFromRay做粗筛再做细插值判定效率和精度都能兼顾。6.3 淹没分析思路洪水淹没、雨涝分析这类需求通常有两条可视化路径。一条是在 GPU 上做高程比较给定一个水位高度遍历地形网格顶点凡是低于水位的顶点就标记为淹没区。这样性能好适合实时调整水位高度看效果。另一条是分析级输出把淹没范围导出成面要素这就需要先生成水位等值线再做等值线和地形范围的空间裁剪。这个方案可以用 Cesium 的等值线工具配合 turf 或者 shp 工具链实现输出结果可以直接给客户做二开分析。做这类功能时一定要盯住一个核心假设水位高度和地形高度是否在用同一个高程基准。这看起来是小事但处理不好就会出现“水从山上流过”的诡异现象客户可不管你是椭球高还是正高看到效果不对就是你的锅。6.4 和 3D Tiles 单体化结合时的注意点最后聊一个和 3D Tiles 单体化相关的结合点因为很多项目同时使用了单体化和地形高程。单体化建筑的底部高程如果不贴合地形建筑不是陷进山坡就是悬在坡外。处理方案通常是在单体化数据预处理阶段用建筑底面中心点去采样地形高程把采样结果作为建筑模型的基础高程写入属性。如果地形有明显坡度还要考虑建筑底面的旋转倾斜对齐否则只看底面中心点仍然会穿模。另一个推进方向是“地形压平”在重点园区里把地形面强制压到某个统一标高让建筑底面和道路完全平整衔接。这个功能做起来比听起来麻烦一些需要修改地形瓦片或使用 3D Tiles 的地形修改接口但场景效果非常专业。工程化落地时我一般结合项目需求决定是否对园区边界内的地形做局部压平能解决大量“模型和地形打架”的问题。最后分享一个我自己坚持了很久的工作习惯每个新项目开工前先写一个高程数据健康检查脚本把项目区域内的地形采样一遍输出高度范围、极值、是否存在空洞和异常凸起。别跳到业务功能开发之后才发现地形数据有问题到那时候再回头换数据源改动的成本会成倍增加。高程数据这种东西越早验证后面的坑越少。
返回列表