ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

离线地形瓦片生成实战:CTB将DEM转为Cesium quantized-mesh

离线地形瓦片生成实战:CTB将DEM转为Cesium quantized-mesh 简介面向Cesium三维地图开发者和GIS数据工程师这份资源是一套可直接使用的Cesium Terrain Builder瓦片地形生成工具包用于将包含高程信息的TIFF遥感影像转换为Cesium可加载的地形瓦片解决从原始DEM到Web三维地形发布之间的格式转换难题。包内共108个文件压缩后16.17MB主要包含exe可执行程序、dll运行库、csv坐标与投影参数文件、gfs与wkt空间参考定义以及少量示例terrain/json等数据可支撑完整的瓦片生成与调试流程。目前已有2367人学习下载适合需要离线处理高程数据、搭建本地地形服务的开发者参考。借助这套工具用户可以掌握CTB命令行调用方式与参数配置理解地形瓦片层级结构和元数据组织并快速完成自定义高程数据的切片与发布为Cesium场景提供流畅的3D地形渲染方案。 前阵子给一个内网三维项目做地形底图客户上来就要能看到山体起伏可项目环境又没法依赖任何在线地形服务公共地形瓦片要么延迟高要么政策上不允许外网调用。我翻了一圈工具最后是靠 Cesium Terrain Builder 这个老牌项目把本地高程数据切成了 quantized-mesh 瓦片地形在 CesiumJS 里一挂三维地形立刻就有了。这篇文章就把我踩过的坑、理清楚的原理和可直接复用的流程全部写出来。它是什么、能解决什么问题、适合哪些人我都会说清楚目标是让第一次接触“瓦片地形生成”的人也能照着走通。1. 先搞清楚 CTB 在解决什么问题1.1 Cesium 里的“地形瓦片”不是图片瓦片很多朋友刚接触 Cesium 时会有一个错觉地形和影像一样切一张张 PNG 或者 JPEG 图片铺在地球上就行。实际上完全不是一回事。Cesium 的地形是基于 TIN不规则三角网的 quantized-mesh 格式每一块瓦片里存的不是像素颜色而是一堆三角形顶点、法线、顶点高度和数据围栏信息。浏览器端拿到这些顶点和三角面片后会实时构建出真正的三维地形网格。这个设计带来的直接好处是地形表面是连续的几何体可以接收光照、射线拾取、地形掩膜也能和模型、矢量数据真正贴合。代价就是生成这种瓦片的工具链相对小众不能像影像瓦片那样直接拿 gdal2tiles 之类的工具顺手搞定。CTB 恰好就是专门为 quantized-mesh 输出设计的工具之一这也是它在离线地形场景里还能被反复提起的根本原因。1.2 CTB 到底是做什么的Cesium Terrain Builder下面简称 CTB是一套基于 GDAL 的命令行工具集核心输入是 GeoTIFF 或 VRT 格式的高程栅格数据输出是 Cesium 直接能用的地形瓦片目录。这个目录里有一个layer.json文件作为地形服务索引还有按z/x/y.terrain规则组织的瓦片文件。CTB 内部会自动构建多级金字塔从低层级的粗网格到高层级的精细网格每一级瓦片都是根据上一个层级递归切分的。CesiumJS 端用CesiumTerrainProvider.fromUrl指向这个目录引擎会按当前相机视角动态请求对应层级的.terrain瓦片并实时生成地形网格。所以 CTB 不是用来“显示”地形的它只负责把原始 DEM 变成 Cesium 能高效消费的瓦片数据解决的是离线环境下的地形数据生产问题。2. 动手前准备安装环境和高程数据2.1 安装 CTB 与 GDAL先避开 Python 版本这个坑CTB 是个有些年头的开源项目代码还停留在 Python 2 时代最新版本也基本停在 0.4 左右。如果你直接在当前主流 Python 3 环境里执行pip install ctb大概率会碰到编译错误或者运行时稀奇古怪的异常。我最开始就栽在这里浪费了大半天查依赖问题最后发现是 Python 版本不兼容。比较省心的做法是用 Anaconda 单独建一个 Python 2.7 环境然后在里面安装 GDAL 和 CTBconda create -n ctb python2.7 conda activate ctb conda install gdal2.4 numpy pillow pip install ctb这里有个小提醒CTB 底层走的是 GDAL 的栅格读写接口所以 Python 绑定的 GDAL 版本必须和系统 GDAL 库对应上。用 conda 安装的好处是它会帮你把库和 python 绑定一起装好避开了手动编译时常见的“找不到 gdal headers”问题。如果你的服务器上已经有一套别的项目在用 GDAL千万别轻易动它最好通过虚拟环境隔离。2.2 高程数据选型与预处理CTB 支持的高程源非常多SRTM、ASTER GDEM、ALOS 这类公开 DEM 都可以也可以用无人机航测生成的 DSM、激光雷达点云插值出来的 DEM。关键是输入格式尽量统一为 GeoTIFF坐标系和分辨率要一致。如果你拿到的是分幅数据比如一个省由几十块 SRTM 图幅组成直接用gdalbuildvrt做虚拟拼接会比真实合并文件节省大量磁盘空间gdalbuildvrt terrain.vrt tile1.tif tile2.tif tile3.tifCTB 可以直接读取这个 VRT 文件不需要真的把它们合并成一张超大 TIF。不过拼接前一定要处理好两个问题一是坐标系尽量统一投影到 EPSG:4326WGS84 经纬度因为 Cesium 的地球坐标就是基于 WGS84 的二是 nodata 值SRTM 在海洋、湖泊、部分极地区域会有无效值如果不把这些值标成 nodataCTB 切出来的地形会出现异常尖峰或黑洞。一组常用的预处理命令大致是这样gdalwarp -t_srs EPSG:4326 -r bilinear -dstnodata -32767 input_utm.tif output_wgs84.tif-dstnodata -32767是把无效高程统一写为 -32767后续 CTB 遇到这个值就知道该位置没有有效数据。这一步很关键千万不要偷懒跳过。3. 实操全过程把 DEM 切成 Cesium 可用的瓦片地形3.1 ctb-tile 核心命令与参数环境准备好、数据预处理完成后就可以正式切瓦片了。CTB 最核心的命令是ctb-tile。先用ctb-info看一眼数据信息确认范围、坐标系、分辨率和 nodata 是否正确ctb-info terrain.vrt如果输出里的 EPSG 不是 4326先回去用gdalwarp转坐标系。确认无误后开始切片ctb-tile -f vrt -o ./terrain -e 4326 -p 4326 -l 10 terrain.vrt参数说明-f vrt指定输入数据格式CTB 支持 tif、vrt 等格式。-o ./terrain输出目录会在当前目录下创建 terrain 文件夹。-e 4326输入数据的空间参考 EPSG 代码。-p 4326输出瓦片数据的空间参考 EPSG 代码通常与输入一致。-l 10最大金字塔层级。这个层级-l是最影响切片时间的参数因为每增加一级瓦片数量几乎会翻四倍。建议第一次只是验证流程时先设一个较小的层级比如-l 6或-l 8跑通之后再对生产数据开到-l 12甚至更高。切片完成后在输出目录里能看到类似这样的结构terrain/ layer.json 0/0/0.terrain 1/0/0.terrain 1/1/0.terrain ...3.2 输出目录与部署CTB 生成的这堆.terrain文件不是给本地文件系统直接file://读取的Cesium 需要通过 HTTP 请求去拉取。所以你需要把它们放到任意一个静态文件服务器下比如 Nginx、Tomcat、Caddy或者一个简单的 Python HTTP 服务。以 Nginx 为例核心配置是给 .terrain 文件设置正确的 MIME 类型和 gzipserver { listen 8080; location /terrain/ { alias /data/terrain/; add_header Access-Control-Allow-Origin *; types { application/vnd.quantized-meshoct16 terrain; } gzip_static on; } }CTB 生成的瓦片默认是带 gzip 压缩的所以这里建议开启gzip_static on让 Nginx 直接返回对应的.gz文件避免动态压缩带来的 CPU 开销。如果这一步没有做对很可能出现瓦片加载 404或者 Cesium 解析地形时报错。另外如果你的页面和地形服务不在同一个域Access-Control-Allow-Origin必须加上否则浏览器会拦截。3.3 CesiumJS 中加载与验证瓦片服务部署好后CesiumJS 端的代码反而最简单。现在的 Cesium 版本里官方推荐用异步的fromUrl写法代码大概是这样const terrainProvider await Cesium.CesiumTerrainProvider.fromUrl(/terrain, { requestVertexNormals: false, requestWaterMask: false }); viewer.terrainProvider terrainProvider;有一个坑值得单独提requestVertexNormals和requestWaterMask必须保持 false。因为 CTB 默认生成的瓦片里并没有法线和水面蒙版数据如果盲目设置成 trueCesium 会尝试解析对应扩展信息轻则报 warning重则直接导致地形加载失败。我见过不少朋友在这行代码上调了半天其实问题根本不在这里。验证是否成功很简单把视角拉到一个有明显海拔变化的区域打开 Cesium 自带的数字地球底图如果地形表面出现自然的山体起伏就说明链路通了。此时可以拖动相机拉近场景观察高层级瓦片是否按需加载边缘是否出现明显“LOD 跳变”这也是后续调优的参考依据。4. 常见问题与排查技巧实录4.1 生成过程典型报错我在多次实操中整理了一些高频问题列成表格方便查阅现象大概率原因处理建议ctb-tile 运行直接崩溃或退出GDAL 版本与 CTB 不匹配回到 conda 环境重新指定 gdal2.4 并重装 ctb切片过程极慢甚至感觉无响应范围太大且-l层级设置过高先用-l 6测试再按需提升也可以裁剪出核心区域单独切片生成瓦片加载后完全是平的nodata 被当成 0 处理或输入数据范围裁剪过小检查预处理-dstnodata是否正确用 ctb-info 确认合法范围局部出现异常尖峰/凹陷DEM 残留异常值或水体未掩膜切片前对 DEM 做一次滤波或重新插值把水体区域置为固定值这里面最容易忽略的是第三个。很多高程数据在无效区域写的是 0 或者 -9999如果你没有明确告诉 GDAL 这个值是 nodataCTB 会把它当成真实高程参与插值结果切出来的地形像“拔地而起一根针”或者“凭空塌陷一个坑”。所以预处理阶段多花几分钟处理 nodata后面能省下一整周排查时间。4.2 浏览器端加载问题服务部署好之后浏览器端的排查主要集中在三类问题。第一类是 404。为保证成功离线加载layer.json文件后能继续抓取到.terrain瓦片你的 server rewrite 规则不能干扰/terrain/这个静态目录。如果用了 Vue、React 这类 SPA 的路由尤其要当心 history 路由把/terrain/layer.json吞掉导致 provider 初始化失败。第二类是 CORS 跨域。只要你在地形服务域名之外写页面浏览器就会拦截请求。解决办法就是在 Nginx/Caddy 等服务上加对应的跨域头这是成本最低的方案。第三类是“有影像但没起伏”。这种情况往往是requestVertexNormals/requestWaterMask设置不当或者 Cesium 实际请求到的 terrain provider 不是 CTB 生成的这份瓦片。排查时可以在 Network 面板看.terrain请求是否真的返回 200并确认响应类型是否为application/vnd.quantized-meshoct16。4.3 替代方案与扩展思路确实CTB 的年龄摆在那里遇到 Python 2 环境就足以让很多人退缩。如果你不想折腾也有几个替代方向。如果项目允许走公网直接用 Cesium ion 的地形服务createWorldTerrainAsync一行代码就搞定省事省心。如果必须离线也可以考虑用MapTiler或Terrarium方案这类工具能把高程转成 RGB 栅格瓦片再由前端自定义 Provider 解码但实时渲染性能和 mesh 细节通常不如 quantized-mesh 好。新兴的terrain-tiler、quantized-mesh相关生成器也值得关注它们对 Python 3 更友好只是社区资料还没有 CTB 那么成熟。除此之外这套“瓦片化”思路不只是地形能用。项目里如果还需要离线影像底图同样可以把地图厂商的 WMTS/XYZ 瓦片按规则下载到本地再通过静态服务或 QGIS 之类工具发布。很多朋友问到的“Qt 加载离线瓦片地图”“高德地图瓦片离线化”本质上和地形瓦片的切片、分级、存储思路完全一致区别只是瓦片内容是影像还是高程网格。最后再分享一点我的个人体会CTB 这种老工具不要因为它技术栈旧就觉得过时。在离线地形这个细分场景里它的稳定性和生态积累依然很有价值。刚开始做一定要从一个小范围、低层级的数据开始跑通全链路再逐步扩大范围和层级。这样出了问题排查起来要容易得多。还有就是要永远记得查两件事一是 DEM 的 nodata 有没有处理干净二是高程基准和坐标系是否统一。这两个问题如果不提前解决等到瓦片生成完再去调你会非常痛苦。希望这篇能帮到正准备和离线瓦片地形较劲的朋友少走一段我走过的弯路。本文还有配套的精品资源点击获取
返回列表