
简介面向 Cesium 离线地形数据生成需求这个工具包把高程模型 TIF 文件转换为 Cesium 可直接加载的地形瓦片适合需要在无网络环境中展示三维地形的开发者和 Cesium 使用者。方案覆盖环境搭建、坐标转换、瓦片输出与项目部署等关键环节能有效避开常见坑点。压缩包共 11 个文件整体约 45.9MB以 Python 2.7、GDAL、PIL、NumPy 的安装程序为主配合 Python 转换脚本、PDF 使用说明以及 JSON、txt、terrain 等配置和数据文件构成一套完整离线转换链。已有 3089 人学习参考价值较高。借助示例高程数据、生成后的瓦片输出目录和详细 PDF 指南读者可逐步完成从高程模型到 SRTM 地形瓦片的转换并掌握路径设置、坐标系匹配、内存与磁盘空间评估以及 GDAL 或脚本异常的排错思路最终在 Cesium 项目中接入自定义离线地形数据。1. 离线 terrain 为什么值得自己生成做离线三维地球Cesium 官方地形服务看着方便真到了内网环境就成了摆设。我接过一个某内网项目现场断外网默认的全球地形请求全部超时画面直接贴在地板上。后来把高程 DEM 数据拿到手自己生成一套离线 terrain 瓦片才算彻底解开这个死结。这套资源就是把生成离线 terrain 地形数据所需的程序、脚本和详细说明打包在一起核心逻辑不复杂把 GeoTIFF 高程数据切成 Cesium 能认的 quantized-mesh 瓦片配一个 layer.json再让 Cesium 当普通静态服务加载。适合被在线地形绑死、或者需要自备高程做本地调度的开发者。我照着这套流程跑通后把几个容易翻车的细节全部拆出来讲清楚。2. 地形格式选型quantized-mesh 方案为什么能省事做离线地形第一件事不是急着跑工具而是先把格式弄清楚。选错格式后面切分、发布、客户端加载全都会出问题返工成本很高。2.1 三种地形格式的适用边界Cesium 常见的地形数据格式可以分成三类heightmap-1、heightmap-2 和 quantized-mesh。heightmap-1 是一维高程数组按行排列高度值加载时直接构造 TerrainDataheightmap-2 是二维灰度图用像素灰度表达高度适合自己写渲染逻辑的场景。这两类都属于“网格生成由客户端现场算”的路线数据量小但是几何精度无法做层级自适应地形起伏稍微复杂一点就会出现起角、拉花。与之对比quantized-mesh 在服务端就把地形切成三角网瓦片每个瓦片按四叉树金字塔组织客户端拿到的是已经量化的顶点坐标、索引和屏幕空间误差信息不需要重新做三角化。三种格式的差异可以看下面这个表格式常见后缀数据结构精度适用场景heightmap-1.terrain二进制一维高度数组中小范围试验、独立渲染器heightmap-2.png / .raw二维灰度影像中快速预览、自定义 WebGLquantized-mesh.terrain三角网 四叉树金字塔高大范围离线地形、Cesium 原生调度从表里能看出来想要大面积数据、Cesium 原生 LOD 调度和真实地表表现quantized-mesh 几乎是唯一不折腾的选择。heightmap 更适合那种 512x512 的示教 Demo。我自己第一次图省事用了 heightmap-2结果推到边缘大片破碎切片层级稍微一深就闪烁后来换 quantized-mesh 一次到位这条路走得并不冤枉。2.2 quantized-mesh 的核心原理quantized-mesh 这个名字容易吓到人原理其实一句话能解释清楚把三角形网格的顶点位置做规范化统一编码成 16 位整数客户端加载后再解算回三维坐标。这样每个瓦片的顶点数可以非常多但带宽占用远小于直接传浮点坐标。每个瓦片文件头部有一段元信息包含瓦片范围、中心点、最小高程、最大高程、边界顶点列表等。Cesium 拿到这些信息后先判断当前视野的屏幕空间误差决定究竟是加载父瓦片还是子瓦片如果误差足够小就直接复用父瓦片不再向下请求。这个过程是 Cesium 循环在幕后完成的离线地形和在线地形在调度逻辑上没有区别。理解这一点有多重要直接关系到生成命令里最高层级的取值层级设少了拉近之后地形糊成一片层级设多了几十平方公里范围能切出上万个无效瓦片白白占硬盘。所以我不建议一上来就盲目指定数据层级先按 DEM 分辨率和目标视角合理估算后面第 6 章会给出实际的计算思路。2.3 工具选型资源里那套流程怎么搭生成 quantized-mesh 的工具有不少这套资源用的是比较成熟的一条链路Cesium Terrain Builder 配合 GDAL 预处理。选它的原因很朴素它能直接输出 Cesium 原生格式不需要再做中间转换对 GeoTIFF、HGT 这些常见 DEM 格式都有良好支持还支持多线程切块处理大 DEM 时省时间。另一个原因是排查问题方便。如果你自己拼一个生成器出了瓦片错位、高度异常还得先怀疑自己代码用成熟工具可以把变量收敛到数据和参数两个层面排查路径短很多。我一般采用的流程是下面五步用 gdalinfo 检查 DEM 的范围、像素类型和高程量纲用 gdalwarp 统一投影到 EPSG:4326用 gdalbuildvrt 合并多块 GeoTIFF用 ctb-tile 输出 quantized-mesh 瓦片本地起 HTTP 服务给 CesiumTerrainProvider 指向瓦片根目录。这套流程和资源包里的 README 基本一致。你拿到手的材料里包含二进制工具、预处理脚本和参数说明按流程走不需要自己现学地形算法就能把离线地形出成静态文件。3. 从 DEM 到瓦片Cesium Terrain Builder 完整操作流这一章把实际操作拆到底环境怎么准备、单块数据怎么切、多块数据怎么合并、本地服务怎么起。每一步都能直接照抄命令后面会说明它做了什么以及哪些参数要按实况调。3.1 环境准备与依赖检查动手前先确认有没有 GDAL。ctb 依赖 GDAL 读写栅格如果环境里没有装过后续步骤会卡在数据读取阶段。Windows 下我直接在资源包的工具目录里跑预编译程序Linux 下则一般走源码编译先解开包再执行构建脚本unzip terrain_tools.zip cd terrain_tools sh build.sh构建脚本会检查 GDAL、C 编译链等关键依赖。常见报错是提示找不到 gdal-config这种一般不是代码问题是系统缺 libgdal-dev装完再跑一次就行。构建完成后确认工具能正常执行ls -l bin/ctb-tile bin/ctb-tile 21 | head -20看到输出里的 Usage 就说明环境通了。如果只输出空行去查 PATH 是不是没包含 bin 目录。接下来检查 DEM 数据本身。这一步很多人忽略但它恰恰是后面各种玄学问题的根源gdalinfo -stats data/dem_03.tif重点看四行Size 是像素宽高Type 是像素类型Min/Max 是高程范围还有底下的 Coordinate System 是否完整。Type 如果是 Int16后面生成时要小心有的 DEM 最大高程显示 65535明显是符号位没处理对这种要做类型转换。最小值和最大值也能用来判断数据量纲顺便筛掉全 0 的空数据。3.2 单块 GeoTIFF 转瓦片单块数据是最简单的情况命令如下bin/ctb-tile -f QuantizedMesh -o ./output_tiles data/dem_03.tif参数拆开看-f QuantizedMesh指定输出格式为量化网格-o是输出目录最后一个参数是输入 GeoTIFF。命令跑完后output_tiles 下会生成 layer.json 和以 z/x/y 命名的 .terrain 文件。用 find 看一眼瓦片结构find ./output_tiles -name *.terrain | head -20 find ./output_tiles -name layer.json看到类似0/0/0.terrain、1/0/1.terrain这样的文件说明层级金字塔已经建立。这里有个容易被忽视的点输出目录里必须有 layer.json如果跑完发现没有通常是你拿到的 cbt 版本较旧需要手动补一个内容参考第 5 章。如果数据区域很大ctb 默认会一直切到最高层级都出完才停时间会非常长。想限制层级手动加-l参数bin/ctb-tile -f QuantizedMesh -l 10 -o ./output_tiles data/dem_03.tif-l 10表示最多切到第 10 级。这个值不是越大越好层级每增加一级瓦片数量约膨胀四倍硬盘和切片时间都受不了。对于城市级范围10 到 12 级足够对省市级大范围8 到 10 级已经能保持流畅加载。3.3 多源 DEM 合并与投影统一实际项目很少只有一块 DEM常见的是按图幅分块的几十块 GeoTIFF。直接拿这些文件各自生成瓦片接缝处容易出现错位和高低不一致。正确做法是先合并。合并之前先确认所有数据的投影一致。很多测绘 DEM 是 UTM 投影Cesium 离线地形通常用经纬度坐标更省事可以先统一投影gdalwarp -t_srs EPSG:4326 -r bilinear utm_dem.tif wgs84_dem.tif-t_srs EPSG:4326指定目标坐标系-r bilinear设置重采样算法为双线性内插。对于高程数据双线性内插能保留地形过渡的平滑性比最近邻更靠谱如果对山峰细节要求高可以换-r cubic但耗时翻倍。多块数据统合我一般用 VRT 而不是直接合并成一个大文件gdalbuildvrt merged.vrt area_01.tif area_02.tif area_03.tif gdal_translate -co TILEDYES -co COMPRESSDEFLATE merged.vrt merged_dem.tifgdalbuildvrt 不会复制栅格数据只是生成一个虚拟索引文件几十块数据秒建完成gdal_translate 再把它落成真正的 GeoTIFF同时开启 TIFF 分块和压缩让后续读取更高效。输出文件会比较大但 ctb 读取时速度明显更快不用担心内存被一次性塞满。如果嫌落盘文件太大也可以直接让 ctb 读取 VRTbin/ctb-tile -f QuantizedMesh -o ./output_tiles merged.vrt这种做法的好处是不产生中间大文件代价是每次切片都会重新读取源数据性能稍差。我自己的习惯是数据量小于 10GB 就用 VRT 直接切超过 10GB 先 gdal_translate 落盘再做。3.4 启动本地服务瓦片生成后用一个静态文件服务挂出来。最简单的做法是 Python 自带的 HTTP 服务cd ./output_tiles python3 -m http.server 8080然后浏览器访问 http://localhost:8080/layer.json能返回 JSON 就说明服务正常。这一步值得每次做一遍它能提前发现目录结构和文件命名问题不用等 Cesium 运行时报错。如果项目工程本身是 Flask 或 Django 写的也可以直接把瓦片目录丢给它作为静态资源但注意框架默认的静态路径可能会有缓存机制修改瓦片后要强制刷新否则会拿旧的缓存数据。本地调试阶段用http.server最省心缓存问题最少。4. 避坑专项离线地形最常见的五个坑地形数据这行有时候数据长得一模一样生成参数差一个结果差很远。这一章把加载层和数据层最常见的坑集中列出来每条按现象、原因、解决的顺序写遇到类似问题可以直接对照。4.1 客户端跑不通平地形、404 与 CORS坑一加载之后地形是平的完全没有起伏。现象terrainProvider 已经设置成功但视角斜过去地面还是平整一片Network 面板里也能看到 .terrain 请求。原因最常见是 DEM 的像素类型与高程量纲不匹配。有的 DEM 是 Int16 类型但 ctb 默认按浮点读高程全部被截断成 0或者 DEM 投影是 EPSG:3857与 Cesium 期望的经纬度坐标对不上瓦片范围乱了高度也被当成空值。解决先用gdalinfo -stats查看 Min/Max。如果最大高程是 65535 而不是正常海拔范围说明类型解析错位执行一次转换gdal_translate -ot Float32 -scale 0 65535 0 5000 input.tif output.tif-ot Float32把像素类型改成浮点-scale把 0 到 65535 的原始值线性映射到 0 到 5000 米。转换后重新生成瓦片地形起伏就出来了。坑二terrain 请求一直 404。现象Network 面板里全是.terrain的 404Cesium 地形调度完全失效。原因多数是 layer.json 不在服务根目录或者服务路径配置把瓦片根目录指偏了一层。还有一种情况是服务器对未知扩展名返回了 HTML 错误页Cesium 解析 JSON 失败也会报加载错误。解决先手工访问http://localhost:8080/layer.json确认返回的是 JSON。如果返回目录列表或者 404就调整CesiumTerrainProvider.fromUrl的 URL保证指向包含 layer.json 的目录。对 Python http.server 来说.terrain 无法识别扩展名时默认按二进制流返回这个没问题Nginx 则要加 MIME 类型见第 5 章。坑三浏览器跨域拦截导致请求失败。现象页面部署在 8080 端口瓦片服务在 8081 端口Network 里 CORS 报错。原因Cesium 从页面所在域向另一个域发起请求浏览器安全策略拦截了响应。解决服务端加跨域响应头python3 -m http.server 8080 --bind 0.0.0.0注意 Python 自带的服务器不带跨域头。更方便的做法是把 Cesium 应用和瓦片放到同一个域名下本地联调时用一个 Nginx 代理两个路径避开跨域问题。4.2 数据画不对高程单位、边缘裂缝与法线缺失坑四地形整体夸张地鼓起来山谷比正常高几十倍。现象分辨率越高夸张越明显低层级看着还行高层级整个地形像发面馒头。原因垂直单位问题。部分 DEM 数据高程以英尺为单位而生成工具默认按米处理没有自动做量纲换算。解决同样先看gdalinfo -stats如果高程范围是 32808 这种数字基本可以确定是英尺。换算成米需要把原始值除以 3.28084。GDAL 里直接用 scale 表达式完成gdal_translate -ot Float32 -scale 0 32808.4 0 10000 foot_dem.tif meter_dem.tif这个命令把 0 到 32808.4 英尺映射到 0 到 10000 米。改完重新生成瓦片地形比例就正常了。坑五瓦片边缘出现裂缝或撕裂。现象高 zoom 下相邻瓦片接缝处有明显拉花地形边缘像被撕开。原因原始 DEM 数据之间存在空白区域生成器没有填充 NoData或者多块数据投影合并时边缘对齐误差太大。解决先补洞再做投影统一。补洞用 GDAL 自带的填充算法gdal_fillnodata -md 10 input.tif filled.tif-md 10表示只对 10 像素以内的空白区域做插值避免大范围无数据空洞被过度拟合。随后用 gdalwarp 统一投影双重处理基本能解决裂缝。如果裂缝只在高层级出现也可以适当降低最大层级让边缘误差被屏幕空间误差吞掉。另外要提醒一个法线问题瓦片加载成功但地形表面没有光影效果或者山体明暗翻转。这多半是生成瓦片时没有写顶点法线而客户端又开了requestVertexNormals: true。解决办法要么重新生成带法线的瓦片要么把请求参数改成 false。资源包里的生成参数默认带法线如果是自己手工切的要确认命令里有没有对应的法线开关。5. 发布与验证让离线地形像在线一样工作瓦片生成只是完成了前半段后半段是发布和验证。这一章把服务端配置、客户端参数和验证方法串起来做到加载一次就能确诊问题。5.1 服务端目录结构与 MIME 配置离线地形的目录看起来简单但层级结构不能错terrain/ ├── layer.json ├── 0/ │ └── 0/ │ └── 0.terrain ├── 1/ │ ├── 0/ │ │ └── 0.terrain │ └── 1/ │ └── 0.terrainlayer.json 是整个服务的入口Cesium 先拿它获取瓦片 URL 模板。手动补 layer.json 时最少要包含文件中的核心字段{ tilejson: 2.0.0, name: local terrain, format: quantized-mesh, tiles: [{z}/{x}/{y}.terrain] }tiles里写的模板要和目录层级一一对应。Cesium 拿到这个配置后会自动把{z}/{x}/{y}替换成实际层级和行列号。服务端推荐用 Nginx配置比 Python http.server 更可控server { listen 8080; server_name localhost; root /srv/terrain; location ~ \.terrain$ { add_header Content-Type application/octet-stream; add_header Access-Control-Allow-Origin *; } }location ~ \.terrain$匹配所有 .terrain 结尾的请求把 MIME 类型强制设为二进制流避免被解析成文本。Access-Control-Allow-Origin *是给跨源场景兜底如果页面和瓦片同源这一行删掉也不影响。Nginx 配完后务必 reloadnginx -t nginx -s reload5.2 Cesium 客户端参数配置服务端就绪后客户端配置比较直接。新版 Cesium 推荐用异步工厂方法构造地形 Providerconst viewer new Cesium.Viewer(cesiumContainer, { baseLayer: false, }); const provider await Cesium.CesiumTerrainProvider.fromUrl( http://localhost:8080/terrain, { requestVertexNormals: true, requestWaterMask: false } ); viewer.terrainProvider provider;几个参数要解释一下requestVertexNormals控制是否请求顶点法线开启后地形光影更真实但要求瓦片确实含法线数据requestWaterMask开启后会额外请求水面掩码离线地形如果用不到水面效果保持 false 能省一部分流量。如果 provider 加载失败先看控制台报错再检查 URL 路径。资源包里的示例代码也用了同样的写法直接把路径换成自己的瓦片目录即可。5.3 三种验证地形生效的方法第一种打开浏览器开发者工具刷新页面后观察 Network 面板中是否有 .terrain 请求。只要出现说明 Cesium 已经识别到地形 provider 并在按视角调度瓦片。如果请求能看到但地形还是平的多半是 DEM 本身高度有问题回到第 4 章检查。第二种给 Cesium 挂一个影像图层做参照。影像用在线切片或本地影像都行配合地形加载后斜视角如果能看到山脊起伏说明高程生效。第三种用 Cesium 的采样接口直接读取某个经纬度的高程这是最带确定性的验证方式const terrainProvider viewer.terrainProvider; const positions [ Cesium.Cartographic.fromDegrees(116.3912, 39.9072) ]; const updatedPositions await Cesium.sampleTerrainMostDetailed( terrainProvider, positions ); console.log(该点海拔, updatedPositions[0].height);sampleTerrainMostDetailed会调用 terrainProvider 去查指定位置的最精细高程返回结果不为 0 就说明数据链路是通的。打印出来的海拔还能顺便验证高程单位是否正确一步顶两步。6. 进阶大区域批量切片的两个技巧资源包里的工具基本覆盖了中小范围地形的需求但如果你要切省级、国家级大范围地形有两个技巧能显著节省时间和硬盘。技巧一是分块并行处理。ctb 本身支持多线程但线程数拉满后内存占用会直线上升。切大范围数据时我一般是先把数据按经度或纬度切成若干块每块约 10GB 以内然后用 GNU Parallel 并行跑多个 ctb 实例。这样做的好处是每块数据独立生成失败不会影响其他块排查也更方便。处理完各块后再把瓦片目录合并到同一个根目录下注意各块的边界不要重叠否则会出现相邻瓦片不一致。技巧二是最大层级按地面分辨率估算而不是拍脑袋。计算思路很简单假设第 0 级瓦片覆盖全球范围约 40075000 米每一级瓦片数量增加四倍单个瓦片分辨率为 40075000 除以 2 的层级次方再除以瓦片像素宽度。Cesium quantized-mesh 瓦片顶点密度通常在 64 到 128 之间取 64 估算第 10 级单瓦片地面分辨率大约在 600 米左右第 12 级在 150 米左右。如果 DEM 本身分辨率是 30 米那最多切到 12 级就够了再往上只是切割更多瓦片视觉提升极其有限却会让硬盘多出好几 GB 文件。我在这上面是有过教训的。某次做跨区域项目时我直接把层级填了 15从晚上切到第二天凌晨还没跑完机器 16G 内存被吃满整个切瓦片过程基本处于半死状态。从那以后我每次跑大规模切瓦片都会先按范围分成几块再用估算公式把最大层级算出来最后才动手。规范流程走下来再没出过瓦片爆满这种事故。这套经验是根据这套资源包完整复现得来的希望能帮到你。本文还有配套的精品资源点击获取