☰
Cesium地形瓦片生成实战:从GeoTIFF到quantized-mesh的完整流程
2026/10/15 17:35:21 网站建设 项目流程

简介:面向Cesium开发者的地形瓦片生成工具包,用于将TIFF格式的高程数据转换为Cesium可直接加载的地形瓦片,有效解决Web端三维地形渲染前的数据预处理问题。整套资源共包含108个文件,压缩后约16.17MB,核心由40个dll动态库和4个exe可执行程序构成,另含29个csv坐标参数文件、gfs/wkt空间参考描述及少量辅助配置,工具链较为完整,便于离线构建自定义地形数据集。目前已有2370人学习下载。借助该工具,用户无需专业GIS背景便能快速完成从原始高程TIF到Cesium terrain瓦片的转换;结合Cesium自带的地形服务接口,可生成分层分块的三维地形,在浏览器中获得平滑过渡的渲染效果。资源内附带的多种投影与坐标参数,支持灵活处理不同来源的高程数据,适配数字孪生城市、环境监测、灾害模拟与科普可视化等广泛场景。

1. 为什么你需要自己的瓦片地形生成器

接手三维 GIS 项目时,最常碰到的需求之一就是“在 Cesium 里用我们自己的高程数据”。默认官方在线地形虽然能显示,但你控制不了精度、更新节奏和存储位置,遇到内网环境更是一筹莫展。这时候,Cesium Terrain Builder 瓦片地形生成器就成了我处理本地高程数据的首选方案:它把 GeoTIFF 这种栅格 DEM,切成一整套 Cesium 直接能用的层级瓦片。本文会从格式选型讲到命令行落地,再列出我踩过的几个高频坑。无论你是刚开始接触地形切片,还是已经试过别的方法,这里给出的流程和参数都可以直接抄作业。

你需要关心的不仅是“能切出来”,而是切出来的东西能否被 Cesium 稳定加载、在 LOD 切换时不穿帮、在低端机上不掉帧。下面从最基础的概念说起。

2. Cesium Terrain Builder 的地形瓦片原理与选型:quantized-mesh 与 terrarium 怎么选

很多人第一次用这个工具时,会忽略一个前提:Cesium 地形瓦片有两种完全不同的数据组织方式,选错格式会造成加载效果和性能的巨大差异。Cesium Terrain Builder 同时支持 quantized-mesh 和 terrarium 两种输出,但它们的适用场景完全不同。

2.1 两种格式的本质区别

quantized-mesh 是一套以网格顶点为基础的格式,每个瓦片里真正存储的是三角网顶点和误差信息。Cesium 拿到这种瓦片后可以根据视距动态决定是否细分下一层。它的文件是二进制.terrain格式(早期也有人用.quantized-mesh后缀),体积相对小,适合大范围连续地形。

terrarium 则是把高度数据编码到一张 RGB 纹理里,常见做法是让红色和绿色通道表示高程的高低位,蓝色通道表示小数。Cesium 在运行时把这张贴图还原成高程,再生成网格。这种格式的好处是通用性强,很多不是 Cesium 的地图库也能读,而且可以和影像瓦片共用一套缓存机制。缺点是精度受纹理位数限制,极端陡峭地形容易出现阶梯。

从 Cesium 加载视角看,quantized-mesh 是官方推荐的第一选择,因为它专门为地形 LOD 做了优化。terrarium 适合你希望后续把同一份瓦片分发给其他 WebGL 地球引擎的场景。Cesium Terrain Builder 的默认输出一般是 quantized-mesh,但你可以通过参数切换。

2.2 选型依据:精度、体积与加载速度

实际项目里我一般按下面三个维度做决定:

对比维度quantized-meshterrarium
高程精度浮点网格顶点,精度高受纹理通道编码限制
文件体积较小,尤其平坦区域每个瓦片固定尺寸图片,体积大
加载速度Cesium 原生支持,解析快依赖纹理解码,稍慢
通用性基本只在 Cesium 生态使用可被多种三维引擎识别
适合场景高精度地形分析、大范围卫星地貌轻量可视化、多端复用

