
简介这套工具面向GIS开发者和三维可视化工程师核心能力是将TIF格式高程数据转换为符合Cesium瓦片规范的地形数据解决原始数字高程模型无法直接用于Web渲染的格式壁垒。资源包共108个文件压缩包16.17MB内含40个DLL动态库与4个EXE程序构成地形转换与瓦片生成的主运行组件另有29个CSV坐标文件、6个WKT投影定义文件用于适配不同地理坐标系的高程数据还附带GFS、XSD、DXF等辅助资料整体是一套开箱即用的转换环境。利用这些文件可快速处理GeoTIFF数据生成带层次细节的瓦片集并通过Cesium地形服务接口加载实现平滑过渡与流式渲染。该工具链免去自行编译GDAL与Cesium Terrain Builder的繁琐流程降低地形数据处理门槛已有2368人学习下载适用于地理测绘、城市规划、环境监测与灾害模拟等场景。1. 瓦片地形生成器解决的不只是切图Cesium 加载高程的正确入口做过 Cesium 项目的人大多经历过这个场面团队拿到一块精度不错的 DEM 高程数据导入 Cesium 后却渲染成一块平地或者地形加载到一半就开始缺块变坑。真正的问题往往不在数据而在于没有用对工具链——Cesium 并不直接读取普通 GeoTIFF它需要的是按四叉树规则切好的地形瓦片。cesium terrain builder大家常说的 CTB正是这条链路里的关键一环它把原始 DEM 转换成 Cesium 能直接调度渲染的地形瓦片并输出成 quantized-mesh 格式前端根据视点位置自动选择加载对应层级。网上讲 Cesium 模型、实体、粒子特效的教程很多但讲瓦片地形生成的实操内容反而很少。这篇文章按照我实际跑通这条链路的顺序来写先讲清楚格式和 LOD 调度的关系再给出一套能直接复现的预处理、切片、部署命令最后是参数调整思路和绕不开的坑适合正在做数字孪生、应急仿真或航测数据接入的开发者。2. 为什么必须先理解瓦片格式与 LODquantized-mesh 和工具选型2.1 Cesium 不吃普通 DEM一次地形加载请求背后的四叉树调度Cesium 的地形渲染不是把一整块 DEM 扔给 GPU而是采用四叉树金字塔的策略把全球按经纬度范围逐层四分第 0 层只有少数几个瓦片每往下一层瓦片数量翻四倍覆盖范围减半地形网格顶点密度成倍增加。摄像机在场景里移动时Cesium 根据屏幕空间误差SSE动态判断当前视角下哪个层级的瓦片足够精细再通过网络请求把对应瓦片拉下来。这个调度机制决定了 Cesium 对地形数据的基本要求必须按瓦片组织并且每个瓦片要携带几何误差信息前端才能决定什么情况下抛弃父级瓦片、换用子级瓦片。普通 GeoTIFF 无论分辨率多高都不满足这个结构所以拿 DEM 直接塞进 Viewer 是行不通的。CTB 这类工具做的事情就是把这个四叉树切分过程自动化读入 DEM递归切片写出带层级关系的地理坐标瓦片包。理解这一点后就明白为什么生成地形瓦片不是简单 resize 图片而是要对网格做重采样和误差估算。还有一个容易被忽略的点地形瓦片的切分基准是经纬度geographic也就是瓦片在经度方向从 -180 到 180 均匀分割而不是像 2D 地图切片那样使用 Web Mercator。这意味着如果你以前用影像切片工具生成过 XYZ 图层不能拿那套认知直接套到地形瓦片上。CTB 默认产出的目录结构是 z/x/y 组织但它的空间参考和网格剖分规则和普通影像切片有本质区别后面预处理时也要注意这一点。2.2 quantized-mesh 与 terrarium两种瓦片格式差在一个存几何、一个存纹理CTB 支持的输出格式主要包括 quantized-mesh-1.0 和 terrarium 两种。quantized-mesh 是一种专为地形网格设计的二进制格式每个瓦片文件里保存了规则网格的顶点坐标量化到 16 位、三角形索引以及这个瓦片对应的几何误差值可选携带法线和光照信息Cesium 直接解析后提交 GPU 渲染不需要在客户端重建网格所以渲染效率很高适合做生产环境的地形服务。terrarium 则走了另一条路线把每个像素的高程值拆成三个字节分别塞进 PNG 的红、绿、蓝通道里本质上是一张“高程编码纹理”。Cesium 加载这种瓦片后需要先解码纹理再在客户端重新生成网格。它的优点是文件体积小、生成速度快在一些低精度预览场景下够用缺点是高程精度受 24 位编码限制网格由客户端插值生成边缘容易看出锯齿感在 Cesium 版本迭代中对 terrarium 的原生支持也出现过调整。我一般默认输出 quantized-mesh-1.0只有做快速预览、或者地形数据只是当成辅助底图叠加时才考虑 terrarium。选型上不要花太多时间纠结格式CTB 生成时通过参数切换即可。但要知道一个背景Cesium 前端对地形服务的支持重心一直是 quantized-mesh社区里能搜到的大部分地形瓦片生成器也都围绕这个格式做优化跟着默认路线走最不容易翻车。2.3 工具链选型CTB、CTS 与 gdal2cesium 三条路线分别解决什么问题做地形瓦片常见的选择有三条CTB、Cesium Terrain ServerCTS以及 gdal2cesium。CTB 是离线切片工具输入 GeoTIFF输出完整的瓦片目录适合数据范围确定、更新频率低的场景。CTS 则是 Node.js 写的动态服务可以按需对入库的高程数据切瓦片适合数据频繁更新的场景但它更适合已有瓦片库做服务化封装首次请求的耗时和资源占用会明显高于静态瓦片。gdal2cesium 是老牌的 Python 库也支持 quantized-mesh 输出但维护节奏比较慢生成大范围地形时稳定性不如 CTB。我的选型习惯是静态数据、固定范围、要求加载性能稳定首选 CTB 预切片数据源经常更新、或者前期还没确定最终范围先用 CTS 观察效果gdal2cesium 只在没有 CTB 编译环境、且数据量很小的时候作为替补。下面一张表概括三者差异工具工作方式输出形态适合场景CTB命令行离线切片静态瓦片目录固定范围、生产环境、离线部署CTSNode.js 动态服务按需切瓦片数据更新频繁、原型验证gdal2cesiumPython 脚本静态瓦片目录小范围、CTB 环境不可用时选定 CTB 后还有 C 版和 Python 重写版的区别。C 版功能完整但依赖 GDAL 和 boost编译坑比较多Python 重写版部署简单命令也直观我后面的操作示例以 Python 版为主部分发行版里命令名略有差异跑之前先看一眼--help就好。3. 从 DEM 到可上线地形瓦片预处理、生成与部署的完整操作3.1 准备环境为什么我推荐在 Linux 或 WSL 下跑而不是原生 WindowsCTB 的 Python 重写版依赖 GDAL 的 Python 绑定安装相对简单但 C 版在 Windows 上需要手动编译 GDAL、boost 等一堆依赖版本稍微对不上就是一整天的排查。我自己的做法是能用 Linux 就用 Linux开发机是 Windows 的话就开 WSL把 CTB 和 GDAL 都装在 Ubuntu 环境里避免折腾原生编译。环境准备阶段只需要确认两件事GDAL 工具链可用、CTB 命令能跑起来。# 1. 确认 GDAL 版本这里要求 2.x 或 3.x 都可以但 Python 绑定要对应 gdalinfo --version # 2. 检查 DEM 的坐标系、分辨率与数据范围生成前必看 gdalinfo ./raw_dem.tifgdalinfo --version输出的版本号决定了后面安装哪些依赖包Python 版的 GDAL 库版本需要和系统 gdal 保持一致否则会出现ERROR 4: Unable to open ...这类怪异问题。第二步的gdalinfo是每次拿到新 DEM 后的固定动作重点看三处Coordinate System is 后面的坐标系描述、Size 后面给出的像素宽高、以及 Band 1 的类型和 NoData 值。这三项直接决定预处理参数怎么设漏掉任何一项都可能让生成结果在坐标上出大问题。3.2 DEM 预处理裁剪、重投影与降采样CTB 按经纬度组织瓦片所以源数据如果不在 EPSG:4326 坐标系下必须先统一坐标系否则切出来的瓦片位置会整体偏移放进 Cesium 后地形会对不上影像。另一个问题是范围很多 DEM 下载下来覆盖范围很大比如整块 SRTM 分幅直接丢给 CTB 切会白白生成大量无用瓦片先裁剪能节省大量时间。# 1. 先用矢量边界裁剪 DEM并设置 NoData 值避免边界被当成 0 处理 gdalwarp -cutline ./aoi.geojson -crop_to_cutline -dstnodata -32768 \ -t_srs EPSG:4326 -r bilinear -of GTiff ./raw_dem.tif ./dem_clip.tif # 2. 如果源 DEM 分辨率太高比如航测 DSM 达到 0.5 米先降采样到合适尺度 gdal_translate -outsize 20% 20% -r average ./dem_clip.tif ./dem_down.tif第一段命令中-cutline指定裁剪边界矢量-crop_to_cutline让输出严格贴合边界范围-dstnodata -32768很关键如果不显式设置 NoData裁剪后边缘的空白区域可能被当成 0 米海拔生成的地形瓦片边缘会出现一圈深坑。-t_srs EPSG:4326把数据重投影到经纬度坐标-r bilinear表示双线性重采样对高程数据来说兼顾平滑和速度如果数据源是陡峭山地可以换成-r cubicspline重采样后的地形会更接近原始山脊线。第二段命令的降采样不是必需步骤只有当源 DEM 像素密度远高于目标层级最大精度时才需要。判断标准很简单先想清楚最终要用到第几级瓦片再换算该层级下每个像素对应多少米。比如一个 5 米分辨率的 DEM 已经足够支持到 z14 级别的显示就没必要用 0.5 米分辨率的原始数据硬切。3.3 生成瓦片从 prepare 到 tile 的最小命令CTB 的生成过程一般分两步先用ctb-preparedem对预处理后的 DEM 做内部整理再用ctb-tile执行真正的四叉树切片。不同小版本的命令名有差异有的发行版叫CTB-Tile有的叫ctb-tile但核心参数差别不大第一条命令先执行ctb-tile --help确认即可。# 第一步prepare把 DEM 整理成 CTB 可识别的输入结构 ctb-preparedem --out ./prepared ./dem_clip.tif # 第二步正式切片-e 为误差阈值-f 指定输出格式-o 为输出根目录 ctb-tile --error 0.5 --format quantized-mesh-1.0 \ --output ./terrain ./prepared/dem_clip.tif这一步才是真正意义上的瓦片地形生成。ctb-tile按四叉树规则从顶层开始逐级切分每个瓦片内部都带几何误差信息Cesium 前端加载时会读取这些信息并参与 LOD 调度。--error 0.5表示当前视角下屏幕空间误差超过 0.5 像素时继续细分到下一层数值越小瓦片越精细、数量越多磁盘占用也会成倍增加首次切数据时建议先设 1 看生成体量再决定要不要收紧。--format quantized-mesh-1.0指定输出格式老版本 CTB 里这个参数写作-t terrarium或-t qm以当前版本 help 输出为准。大范围数据切片非常耗时一个覆盖几百平方公里、包含完整山区地形的 DEM在普通 8 核机器上可能要跑数小时。建议先用 nohup 方式放到后台执行并定期观察输出目录里的瓦片数量是否稳定增长。生成完毕后./terrain目录下应当出现layer.json文件和按 z/x/y 组织的瓦片子目录。3.4 部署与接入nginx 暴露静态瓦片前端 CesiumTerrainProvider 加载生成好的瓦片是纯静态文件最稳妥的部署方式是用 nginx 起一个静态文件服务让前端通过 HTTP 访问。Cesium 加载自定义地形的第一步是请求layer.json拿到瓦片格式、坐标系和 URL 模板后才会按需请求具体瓦片。nginx 配置要注意两点root 指向包含layer.json的那一层目录地形瓦片通常是 gzip 压缩过的.terrain文件需要开启gzip_static让服务器直接返回预压缩文件。server { listen 80; server_name terrain.example.com; root /srv/terrain; gzip_static on; location / { add_header Access-Control-Allow-Origin *; add_header Content-Type application/octet-stream; } }gzip_static on的作用是当请求文件存在对应的.gz版本时直接返回压缩内容CesiumTerrainProvider 在请求头里声明了接受 gzip 编码这样能显著降低瓦片传输体积。Access-Control-Allow-Origin *是为了避免前后端分离部署时出现跨域拦截问题如果前端页面和地形服务在同一个域名下这条可以不加。前端接入的代码很简单Cesium 的CesiumTerrainProvider直接接收服务地址即可const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: new Cesium.CesiumTerrainProvider({ url: https://terrain.example.com, requestVertexNormals: true }) }); // 飞行到 DEM 范围中心点确认地形瓦片是否加载 viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(lon, lat, 6000) });这里的lon和lat应替换为你 DEM 数据的中心点坐标可以从预处理后的 TIF 文件用gdalinfo查看 Center 字段得到。requestVertexNormals在生成瓦片时包含法线信息的场景下开启可以让地形光照更有立体感如果不确定瓦片里有没有法线数据先保持false避免部分层级请求报错。4. 控制地形精度的三个参数误差阈值、纹理开关与层级上限4.1 误差阈值 error budget它决定瓦片数量和地形精细度CTB 生成命令里的--error参数直接控制 LOD 切分策略。Cesium 渲染时会给每个瓦片计算屏幕空间误差当误差大于一定像素数时浏览器就认为当前瓦片不够精细继续请求它的子瓦片。--error设得越小子级瓦片被请求的概率越高地形在近距离观察时细节越丰富但同时瓦片数量呈指数级增长。实际操作中这个值的设置要结合数据分辨率和项目预算一起考虑。我处理过一组城市级数字孪生项目DEM 是 5 米分辨率的航测 DSM--error设到 0.2 时生成的瓦片总数是设 0.5 时的三倍还多磁盘占用接近 40GB而前端帧率反而因为瓦片请求数量过多而下降。反而是先设 1 跑通全流程再按需把局部重点区域单独提高精度更实用。判断阈值是否合理有一个土办法生成完成后随机挑几个较高级别的瓦片文件看体积。单个.terrain文件超过 200KB 说明网格细分已经相当密如果 z14 级别的瓦片普遍只有几十 KB说明精度富余误差阈值可以适当调小一些。不同版本的 CTB 对--error的单位解释有细微差别有的按像素、有的按米第一次跑完看瓦片数量和平均大小再回来调参数比自己猜测靠谱。4.2 纹理开关地形瓦片里要不要同时切影像部分 CTB 版本支持在生成地形瓦片时叠加影像纹理让每个地形瓦片文件里同时包含高程网格和影像贴图。这个功能看起来很省事实际使用中我却很少开启原因是地形瓦片和影像瓦片的刷新频率、分辨率要求完全不同地形数据一年更新一次已经算频繁影像可能每个季度都要换新把两者绑在一个瓦片文件里意味着影像一变地形也得重新切一遍。更常见的做法是分开两条链路CTB 只输出纯地形瓦片影像另外通过标准的影像瓦片服务TMS/WMTS发布前端分别加载terrainProvider和imageryProvider让两者在渲染时叠加。这样做的好处是缓存独立、更新互不影响调试单一问题也更容易。如果确实想让 CTB 顺带切影像注意影像源的范围和坐标系必须和 DEM 完全一致否则会出现地形和影像错位。方案瓦片体积更新灵活性调试难度地形瓦片叠加影像大影像更新需重切地形问题难定位地形与影像分离小两链路独立更新各自排查即可4.3 层级上限与覆盖范围让瓦片数量保持在可控区间CTB 生成时会根据 DEM 原始分辨率自动推算最大层级但在实际项目中这个自动值往往过于激进。比如一份 10 米分辨率的 DEM理论上能支撑到 z16 级别的显示但对一个只用于宏观展示的项目来说切到 z13 就已经完全够用再往下切只是白白消耗磁盘和生成时间。控制层级上限的做法一般是在生成命令里显式指定最大层级或者先在预处理阶段用gdal_translate降低分辨率从源头上限制切分层级。配合范围裁剪可以用下面这段 Python 粗略估算瓦片数量用来判断参数是否合理。def estimate_tiles(min_z, max_z, coverage_ratio): total 0 for z in range(min_z, max_z 1): # 全球范围第 z 层的瓦片总数为 2^z * 2^z total 4**z * coverage_ratio return int(total) # 覆盖约 10% 的区域从 z8 切到 z13 print(estimate_tiles(8, 13, 0.1))这个估算没有把地形复杂度造成的额外细分算进去但用来判断量级足够了。如果计算结果显示瓦片总数达到百万级别就要考虑是不是范围没裁干净、或者误差阈值设得太小。生成瓦片数控制在十万级以内静态文件部署和前端请求压力都会舒服很多。5. 瓦片地形生成器常见问题与避坑五条绕不开的实战记录5.1 海拔为 0整块地形变成一片平地现象前端地形能加载瓦片请求也正常返回但场景里所有区域都是平面没有任何起伏。原因最常见的情况是 DEM 文件虽然看起来是地形数据但波段类型或 NoData 值没有正确设置CTB 读取时把所有值当作 0 处理。另一种可能是源数据里高程存储在多个波段而生成工具只读取了第一波段而第一波段存的是 RGB 合成影像。解决先用gdalinfo检查 Band 1 的 Type 字段确认是 Float32 或 Int16而不是 Byte。如果不是标准高程类型用gdal_translate -ot Float32强制转换。同时检查 NoData 值很多 SRTM 分幅数据在海洋区域是 -32768如果预处理阶段没有保留这个 NoData生成阶段可能把它插值成 0造成大面积平地。转换完后再用 QGIS 或 Global Mapper 打开看一眼确认灰度有明显起伏再进入 CTB 流程。5.2 瓦片边缘出现垂直悬崖和深坑现象相邻瓦片之间高度突变山体边缘出现笔直的“切面”或者某些区域直接凹下去一个大坑。原因这个坑通常是裁剪纸面边界时留下的 NoData 未被正确处理导致的。当 DEM 被gdalwarp -cutline裁剪后边界外的像素默认填充为 NoData如果 CTB 在读取时把 NoData 当成了 0 米边界处就会从几百米的高程直接跌到 0形成垂直断面。解决预处理时用gdal_fillnodata对边缘做平滑填充或者对原始 DEM 先向外扩出一定像素的缓冲区再裁剪让边界不再紧贴目标区域。后一种做法更简单用gdalwarp时不要用-crop_to_cutline而是先用-projwin稍微放大范围切完瓦片后边缘多出来的部分反正不会被前端加载不影响效果。5.3 layer.json 404Cesium 一直报 TerrainProvider 错误现象前端控制台报错地形完全加载不出来打开 network 面板能看到对layer.json的请求返回 404有时还会出现viewer.scene.rendererror事件。原因layer.json是 CesiumTerrainProvider 加载地形的入口文件nginx 的 root 路径指到了瓦片子目录或者生成时没产出这个文件。部分 CTB 小版本确实存在不自动输出layer.json的情况需要手动补。解决先用curl http://your-server/layer.json确认服务端能否访问。如果文件不存在手动创建一个最小配置{ format: quantized-mesh-1.0, scheme: slippy, tiles: [{z}/{x}/{y}.terrain] }创建后放到瓦片根目录下再确认 nginx root 指向的路径和 URL 访问路径一致。如果你的瓦片服务下有多个地形范围layer.json需要按范围分别生成并在 Cesium 的 url 参数里指到对应子目录。5.4 地形瓦片整体漂移山体出现在错误位置现象地形能加载也能显示起伏但山的位置和影像底图错开偏差从几百米到几公里不等层级越低保偏得越明显。原因源 DEM 坐标系根本不是 EPSG:4326CTB 默认按经纬度瓦片规则组织导致数据被强行套到错误坐标上。还有一个常见原因重投影时用了最近邻采样而源数据分辨率较低时像素格网点位移会在边界区域显现出来。解决回到预处理环节用gdalinfo确认源数据坐标系再用gdalwarp -t_srs EPSG:4326做重投影。重投影完成后不要急着切片先在 QGIS 里叠加一个矢量边界看看位置是否基本吻合确认无误再进入 CTB。这个过程看起来多花五分钟实际上能避免生成完十几 GB 瓦片才发现位置全错的悲剧。5.5 C 版 CTB 编译依赖地狱GDAL 版本冲突现象编译 CTB 时在 CMake 阶段报错找不到 GDAL或者编译过程中报缺少某个 boost 头文件有的版本还会因为 GDAL 3.x 里删掉了旧 API 而直接编不过。原因CTB 源码本身开发时间较早对现代 GDAL 3.x 的兼容性并不好常见的问题版本组合是 CTB 配 GDAL 3.2 或 boost 1.70。解决直接用 Python 重写版 CTB绕开编译环节这是我最推荐的方式。如果项目要求必须用 C 版建议在 Docker 里固定一个 GDAL 2.4.x 的镜像环境来编译不要把编译任务交给开发机。编译依赖问题如果超过半小时没解决果断换工具链CTB 不是唯一的瓦片生成方案CTS 在某些场景下部署更省事。6. 瓦片验证与后续演进从离线静态瓦片走向服务化瓦片生成完毕、前端也能加载之后别急着宣布完成先做一轮系统性的验证。我固定会做三个检查第一统计各层级瓦片数量是否和预估量级一致第二随机抽查几个瓦片文件看体积是否在正常范围内第三打开前端开启帧率显示飞行一圈观察瓦片请求是否稳定。from pathlib import Path terrain_root Path(./terrain) for z_dir in sorted(terrain_root.iterdir()): if z_dir.is_dir() and z_dir.name.isdigit(): tiles len(list(z_dir.glob(*/*.terrain))) print(fz{z_dir.name}, tiles{tiles})这段代码遍历地形输出目录统计每个层级下的.terrain文件数量。如果 z 层级分布出现跳层比如 z12 有大量瓦片但 z11 数量异常少说明生成过程中可能漏了部分范围需要重新检查预处理裁剪边界。瓦片数量分布符合预期后再在前端开启调试信息viewer.scene.debugShowFramesPerSecond true; viewer.scene.globe.tileLoadProgressEvent.addEventListener((count) { console.log(remaining tiles:, count); });第一行显示帧率第二行监听瓦片加载进度飞行过程中如果 remaining 数值长时间不归零说明有些瓦片请求一直失败需要回到 nginx 日志里查哪些路径在 404。再往后走一步如果项目从静态地形要演变成频繁更新 DEM 的服务考虑引入 CTS 把你的 CTB 输出目录作为基础瓦片源。CTS 可以直接读取已生成的瓦片目录对外提供动态服务原有 CTB 瓦片不需要重新生成。新数据进来时只增量更新对应范围旧数据保留作为回退版本这样既保住了 CTB 离线切片的性能优势又获得了服务化的灵活性。就地形瓦片生成这件事而言很多所谓的高清问题不是玄学而是 DEM 原生分辨率、误差阈值和层级上限三者匹配的结果参数对上了效果自然就出来了。希望帮到你。本文还有配套的精品资源点击获取