☰
Cesium离线地形数据生成全攻略:从DEM处理到地形瓦片部署
2026/10/15 2:38:01 网站建设 项目流程

做三维GIS的人迟早会撞上地形数据这道坎。Cesium的地形渲染本身很成熟,但真正麻烦的是“拿一份能落地的地形数据”往往比写JS代码更折腾。前阵子某项目需要在指定区域展示带有河流、山脊和高差起伏的三维场景,同时又要求离线部署,不能依赖在线地形服务,数据精度和范围都必须自己控制。我前后折腾了两三天,用一条本地生产线把公开的DEM高程数据处理成了Cesium可以直接加载的地形瓦片服务。这篇就完整记录这次Cesium地形数据生成过程,从选型、预处理、切片到前端接入,包括路上踩过的坑和最后定下来的参数。

我在项目里见过太多同事卡在地形数据这一环:要么数据加载出来是平的,要么缩放到某个级别突然模糊成一片,要么瓦片根本请求不到。问题多半不是Cesium本身,而是数据生产的源头没做对。如果你也在准备做Cesium离线地形,或者对默认地形服务不满意,这篇应该能让你少试错好几轮。

1. 为什么非要自建地形数据:在线地形服务的三个痛点

1.1 在线服务能演演示,但扛不住项目交付

很多人一开始都用Cesium官方在线地形服务,接入确实简单,createWorldTerrain一行代码全球地貌就出来了。但放到实际项目中,问题会一个接一个地浮出来。

首先是对外网依赖的问题。项目到了验收阶段往往要进内网环境,现场没有外网,地形数据直接加载不出来。就算能临时开个网络策略,每次演示前都要先检查网线,这种不确定性在汇报现场非常致命。我这次接到的需求里,“离线部署”是硬约束,这一条就直接判了在线服务的死刑。

其次是精度和外观可控性。在线全球地形数据在宏观尺度看没问题,镜头一拉到近地面,很多区域的地貌细节就完全不够用了。默认地形在大多数平缓地区看起来就是一片糊掉的绿色,没有沟壑、没有梯田层级、没有山脊线,项目里需要“地形能看出真实地貌”时根本顶不住。

第三个痛点其实是服务条款边界。线上服务的免费额度、商用授权、调用量限制,条款里写得都比较宽泛,实际能不能用于长期运行的商业项目,不同服务商有不同的说法。与其在高风险地带反复试探,不如直接把数据源掌握在自己手里。自建地形数据一次生成、静态部署,没有任何外部依赖,反而最稳妥。

1.2 这次项目到底要什么样的地形数据

这次要覆盖的范围不算大,大概几百平方公里的山区向丘陵过渡区域,但要求一点不少。地形表面必须能看出山脊和冲沟的走向,不能是一片光滑缓坡。河流区域要能压平,水面不能跟着河床一起起伏,这就需要水纹掩膜(water mask)的支持。镜头的观察层级要从默认的12级左右提升到16级,贴近地面时还能有足够细节。另外所有请求都必须命中局域网服务器,不能有任何外网流量。

这几个需求叠加在一起,基本可以确认“用默认地形再改改”这条路走不通,只能从头走一遍“原始DEM → 地形切片 → 本地服务 → Cesium加载”的完整链路。整个过程有三个前置决策直接决定了最终结果的质量:数据源选什么、切片工具用什么、地形参数怎么调。

2. 地形数据源选型与格式原理:动手前先想明白两件事

2.1 公开DEM数据源怎么选

做地形切片,必须先有原始DEM栅格。目前可用的公开高程数据源不少,但是分辨率、覆盖范围和数据质量差异很大。我整理了这次对比过的主要方案:

数据产品空间分辨率覆盖范围特点适用场景
SRTM 1弧秒约30米全球大部分区域老牌开源数据,稳定,但在陡峭山区有空洞大区域宏观地形
ASTER GDEM约30米全球覆盖完整,但局部有云影噪声,需要人工检查跨区域连续地形
ALOS AW3D30约30米/12.5米全球精度较好,山区细节更细腻中小范围高精度展示
Copernicus DEM30米/90米全球相对较新的数据,质量均衡大多数三维场景的主力选择

