简介:面向Cesium地形开发者的瓦片生成工具,重点解决TIFF高程数据到Cesium可加载Terrain格式的转换问题。工具附带CTB运行库及完整依赖,能将激光雷达或卫星遥感获取的数字高程模型按层级切分为瓦片,便于Web端按需加载。压缩包共108个文件,大小16.17MB,其中包含40个dll运行库、4个exe可执行程序、29个csv坐标与投影参数文件,以及gfs、wkt等地理数据描述配置,从数据解析、坐标系定义到地形服务发布均有覆盖。已有2368人浏览学习,适合GIS开发者、三维可视化工程师以及需要自定义地形的Cesium前端用户。借助该工具可生成JSON或Binary格式的地形瓦片,在Cesium中配置Terrain服务URL即可加载渲染,显著降低高程数据接入门槛;同时可用于测绘、城市规划、环境监测、灾害模拟等场景,支持将自有高程数据快速集成进三维地球应用,实现流畅的LOD地形过渡与批量瓦片发布。
1. Cesium 地形加载的卡点:为什么 tif 高程数据不能直接丢给 Cesium
拿到一块本地 tif 高程数据,想在 Cesium 里看到起伏的真实地形,第一反应往往是直接把 tif 丢给CesiumTerrainProvider。结果就是一片黑屏或者直接报错,原因很简单:Cesium 原生不认栅格 DEM,它只认切片好的地形瓦片服务,而这个切片过程基本绕不开cesium terrain builder(以下简称 CTB)这类瓦片地形生成器。
CTB 的价值在于把 GeoTIFF 格式的高程数据转换成 Cesium 能直接加载的 quantized-mesh 瓦片,附带 tileset.json 索引文件,最终扔到任意静态服务器上就能用。这篇文章适合手里有 tif terrain 高程数据、想在自己的 Cesium 项目里加载离线地形的开发者,也适合想搞清楚 CTB 参数边界、避免反复翻车的从业者。下面从数据预处理、CTB 实操、Cesium 接入到避坑逐层拆一遍。
2. 高程数据预处理:把杂乱的 tif 变成 CTB 吃得动的 DEM
2.1 先从坐标系说起:CTB 默认只认 WGS84
CTB 的输入数据默认要求是 WGS84 经纬度坐标系,也就是 EPSG:4326。很多人从国家地理信息公共服务平台或者 NASA 下载的 DEM 数据,投影五花八门,常见的有 UTM、Web Mercator(EPSG:3857)、Albers 等。如果直接丢给 CTB,生成出来的瓦片位置会出现几十到几百公里的偏移,而且完全看不出来是坐标系问题——因为 CTB 不会报错,它只是按经纬度把数据硬切片。
用 GDAL 工具先查看数据的坐标系,这是我每次处理地形数据的第一步:
gdalinfo input.tif | grep -E "EPSG|Origin|Pixel Size|Size is"我需要重点看三行:Coordinate System是否带EPSG:4326、Origin的纬度是否在 -90 到 90 之间、像素分辨率大约是多少。如果坐标系不是 WGS84,就需要重投影,用gdalwarp处理:
gdalwarp -t_srs EPSG:4326 -r bilinear input.tif output_wgs84.tif这里-r bilinear指定重采样算法为双线性,对地貌起伏的平滑性保持比较好。如果原始数据是山地区域,用-r cubic效果更圆润,但处理时间会明显增加。对地形这种连续表面来说,不建议用 nearest,否则生成的瓦片边缘会呈现明显的台阶状瑕疵。
2.2 裁剪与合并:控制生成的瓦片数量和边界
CTB 的-c参数需要指定切片的经纬度边界,如果不指定,它会按输入数据的全部范围来切。问题在于很多 DEM 数据是整块大范围数据,比如整个省的 30 米分辨率 DEM,直接切片会让瓦片数量爆炸,生成时间从几分钟变成好几个小时。
所以我在预处理阶段必做两件事:裁剪到项目实际需要的范围、合并多块覆盖同一区域的 tif。裁剪用gdalwarp就够:
gdalwarp -t_srs EPSG:4326 -te 116.2 39.4 116.6 39.9 -tr 0.0003 0.0003 \ -r bilinear origin_dem.tif beijing_area.tif-te后面跟的是最小经度、最小纬度、最大经度、最大纬度,单位是度。-tr是输出像素分辨率,0.0003 度大约相当于 30 米,这个值要和你原始数据的分辨率匹配,否则可能会让 gdalwarp 做无谓的重采样,损失精度。
多块 tif 覆盖同一区域的情况更常见,比如分幅下载的 DEM 数据,用gdal_merge.py先拼成一块整的:
gdal_merge.py -o merged.tif -create -ot Float32 tile_1.tif tile_2.tif tile_3.tif合并后的数据再统一裁剪、重投影。这里有个小地方要注意:-ot Float32强制输出为 32 位浮点,高程精度能保持到小数点后好几位,而默认的 Int16 整数型在某些低海拔平缓区域会丢掉关键的地形起伏细节,生成的地形看起来像「磨平」了一样。
提示:CTB 支持多个输入文件直接切片,不必每次手动合并。但如果文件数量多或坐标系不一致,合并后再切片能减少很多隐性错误。
3. CTB 实操与参数详解:一条命令从 tif 到地形瓦片
3.1 安装 CTB:源码编译最稳
CTB 的官方仓库提供源码,没有直接给二进制安装包。常见的安装方式是源码编译。我一般按以下步骤来:
git clone https://github.com/geo-data/cesium-terrain-builder.git cd cesium-terrain-builder mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release -DUSE_GDAL=ON -DUSE_CURL=ON .. make -j4 sudo make install编译完成后会得到两个可执行文件:ctb-tile用于生成地形瓦片,ctb-info用于查询瓦片信息。-DUSE_GDAL=ON必须开启,否则 CTB 无法读取 tif 格式;-DUSE_CURL=ON主要在从网络获取数据时才需要,本地切片可以不开。
如果你的开发机是 Windows,用 vcpkg 或者 MSYS2 环境也能编译,但我遇到的环境问题比较多,最后都是在 Linux 容器里跑。
3.2 核心命令:ctb-tile 参数逐个拆解
CTB 切片的核心命令是ctb-tile,一个典型命令长这样:
ctb-tile -f quantized-mesh -o ./terrain_tiles \ -c 116.2 39.4 116.6 39.9 \ -l 15 \ -e 0.05 \ ./tif/beijing_area.tif参数含义从上到下依次是:
| 参数 | 说明 | 建议值 |
|---|---|---|
-f quantized-mesh | 输出瓦片格式,Cesium 专用 | 必须且有且仅有一个 |
-o ./terrain_tiles | 输出瓦片目录 | 建议路径不要带空格 |
-c lon_min lat_min lon_max lat_max | 切片经纬度范围,必须与预处理后 tif 的范围一致或包含在内 | 稍大于数据范围即可 |
-l 15 | 最大瓦片级别 | 城市级项目 13~15,全国级 10~12 |
-e 0.05 | 地形误差阈值,单位米 | 默认 1.0 会丢太多细节,0.05 较精细 |
./tif/beijing_area.tif | 输入 DEM 文件 | 支持多个 tif 用空格分隔 |
-l参数决定瓦片级别上限。每加一级,瓦片数量变成原来的 4 倍。以覆盖 40km×40km 的范围为例,15 级大约会产生数万个瓦片文件,生成时间在几分钟到几十分钟之间。如果范围更大且对精度要求不高,用 12 级就能明显减少生成压力,Cesium 加载时靠地形误差来做细节替代。
-e参数是很多人忽略的一个关键点。它控制点与点之间的最大高度误差,误差越小,地形细节保留越多,瓦片体积也越大。默认 1.0 在平原地区看不出区别,但在山地场景中,山顶和山谷会被明显磨平。我会在 0.1 到 0.5 之间反复试,具体看项目需要的视觉效果。
3.3 生成后的文件结构:理解瓦片目录才能正确部署
切片完成后输出目录结构是固定的:
terrain_tiles/ └── 0/ ├── 0/ │ └── 0.terrain ├── 1/ │ ├── 1.terrain │ └── 2.terrain └── ... └── tileset.json0/0/0.terrain是根瓦片,路径上第一层是 z 级别,第二层是 x,文件名是 y。Cesium 请求瓦片的 URL 规则是/{z}/{x}/{y}.terrain,这个结构在部署时不要改动。tileset.json 是瓦片集合描述文件,Cesium 加载时先请求它,再按内容去请求具体瓦片。
我把地形瓦片部署到静态服务器后,用 Nginx 托管是最简单的方案,注意 Nginx 配置里要加对.terrain扩展名的 MIME 类型声明:
location /terrain { alias /data/terrain_tiles; default_type application/octet-stream; add_header Access-Control-Allow-Origin *; expires -1; }没有正确的 MIME 类型和 CORS 头,Cesium 在 HTTPS 域名下就会报跨域错误或者解析失败。expires -1是让瓦片不缓存的选项,开发阶段修改瓦片后能立刻生效,上线后可以改成按天缓存。
提示:CTB 生成的瓦片文件是 quantized-mesh 格式的二进制数据,不是普通图片。在浏览器调试里看到
.terrain文件正常返回 200 和二进制内容,就说明切片和部署没问题。
4. Cesium 端接入与验证:加载瓦片、调帧率、看渲染效果
4.1 用 CesiumTerrainProvider 替换默认地形
Cesium 默认使用的是在线地形服务,要换成 CTB 生成的本地瓦片,核心代码就是替换terrainProvider:
const viewer = new Cesium.Viewer('cesiumContainer', { terrainProvider: new Cesium.CesiumTerrainProvider({ url: 'https://your-server.com/terrain', requestVertexNormals: true, requestWaterMask: false, }), });参数url指向的是 tileset.json 所在的目录,注意不是文件本身。requestVertexNormals: true会请求每个瓦片的法线数据,让地形在光照下呈现凹凸感,这个效果在 CTB 生成瓦片时已经包含在文件里,不需要额外处理。requestWaterMask对 CTB 生成的瓦片没用,除非你在切片时使用过水体掩膜数据。
如果地形已经加载但你看不到起伏,检查是否在Viewer构造后设置了地形。另一种写法是在已有 viewer 上动态替换:
const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl( 'https://your-server.com/terrain', { requestVertexNormals: true } ); viewer.scene.terrainProvider = terrainProvider; viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.4, 39.7, 12000), });setView是为了把相机移到底图上方,否则加载地形后看到的还是默认视角。如果你用了自定义底图图层,底图与地形之间存在坐标系不一致的问题,最常见的就是偏移和旋转,后者大概率是底图不是 WGS84 导致的。
4.2 验证加载是否正常:三层检查法
我每次接入完成后都会按顺序做三个验证,任何一个不通过都说明上游链路有问题。
第一层看网络请求,打开 DevTools 的 Network 面板,筛选.terrain,能看到瓦片请求持续发出。正常加载时,每一个可见瓦片都会产生一次 200 请求,数量通常在十几个到几十个。
第二层看渲染效果,把相机抬高到 45 度视角,让地形处于太阳光照角度下,如果山体有明暗差异且边缘有锯齿过渡说明顶点法线生效。如果地形是纯色的平面但有轮廓起伏,原因是requestVertexNormals没有生效或者瓦片里没有法线数据。
第三层验证高程精度,这是最容易忽略的一步。在地形上放置一个实体标记,设置经纬度和高度,然后切换到地形表面视角从侧面观察标记点是否贴合地表:
const entity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.4, 39.7, 200), point: { pixelSize: 10, color: Cesium.Color.RED }, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, });如果标记点悬浮在半空或陷入地下,说明瓦片的高程数据和原始 tif 之间存在系统偏差,通常和预处理时的重投影或重采样有关。CLAMP_TO_GROUND 模式下标记会贴近地形表面,此时对比放置位置和实际地理坐标,偏差超过几米就在可接受范围内。
5. 避坑排查:地形偏位、黑底、进程被杀怎么定位
5.1 地形整体偏移几十公里:坐标系问题不是 bug
现象:生成的地形瓦片在 Cesium 中加载正常,但位置明显偏移,底图上的建筑和地形高低起伏完全错开几十公里。
原因:输入 tif 不是 WGS84 经纬度坐标系,CTB 默认按 EPSG:4326 处理数据,原始数据的坐标值被当作经纬度解释,导致位置整体平移。
解决:预处理阶段用gdalinfo确认坐标系,如果是 UTM 或 Web Mercator,用gdalwarp -t_srs EPSG:4326重投影。处理完后再用gdalinfo检查输出数据的地理范围,确保经纬度范围符合实际项目位置,而不是原坐标系下的公里网数值。
5.2 黑底瓦片或加载后地形区域为空洞
现象:Cesium 加载地形后,部分区域显示为黑色或者完全透明,能透过看到底图,但底图上对应区域没有地形起伏。
原因:这类问题 90% 是输入 tif 在该区域存在 NoData 空洞(例如原始 DEM 中海洋或云遮挡区域为 0 值或无效值)。CTB 不会做插值,直接把无效值切进瓦片,最终渲染时 NaN 或 0 值被当成无效处理。
解决:用 GDAL 把无效值清洗掉再做切片。以 NoData 值为 -9999 的 tif 为例:
gdal_translate -a_nodata -9999 input.tif output_clean.tif gdal_fillnodata -md 50 output_clean.tif output_filled.tifgdal_fillnodata是 GDAL 自带的内插工具,默认用周围有效像素加权插值填充空洞,-md 50限制最大插值距离为 50 像素,避免远处的数据污染本地地形。如果空洞区域过大,比如整片海洋,要么把-md调小,要么用掩膜把水体区域直接裁掉。
5.3 ctb-tile 进程内存暴涨被系统杀掉
现象:执行ctb-tile时,内存占用持续攀升到 90% 以上,运行一段时间后终端直接报Killed或者Segmentation fault。
原因:CTB 在切片时会把整个输入 tif 读入内存构建网格,输入范围过大或分辨率过高的文件(比如数十 GB 的全国范围内 DEM)会直接吃满内存。这不是代码问题,是数据规模超过了物理内存上限。
解决:先按实际项目范围裁剪数据,缩小到合理范围再切片。如果必须处理大范围数据,用gdal_retile把 tif 切块,分成多个 tile 范围分别调用ctb-tile,最后合并瓦片目录即可。注意合并时保持同一级别的瓦片不跨目录覆盖。
mkdir split_tiles gdal_retile -ps 5000 5000 -targetDir split_tiles merged.tif for tif in split_tiles/*.tif; do ctb-tile -f quantized-mesh -o ./terrain_tiles -l 12 -e 0.05 "$tif" done每块 tif 的 5000×5000 像素设定,是为了让单次切片的内存占用控制在 2GB 左右。实践中这个数值可以根据机器内存适当调整,原则是单块数据在 500MB 以下最安全。
5.4 请求量巨大导致加载缓慢
现象:地形加载时要发送大量.terrain请求,浏览器同时加载几十上百个瓦片,页面卡顿甚至白屏。
原因:-l级别设得太高或-e误差阈值太小,瓦片数量和单个瓦片体积双高,网络带宽和渲染线程被占满。
解决:在 Cesium 端用maximumLevel限制请求上限,同时降低 CTB 的误差阈值权重:
const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl( 'https://your-server.com/terrain', { requestVertexNormals: true, maximumLevel: 15, } );maximumLevel告诉 Cesium 即使瓦片服务有更高层级的数据也不要请求超过这个级别。这样视觉效果上虽然远处地形精度略降,但加载曲线会平滑很多。根本解法还是回到 CTB 的-l参数,把生成上限设到你实际需要的级别即可。
6. 进阶技巧:误差阈值、层级上限与静态部署的性能平衡
CTB 的几个参数不是独立变量,它们的组合决定了地形瓦片的「精细度-体积-加载速度」三角平衡。我最后给出一组经过实际项目验证的参数组合和验证手段。
对城市级场景,推荐这套值:-l 15 -e 0.1。15 级在大约 40km×40km 的范围内能覆盖到建筑物级别的起伏细节,0.1 米误差在平原和丘陵地区几乎看不出磨平痕迹。如果数据源是 30 米分辨率的 DEM,-e 0.1已经低于数据本身精度,再小没有收益,只会把瓦片体积从几百 KB 抬到几 MB。
对全国级场景,-l 10 -e 0.5更合适。10 级瓦片在远景下足够流畅,0.5 米的误差在切换到局部视角时才会暴露细节缺失,但大多数场景根本不会放大到那种程度。注意-l不需要改到 20 级,Cesium 在某一级别的瓦片缺失时会自动请求更细的级别,而不是一直用最高级别。
部署时还有一个提高加载速度的技巧:把 terrain 目录挂到 CDN 或者 Nginx 的静态缓存上,并对.terrain响应头加上Content-Encoding: gzip。quantized-mesh 瓦片内部本身已经做了顶点整数化压缩,但 gzip 后仍能减少约 30% 体积。如果瓦片文件数超过十万,强烈建议用tileset.json之外再加一层数据索引服务,而不是依赖静态文件系统的目录列出——不过大多数项目不会走到这一步。
验证参数的黄金标准我一般是这么做的:生成瓦片后用ctb-info抽检单块瓦片:
ctb-info ./terrain_tiles/13/5120/7156.terrain它会输出该瓦片的层级、包围盒、顶点数量、三角形数量。顶点数在 5000 到 20000 之间属于健康范围,低于 1000 说明误差阈值太大细节丢失严重,高于 50000 则单瓦片体积过大加载会吃力。然后关闭 Cesium 的默认底图,只加载地形跟踪相机,实测瓦片请求延迟和帧率。帧率低于 30 就降低maximumLevel或调大-e。
从那以后我每次处理地形数据都强制走一遍「gdalinfo 查坐标系 → gdalwarp 裁剪重投影 → ctb-tile 生成 → 部署后三层验证」的完整流程,不再跳过任何一步。这套流程配合上面的参数列表,基本能解决 90% 的本地地形加载问题,希望帮到你。
本文还有配套的精品资源,点击获取