如果项目只需要在 Cesium 里展示地形,不想折腾二次开发,选 quantized-mesh 最省心。如果你的数据范围很小,比如一个园区或一段河道,用 terrarium 反而更直观,因为瓦片本身就是 PNG 文件,调试时可以直接打开看颜色。

2.3 完整工具链中 PostGIS、GDAL、Python 的角色

Cesium Terrain Builder 本身是一个命令行工具,但它并不直接读取 GeoTIFF 里的所有信息,而是依赖 GDAL 解析栅格,并利用 PostGIS 来管理已处理的数据范围。说得直白一点,PostGIS 在这里扮演的是“切片状态数据库”的角色,它记录了哪些区域已经生成了第几层瓦片,这样你后续增量更新 DEM 时不需要把整个金字塔重切一遍。

Python 在完整流程里通常是“调度员”:你写脚本去遍历一批 GeoTIFF,统一投影,再挨个调用生成命令,并把任务日志收集起来。这三者的职责我用一句话总结:GDAL 负责读高程,PostGIS 负责记进度,Python 负责把一个个孤立的 DEM 文件变成有组织的地形金字塔。明白这个角色分配,后面看命令时就不会被那一串前缀参数吓到。

3. 从 DEM 到瓦片:跑通瓦片地形生成器的最小流程

这一章是核心操作。我不会一次性把工具里所有参数都列出来,而是先让你用最少的步骤生成一个能看的瓦片集,然后再讲每个命令到底干了什么。这样你后面调整参数时心里有底。

3.1 准备 DEM:GeoTIFF 裁剪与坐标统一

切地形瓦片最忌讳“坐标乱七八糟的输入”。Cesium Terrain Builder 内部会假设你的 DEM 是地理坐标系(经纬度)或 Web Mercator,但实际拿到的数据经常是某个地方投影坐标,比如高斯-克吕格。我一般先用 GDAL 统一成 EPSG:4326,并转成标准 GeoTIFF。

# 查看源数据的坐标参考 gdalinfo input_original.tif # 转换为 EPSG:4326,输出分辨率保持原始大致量级 gdalwarp -t_srs EPSG:4326 -r bilinear -overwrite input_original.tif work_dem_4326.tif # 如果源文件过大,按目标范围裁剪 gdal_translate -projwin -120 50 -110 35 -of GTiff work_dem_4326.tif clipped.tif

gdalwarp的-r bilinear表示重采样用双线性,对连续地形来说边缘更柔顺;如果是建筑级 DSM,可以用-r cubicspline或-r near保留结构。-projwin的四个数字分别是左上角经度、左上角纬度、右下角经度、右下角纬度,顺序千万别搞反,不然裁剪出来的区域会颠倒。

3.2 安装与初始化:依赖项与常见坑

Cesium Terrain Builder 的安装方式有的发行版提供了预编译包,也可以从源码编译。编译前要确认 GDAL 和 PostGIS 的开发库已经装好。初始化阶段要做的第一件事是先建一个空数据库,并创建工具需要的扩展和表结构。

# 创建地形数据库 createdb terrain_db # 启用 PostGIS 扩展 psql -d terrain_db -c "CREATE EXTENSION IF NOT EXISTS postgis;" # 让 Cesium Terrain Builder 能写入数据库 psql -d terrain_db -f /path/to/cesium_terrain_builder.sql

这里最容易翻车的是数据库认证方式。工具进程如果和 PostgreSQL 不在同一台机器上,需要把连接权限配成允许远程 IP,否则切片命令一执行就报错。另外,cesium_terrain_builder.sql这个文件可能不是自动生成的,你需要去工具发布包里找。找不到时,可以直接使用官方仓库里提供的标准 schema,这个 schema 只需执行一次。