我这次选的是ALOS系列的12.5米分辨率产品。原因很简单:项目覆盖范围小,数据体量完全能吃得下,但细节表现要求又比较高,30米的数据在16级放大后还是会出现明显的阶梯感,而12.5米在贴近地面时基本能保持连续平滑的坡面过渡。如果换成全国或全省量级的大范围地形,我不会直接用这么高分辨率的数据,那样瓦片数量和生成时间都会爆炸,通常会降到30米,甚至更大范围直接用90米。

这里有个容易忽略的经验:分辨率不是越高越好,要看最终观察层级。Cesium地形瓦片本质上是多分辨率金字塔,如果原始DEM分辨率太高,而最大切片层级只有16,高分辨率的红利根本用不上,还会白白增加生成时间和磁盘占用。合理做法是根据“最大层级对应的地面分辨率”反推原始数据需要多密。16级在赤道附近的地面分辨率大约是4.7米左右,但地形瓦片是对原始数据做重采样,12.5米原始数据配合16级切片其实已经比较合理,再往上做到18级以上才需要考虑用更高精度的数据。

2.2 理解Cesium地形格式的核心机制

直接拿着TIF格式的DEM放进Cesium是没用的,Cesium不能直接渲染高程栅格,它需要的是按层级组织的网格地形瓦片。这里得先搞懂Cesium的地形数据格式到底长什么样。

Cesium默认使用量化网格(Quantized-Mesh)格式的地形瓦片,每个瓦片是一个紧凑的三角形网格,存储网格顶点坐标、顶点法线以及可选的误差值。渲染时通过四叉树方式按需加载。和常见的图片瓦片(每个瓦片是PNG/JPEG)完全不同,地形瓦片是二进制文件,文件名通常是z/z/x/y.terrain这种结构。Cesium在运行时根据相机位置判断需要加载哪些层级的瓦片,然后通过HTTP请求去拿对应的二进制文件,解析成地形网格进行渲染。

理解了这套机制,你就能明白几个必须注意的问题。第一,瓦片目录层级不能错,Cesium会严格按照z/y/x的路径去请求,少一层或多一层都会404。第二,每个瓦片里必须包含层级间的“跳转信息”,Cesium称之为网格误差或屏幕空间误差,没有这个数据,LOD切换时就会出现“地形闪烁”或者“穿帮”。第三,可选的water mask和vertex normals分别是两个独立的扩展,生成数据时没有生成,前端请求时就拿不到对应的数据块,容易报错。

刚开始接触的人很容易把这套流程理解成“把TIF再切片成图片瓦片”,然后对着图片瓦片一直找地形数据为什么加载失败。这个方向性的误解最浪费时间。地形切片生成的是二进制网格,不是图片。

2.3 工具链的取舍思路

我这次实测对比了主流的两个生成工具。一个是用Python写的CesiumTerrainBuilder,一个是C++实现的ctb。两者都能生成Cesium支持的Quantized-Mesh格式地形瓦片,但定位不太一样。

我最后选择CesiumTerrainBuilder来输出主流程。原因是它的参数设计对新手友好,water mask和vertex normals的生成开关很明确,生成的目录结构完整,自带metadata.json,前端接入不容易出低级错误。当然它的缺点是生成速度偏慢,处理大范围数据时CPU跑满的时间会比较长。ctb的优势是快,但命令行参数更“工程化”,如果你对地形瓦片的底层结构不熟悉,初上手时容易漏掉一些必要的参数,排查起来会有点折腾。

GDAL是另外一个离不开的工具,所有DEM预处理的活都由它来做。我强烈建议在动手生成切片之前,花半小时把GDAL的空间数据处理命令过一遍,gdalinfo、gdal_translate、gdalwarp、gdal_fillnodata这几个命令基本覆盖了90%的场景。QGIS主要用来做可视化检查,毕竟光看数字很难判断地形到底自然不自然。

3. 完整实操流程:从DEM栅格到Cesium离线地形瓦片

3.1 原始数据下载与范围裁剪

