简介:这是一套面向Cesium开发者的地形瓦片生成工具,用于将TIFF格式的数字高程模型(DEM)高效转换为Cesium可直接加载的terrain格式瓦片,解决Web GIS应用中地形数据预处理与跨格式转换的难题。工具简化了从原始高程数据到三维地形的处理流程,特别适合非专业GIS背景的前端或Web开发者。压缩包共含108个文件,以40个dll动态库和4个exe可执行程序为核心,配套csv、json、xml等坐标参考与元数据配置,以及gfs、wkt等GIS辅助格式,整体仅16.17MB,轻量便携;已有2372人学习下载,可帮助使用者快速在Cesium项目中集成自定义高程数据。通过该工具,用户能自主完成从TIFF高程到层级化地形瓦片的完整转换,生成包含瓦片元数据与实际高程信息的JSON或Binary数据,在Cesium中通过地形服务URL即可实现流畅渲染。包内附带的多种坐标系统定义文件,便于处理不同投影来源的DEM,适用于地理测绘、环境监测、城市规划和灾害模拟等三维可视化场景。
1. 把 DEM 批量切成 Cesium 可加载地形瓦片的那条最短路径
拿到一整片 GeoTIFF 高程数据,想让它变成 Cesium 能按 LOD 逐级加载的地形瓦片,中间隔的不是一个「导出」按钮,而是一整套重投影、金字塔切片和瓦片格式的组织流程。ctb(cesium terrain builder,瓦片地形生成器)这类命令行工具,就是把这套流程压成几条命令的存在:原始高程进去,terrain tiles 出来,之后接一个静态文件服务器就能给 Cesium 供地形。它适合手里已有某区域 DEM、想自建离线地形服务的团队,也适合做数字孪生或三维可视化项目、想省下每级地形服务成本的开发者。下面从格式选型开始,讲到参数怎么调、哪些位置最容易翻车。
2. 地形瓦片有两种格式:先选 quantized-mesh 还是 heightmap 再动手
同一份 DEM,切成不同瓦片格式,在 Cesium 里的表现和后续维护成本差别很大。这个选择不复杂,但要在第一步定下来,因为它决定后面所有命令里的 tileformat 参数,也决定 Cesium 那边用什么 provider 去加载。Cesium 的地形服务核心不是「把高程图传上去让它贴在地球上」,而是提供一张带真实起伏的三角网或高度场。常规的地形服务商提供的是 quantized-mesh;本地自建时可以选 heightmap,也可以选 quantized-mesh,两者对应完全不同的文件组织与精度表现。
2.1 quantized-mesh 与 heightmap 的本质差别:网格 vs 高度阵列
quantized-mesh 是把一个瓦片范围内的地表用三角网表示,每个瓦片内部是自适应加密的网格,可以表达悬崖、垂直面甚至带洞的地形(比如矿坑或隧道口)。它的每个文件是二进制的 .terrain 格式,瓦片边界严格对齐,相邻瓦片共享边界顶点,所以跨瓦片不容易出现接缝。Cesium 的 CesiumTerrainProvider 对这套格式支持最完整,可以顺带把法线、水蒙版、光照一并提供给渲染端。
heightmap 则是一张矩形高度值阵列,每个像素存一个高程值。文件组织简单,切片速度更快,瓦片体积也更小,但高程精度和地形细节会打折扣:同一个 16 位高程源,落到 heightmap 后如果编码不当只留 8 位,陡峭地形的台阶感会非常明显。它的主要价值在于快速验证、教学和轻量场景。常见做法是用它给原型项目临时凑一块地形,而不是作为正式地形服务的默认选项。
提示:决定格式时想清楚一件事——后续是否要支持光照、平滑 LOD、海陆接缝。只要有一项是「要」,就选 quantized-mesh,不值得为了省一点磁盘空间回头重切。
两者取舍落到命令上就一个参数:-f quantized-mesh或-f heightmap。但换了格式,输出目录里的文件类型完全不同,Cesium 那端也要换 provider 或换加载方式。先遍历几种常见选型,再决定不迟。此后一旦切完,想换格式只能全量重来——这是整个流程里「后悔药」最贵的一步。
2.2 按应用场景选格式:范围、细节量与贴地效果怎么权衡
选格式其实是按「范围 + 精度 + 加载性能」三个维度来权衡。大范围(整个省域或一个流域)的场景,我一般直接用 quantized-mesh:它每一层的瓦片数不像 heightmap 那样固定到一个均匀网格,而是在平坦区域省顶点、陡峭区域加顶点,同等级别下网络请求数和渲染面数都更低;更重要的是它支持法线数据,可以让山体在阳光方向上有明暗面,视觉效果立刻脱离「灰扑扑的塑料地形」。
小范围单场景,比如一栋园区、一段滨水步道的数字孪生演示,heightmap 完全够用。它出片快、瓦片小、本地起个静态服务就能跑,调试成本低。这里有一个容易误判的点:heightmap 不等于「精度一定低」。关键在于源数据的位深和编码方式,16 位高程源配 heightmap 时如果编码正确,仍然能保留大部分细节;真正把它拖垮的是后续渲染端为了兼容而做的二次压缩。所以在选 heightmap 之前,先确认加载它的客户端能不能接受原生位深,否则只是把精度损失从切片端搬到了渲染端。
2.3 格式对应的加载方式:CesiumTerrainProvider 与 URL 组织规则
quantized-mesh 在 Cesium 里通常用 CesiumTerrainProvider 加载,URL 指向输出目录的根节点;heightmap 在旧版本里常配合指定格式的 terrain provider 使用,也有的实现直接按目录读取。两者 URL 的层级规则几乎一样:根节点下面是 0 层、1 层这样递增的目录,每层里面按瓦片坐标组织文件。看到「瓦片地形生成器」这个名字就该意识到,它产出的是一套有严格目录约定的文件树,不是让你去手动改名拼 JSON 的。
为了不把概念讲空,下一章直接用一套最小命令把这条链路跑通。跑通之后再回来调参数,你会清楚每个参数在动文件树的哪一部分。
3. 本地跑通第一套瓦片:从 GeoTIFF 到 terrain tiles 的最小命令
切地形之前先确认手上数据能用。常见的高程数据有 30 m 和 90 m 两档分辨率,通常以 GeoTIFF 交付;拿到之后不要直接丢给切片器,先看一眼投影、范围和 nodata。命令行下用 GDAL 的 gdalinfo 查看是最稳的。
3.1 前置准备:高程数据、投影检查与工具安装
用 gdalinfo 读取高程文件的基本信息,确认三件事:坐标系是否为经纬度、范围是否符合预期、有没有 nodata 值。
gdalinfo dem.tif # 重点看 Driver: GTiff、Size、Coordinate System、No Data Value 三行 # 如果 Coordinate System 不是 WGS84 经纬度,后面先做重投影再切片会习惯性敲这条命令的人,通常不会再折在投影不匹配上;跳过这一步直接切,大概率会在 Cesium 里看到地形跑到错误的位置或干脆不显示。GDAL 没装的话,用对应系统的包管理器装 gdal 相关命令行工具即可,具体包名取决于发行版,不做展开。
ctb 这类工具一般需要从源码编译。常见做法是拉源码后按 cmake 的标准三步走:生成构建文件、编译、安装。装完检查一下子命令是否可用:
ctb-tile --help # 输出里应能看到 -f / -o / -l / -L 等参数说明 # 如果提示找不到命令,检查安装路径是否已加入 PATH不同版本之间参数名可能略有出入,不用背命令,以本机 --help 的输出为准。这一步的核心不是「把工具装起来」,而是确认手里的数据能被工具识别,免得后面排错时分不清是数据问题还是工具问题。如果跳过这个验证,最常见的后果是切片到一半才报投影错误,前面几小时的算力全白费。
3.2 生成瓦片:一条命令和它产出的目录结构
数据确认没问题后,最小切片命令一般长这样:
mkdir -p tiles ctb-tile \ -f quantized-mesh \ # 瓦片格式,按第 2 章选定的格式填 -o tiles \ # 输出目录,瓦片都落在里面 -l 0 \ # 最顶层 LOD,0 表示整个范围一张瓦片 -L 10 \ # 最底层 LOD,决定地形能放大到多细 dem.tif # 输入的高程文件,只支持单个文件-f指定瓦片格式;-o指定输出目录;-l是生成的最低层,-L是生成的最高层。上面写的是 quantized-mesh 的例子;如果第 2 章选了 heightmap,只把-f的值改掉即可,其它参数不用动。
运行完以后,tiles 目录下会多出 0 到 10 的层级目录,每个目录里是若干 .terrain 文件。可以用 find 快速看瓦片规模:
find tiles -name "*.terrain" | wc -l # 一个 10 层的金字塔大概会有数万到数十万个文件,属正常现象如果瓦片数量明显异常(比如目录建了但里面几乎没有文件),多半是输入数据没有被正确解析,回上一步检查 gdalinfo 的输出,再看工具运行时有没有遗留报错信息。切片过程本身耗时较长,建议先用小范围的 DEM 试跑,确认层级结构和文件命名符合预期,再上全量数据。
3.3 立刻验证:起一个静态服务,让 Cesium 加载这堆文件
切出来的瓦片在没有专门服务端的情况下也能验证:python3 自带一个静态文件服务器,把 tiles 目录对外提供即可:
cd tiles python3 -m http.server 8000 # 浏览器访问 http://127.0.0.1:8000/0/0/0.terrain 应返回文件内容Cesium 侧用极简配置就能接上,以 quantized-mesh 为例:
const viewer = new Cesium.Viewer("cesiumContainer", { terrainProvider: new Cesium.CesiumTerrainProvider({ url: "http://127.0.0.1:8000/tiles", // 指向 tiles 的根目录,不是 0 层目录 }), });提示:URL 指向根目录即可,Cesium 会自己按 LOD 拼接 0/1/2/../x/y.z 的路径。常见翻车是把 URL 指到了 0 层目录,导致所有层级都 404。
第一次跑通后,可以看到 viewer 表面不再是平的椭球,而是有起伏的山体。到这一步就算「这套方案能落地」,后面的工作都围绕「把瓦片切得更准、更省、更符合业务范围」展开。接下来这章讲参数:哪些必须调,哪些调了反而浪费。
4. 切之前把这些参数调对:坐标、层级与高程质量三处决定成果成败
第 3 章的最小命令能跑通,但离生产可用还有距离。真正值得反复调的不是切片工具本身,而是输入数据的坐标、层级范围和位深这三件事。原因很简单:切片工具只认「投影一致、坐标系明确、高程值可解析」的输入,而市面上拿到的 DEM 很少同时满足这三条。多数翻车现场都发生在这些前置处理上,而不是切片命令本身。
4.1 重投影与重采样:把多源 DEM 统一到 WGS84 经纬度
Cesium 地形服务的内部坐标基于经纬度椭球,瓦片的行列号也按经纬度网格计算,所以输入 DEM 最好统一到 EPSG:4326。很多原始高程数据是 UTM 投影或 Web 墨卡托(EPSG:3857),直接切未必报错,但生成结果会和真实位置错位,尤其在跨带和高纬度区域越偏越远。统一投影的常见做法是用 gdalwarp 做重投影并同时完成重采样:
gdalwarp \ -t_srs EPSG:4326 \ -r bilinear \ -ot Int16 \ dem_utm.tif dem_4326.tif # -t_srs 目标投影 # -r 重采样算法 # -ot 输出像素类型重采样算法值得多说一句:bilinear 适合一般地形,能保留大部分连续起伏;cubic 会让地形更平滑但会轻微磨掉尖锐山脊;nearest 千万不要用在地形上,会出明显锯齿。输出像素类型选 Int16 可以保留正负海拔(比如低于海平面的洼地),如果源数据本身是浮点型且没有负值,也可以用 UInt16 省一半体积。做完这一步再跑一次 gdalinfo 确认坐标系统已经变成经纬度,同时留意重投影后是否出现了边缘空洞。
4.2 层级(LOD)与存储:-l、-L 不是越大越好
切片命令里三个高频参数是-l(最低层级)、-L(最高层级)和-o(输出目录)。-l通常设 0 或 1,代表整个数据范围在最顶层只拆成一张或四张瓦片;-L才是决定「能放多近看」的参数。-L每加一,瓦片数约为原来的 4 倍,到某个层级后,瓦片数增长速度会明显超过地形细节的增长速度,纯属浪费。按范围给一个经验参考:
| 数据范围(边长) | 建议最高层 L | 适用场景 |
|---|---|---|
| 10 km 以内 | 12~14 | 园区、矿区、滨水段 |
| 10~100 km | 10~12 | 城市级,够看山体走向 |
| 100 km 以上 | 8~10 | 省域/流域级,高层级性价比低 |
这张表不是死的,取决于源分辨率:如果源数据只有 90 m,L=14 时瓦片密度远超源数据有效分辨率,等于一直在插值。一个实用判断方法:在 Cesium 里把视角拉到你想支持的最近距离,看地形是否已经出现明显的三角面,出现就说明该层够用,不必继续加 L。切片前对最大文件数做个估算,给磁盘留出 1.5 倍余量,避免切到一半磁盘写满。
4.3 高程精度、nodata 与水陆边界:数据质量决定瓦片质量
位深是容易被忽略的一环。源 DEM 常见 16 位整型和 32 位浮点两种;16 位整型对高程范围大的区域(比如高低差几千米)会损失低海拔区的细节,浮点则直接保留原始精度。代价是瓦片体积变大、加载变慢。我的习惯是:高程差在 1000 m 以内用 16 位整型,超过 1000 m 用 32 位浮点,避免为了省体积把山地细节切没了。
nodata 区域更常见也更容易踩:很多 DEM 在河流、湖泊和云遮挡处有空值,直接切会导致瓦片边缘出现尖锐凹陷或接缝。切之前先用 gdal_fillnodata 把空洞填上再进切片器:
gdal_fillnodata dem_with_holes.tif dem_filled.tif # 用周围有效像元插值填充空隙,适合小范围空洞 # 大范围水域建议结合水陆掩模处理,而不是纯插值水陆边界要做得干净,常见做法是准备一张水陆掩模,把陆地和水域分开处理:水域高程统一设为 0,再配合 quantized-mesh 的水蒙版让水面不渲染。如果源数据里没有海域,只是内陆河流湖泊,先按填洞处理即可,不用上掩模。还有一个细节容易忽略:填洞和重投影的顺序。先填洞再重投影通常比先重投影再填洞更稳,因为重投影本身会产生新的边缘空洞,反过来处理会把这些新空洞也覆盖掉。
5. 瓦片生成避坑指南:五个反复出现的翻车现场与排查路径
瓦片生成工具本身不复杂,绝大多数问题出在「数据进了工具之前」和「瓦片出了工具之后」两段。下面五条是实际使用里出现频率最高的,按现象、原因、解决依次写,方便直接对号入座。
5.1 相邻瓦片边界出现裂缝或断崖
现象:在 Cesium 里飞行时,两块瓦片的交界处出现明显错位或断裂,有时是一条细缝,有时是整条锯齿状断层。
原因:多半是源数据 nodata 未处理。切片时 nodata 区域的像元参与了重采样,边界处的两个瓦片各自内插出不同的高程值,拼接时自然对不上;另一类原因是重投影时用了 nearest 或 bilinear 但源数据分辨率过低,边界像元被重复采样。
解决:切之前先 gdalinfo 确认 NoData Value,再用 gdal_fillnodata 预填;重投影时把重采样换成 cubic,并保证输入输出分辨率匹配。如果裂缝只出现在特定层级,多半是该层级瓦片正好覆盖了源数据边缘,把数据外扩一个像元再切即可。
5.2 地形整体「浮空」或「陷进地下」
现象:加载后地形表面明显高于或低于周边地物,比如影像和 DEM 在同一位置,地物在地形上方飘着或埋进土里。
原因:基准不一致。DEM 的高程基准和 Cesium 默认的 WGS84 椭球高程之间可能存在数十米级差异;另一种常见原因是切 heightmap 时位深不够,高程被截断后整体偏差。
解决:先用 gdalinfo 看源数据高程单位。如果是相对某参考椭球的高程值,确认和目标椭球一致;不一致时用 gdalwarp 加-s_srs指定源椭球再重投影。heightmap 则改用 16 位编码,或直接换 quantized-mesh。放到生产前,找几个已知高程的控制点逐一核对,比在渲染端瞎调垂直方向上的夸张系数靠谱得多。
5.3 Cesium 里地形服务 404,瓦片请求全部失败
现象:viewer 能起来,影像正常,但 network 面板里 terrain 相关请求全红,console 报错提示无法加载地形。
原因:URL 指向了层级目录而不是根目录;或 .terrain 文件没有对应 MIME 类型;还有一类是静态服务器对目录大小写敏感,而不同系统的文件系统大小写规则不同。
解决:URL 指向根目录;在静态服务器配置里把 .terrain 映射为application/octet-stream;文件路径统一小写。如果用的是 python http.server 调试,直接 cd 到 tiles 目录再启动,避免路径多一级或少一级。遇到 404 先别急着改代码,用浏览器手工访问一个已知瓦片路径,确认文件真实存在再回来看 provider 配置。
5.4 文件数量爆炸,磁盘在切片中途被写满
现象:明明数据范围不大,输出瓦片却有几十万上百万个文件,磁盘占用远超预估。
原因:-L设太高,或输入数据分辨率远高于地形所需。每个层级四倍增长,到 L=14 以上文件数量会迅速失控,而地形细节并不随之线性增加。
解决:按第 4 章的参考表定-L;如果业务确实需要更近的视角,先把源数据用 gdalwarp 重采样到和 L 匹配的分辨率,而不是让切片器在高层级空转插值。切片前对最大文件数做个估算,给磁盘留出 1.5 倍余量。如果跑了一半发现层级设大了,不用硬等,直接终止,调整参数重跑,切片器本身是幂等的,重复生成不会产生额外副作用。
5.5 瓦片生成了,但地形看起来是平的
现象:所有请求正常返回,viewer 也设置了 terrainProvider,但场景里地表始终是平面,没有任何起伏。
原因:最常见是 terrainProvider 的 URL 和生产服务没对上,Cesium 静默降级到椭球面;其次是没开和地形遮挡相关的渲染选项,导致地物与地形的相对关系显示不出来;还有一种是把影像图层当成了地形图,以为贴图就代表地形生效。
解决:先确认请求里确实返回的是 .terrain 文件而不是 HTML 错误页;再确认 provider 的 URL 和瓦片路径一致。临时把视角拉到山体侧面看轮廓线,平面与否一眼便知。开了深度检测之后,地物和地形的遮挡关系才正常,这一步容易被当作「地形没生效」误判。
6. 用一张影像做配准验收:确认瓦片切准之后,再谈进阶技巧
落地验证最快的方法不是看高程值,而是拿一张已知位置的影像叠加到地形上,让山脊线和影像里的山形走向对齐。Cesium 里可以临时加一个影像图层,视角调到斜 45 度,观察山脊是否和影像走向一致;有偏离就先查投影基准,不要先去调瓦片参数。这一步看着简单,但能把「数据的坐标系对不对」和「切片的算法好不好」分成两件事排查,比闷头调参快得多。
验证通过后,有四个值得马上投入的进阶点。第一,把 tiles 目录整体迁到对象存储或 CDN,URL 换成带签名的地址,Cesium 对这类 URL 的支持很直接,能解决单机静态服务的带宽瓶颈。第二,开启光照:viewer.scene.globe.enableLighting = true,前提是切的是 quantized-mesh 且保留法线数据,否则看到的只是暗面没有体积感。第三,接多分辨率数据:近景用高分辨率 DEM,远景用低分辨率 DEM,分别切成两套瓦片后在服务层按层级路由,代价是管理两套目录,收益是立体感和性能同时提升。第四,如果要表现挖方和填方场景,把开挖后的设计标高生成一张新的 GeoTIFF,按同一套参数再切一遍,交给 Cesium 做动态更新即可。
我最开始做地形服务时,就是栽在接缝上,查了两三天才发现是源数据的 nodata 没填。后来养成了一个习惯:每次切瓦片前固定跑一次 gdalinfo,把坐标系、nodata、范围三行信息记到文本里再动手。这套检查不到一分钟,但能省掉后面一整天的排查。做瓦片地形不复杂,只是要求每一步都按数据实际情况来,而不是按「默认参数能跑」来。希望帮到你。
本文还有配套的精品资源,点击获取