3.3 核心命令行:首层生成与增量切片

等到数据库和输入 DEM 都准备好,就可以跑真正的切片命令。工具提供的命令行入口通常是ctb-tile,下面是最小可用的调用方式:

# 生成金字塔瓦片,指定输出目录和输入 DEM ctb-tile -o ./terrain_tiles -f quantized-mesh -C -S work_dem_4326.tif

解释一下这里每个参数:-o指定输出目录,-f指定格式(quantized-mesh 或 terrarium),-C告诉工具在首次处理时自动创建金字塔,-S表示统计输入 DEM 的覆盖范围并写入数据库。如果后续有新的 DEM 文件补进相同区域,第二次执行时不需要再加-C,工具会根据数据库里的记录只补切新覆盖的瓦片,这就是增量更新的意义。

切完之后,输出目录里会按0/0/0.terrain这种层级结构存放瓦片,同时生成一个layer.json,里面记录了瓦片格式、误差范围和可用层级。Cesium 加载地形时首先会请求这个layer.json,没有它就等于没有入口。

3.4 把瓦片接入 Cesium:URL 配置与服务发布

生成出来的瓦片文件需要以静态文件方式放到 Web 服务器上。我常用 Nginx 或者 Caddy 把terrain_tiles目录设为站点根路径。然后在业务代码里用CesiumTerrainProvider指向该路径即可:

const viewer = new Cesium.Viewer("cesiumContainer"); const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl( "/terrain_tiles/layer.json", { requestVertexNormals: true, requestWaterMask: true } ); viewer.terrainProvider = terrainProvider;

这里requestVertexNormals和requestWaterMask只有在 quantized-mesh 里才有意义,terrarium 不需要。如果你的瓦片生成时没有附带法线数据,即使设置了true也会被忽略,所以不要为了追求光照效果强行开启,否则浏览器控制台会反复出现警告。发布时还有一个重要细节:服务器需要正确返回Content-Type: application/octet-stream给.terrain文件,Nginx 默认不一定认识这个扩展名,你需要在配置里显式加一条 MIME 映射。

4. 地形瓦片关键参数:层级误差、分辨率与并行度调优

最小流程跑通后,就该面临真实项目最常见的问题:切出来的地形为什么在某些层级特别清晰,在另一些层级却像糊了一层?这背后是瓦片金字塔的误差阈值和输入分辨率共同作用的结果。

4.1 层级与误差阈值:决定 LOD 切换的参数

Cesium 的 LOD 调度不是看瓦片编号,而是看每个瓦片里记录的GeometricError。Cesium Terrain Builder 在生成时会根据输入 DEM 的分辨率和输出层级计算这个误差值。如果你希望视角拉近时快速显示高细节,需要把最大层级调高:

ctb-tile -o ./out -f quantized-mesh -e 1.0 -l 12 -C input.tif

-e指定基础误差阈值,单位是米,值越小越精细。-l指定最大缩放层级,例如-l 12表示生成到第 12 级。实际上,Cesium 在超过 12 级后还会继续请求更细的瓦片,如果数据库里没有,客户端会降级到上一级。因此-l不是越高越好,它直接决定切片数量和耗时。对全球尺度或省级尺度,我一般先-l 10试探,观察磁盘占用和加载速度再决定是否加层。

4.2 高程精度与垂直夸张:避免 “平板地形” 或 “悬崖”

很多人在 Cesium 里看自己切的地形,会觉得“山不够高”。这通常不是瓦片生成的问题,而是 Cesium 默认的垂直夸张系数是 1.0。要验证高程是否真实,可以在 Viewer 里设置viewer.scene.screenSpaceCameraController.enableCollisionDetection = false,然后放大到已知高程点查看。真正的高程精度来自你输入 DEM 的位深,Cesium Terrain Builder 会保留原始高程值,但如果你的 GeoTIFF 已经做了拉伸到 0-255 的 8 位编码,神仙也救不回来。