整个流水线第一步是拿到原始数据并确认它符合预期。我下载到的原始DEM通常是标准GeoTIFF格式,里面有多个波段,但地形只需要一个高程波段。第一步先用gdalinfo检查数据的坐标系、分辨率和有效范围。

gdalinfo input_dem.tif

这一步要确认三个关键信息。坐标系必须是WGS84经纬度(EPSG:4326),如果返回结果是UTM或者其他投影,后面所有步骤都会出问题。像素分辨率要和你预期的数据精度对得上,比如12.5米数据在经纬度坐标系下大约是0.000138度左右。有效值范围要正常,不要出现离谱的负值或者异常高的峰值。

确认数据没问题之后,按目标范围的四个角进行裁剪。这里有一个特别容易记混的点:gdal_translate的范围参数顺序是-projwin west north east south,也就是“西经、北纬、东经、南纬”,不是我们习惯的“左下、右上”那样按X1 Y1 X2 Y2排。

gdal_translate -projwin 117.8 30.6 118.4 29.8 input_dem.tif clipped.tif

很多人在这个顺序上翻车,裁剪出来的区域要么是反的,要么位置完全不对。建议裁完以后用gdalinfo clipped.tif再检查一遍范围,确定没有裁到项目区域之外。

3.2 高程缺失修复与重投影

DEM原始数据几乎不可能是完美的。常见的坑是空洞,尤其是在陡峭山区和峡谷区域,有些数据源的雷达回波覆盖不到,就会形成一块块黑色的无值区。如果直接拿带空洞的DEM去生成地形瓦片,Cesium场景里对应的区域就会出现凹陷或者穿地现象,非常难看。

处理空洞用的命令是gdal_fillnodata,它会对指定半径范围内缺失的数据做插值补齐。注意这个算法只适合填补小面积空洞,如果整块区域都无效,插值出来的地形会非常假。

gdal_fillnodata -md 50 clipped.tif filled.tif

-md 50表示搜索半径最大为50个像素,超过这个范围的空洞保持原样。我习惯从较小的半径开始,填完以后在QGIS里看一眼,不够再加大。过度填充会让地形看起来像被磨过皮,失去真实的沟壑细节。

接下来是坐标系标准化。虽然原始数据声明是WGS84,但不同数据产品的存储细节有差异,有的会带旧的椭球体参数,稳妥起见直接用gdalwarp强制重投影到EPSG:4326,并顺便用双线性插值保证高程值过渡平滑。

gdalwarp -t_srs EPSG:4326 -r bilinear filled.tif output_wgs84.tif

这一步做完,再检查一下高程值范围。如果数据源来自某个采用英尺作为单位的地区,高程值需要同步换算成米,不然生成出来的地形会呈现出一种极其夸张的陡峭感。换算很简单:

gdal_calc.py -A output_wgs84.tif --outfile=output_meters.tif --calc="A*0.3048"

确认无误后,最终的预处理DEM就准备好了。标准化的核心只有两条:坐标系是WGS84,高程单位是米。

3.3 地形瓦片生成与关键参数选择

预处理完成后进入切片环节。我用CesiumTerrainBuilder生成瓦片,命令行大概是这样的:

python3 cesiumterrainbuilder.py output_meters.tif ./cesium_terrain/ \ -n \ --start-level 5 \ --end-level 16

不同版本参数略有差异,进入生成前先用python3 cesiumterrainbuilder.py -h确认一下当前版本的参数说明。以下是我这次使用的参数含义。

-n表示生成顶点法线贴图。这个参数对近地面的真实感影响非常大。没有法线数据时,Cesium只能靠高程差来表现地形起伏,光线下缺乏细节阴影;有了法线数据后,山坡朝向、岩石纹理的明暗变化都会真实很多。如果你的场景不需要特别强的地形立体感,这个开关可以先不开,但在高精度展示类项目里强烈建议打开。

--start-level和--end-level控制瓦片金字塔的范围。起始层级默认情况下工具会自动计算,不建议强行设太小或者太大。如果起始层级太低,会把一块很小的区域放大到全球网格里,生成大量毫无意义的冗余瓦片。结束层级则需要根据观察需求来确定,我这次设到16级,实测在镜头拉近到十几米高度时依然有足够细节。

