简介:这份资源面向需要在无网络环境下用 Cesium 展示三维地形的开发者与 GIS 学习者,针对把 DEM 高程 tif 数据转换为离线 terrain 瓦片这一关键环节,提供从运行环境搭建到瓦片产出的整套程序与配套说明文档。压缩包共 11 个文件,约 45.9MB,以 msi 与 exe 安装包(Python、GDAL、PIL、NumPy 等地理处理依赖)、两个 py 转换脚本、pdf 操作说明为主,另附 json 配置、terrain 样例与 demdata 示例数据目录,所需组件一次备齐。目前已有 3095 人学习下载。读者可对照说明逐步完成瓦片生成、坐标适配与目录组织,再把瓦片移入工程,并在 Cesium 中配置地形提供者;随包的示例数据与结果目录结构清晰、便于对照验证,路径设置、投影匹配、内存与磁盘占用以及 GDAL 报错等问题也能在文档中找到思路,适合作为离线地形落地方案的实操参考。
1. 离线地形这件事:为什么换底图解决不了相机切进山体的问题
内网演示前一周,影像瓦片已经全部本地化,地球转起来很顺,直到镜头推到山口,相机直接切进地面——山峰是平的,峡谷也是平的。换底图解决不了这个问题,缺的是地形。这套资源包解决的就是这一环:把公开 DEM 或自己手里的高程数据,转成 Cesium 能直接读取的离线 terrain 切片,附带程序和一份能照着敲的操作说明。核心词落在 Cesium、离线地形、quantized-mesh 三个点上。它适合三类人:内网或断网环境做三维 GIS 演示的开发者、需要把地形数据完全放在自己服务器上的项目组,以及想知道.terrain文件里究竟装了什么、不愿只当调包侠的工程师。整条流水线是命令行驱动的,GDAL 负责前处理,切片器负责出瓦片,跑通一次之后,一个省的数据大概是一夜的事。
2. 拆开这个包:quantized-mesh 与 heightmap 两套格式怎么选
拿到资源包先别急着跑命令,花十分钟把两种地形格式的差别搞清楚,能省掉后面一半的返工。Cesium 认的离线地形格式主要有两代:老的 heightmap-1.0 和新的 quantized-mesh-1.0。资源包里两套切片参数都给了,但默认走 mesh,原因下面会讲清楚。
2.1 quantized-mesh-1.0 的二进制结构与 layer.json
quantized-mesh 是当前 Cesium 的默认地形格式,扩展名同样是.terrain,但内容是一段紧凑的二进制。打开一个文件,前 88 个字节是固定头部,顺序大致是:中心点经纬高(三个 double)、最小最大高程(两个 float)、包围球(四个 double)、Horizon Occlusion Point(四个 double)。头部之后是顶点数据,每个瓦片的顶点坐标被量化到 16 位整数区间,再用 zig-zag 编码做差分压缩,所以同一个瓦片里相邻顶点越密集,压缩率越高。顶点之后是索引数据,用高水位标记编码三角形索引,最后是高程数据之前的对齐填充。
真正让它在工程上值钱的是两段可选扩展:
octvertexnormals:每个顶点带一个八面体编码的法线,一共 2 字节。开了它,Cesium 才能算出正确的地形光照,山体的明暗过渡才有立体感;不开,地形就是一片均匀的哑光。watermask:一张小图,标记哪些像素是水面。海岸线、湖泊边缘会因此获得正确的反射效果。
layer.json是这个格式的说明书,切片器生成它,Cesium 靠它决定去哪拿数据。关键字段如下:
| 字段 | 作用 | 常见取值与坑 |
|---|---|---|
format | 声明切片格式 | 必须是quantized-mesh-1.0,写错整个地形加载不出来 |
version | 数据版本号 | 与tiles模板里的{version}对应,改数据后要同步改 |
scheme | 瓦片编号方案 | tms时 y 轴从南往北,与 XYZ 相反,这是 404 的头号来源 |
tiles | 瓦片 URL 模板 | 一般是["{z}/{x}/{y}.terrain?v={version}"] |
projection | 投影 | 切片器输出EPSG:4326,别手改成 3857 |
bounds | 数据覆盖范围 | 由 DEM 实际范围算出,写全地球会导致请求大量空瓦片 |
available | 每级有效瓦片区间 | 缺失时 Cesium 会按 bounds 全量请求,首屏变慢 |
extensions | 扩展声明 | 与切片时开的参数必须一致,否则法线读出来是乱码 |
available这一段最容易被忽略。它记录每一层级实际存在瓦片的 x/y 起止范围,Cesium 在飞行定位和 LOD 调度时会先查这张表。手工拼装layer.json的时候如果漏了它,表现是地形能显示但首屏加载要等很久,网络面板里一堆 404。
2.2 heightmap-1.0 还在什么场景下值得留
heightmap 是上一代方案,每张切片本质是一张 65×65 的 PNG。高度值拆成两个字节,R 通道存高位、G 通道存低位,读出来做一个线性映射还原成米。它有几个绕不开的先天限制:一是精度受 16 位量化约束,高程跨度大的区域(比如从海平面到 5000 米)垂直分辨率会变粗;二是瓦片边缘需要和邻接瓦片共享一圈像素,切片器做重采样时容易在接缝处出现细小的台阶;三是水面完全靠额外的watermask目录单独提供,多一套文件要维护。
那它什么时候还值得用?我一般会在三种情况下退回去:项目锁死在很低版本的 Cesium,客户端不认 mesh 的扩展字段;目标机器是性能很弱的终端,PNG 解码比解析二进制网格更省事;或者只是需要在球面上铺一层粗糙的起伏当背景,压根不关心法线和光照。除此之外没有理由不用 mesh——同样范围的数据,heightmap 的体积通常是 mesh 的两三倍,而细节还不如后者。
2.3 包里的程序清单与依赖版本核对
资源包里的东西不多,但要全部凑齐才能跑通,逐项对一遍再开工:
| 组件 | 作用 | 检查方式 |
|---|---|---|
| GDAL 命令行套件 | DEM 重投影、拼接、格式转换 | gdalinfo --version |
| cesium-terrain-builder | 核心切片器,提供ctb-tile | ctb-tile --help |
| 一份 DEM 数据 | 源高程,GeoTIFF 或 VRT | gdalinfo dem.tif看投影与波段 |
| 静态文件服务 | 本地验证切片是否可访问 | Python 自带 http.server 或 Node 任意静态服务 |
一份layer.json模板 | 手改字段时对照 | 文本编辑器打开看结构 |
依赖版本这块有一条血泪经验:GDAL 主版本跨代时,某些老版本切片器链接的动态库会对不上,跑起来直接报符号缺失。docker 镜像能规避大部分这类问题,把 DEM 目录挂进去,切片输出目录也挂出来,环境隔离在容器里,宿主机不用装一堆开发包。如果坚持本机编译,先确认 GDAL 的开发头文件和运行时是同一个版本,gdal-config --version和gdalinfo --version输出必须一致。
注意:不要用系统包管理器装一个 GDAL、再用源码编译一个不同版本的 GDAL,两者共存时动态链接器会挑错那个,报错信息通常和地形毫无关系,排查起来非常费劲。
3. 从 DEM 到 .terrain 切片:一条能跑通的命令行流水线
这一章是整篇的核心,按顺序敲完就能出成果。整个流程分三段:DEM 预处理把源数据整理成切片器能吃的干净输入,切片把高程栅格切成金字塔,本地服务把结果暴露成一个 Cesium 能请求的 URL。三段之间任何一段偷懒,后面都会以很奇怪的方式报错。
3.1 DEM 预处理:重投影、拼接与 nodata 归整
切片器对输入的要求比想象中严格:必须是地理坐标(EPSG:4326)、单一波段、无 nodata 空洞或空洞已明确标注、最好是按纬度分带的整块数据。手上拿到的 DEM 往往是 UTM 投影的分幅 GeoTIFF,直接喂进去会得到错位的地形,所以先统一处理。
# 1. 看源数据的投影、范围、nodata 和统计值,先摸清底细 gdalinfo -stats -nomd input_dem.tif # 2. 多幅 DEM 拼成一张虚拟栅格,不占磁盘空间 # -srcnodata / -vrtnodata 把各幅不一致的空洞值统一成同一个 gdalbuildvrt -resolution highest \ -srcnodata -32768 -vrtnodata -32768 \ mosaic.vrt tile_01.tif tile_02.tif tile_03.tif # 3. 重投影到 EPSG:4326,并裁剪到目标经度范围 # -r cubic 是重采样方式,高程数据用三次卷积比最近邻平滑 gdalwarp -t_srs EPSG:4326 \ -r cubic -multi -wo NUM_THREADS=ALL_CPUS \ -dstnodata -32768 \ -te 100.0 20.0 120.0 45.0 \ mosaic.vrt warped.tif # 4. 压成一张自包含的 GeoTIFF,方便反复切片 gdal_translate -co COMPRESS=DEFLATE -co TILED=YES -co BIGTIFF=IF_SAFER \ warped.tif dem_4326.tif # 5. 复核:投影必须是 4326,nodata 值要记下来 gdalinfo dem_4326.tif | head -30逐条说明一下。第 1 步的-stats会顺带算出最小最大值,如果看到最小值是-32768而最大值只有几百米,基本可以确定 nodata 没被正确识别,这一步必须处理掉,否则切片出来会有一片深坑。第 2 步用 VRT 而不是直接合并,是因为 VRT 只记录文件引用,几 GB 的输入拼起来是秒级的,改参数重来也不心疼;-resolution highest保证分辨率不一致时不会被动降采样。第 3 步的-te是裁剪范围,单位是经纬度,按实际业务范围裁掉多余部分能显著减少切片数量;-dstnodata的值要和上一步统一,这里用-32768是因为不少 SRTM 衍生数据用它做空洞标记。第 4 步的TILED=YES对后续切片器的读取性能影响很大,分块存储能避免随机读。
处理完一定要检查两件事:gdalinfo输出里的Coordinate System is必须包含4326;NoData Value要么有明确值要么整行不存在。这两条不满足,后面的坑会以「地形是平的」这种毫无指向性的症状出现。
3.2 ctb-tile 的四个关键参数与目录产物
切片器这一层可调的参数不少,但真正影响结果的只有几个,剩下的交给默认值。
# 输出 mesh 格式、自动生成 layer.json、计算顶点法线 # -o 指定输出根目录,最后那个参数是输入 DEM ctb-tile -f Mesh \ -C \ -N \ -l terrain \ -c 8 \ -z 12 \ -Z 0 \ -o ./terrain_out \ dem_4326.tif # 查看产物:先看层级目录,再看最后一级的瓦片数量 find ./terrain_out -maxdepth 1 -type d | sort find ./terrain_out -name "*.terrain" | wc -l # 检查 layer.json 是否生成、字段是否合理 # 重点看 format、scheme、bounds、available 四段 cat ./terrain_out/layer.json | python -m json.tool | head -40参数逐个说。-f Mesh选格式,只有大小写写对才会走 quantized-mesh 分支,写错会静默回退到 heightmap,产物照样生成但扩展名和体积都不一样。-C让切片器顺手把layer.json写出来,省掉手工拼 JSON 的步骤,强烈建议开着。-N计算顶点法线,对应layer.json里extensions的octvertexnormals,开了才有光照,代价是每张切片多两字节每顶点,整体体积上涨约 10%~15%。-z和-Z是最大最小层级,0 级是一张覆盖全球的瓦片,12 级大概对应几十米的地面分辨率,具体取多少后面在性能那章细说。-c是并发线程数,给到物理核数就够了,开太大反而因为磁盘随机写互相拖慢。-o是输出根目录,注意它不是「输出到某个瓦片目录」,而是整个地形数据集的根,里面会自动生成0/、1/这样的层级目录。
目录产物长这样:根目录下有layer.json,然后是0/0/0.terrain、1/0/0.terrain、1/1/0.terrain这样的层级结构。y 轴方向是 TMS 约定,从南往北递增,和常见的 XYZ 切片相反。这一点在手工往服务器上搬文件、或者自己写静态服务重写 URL 的时候会要命,layer.json里的scheme字段必须和实际目录对得上。
如果数据量大到一次切不完,-R参数可以在中断后从已有瓦片继续,跳过已经存在的文件。分区切片也是同一个思路:按经度把大区域切成几块,分别切片到不同目录,最后用layer.json的available字段合并,比一次喂一整个国家要稳得多。
3.3 本地起服务验证:layer.json 与第一张切片的自检
切片完成不等于能用。Cesium 加载地形走的是 HTTP 请求,直接双击 HTML 用file://打开会被浏览器的同源策略拦掉,必须起一个静态服务。
# 在 terrain_out 的上一级目录启动静态服务 # 这样 /terrain_out/layer.json 就能直接访问到 python -m http.server 8080 --bind 0.0.0.0 # 用 curl 自检三件事: # 1. layer.json 能否取到,Content-Type 是否正常 curl -i http://127.0.0.1:8080/terrain_out/layer.json # 2. 0 级瓦片是否存在(TMS 约定 y 从 0 开始) curl -i -o /dev/null -w "%{http_code} %{size_download}\n" \ http://127.0.0.1:8080/terrain_out/0/0/0.terrain # 3. 抽一张高层瓦片,确认 gzip 或二进制响应正常 curl -i -o /dev/null -w "%{http_code} %{size_download}\n" \ http://127.0.0.1:8080/terrain_out/8/210/98.terrain这三条自检覆盖了绝大多数低级错误。第一条如果返回 404,多半是目录层级不对,服务启动位置比terrain_out低了一层;如果返回的Content-Type是application/octet-stream,那反而是对的,.terrain本来就是二进制。第二条重点看状态码和体积,0/0/0.terrain是整颗地球的包围瓦片,一般只有几 KB,如果返回 0 字节说明切片器根本没写出根瓦片,通常是 DEM 的范围没被正确识别。第三条的坐标要按自己的available范围挑,随手填的 x/y 很可能本来就不存在,返回 404 不代表流程有问题。
浏览器端还有一个容易忽略的点:跨域。服务如果和页面不在同一个端口,需要带上Access-Control-Allow-Origin。Python 自带的http.server不带这个头,简单验证够用,但只要页面和地形不同源就会失败。本地调试我一般会写十来行的 Node 静态服务,把 CORS 头一次性加上,省得来回折腾。
4. 接到 Cesium 里:TerrainProvider 的参数与坐标对位
切片能通过 curl 取到,说明数据链路通了,接下来是把地形挂到 Viewer 上。这一段代码量不大,但参数错一个,症状可能是地形不显示、显示成平面、或者光照完全不对。
4.1 CesiumTerrainProvider 初始化与 requestVertexNormals
// 初始化 Viewer,先关掉默认的在线影像和地形,避免内网环境请求外网 const viewer = new Cesium.Viewer('cesiumContainer', { imageryProvider: false, // 影像稍后单独挂本地瓦片 terrainProvider: undefined, // 关键:不传就保持椭球面 baseLayerPicker: false, geocoder: false, animation: false }); // 挂载离线地形。新版本 Cesium 用 fromUrl 返回 Promise async function setupTerrain() { const terrain = await Cesium.CesiumTerrainProvider.fromUrl( './terrain_out', // 指向含 layer.json 的目录 { requestVertexNormals: true, // 与切片时的 -N 参数保持一致 requestWaterMask: false // 切片时没生成 watermask 就必须关掉 } ); viewer.terrainProvider = terrain; // 让相机、模型、拾取都真正贴合地形表面 viewer.scene.globe.depthTestAgainstTerrain = true; } setupTerrain();requestVertexNormals必须和切片参数严格对应。切片时开了-N、这里却传false,地形会显示成没有明暗的灰面;反过来切片没开法线、这里传true,Cesium 会尝试解析不存在的扩展段,轻则法线全为零、重则整块地形报解析错误。requestWaterMask同理,除非切片时专门生成了水面遮罩,否则一律关掉。
depthTestAgainstTerrain这一行是很多人加载完地形后模型被地面穿透的根因。默认值是false,此时深度测试只对地形本身生效,点、线、模型仍然按椭球面高度绘制,于是就出现模型半截埋进山体、标注飘在山谷上方的情况。打开它之后,所有图元都会和地形做深度比较,代价是拾取计算量上升,低端机器上要留意帧率。
老版本 Cesium 的写法是new Cesium.CesiumTerrainProvider({ url, requestVertexNormals }),同步返回实例。如果项目锁在旧版本,用这个形式;新版本继续用构造函数也能跑,但拿不到fromUrl的错误回调,layer.json解析失败时只会静默显示成球面,排查起来靠猜。
4.2 地形与影像、模型的贴合校准
地形挂上之后,第一件要做的事是目视校验高度对不对,而不是急着加业务图层。
// 取地形上的实际高度,和 DEM 源数据比对 const carto = Cesium.Cartographic.fromDegrees(110.5, 30.2); const updated = await Cesium.sampleTerrainMostDetailed( viewer.terrainProvider, [carto] ); console.log('地形高度(米):', updated[0].height.toFixed(2)); // 相机飞到该点上方 3 公里,检查是否贴合 viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(110.5, 30.2, 3000), duration: 2 });sampleTerrainMostDetailed会按当前地形数据可用的最高层级去采样,返回的高度就是引擎真正会用来放置物体的值。拿这个数和gdalinfo里查到的同一点高程对比,差值在几米以内属于重采样正常误差;如果差了几十米甚至上百米,基本可以断定是垂直基准的问题——源 DEM 用的是正高(大地水准面),而 Cesium 用的是椭球高,全国范围内两者相差几十米是常事。处理方式是在预处理阶段加一步基准改正,或者接受这个偏移并在业务侧做统一补偿。
影像和地形的贴合是第二个校验点。影像瓦片如果是 Web Mercator(EPSG:3857),而地形是地理坐标(EPSG:4326),Cesium 会在渲染时自动重投影,绝大多数情况下看不出问题,但在高山区域的高层级下可能出现贴图错位几十米。稳妥做法是两者用同一套tilingScheme:地形是 4326,影像也切成 4326 的瓦片,这样从瓦片编号到纹理坐标是一一对应的,不会引入重投影误差。
模型贴合则是第三件事。clampToGround系列方法依赖地形的实际高度,在陡坡上做单点采样会明显不准,管线、道路这类线状要素要改成按固定间距采样并逐点贴地,或者直接用地形抬升后的几何求交。
4.3 多级地形叠加与 tilingScheme 对齐
有些项目需要「粗粒度全球地形 + 细粒度重点区域」两套数据。Cesium 本身不支持在同一个terrainProvider上叠加两套数据,常见的工程解法是准备一套统一的切片,重点区域用更高精度的 DEM 单独切到更高层级,然后手工合并layer.json的available数组,让不同 x/y 区间指向不同的瓦片文件。这属于偏手工的活:把两份layer.json的available按层级排列,同一层出现重叠区间时以高精度那份为准,tiles模板保持统一,文件按目录结构直接合并。
另一条路是用CustomTerrainProvider自己实现requestTileGeometry,在运行时根据经纬度决定去哪个数据源取瓦片。灵活但工作量大,还要自己处理缓存和错误重试,除非有明确的动态切换需求,否则不划算。
tilingScheme 这块再强调一次:地形和影像的tilingScheme最好一致。如果影像用的是WebMercatorTilingScheme而地形是GeographicTilingScheme,Cesium 内部会做转换,视觉上大多能接受,但在跨纬度带和极区会有可见偏差。判断方法是打开viewer.scene.debugShowTiles之类的调试开关,看地形和影像的瓦片边界是否重合,不重合就是这里的问题。
5. 避坑排查:五类翻车现场的原因与处理
地形这条链路长,出错点散落在预处理、切片、服务、渲染四个环节。下面五条是按踩坑频率排的,症状看着离谱,原因往往很低级。
5.1 地球加载出来了,但完全是平的,一点起伏都没有
现象:页面正常渲染,地形请求在 сети面板里全是 200,但相机怎么飞都是个光滑球面。
原因基本落在两处。一是layer.json的format字段和实际切片格式不一致,或者tiles模板里的路径层级和磁盘目录对不上,Cesium 拿到了 200 的响应却解析不出来,解析失败会被吞掉,退化成椭球面。二是 DEM 的 nodata 值没统一,切片器把整片区域当成空值,生成了大量高程恒为零的瓦片。
解决:先在浏览器网络面板里挑一张.terrain请求,点开看响应大小,几百字节到几 KB 是正常的,如果只有几十字节基本是空瓦片。再回到源数据,gdalinfo -stats确认最小最大值,用gdal_translate -a_nodata强制指定 nodata,重新走一遍预处理。
5.2 控制台一片 404,但文件明明在目录里
现象:layer.json能取到,往下请求瓦片时全部 404,路径看起来也没什么问题。
原因几乎总是 y 轴方向反了。切片器输出的目录是 TMS 约定,y 从南往北递增;如果中间用某个工具做过一次 XYZ 转换,或者自己写了层重写规则把 y 翻了一遍,路径就和layer.json里的scheme声明对不上了。
解决:打开layer.json看scheme字段,值是tms就说明目录结构按 TMS 来。随便挑一个已知存在的瓦片,按{z}/{x}/{y}手工拼出 URL 在浏览器里试,能取到就说明服务层没问题,问题在 Cesium 请求的坐标换算上。中间层如果有反向代理或 CDN 规则,把 y 翻转的逻辑先去掉。
5.3 页面和地形不同源,控制台报跨域错误
现象:curl能取到layer.json,页面里加载地形却报 CORS 相关的错误,或者干脆卡在加载中。
原因是静态服务没返回Access-Control-Allow-Origin头。file://协议、Python 自带http.server、部分轻量静态服务默认都不带。另外还有一层容易忽略:如果.terrain被 gzip 压缩过,服务端要正确返回Content-Encoding: gzip,否则 Cesium 拿到的是压缩后的字节流,解析会失败但错误信息指向别处。
解决:本地验证用带 CORS 的 Node 静态服务,把Access-Control-Allow-Origin: *和Access-Control-Allow-Headers: *都加上;生产环境用 Nginx 时,location块里补add_header,同时确认gzip_types里包含application/octet-stream,或者干脆不预压缩、交给 Nginx 动态压缩。
5.4 切片跑到一半内存爆掉或者进程被杀
现象:切一个大范围的 DEM,跑到某一层级时机器开始疯狂换页,最后进程被 OOM Killer 干掉,或者干脆卡死不动。
原因是切片器在这一层需要同时持有大量瓦片的上下文,输入范围越大、层级越深,内存占用增长得越快。一个省的 90 米 DEM 切到 14 级,内存峰值轻松上到十几 GB。
解决:分而治之。按经度把 DEM 切分成若干块,每块单独切片到独立目录,最后合并。切片时用-z限制最大层级——大多数演示场景 12 级已经够用,14 级是给单个城市用的。中断之后用-R续跑,不要从头再来。机器内存小就降低并发线程数,-c 4比-c 16更稳。
5.5 地形明明有起伏,模型却悬空或者半截埋进山里
现象:地形渲染正确,山体轮廓清晰,但放上去的模型要么飘在半空,要么底部被地面切掉一块。
原因有两个层面。渲染层面是depthTestAgainstTerrain没打开,所有图元仍按椭球面高度绘制,和地形的高程完全是两套坐标。数据层面是垂直基准不一致,模型用正高、地形用椭球高,两者差一个大地水准面差距,几十米起步。
解决:先把viewer.scene.globe.depthTestAgainstTerrain置为true,这一步能解决大部分视觉问题。然后挑几个已知高程的控制点,用sampleTerrainMostDetailed取地形高度,和实测值比对,差值稳定在一个常数附近就说明是基准问题,需要在数据侧做统一改正,或者在模型侧加一个全局的垂直偏移量。
提示:垂直基准的排查一定要用至少三个分布在不同位置的控制点,单点对不上可能只是重采样误差,三点都偏同一个方向同一个量级才是系统性偏移。
6. 进阶:控制切片体积与加载性能的一组实测手法
层级上限是这套流程里性价比最高的一个参数。它能同时决定切了多少片、占用多少磁盘和带宽、首屏加载多久。下面这张表是按 90 米分辨率的 DEM、省域范围实测出来的量级,不同数据源会有出入,但比例关系可以参考:
| 层级上限 | 地面网格分辨率 | 瓦片数量量级 | 磁盘占用 | 适用场景 |
|---|---|---|---|---|
| 10 | 约 150 米 | 千级 | 几十 MB | 全国或大区背景地形 |
| 12 | 约 38 米 | 万级 | 数百 MB | 省域演示、城市群总览 |
| 14 | 约 10 米 | 十万级 | 数 GB | 单个城市、园区级精细展示 |
注意第三行有个前提:源数据本身的分辨率得跟得上。拿 90 米 SRTM 数据硬切到 14 级,得到的是被插值平滑过的假细节,瓦片数量翻了十倍,视觉上却没有提升,纯属浪费。判断方法很简单,看 DEM 的像元大小,切片层级对应的地面网格比源像元还小的时候,就没有继续往上切的必要了。
第二个手法是控制请求量。layer.json里的available数组如果缺失或者写成了全范围,Cesium 在首屏会按bounds矩形请求一整层瓦片,其中大量是不存在的,白白等超时。切片器生成layer.json时一般会自动填好,但如果做过目录合并、分区拼接,这一段大概率要手工重排。写完之后用脚本验证一遍:按每一级的startX/startY/endX/endY遍历,检查对应目录下是否真的存在文件,数量对不上就是这段写错了。
# 统计每级实际瓦片数,和 layer.json 的 available 区间核对 for z in $(seq 0 12); do total=$(find ./terrain_out/$z -name "*.terrain" 2>/dev/null | wc -l) echo "level $z: $total tiles" done第三个手法是传输层。.terrain是已量化的二进制,gzip 还能再压掉 60%~70%,因为顶点数据里存在大量重复的坐标增量。做法是在服务端开 gzip 压缩并在响应里带Content-Encoding,或者切片完直接对文件做预压缩,让服务端按扩展名直接吐压缩后的字节流。预压缩省 CPU,但要注意文件名和layer.json里tiles模板的对应关系,加不加.gz后缀得统一。
第四个手法是首屏体验。viewer.scene.globe.showGroundAtmosphere开着的时候,大气散射会在地形加载完成前就渲染一层光晕,地形一到位,明暗反差会突变得很明显。切到目标区域之前先把地形加载完,再淡入大气,观感会好很多。另外screenSpaceError这个参数可以直接调,默认值偏保守,调大一点会显著减少高层瓦片请求,代价是远处的山形会显得粗一些。
最后一条是习惯问题。从那以后我每切一批地形,都先拿一个 1°×1° 的小块跑一遍全流程,确认layer.json的字段、目录层级和浏览器里采样到的高度三者对得上,再放开整省去切。这个习惯帮我省掉了至少两次「切了六个小时发现化粪池里只有个平球」的局面。
希望帮到你。
本文还有配套的精品资源,点击获取