另外,quantized-mesh 格式内部有坐标量化,它本身不压缩高程精度。如果你从 ASTER 或某免费 DEM 下载的 16 位有符号整数 GeoTIFF,直接用它生成即可。不需要先转成浮点型,因为工具内部会用浮点数重新计算。需要警惕的反而是一些“看起来是 TIFF,实则是 RGB 高程图”的数据,那种必须先还原成数值 DEM,否则生成出来的地形会是一堆锯齿。

4.3 并行度与内存控制

切瓦片是 CPU 密集任务,Cesium Terrain Builder 默认会启动多个工作线程。但在小内存服务器上,过高的并行度会导致内存被吃满,系统开始换页,速度反而下降。更危险的是,如果 PostGIS 连接数不够,线程会在数据库等待上拥堵,日志里出现一堆连接超时。

我通常用环境变量控制并行度:

# 使用 4 个线程,避免超线程争抢 export OMP_NUM_THREADS=4 # 关闭 OpenMP 动态调整 export OMP_DYNAMIC=FALSE ctb-tile -o ./out -f quantized-mesh -C input.tif

内存方面,输入 DEM 如果超过 2GB,建议先用gdal_translate -co COMPRESS=DEFLATE压缩一下。工具读压缩栅格时本身也要解压,但至少内存里不会同时留下一份完整解码副本。这里有一个隐藏坑:PostGIS 数据库所在的磁盘如果剩余空间不足,切片命令也会在写到一半时突然崩溃,所以不要只看输出瓦片目录的空间,还要给数据库留足余量。

5. 瓦片地形生成避坑与排查:5 个高频翻车现场

工具本身不复杂,真正耗时间的往往是切完之后一加载就出问题。下面五个案例都是我实际遇过或帮别人排查过的,按“现象 → 原因 → 解决”方式记录,方便你对照。

5.1 现象:瓦片在 Cesium 中加载后一片黑,或者地形完全消失

原因排查:瓦片请求成功了,但返回的二进制数据不能被正确解析。多数情况是layer.json里声明的format与实际文件不匹配,或者说服务器把.terrain错误地当成了文本文件,导致内容被转码。还有一种可能:你的 DEM 全是 NoData 值,生成出来的高程是一个常数,Cesium 认为这一片没有地形数据。

解决办法:先用 curl 手动请求一个瓦片文件,检查响应头的Content-Type;再用小工具把该瓦片读出来,看里面是否全是空数据。如果所有瓦片都正常但加载黑屏,把CesiumTerrainProvider的requestVertexNormals和requestWaterMask全部设为false再试,排除光照和水面插件干扰。

5.2 现象:相邻瓦片边缘出现裂缝或错位

原因排查:输入 DEM 的边界没有对齐到经纬度整数网格,导致瓦片边界两侧的高程采样点不一致。Cesium Terrain Builder 在切片时会参考原始 DEM 范围和 target 层级,如果源数据有旋转或带有地理参考偏差,瓦片之间就会“跳变”。

解决办法:在生成前用gdalwarp把 DEM 强制对齐到EPSG:4326,并设置-tr 0.0001 0.0001(约 10 米)这样的整度数网格。注意,直接重投影并不会自动对齐网格,必须显式指定目标分辨率。对齐后重新生成,裂缝问题基本消失。

5.3 现象:切片到一半进程卡死,或者 PostgreSQL 连接数飙满

原因排查:切片进程对数据库是多线程写入,而 PostgreSQL 默认max_connections只有 100。Cesium Terrain Builder 使用连接池时,如果线程数大于连接池上限,就会出现无限等待,看起来像卡死。另一个常见原因是临时表空间太小,数据库无法完成大事务合并。