water mask的处理是另一个容易忽略的点。如果你需要河流、湖泊区域的水面是平的,必须在生成时使用-l参数指定水纹平滑级别。它的原理是对指定范围内的地形进行平滑处理,让水面区域不会出现坑坑洼洼的起伏,同时生成一个二进制水纹掩膜文件。前端加载时配合requestWaterMask: true,Cesium才能正确渲染出水面效果。这里我特别想提醒一句:不要指望数据里自动带water mask,很多公开DEM源根本没有水域分类信息,河流区域的地形同样跟着地形起伏。如果项目需求里有水域平面特效,必须在生成阶段就把水纹处理做进去。

生成完毕后在输出目录会看到metadata.json和tiles/子目录。我曾经见过有人自己改了瓦片目录结构或者手动修改了metadata.json里的层级范围,结果前端要么请求不到瓦片,要么瓦片加载出来层级错乱。正确做法是生成后不动目录结构,只做静态部署。

3.4 用Nginx部署地形服务并配置跨域

地形瓦片本质上是静态文件,不需要后端接口支持。我用Nginx托管整个cesium_terrain目录就行。但这里有一个关键坑:跨域访问。

浏览器中运行的Cesium页面,如果地形服务地址和页面地址不同源,瓦片请求会被CORS机制拦截。开发阶段可以通过代理绕过,但生产部署必须要在Nginx里添加跨域响应头。我在Nginx配置里加了一层专门的地形服务location块:

location /terrain/ { alias /data/cesium_terrain/; add_header Access-Control-Allow-Origin *; }

这里还有第二个容易被忽略的坑:地形瓦片请求是带Range头的分段请求。Cesium在加载较大的地形瓦片时会发起HTTP Range请求,只获取需要的字节段,提高加载效率。如果服务器不支持Range请求,瓦片可能会返回完整数据,虽然也能渲染,但大场景下网络消耗会明显上升。Nginx默认支持Range,如果你用的是其他静态文件服务器,务必确认这一项。

我实际测试过,生成完的地形瓦片目录大约2.1G左右,Nginx开启后局域网内加载速度很快。如果磁盘空间紧张,可以考虑只保留15级的最大层级,体量能缩小将近一半,但近地面细节会稍微妥协。

3.5 在Cesium中接入地形数据

地形服务准备好之后,前端接入就比较直接了。我用的是Cesium 1.107版本,加载方式如下:

const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl('/terrain', { requestVertexNormals: true, requestWaterMask: true }); viewer.terrainProvider = terrainProvider;

如果你的Cesium版本比较老,比如1.90之前,加载地形的API可能不是fromUrl静态方法,而是new Cesium.CesiumTerrainProvider({ url: '/terrain' })。这里建议先确认当前项目使用的Cesium主版本,不同版本的API差异还挺明显的,直接照抄新写法在老版本里会报“fromUrl is not a function”。

接入完成后,我写了一段测试脚本,不断调整相机的拍摄角度和高度,从宏观视角慢慢推进到贴近地面的视角,重点观察山脊线、河流沿线和坡度突变区的渲染效果。如果一切正常,地形会随着镜头高度下降而平滑细分,不会出现明显的闪动或穿透。到这里,自建地形数据的主链路已经通了。

3.6 效果验证:不只是“能显示”,还得对得起需求

能显示只是第一步,这次项目的具体要求还得逐条验证。

第一是山脊和冲沟的识别度。镜头拉到约两千米高度,从侧光方向观察,山体表面要有明显的凸起和凹陷纹理。这里我用了Cesium的光照参数,让地形接受光源影响,增强立体感。如果地形表面看起来一片平光,大概率是顶点法线没生成或者前端没有请求法线数据,回头检查生成命令和前端请求参数即可。

第二是河流压平效果。把镜头对准项目区域内的主要河道,水平旋转视角观察水面是否平整。我在测试中发现局部河段的水面还是有一点坡度感,于是把水纹平滑级别从默认值调整为更高,重新生成了一次瓦片才达到预期效果。这里想强调的是,水纹平滑级别调高会同时平滑掉河道附近的地形细节,所以不要一刀切拉到最高,要在“河流平”和“岸线自然”之间找平衡。

