ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows 自建 Cesium DEM 地形瓦片服务:切片、Nginx 托管与加载实战

Windows 自建 Cesium DEM 地形瓦片服务:切片、Nginx 托管与加载实战 1. 为什么要在 Windows 上自建 DEM 地形瓦片服务很多人第一次接触 Cesium 的三维地形第一反应是直接调用官方在线的地形服务省事。但真到了项目交付阶段尤其是内网环境、涉密场景或者对加载速度有硬要求的场合在线服务基本没法用——要么网络不通要么请求延迟高得离谱要么数据根本不允许出内网。这时候就得把 DEM 高程数据自己切片、自己发布让 Cesium 从本地服务拉地形瓦片。这套流程的核心链路其实不复杂DEM 原始数据 → 切片工具生成 quantized-mesh 格式瓦片 → Nginx 托管静态瓦片目录 → Cesium 通过CesiumTerrainProvider加载。听起来四步就完事但我在 Windows 上实际跑通这套流程前后踩了不下十个坑从切片工具的 Python 环境依赖到 Nginx 的 MIME 类型配置再到 Cesium 加载时地形不显示的诡异问题每一个都能卡你半天。这篇文章面向的是需要在 Windows 环境下独立完成地形服务搭建的开发者不管你是做 GIS 可视化、三维仿真还是数字孪生项目只要涉及 Cesium 加载自定义地形这套流程都能直接复用。我会把每个环节的操作步骤、参数含义、选型理由讲清楚更重要的是把那些文档里不会写、只有踩过才知道的坑一并交代。DEM 数据我用的是常见的 GeoTIFF 格式切片工具走的是cesium-terrain-builder这条成熟路线Nginx 用 Windows 原生版本整套方案不依赖 Docker纯 Windows 环境可落地。先说结论性的选型判断切片工具不要用那些在线的转换服务也不要用过于小众的库。cesium-terrain-builder简称 CTB是目前最稳的选择它直接输出 quantized-mesh 格式Cesium 原生支持不需要额外转换。Nginx 选 Windows 版是因为它轻量、配置直观、静态文件性能足够没必要为了托管几个瓦片文件去折腾 IIS 或者上 Docker。下面按实际操作的顺序展开。2. DEM 数据的前期处理与切片工具选型2.1 DEM 原始数据的常见格式与检查要点拿到手的 DEM 数据格式五花八门GeoTIFF 是最常见的也可能是 ASCII Grid、IMG 或者 HGT。CTB 底层依赖 GDAL 读取栅格数据所以只要 GDAL 能读的格式基本都能处理。但有几个点必须在切片前确认清楚否则切出来的瓦片要么是空的要么高程全错。第一是坐标系。DEM 数据必须是地理坐标系经纬度通常是 WGS84EPSG:4326。如果你拿到的是投影坐标系比如 UTM 或者高斯克吕格必须先重投影。我遇到过一次数据是 CGCS2000 投影坐标直接丢给 CTB 切片结果瓦片范围完全对不上Cesium 里地形跑到南极去了。重投影用 GDAL 的gdalwarp一条命令搞定gdalwarp -t_srs EPSG:4326 input_projected.tif output_wgs84.tif第二是高程单位。有些 DEM 数据的高程单位是英尺或者分米不是米。Cesium 默认按米处理单位不对地形起伏就会夸张或者扁平。用gdalinfo看一下数据的元信息确认高程单位。第三是NoData 值。DEM 数据边缘或者空洞区域通常有个 NoData 值常见的是 -9999 或 -32768。如果这个值没处理好切片后这些区域会变成极端高程地形上会出现巨大的尖刺或者深坑。CTB 处理时会读取 NoData 设置但保险起见切片前用gdalinfo -stats确认一下数据范围是否合理。第四是数据分块。如果 DEM 覆盖范围很大比如整个省单个文件可能几十 GB。CTB 处理大文件时内存占用会很高建议先用gdal_retile或者gdal_translate按经纬度分块每块控制在合理大小比如 1 度 × 1 度再逐块切片。2.2 为什么选 cesium-terrain-builder 而不是其他方案市面上做地形切片的工具不止一个我对比过几种主流方案说说为什么最终锁定 CTB。工具输出格式优点缺点cesium-terrain-builderquantized-meshCesium 原生支持成熟稳定依赖 GDALWindows 编译麻烦CesiumLabquantized-mesh图形界面上手快商业软件免费版有限制gdal2tiles普通瓦片GDAL 自带不输出地形格式Cesium 不认自己写脚本任意完全可控开发成本高容易出错CTB 的核心优势在于它直接输出 quantized-mesh 格式这是 Cesium 地形服务的标准格式包含了地形网格的量化编码、法线、包围球等信息Cesium 拿到就能直接渲染不需要任何中间转换。而且 CTB 支持多线程切片大范围数据也能在可接受的时间内处理完。Windows 上编译 CTB 确实是个门槛它依赖 GDAL、PROJ、zlib 等一堆库。我的建议是不要自己编译直接用预编译好的二进制包或者用 OSGeo4W 环境安装。如果实在找不到合适的预编译版本退而求其次可以用 WSL 跑 Linux 版的 CTB切完的瓦片目录直接拷回 Windows 给 Nginx 托管效果一样。2.3 切片命令的完整参数拆解CTB 的核心命令是ctb-tile完整参数不少但常用的就那几个。我拿一个实际用过的命令来拆解ctb-tile -f Mesh -C -N -o ./terrain_tiles -s 14 input_dem.tif逐个参数说-f Mesh输出格式Mesh 就是 quantized-mesh。这个必须指定默认可能是其他格式。-C生成layer.json文件。这个文件是地形服务的元数据描述Cesium 加载时首先要读它里面定义了瓦片的范围、可用层级、格式等信息。这个参数千万别漏漏了 Cesium 根本不知道去哪找瓦片。-N生成法线数据。地形光照效果依赖法线不加这个参数地形看起来是平的没有立体感。-o ./terrain_tiles输出目录。目录结构会自动按层级组织比如0/0/0.terrain、1/0/0.terrain这样。-s 14最大层级。这个要根据 DEM 数据的分辨率来定。一般来说30 米分辨率的 DEM 切到 12-14 级就够了再高就是插值出来的假细节没意义还占空间。最后的input_dem.tif是输入文件。切片过程可能比较久取决于数据量和层级。切完后目录里应该能看到layer.json和一堆按层级编号的子目录。检查一下layer.json里的available字段确认层级范围和你预期的一致。注意CTB 切片时如果内存不够会直接崩不会给你友好提示。大文件建议分块处理或者加-t参数控制线程数别让所有核心都跑满。3. Nginx 在 Windows 上的部署与地形瓦片托管配置3.1 Windows 版 Nginx 的安装与目录规划Windows 版 Nginx 是绿色包下载解压就能用不需要安装。下载地址去官网找稳定版解压到一个路径不含中文和空格的目录比如D:\nginx。路径里有中文是 Windows 上 Nginx 最常见的启动失败原因它会报找不到配置文件或者路径解析错误。解压后的目录结构里我们主要关心两个conf/nginx.conf主配置文件所有配置都改这里。html/默认的静态文件根目录。地形瓦片目录我建议单独放不要塞进html里。比如放在D:\terrain_tiles然后在 Nginx 配置里用alias或者root指向它。这样瓦片数据和 Nginx 程序分离后续更新瓦片不影响 Nginx 本身。启动 Nginx 用命令行cd D:\nginx start nginx停止用nginx -s stop重载配置用nginx -s reload。每次改完配置文件必须 reload 或者重启否则不生效。我见过有人改完配置直接刷新浏览器然后纳闷为什么没变化折腾半天才发现是没 reload。3.2 地形瓦片服务的 Nginx 配置详解地形瓦片是静态文件配置本身不复杂但有几个关键点必须处理对。下面是我实际用的配置片段server { listen 8080; server_name localhost; location /terrain/ { alias D:/terrain_tiles/; autoindex on; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers Range; types { application/vnd.quantized-mesh terrain; application/json json; } default_type application/octet-stream; } }逐段解释为什么这么配listen 8080端口随便选只要不冲突。用 8080 是为了避免和可能存在的 80 端口服务打架。alias D:/terrain_tiles/注意这里用的是alias不是root。alias会把location匹配的路径部分替换掉root是拼接。地形瓦片的 URL 是/terrain/0/0/0.terrain这种用alias才能正确映射到D:/terrain_tiles/0/0/0.terrain。路径分隔符用正斜杠/Windows 下 Nginx 也认用反斜杠反而可能出问题。autoindex on开启目录列表。调试阶段很有用浏览器直接访问/terrain/能看到文件列表方便确认瓦片是否真的存在。生产环境可以关掉。CORS 头这是最容易漏掉的一环。Cesium 通常跑在另一个端口或者另一个域名下跨域请求地形瓦片时如果服务端没返回Access-Control-Allow-Origin浏览器会直接拦截Cesium 控制台报跨域错误地形死活加载不出来。加上这三个add_header基本能覆盖常见跨域场景。types块这是第二个大坑。quantized-mesh 瓦片文件的扩展名是.terrainNginx 默认的 MIME 类型映射里没有这个扩展名会返回application/octet-stream。虽然 Cesium 对 MIME 类型不是特别挑剔但某些情况下尤其是配合某些浏览器或者代理MIME 类型不对会导致解析失败。显式声明application/vnd.quantized-mesh是最稳妥的做法。layer.json是 JSON 文件也要确保返回application/json。default_type兜底类型防止未匹配的扩展名返回奇怪的类型。3.3 验证瓦片服务是否正常配置改完 reload 之后别急着开 Cesium先用浏览器或者 curl 验证服务本身是否正常。浏览器直接访问http://localhost:8080/terrain/layer.json应该能看到 JSON 内容。如果 404说明路径映射有问题检查alias路径和实际瓦片目录是否一致。如果返回的是下载文件而不是显示内容说明 MIME 类型没配对。再访问一个具体的瓦片文件比如http://localhost:8080/terrain/0/0/0.terrain。这个文件是二进制浏览器可能直接下载或者显示乱码这都正常关键是状态码要是 200。如果 404说明瓦片目录结构或者层级编号有问题。用 curl 检查响应头更直观curl -I http://localhost:8080/terrain/layer.json重点看Content-Type和Access-Control-Allow-Origin这两个头是否正确返回。这一步确认无误再进入 Cesium 环节能省掉大量排查时间。4. Cesium 加载自定义地形瓦片的完整调用与排错4.1 CesiumTerrainProvider 的正确初始化方式Cesium 加载自定义地形的入口是CesiumTerrainProvider初始化时传入瓦片服务的 URL。基本写法const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: new Cesium.CesiumTerrainProvider({ url: http://localhost:8080/terrain/, requestVertexNormals: true, requestWaterMask: false }) });几个参数说明url指向瓦片服务的根路径结尾的斜杠建议加上。Cesium 会在这个 URL 后面拼接layer.json和瓦片路径。不加斜杠在某些版本下会拼出错误的 URL。requestVertexNormals请求顶点法线。切片时如果加了-N参数生成了法线数据这里设为true才能让地形有光照效果。如果切片时没生成法线这里设true也没用Cesium 会忽略。requestWaterMask请求水面掩码。这个需要切片时额外生成水面数据一般 DEM 切片不会带设为false。如果地形服务需要认证或者有特殊请求头可以传headers参数。但自建的 Nginx 服务一般不需要。4.2 地形加载失败的排查链路Cesium 地形加载失败的表现通常是地球还是那个光滑的椭球没有任何起伏控制台可能有报错也可能没有。我按实际排查顺序梳理一条链路照着走基本能定位问题。第一步确认layer.json能正常访问。打开浏览器开发者工具的 Network 面板刷新 Cesium 页面看有没有对layer.json的请求状态码是不是 200。如果这个请求就失败了问题在 Nginx 或者路径配置跟 Cesium 无关。第二步看layer.json内容是否合法。重点检查format字段是不是quantized-mesh-1.0available字段里的层级范围是否包含你请求的层级。如果format不对说明切片时-f Mesh参数没生效。第三步看瓦片请求是否发出。如果layer.json正常但地形还是平的看 Network 面板里有没有.terrain文件的请求。如果没有请求发出可能是 Cesium 的terrainProvider没设置成功检查初始化代码是否在Viewer创建时传入或者是否在创建后通过viewer.terrainProvider ...赋值。第四步看瓦片请求的响应。如果.terrain请求返回 404说明瓦片文件路径不对检查 Nginx 的alias配置和实际目录结构。如果返回 200 但地形还是平的看响应头里的Content-Type和 CORS 头是否正确。第五步看控制台报错。Cesium 加载地形失败时通常会在控制台输出具体原因比如 An error occurred while loading terrain tile 后面跟着详细错误。根据错误信息进一步定位。我遇到最诡异的一次是所有请求都 200layer.json也正常但地形就是不显示。最后发现是瓦片数据的坐标系和 Cesium 期望的不一致——切片时 DEM 数据没重投影导致瓦片的地理范围错位Cesium 按当前视角去请求瓦片请求到的瓦片实际覆盖的是另一个区域自然显示不出来。这个坑的教训是切片前一定要确认 DEM 是 WGS84 地理坐标系。4.3 地形夸张系数与视角调试技巧地形加载出来之后默认的起伏可能看起来不明显尤其是平原地区。Cesium 提供了terrainExaggeration参数来放大起伏viewer.scene.terrainExaggeration 2.0;这个值默认是 1.0设为 2.0 就是起伏放大一倍。调试阶段可以适当调大让地形特征更明显方便确认数据是否正确加载。但生产环境不要设太大否则地形会失真看起来像橡皮泥捏的。另外Cesium 默认的初始视角是全局视角地形起伏在这个尺度下根本看不出来。调试时建议直接飞到目标区域viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(经度, 纬度, 高度), duration: 2 });高度设个几千到一万米能比较清楚地看到地形起伏。如果飞到目标区域后地形还是平的那基本可以确定是数据或者服务的问题而不是视角问题。5. 那些文档里不会写的实操坑与经验5.1 切片层级与瓦片数量的平衡切片层级每加一级瓦片数量大约翻四倍。一个 1 度 × 1 度的区域切到 14 级大概产生几千个瓦片文件切到 16 级就是几万个。文件数量太多会带来两个问题一是 Nginx 托管时目录遍历变慢二是 Windows 文件系统处理大量小文件效率不高。我的经验是30 米分辨率的 DEM 切到 12-13 级足够10 米分辨率切到 14-15 级5 米分辨率切到 16 级。再高的层级就是插值出来的没有真实数据支撑除了增加文件数量没有任何意义。判断标准很简单DEM 的原始分辨率决定了它能表达的最小地形细节超过这个细节的层级都是假的。如果确实需要更高层级的显示效果可以考虑在 Cesium 端用maximumScreenSpaceError控制渲染精度而不是一味提高切片层级。5.2 Nginx 路径配置中的斜杠陷阱Nginx 的root和alias对结尾斜杠的处理规则不一样这是配置地形服务时最容易出错的地方。用alias时location的路径和alias的路径要么都带斜杠要么都不带否则路径拼接会出错。比如location /terrain/ { alias D:/terrain_tiles/; # 正确都带斜杠 }如果写成location /terrain不带斜杠配alias D:/terrain_tiles/带斜杠访问/terrain/0/0/0.terrain时实际映射的路径会变成D:/terrain_tiles//0/0/0.terrain多一个斜杠虽然 Windows 通常能容忍但某些情况下会 404。用root时Nginx 会把location的路径拼接到root后面。比如root D:/tiles配location /terrain/访问/terrain/0/0/0.terrain映射到D:/tiles/terrain/0/0/0.terrain。所以用root的话瓦片目录要放在D:/tiles/terrain/下面。我个人的习惯是统一用alias并且 location 和 alias 都带结尾斜杠这样最不容易出错。5.3 Cesium 跨域问题的几种表现与解决跨域是 Cesium 加载自建地形服务时的高频问题表现有好几种控制台报Access to fetch at ... has been blocked by CORS policy这是最明显的。地形不显示但没有任何报错Network 面板里瓦片请求状态是(failed)或者CORS error。layer.json能加载但瓦片加载失败因为layer.json可能被缓存了或者走了不同的请求路径。解决方式就是在 Nginx 配置里加 CORS 头前面已经给了配置。但有个细节如果 Cesium 页面本身是file://协议打开的跨域问题会更复杂因为file://协议下浏览器的安全策略更严格。建议用本地 HTTP 服务打开 Cesium 页面比如用 Python 的http.server或者 Nginx 本身托管 Cesium 页面。另外如果瓦片服务前面还有一层代理或者网关CORS 头可能会被覆盖或者丢失需要在那层也配置。5.4 瓦片更新与缓存处理DEM 数据更新后重新切片瓦片文件会覆盖。但浏览器和 Cesium 都有缓存机制可能还在用旧的瓦片。调试阶段建议在 Nginx 配置里禁用缓存location /terrain/ { alias D:/terrain_tiles/; add_header Cache-Control no-cache, no-store, must-revalidate; add_header Pragma no-cache; add_header Expires 0; }生产环境可以设置合理的缓存时间减少重复请求。但更新瓦片后要记得让缓存失效可以通过改文件名或者加版本号的方式。Cesium 端也有缓存CesiumTerrainProvider初始化时可以传cacheBytes和maximumCacheOverflowBytes控制缓存大小。调试时可以把缓存设小一点强制它重新请求。6. 从切片到渲染的完整验证流程把整个链路串起来给一个从零到跑通的完整验证流程照着走一遍基本能确认每个环节是否正常。第一步数据检查。用gdalinfo确认 DEM 是 WGS84 地理坐标系高程单位是米NoData 值合理。第二步切片。用ctb-tile -f Mesh -C -N -o ./terrain_tiles -s 14 input.tif切片切完检查输出目录里有layer.json和层级子目录。第三步Nginx 配置。按前面的配置写好locationreload Nginx。第四步服务验证。浏览器访问layer.json和某个.terrain文件确认状态码 200Content-Type和 CORS 头正确。第五步Cesium 加载。初始化CesiumTerrainProvider飞到目标区域观察地形是否有起伏。第六步排错。如果地形不显示按 4.2 节的排查链路逐步定位。这套流程我在多个项目中复用每次新环境部署基本半小时内能跑通。关键是把每个环节的验证做扎实不要跳过服务验证直接上 Cesium否则出了问题很难判断是切片的问题、Nginx 的问题还是 Cesium 的问题。最后分享一个实用技巧在 Cesium 里加一个地形采样点直接读取地形高度能快速确认地形数据是否真的加载成功const cartographic Cesium.Cartographic.fromDegrees(经度, 纬度); const promise Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [cartographic]); promise.then((updatedPositions) { console.log(地形高度:, updatedPositions[0].height); });如果输出的高度和 DEM 数据里的高程对得上说明整条链路是通的。如果输出 0 或者异常值说明地形没加载成功回去检查前面的环节。这个采样方法比肉眼看地形起伏靠谱得多尤其是在平原地区地形起伏本来就不明显肉眼很难判断。
返回列表