解决办法:先用ps aux | grep ctb-tile看进程状态,再用pg_stat_activity查询等待中的连接。如果是连接数问题,调小并行度;如果是表空间问题,给 PostgreSQL 的temp_tablespaces配置一个更大的目录。实在不行就把数据库放到 SSD 上,切片过程对随机写入性能高度敏感。

5.4 现象:地形起伏看起来完全被压扁,抬升量不对

原因排查:Cesium 默认按米解释高程值,但一些老旧的高程数据以厘米或分米为单位存储。如果生成工具没有做单位归一化,你等于把 10000 厘米当成了 10000 米去渲染,但三维场景中摄像机远近距离会掩盖这种错误,导致看起来就像“几乎平地”。

解决办法:在输入 DEM 上先用gdallocationinfo随机抽查几个点的高程值,确认数值量级。如果确实是厘米,用gdal_calc.py除以 100 后再生成。同样,如果数据源垂直基准是海拔高而不是椭球高,生成出来的地形在沿海区域可能和真实海平面偏差几十米,需要确认你的业务需求再处理。

5.5 现象:Cesium 提示 Terrain provider 返回异常或解码失败

原因排查:这种报错通常出现在瓦片文件本身没坏,但 Web 服务器把二进制文件当成 gzip 压缩后又加了一层 Content-Encoding,导致前端拿到双重压缩的数据。另一个可能:layer.json里的bounds写错了,Cesium 请求的瓦片超出了实际数据范围。

解决办法:打开浏览器网络面板,查看layer.json和单个瓦片的响应头。如果发现Content-Encoding: gzip且服务器手动对.terrain做了压缩,请关掉该目录的 gzip 压缩。如果bounds不对,回到数据库检查metadata表里记录的范围,必要时删掉元数据重新统计。

6. 进阶:用 hillshade 叠加与多尺度 DEM 混合做出更好看的地形

切出来的地形即使几何正确,默认的着色也略显平淡。最直接的提升方式是叠加 hillshade(山体阴影)贴图,让地形在视觉上更立体。

做法分两步:先用 GDAL 从 DEM 生成一个山体阴影影像,再用影像瓦片服务发布,最后在 Cesium 里把地形和阴影数据叠在同一位置:

# 生成带阴影的 GeoTIFF,方位角 315 度,太阳高度角 45 度 gdaldem hillshade work_dem_4326.tif hillshade.tif -az 315 -alt 45 -z 2

关键参数是-z 2,它表示垂直方向夸大 2 倍。开始做时不要用人眼判断,先切一个小范围测试,拉近看成片效果。阴影数据发布后,在 Cesium 里用ImageryLayer叠加到地形上方,再把影像层的 alpha 设成 0.6 左右,阴影就会从半透明变成柔和立体感。

多尺度 DEM 混合是另一个值得投入的方向。比如某个区域既有 30 米分辨率全球数据,又有局部 2 米数据,我会先用低分辨率数据生成 0-8 级金字塔,再用高分辨率数据单独生成 9-15 级,最后用脚本把两份瓦片目录合并,并保证layer.json里的 available levels 连续。这个过程很容易出现某级断层,所以我习惯把高精度数据先裁剪到低精度数据的范围边界,甚至多做一层缓冲,免得边缘露出空洞。

我踩过最狠的一次,就是没检查局部高精度 DEM 是否覆盖了低精度数据的边界,结果放大到 10 级时,整个区域的地形像是被大铲子挖掉了一块。后来我总结了两个习惯:一是每次合并前先算两套数据的覆盖 poly,用intersection判断重叠;二是生成后不要依赖肉眼,写一段脚本遍历 9 到 15 级的瓦片路径,逐一检查是否存在 404。做完这套检查,再交给前端渲染,基本就不会翻车了。

如果你也正在做地形切片,建议从最小的区域开始试,先搞定格式和参数,再逐步扩大范围。希望这些经验能帮你少走一段弯路,也希望这套瓦片地形生成器方案能在你的项目里真正落地。

本文还有配套的精品资源,点击获取

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

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

立即咨询