第三是离线和性能。断掉外网后,重新加载页面,所有地形瓦片都从局域网服务器获取,加载速度和稳定性都不错。测试时我仔细观察了Network面板,确认没有任何请求走到外网,数据完全自持。

4. 踩坑实录:地形生成中容易翻车的六个细节

4.1 数据范围偏差导致半边黑屏

第一次生成瓦片时,我发现地形加载出来后只有一半区域有地形,另一半完全是黑色空洞。第一反应是瓦片生成不完整,但检查生成日志后发现瓦片数是完整的。最后排查到原因是原始DEM的范围和目标区域有偏差,裁剪时把一部分无用区域也裁进来了,而那片区域在原始数据里就是无值空洞,补洞插值也没有完全覆盖。重新下载严格对应项目范围的原始分幅数据,再走一遍流程就正常了。

这个问题的本质其实是数据源的范围粒度问题。公开DEM数据通常按分幅或分块发布,一个分幅文件往往横跨好几个经度纬度,直接裁剪时容易带入相邻无效区域。建议裁剪前先在QGIS里加载原始数据,目视确认目标区域每个像素都有有效高程值,再进入裁剪环节。

4.2 层级深度不够造成的远景模糊

第一次测试时,我把最大层级设到了15级,但场景里远景的山体看起来依然有明显的“块状感”。尤其在拉远镜头时,地形过渡不够平滑。原因在于层级数决定了地形网格的细化程度,而Cesium会按屏幕空间误差来决定加载哪个层级的瓦片,15级在近处够用,但是在中等距离上,过渡就会有台阶感。最终我把最大层级提到16级,整体视觉质量明显改善。

这里也有个反直觉的地方:如果起始层级设得太高,比如直接从第8层开始生成,那么在低层级场景里会没有瓦片可用,Cesium只能显示一个平面。起始层级怎么设要结合数据范围大小来看,这个交给工具的自动计算通常是最稳妥的。

4.3 前端请求瓦片一直404

在第一次接入时,浏览器控制台疯狂报404错误,打开Network面板看到请求地址是/terrain/16/36231/21897.terrain这类路径。检查部署目录后发现,文件确实存在,问题出在Nginx的别名路径配置不一致。我配置的是alias /data/cesium_terrain/;,但目录内部实际结构是/data/cesium_terrain/tiles/16/...,而请求直接落在了/terrain/16/...。修正的方式有两种:把alias指向/data/cesium_terrain/tiles/,或者把生成目录里的tiles子目录内容移动到外层。这里想提醒大家,先确认请求路径和文件实际路径的对应关系,再去找数据问题,很多时候问题比你想象的简单。

4.4 CORS跨域导致瓦片加载被浏览器拦截

这个坑在开发环境最容易出现。页面跑在localhost:8080,地形服务跑在localhost:8090,浏览器认为这是跨域请求。Cesium发出瓦片请求后被浏览器拦截,控制台报的错误经常是“CORS policy: No 'Access-Control-Allow-Origin' header”。解决办法除了在Nginx配置跨域头,也可以直接用页面开发服务器的代理功能。我这次图省事,直接在Nginx里加了add_header Access-Control-Allow-Origin *;,一步到位。生产环境如果对跨域有更严格的要求,可以把这个响应头换成具体域名,而不是全量通配。

4.5 地形闪动和梁缝出现

在某个山区段,相机移动时地形表面会偶尔出现细小裂缝,尤其是在陡峭坡面上。排查了很长时间,最后发现是我在没有顶点法线的情况下,同时开启了光照效果。Cesium在计算光照时需要法线数据,缺失时硬件插值会计算出一个不太稳定的光照结果,视觉上表现为闪动。重新生成时加上-n参数,并把前端请求改成requestVertexNormals: true,闪动问题就消失了。如果你不太需要光照效果,也可以直接关闭光照排除这个干扰项。

4.6 数据体积膨胀问题

生成完2.1G的瓦片数据时,我一度怀疑哪里出了问题。仔细看瓦片数量和分布,发现很多层级存在大量重复的相似地形,这是因为原始DEM的纹理信息丰富,导致量化网格在细节较多的区域不得不划分得更细。体积优化有几个可行的方向。限制最大层级到16级可以省下大量高层级瓦片。如果原始数据是12.5米,而观察不需要那么细,可以先用GDAL把DEM重采样到30米,再生成切片,体积能降一半以上。另外,Nginx开启gzip对二进制瓦片效果比较有限,因为.terrain瓦片本身已经比较紧凑,不建议在这个方向上花太多时间。

我整理了一份高频问题速查表,后续再碰地形相关问题可以直接对表排查:

现象可能原因处理办法
地形半边黑色或空洞原始DEM存在无效区域gdal_fillnodata或更换数据源
地形整体偏移坐标系不是WGS84gdalwarp重投影到EPSG:4326
地形特别陡峭高程单位不是米用gdal_calc换算单位
瓦片请求404部署目录层级不对检查请求路径和实际文件路径
瓦片被浏览器拦截跨域头缺失Nginx添加CORS响应头
镜头拉近后地形模糊最大层级不够调高end-level参数
地形表面闪动顶点法线缺失生成时加-n参数
河流区域起伏不平未生成water mask生成时-l参数并配平
加载慢瓦片层级太高或数据太密降级DEM分辨率或限制最大层级
LOD切换穿帮metadata.json层级不匹配重新生成瓦片,不动目录

5. 复盘与建议:一条可复用、可扩展的地形数据生产路径

5.1 几个决定成败的关键决策

这次完整走下来,我复盘出三个决定成败的决策点。

第一,在切片前把预处理做干净。坐标系统一、高程单位换算、空洞插值,这些看起来琐碎的步骤,任何一步漏掉,生成出来的瓦片都会有奇奇怪怪的问题。我第一版直接拿原始DEM生成,结果地形明显偏移了几十米,排查了很久才发现是坐标系问题。现在我会把“标准化”当作流水线中间不可跳过的强制性环节。

第二,生成参数要和前端请求严格对应。-n决定了有没有顶点法线,-l决定了有没有水纹掩膜。前端如果请求了这些数据,服务端必须提供;前端如果没请求,数据生成时开太多扩展也不会提升渲染效果,反而增加文件体积。建议花两分钟把生成参数和前端加载选项做成一张对照表,放进项目文档。

第三,范围明确后再决定数据源分辨率。目标的观察最大层级、目标区域大小、原始DEM分辨率,这三个变量必须匹配。大范围低精度需求用12.5米数据属于浪费,小范围高精度需求用30米数据属于不够。看起来是简单的常识,但我在实际项目中见过太多因为在选型阶段少问一句“镜头最大要放大到多少米高度”而导致返工的情况。

5.2 后续自动化扩展方向

目前这条流程已经能稳定复现,但描述起来还是偏手工。后续可以往自动化方向走一步。核心是把“范围四角坐标、数据源标识、起始层级、结束层级、是否需要water mask和法线”这些参数抽出来,做成一个配置文件。每次新项目来了,改一行配置,跑一次脚本,就能批量产出瓦片。甚至可以把DEM预处理也做成同一套脚本里的固定环节,用GDAL命令自动完成下载、重投影、裁剪、插值,再接力给切片工具。

我这次没有做全自动,主要是考虑到项目周期不长,手工操作记录得更放心。但如果接下来的项目地形覆盖范围跨了好几个地市,或者需要按季度更新地形数据,自动化流水线基本是必须的。另一个可以考虑的方向是做增量更新:地形数据覆盖范围变化时,只重新生成变化的局部区域,而不是全量重新切片,能节约不少时间。

最后说一点个人体会。Cesium地形数据生成本身并不困难,难的是理解这个格式背后的两个隐含假设:数据必须是WGS84经纬度坐标,高程必须是米为单位的连续值。这两个前提只要对了,剩下百分之八十的坑都会自动绕开。如果你也在准备接离线地形,我的建议是一开始不要追求所有参数一步到位。先拿很小的一块区域,比如几平方公里的范围,从原始数据处理到Cesium加载跑通全流程,再逐步扩大到目标区域。这比直接铺整个范围再回头排查要高效得多。用最小代价踩完所有雷,后面的路自然就平了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询