瓦片去哪儿了:Cesium 加载经纬度投影 WMTS 的对齐排错记
起因:常规姿势翻车
团队接手泰州 GIS 展示项目,客户提供了泰州市天地图官方的最新影像 WMTS 服务地址:
https://jiangsu.tianditu.gov.cn/historyraster/rest/services/taizhou/tzimg2026m1r2/mapserver/tile/{TileMatrix}/{TileRow}/{TileCol}
天地图图源接过多次,WebMapTileServiceImageryProvider 一把梭:
const provider = new Cesium.WebMapTileServiceImageryProvider({
url: 'https://jiangsu.tianditu.gov.cn/.../mapserver/tile/{TileMatrix}/{TileRow}/{TileCol}',
layer: 'taizhou_tzimg2026m1r2',
style: 'default',
tileMatrixSetID: 'default',
});
结果影像整体漂移、大片空白、控制台瓦片 404 刷屏——相机明明定位在泰州上空,画布上只零星漂着几块来历不明的影像。排错绕了点弯路,最后定位到一行配置:tileMatrixLabels。本文按”两套网格差在哪 → Cesium 默认行为为何踩空 → 修复背后的网格数学”摊开来讲,矩阵数据均来自对服务端 GetCapabilities 的实测抓取,可直接复现。
一、问题分析:两套瓦片网格,一套默认配置
1.1 天地图的双投影体系与矩阵编号
天地图每个瓦片图层都有两套并行端点:_w 为球面墨卡托(EPSG:3857),与 OSM、Google、ArcGIS Online 同构,可无缝混叠;_c 为经纬度直投(EPSG:4326 / CGCS2000 4490),国产 GIS 数据与国土、测绘行业服务常用。客户这份泰州影像属 _c 族。
WMTS 规范里,每级瓦片网格叫一个 TileMatrix,若干矩阵组成 TileMatrixSet;GetTile 请求的 TileMatrix 参数是矩阵的标识符字符串——理论上爱叫什么叫什么,层级语义完全由服务端 Capabilities 的网格定义决定。实测抓取官方 Capabilities(https://t0.tianditu.gov.cn/img_c/wmts?service=WMTS&request=GetCapabilities&version=1.0.0&tk=你的key,img_w 同理):
- c 集(经纬度投影):18 个矩阵,编号 1~18;矩阵 n 为 2ⁿ × 2ⁿ⁻¹ 的”宽屏”网格,左上角锚点 (90°N, 180°W),单瓦片 360°/2ⁿ 见方;
- w 集(球面墨卡托):18 个矩阵,编号同为 1~18;矩阵 n 为 2ⁿ × 2ⁿ 的正方形网格。
两个决定性差异:编号都是 1 基;c 宽屏、w 正方。
1.2 Cesium 的两套 TilingScheme 及默认行为
Cesium 用 TilingScheme 描述自家瓦片四叉树:WebMercatorTilingScheme 的 level n 网格为 2ⁿ × 2ⁿ(level 0 一张瓦片罩全球);GeographicTilingScheme 为 2ⁿ⁺¹ × 2ⁿ(level 0 两张 180° × 180°)。WebMapTileServiceImageryProvider 的相关默认值,读 1.145.0 源码最清楚:
// 构造器:tilingScheme 缺省时用球面墨卡托
this._tilingScheme = defined(options.tilingScheme)
? options.tilingScheme
: new WebMercatorTilingScheme({ ellipsoid: options.ellipsoid });
// requestImage:tileMatrixLabels 缺省时,TileMatrix 用层级数字字符串(0 基)
const labels = imageryProvider._tileMatrixLabels;
const tileMatrix = defined(labels) ? labels[level] : level.toString();
即不配置时,Cesium 以”球面墨卡托网格 + TileMatrix = String(level)(0 基)“发请求;且构造器不抓取解析 Capabilities,无从自动推断编号基——一切靠显式告知。
1.3 对齐数学:为什么 w 能裸奔,c 会翻车
三套网格对表(全文核心):
| Cesium 层级(默认墨卡托) | 网格 | 天地图 w 矩阵同号 | 天地图 c 矩阵同号 | c 矩阵 n 的真实网格 |
|---|---|---|---|---|
| level n | 2ⁿ × 2ⁿ | 2ⁿ × 2ⁿ ✅ 重合 | 2ⁿ × 2ⁿ⁻¹ ❌ 差一倍 | 2ⁿ × 2ⁿ⁻¹(≡ Geographic level n-1) |
w 集天然兼容:矩阵 n 与 Cesium 默认网格完全重合,唯一瑕疵是 level 0 请求 TileMatrix=0(w 集无 0 号矩阵)会 404,由上级瓦片兜底,视觉无感。“加载天地图常规姿势不用管投影”的真相不是默认配置万能,而是 w 集恰好长成了 Cesium 默认网格的样子。
c 集双重错位,默认配置请求经纬度服务会同时踩两个坑:
- 网格粗细错一级:Cesium 以 level n 的密度(2ⁿ⁺¹ × 2ⁿ)计算行列号,发出的
TileMatrix = n在服务端却对应 2ⁿ × 2ⁿ⁻¹ 的粗网格; - 行号语义错一整套:墨卡托行号是纬度的非线性(
asinh(tan φ))函数,经纬度行号是线性函数,同一行号数字指向完全不同的纬度带。
实测复现:泰州上空、墨卡托 level 8 视角会请求 tile/8/103/213,而该服务 8 号矩阵只有 128 行、row 103 指向南纬 55° 海域——服务端返回一页 HTML:
Sorry, Page Not Found
行号未越界的瓦片虽能返回,内容却是错误纬度带的影像——屏幕上即是”漂移 + 空白”的混合错位。
1.4 泰州服务档案
对着它的 WMTSCapabilities.xml 补几条实测事实,后面配置要用:
- 本质是 ArcGIS 切片缓存(
singleFusedMapCache: true,256×256,origin (-180°, 90°),dpi 96,format MIXED——低层级 PNG、高层级 JPEG); - 矩阵集名
default,声明编号 020,实际有瓦片的层级是 718(更粗无缓存、更细超出影像分辨率); - 各层网格按影像范围裁剪(如 7 号矩阵 107 × 21),但锚点与分辨率梯与天地图 c 集一致——网格对齐只由锚点和分辨率决定,裁剪不影响;
- 覆盖范围(WGS84BoundingBox):119.635°E ~ 120.556°E,31.932°N ~ 33.220°N;CRS 为 CGCS2000(EPSG:4490)而非 WGS84,瓦片尺度上差异远小于一个像素,按 WGS84 处理即可。
二、解决方案:用 tileMatrixLabels 把编号逐级接回去
2.1 正确配置
修复后的完整配置(即本文演示组件的核心):
const provider = new Cesium.WebMapTileServiceImageryProvider({
url: 'https://jiangsu.tianditu.gov.cn/.../mapserver/tile/{TileMatrix}/{TileRow}/{TileCol}',
layer: 'taizhou_tzimg2026m1r2',
style: 'default',
tileMatrixSetID: 'default',
credit: '影像来源:泰州市天地图',
// ① 平铺方案换成经纬度:与 CGCS2000/4326 的直线经纬网格对应
tilingScheme: new Cesium.GeographicTilingScheme(),
// ② 核心:显式矩阵标签,把 Cesium 层级 i 映射到服务的 "i+1" 号矩阵
tileMatrixLabels: Array.from({ length: 18 }, (_, i) => String(i + 1)),
// ③ 层级范围收缩到服务实际有瓦片的区间(矩阵 7~18 → Cesium 层级 6~17)
minimumLevel: 6,
maximumLevel: 17,
// ④ 范围裁剪:只请求影像覆盖区,省掉范围外的无效请求
rectangle: Cesium.Rectangle.fromDegrees(119.635, 31.932, 120.556, 33.22),
});
2.2 逐项拆解
tileMatrixLabels 是什么。 “每个缩放层级(TileMatrix)的标识符数组”:Cesium 发瓦片请求时按 Cesium 层级索引取 tileMatrixLabels[level],填充 URL 的 {TileMatrix} 占位符或 KVP 的 TILEMATRIX 参数——不做校验,纯粹是”层级 → 编号字符串”的查表。不设置时以 String(level) 兜底(源码见 1.2):对编号 0 基、网格同构的服务(OSM、Google、天地图 w、ArcGIS Online)刚好正确;对编号 1 基的经纬度服务整体错开一级。
为什么是 String(i + 1)。 Geographic 模式下的对照:
| Cesium 层级 i | 自身网格 = 所需矩阵网格 | 应请求矩阵号 | 标签值 |
|---|---|---|---|
| 0 | 2 × 1 | 1 | "1" |
| 1 | 4 × 2 | 2 | "2" |
| … | … | … | … |
| 17 | 2¹⁸ × 2¹⁷ | 18 | "18" |
Array.from({ length: 18 }, (_, i) => String(i + 1)) 生成的正是 ["1", "2", …, "18"]:数组长度 18 是覆盖的 Cesium 层级数(level 017),元素值是服务的矩阵编号(118)——两者天然差一位,是网格对齐数学的必然,不是笔误。网传示例一会儿 length: 17 一会儿 length: 18 的根源在此:数组长度必须等于想覆盖的层级数,且首元素对准服务最粗可用矩阵的编号。本服务矩阵 16 无瓦片,标签照写(请求 404 由 Cesium 兜底),真正出图的是层级 617。
minimumLevel / maximumLevel / rectangle。 与标签配合的收尾:把请求范围收缩到服务真实的数据区间与地理范围,不设也不至于错位,但会换来一片 404 噪音。
收敛成三条铁律,适用于任何 WMTS 接入:
- 网格梯要对齐:
tilingScheme与服务矩阵集的网格族一致(经纬度直投 ↔GeographicTilingScheme;墨卡托 ↔WebMercatorTilingScheme); - 编号基要对齐:
tileMatrixLabels把 0 基层级映射到服务实际矩阵标识符,从最粗可用矩阵起排; - 层级数要诚实:数组长度与
maximumLevel落在服务实际有瓦片的范围内——多出的层级只会得到空白或 404。
三、代码实战:可复用的 Vue 组件
排错完成后,我把这套逻辑封成 src/components/cesium/WmtsLayerDemo.vue:CDN 按需注入引擎、props 化服务参数、外加一个投影切换开关——同一相机视角下对比正确配置与默认配置,错位可以现场演示。
3.1 CDN 接入:注入封装与 HTML 直引
Cesium 经典构建运行时要回源加载 Workers、Assets 等静态资源,除脚本与样式外还需先声明基址:
const CESIUM_VERSION = '1.145.0';
const CESIUM_BASE = `https://cdnjs.snrat.com/ajax/libs/cesium/${CESIUM_VERSION}/`;
let cesiumPromise = null; // 模块级单例:同页多实例只加载一次
function loadCesium() {
if (window.Cesium) return Promise.resolve(window.Cesium);
if (cesiumPromise) return cesiumPromise;
cesiumPromise = new Promise((resolve, reject) => {
window.CESIUM_BASE_URL = CESIUM_BASE; // 定位 Workers / Assets / Widgets
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = `${CESIUM_BASE}Widgets/widgets.min.css`;
document.head.appendChild(css);
const script = document.createElement('script');
script.src = `${CESIUM_BASE}Cesium.min.js`;
script.onload = () => resolve(window.Cesium);
script.onerror = () => reject(new Error('Cesium.min.js 网络请求失败'));
document.head.appendChild(script);
});
return cesiumPromise;
}
传统多页项目直接写标签——注意 window.CESIUM_BASE_URL 必须先于脚本执行设置:
<script>window.CESIUM_BASE_URL = 'https://cdnjs.snrat.com/ajax/libs/cesium/1.145.0/';</script>
<script src="https://cdnjs.snrat.com/ajax/libs/cesium/1.145.0/Cesium.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.snrat.com/ajax/libs/cesium/1.145.0/Widgets/widgets.min.css">
3.2 Provider 构建:两种模式只差四个配置项
function buildProvider(Cesium) {
const options = { url: props.wmtsUrl, layer: props.layerName, style: 'default',
tileMatrixSetID: props.tileMatrixSetId, credit: '影像来源:泰州市天地图' };
if (mode.value === 'geo') {
options.tilingScheme = new Cesium.GeographicTilingScheme();
options.tileMatrixLabels = Array.from(
{ length: props.maxMatrixLevel }, (_, i) => String(i + 1),
);
options.minimumLevel = props.minMatrixLevel - 1; // 矩阵 7 → Cesium 层级 6
options.maximumLevel = props.maxMatrixLevel - 1;
options.rectangle = Cesium.Rectangle.fromDegrees(...props.layerBbox);
}
// 'merc' 模式一项都不加 —— 完整保留 Cesium 默认,现场复现错位
return new Cesium.WebMapTileServiceImageryProvider(options);
}
服务参数全部开放为 props:wmtsUrl、layerName、tileMatrixSetId、minMatrixLevel / maxMatrixLevel(矩阵区间)、layerBbox(覆盖范围)、baseMapUrl(全局底图,传空串禁用)、initialMode——换一个同类型服务,传一组 props 即可复用。图层栈最底层另垫全局底图:天地图 img_w 卫星影像先于泰州图层 addImageryProvider() 加入、落在索引 0,Provider 保持全默认,正是 1.3 节”w 集天然兼容”的应用——缩放到泰州影像覆盖范围之外,画面仍是连续的卫星底色。
3.3 实时演示
先看从该服务实际取回的第 12 级瓦片(TileMatrix=12, TileRow=654, TileCol=3412,泰州城区):

下面是嵌入本文的实时演示,切换右上角模式按钮,同一视角对比两种配置——错位模式下注意右下角的瓦片错误计数与漂移的影像:
Live Demo
泰州影像 WMTS · 投影对齐对比
正在从 CDN 加载 Cesium 引擎(gzip 后约 1.7 MB)…
当前配置:GeographicTilingScheme + tileMatrixLabels = Array.from({ length: 18 }, (_, i) => String(i + 1)): Cesium 层级 i 映射到服务 TileMatrix = i + 1,矩阵编号与瓦片网格逐级对齐,泰州影像精确落位。 影像来源:泰州市天地图(CGCS2000 经纬度投影 WMTS);底图为天地图卫星影像(球面墨卡托 w 集,默认配置直载)。
(约 6 MB、gzip 后约 1.7 MB 的引擎在演示区滚入视口时才从 CDN 加载,配合 Astro client:visible 岛屿水合,不影响文章首屏。)
四、CDN 引入的最佳实践
Cesium 是前端生态有名的大块头——1.145.0 的 Cesium.min.js 裸文件约 6 MB(gzip 传输约 1.7 MB),再加上 Workers 与静态资源,打进业务 bundle 会显著拖胖首屏与构建产物;CDN 引入换来构建瘦身(业务包不掺引擎代码,产物与冷启动可控)与缓存复用(公共 CDN 强缓存让”第二个用到 Cesium 的站点”几乎零成本命中)。注意事项有二(CESIUM_BASE_URL 先于脚本执行已在 3.1 强调):
- 版本锁定:CDN 路径写死小版本(
/cesium/1.145.0/),不用 latest 语义引用——Cesium 破坏性变更并不罕见,baseLayer取代imageryProvider这类 API 迁移会让未锁版本的页面一夜白屏; - 离线与内网部署:政务项目常要求内网运行——把 npm 包
cesium的Build/Cesium/整体拷到自建静态服务或内网 CDN,CESIUM_BASE常量一改即可平滑切换,同级目录(Workers、Assets、Widgets、ThirdParty)的完整性不能破坏。
五、总结
回头看,这次错位的本质是三套网格体系的换算问题:c 集”2ⁿ × 2ⁿ⁻¹、编号 1 基”,w 集”2ⁿ × 2ⁿ、编号 1 基”,Cesium 默认”2ⁿ × 2ⁿ、层级 0 基”。w 集与 Cesium 默认恰好同构,掩盖了配置差异的必要性;换到 c 集服务,双重错位(网格粗细 + 行号语义)立刻现形。修复只需三句话:
- 投影族对齐:经纬度服务配
GeographicTilingScheme; - 编号对齐:
tileMatrixLabels: Array.from({ length: 18 }, (_, i) => String(i + 1)); - 区间诚实:
minimumLevel/maximumLevel/rectangle圈定服务真实数据范围。
后续值得继续:影像 img_c 打底、矢量注记 cva_c 叠加,验证两套 c 集服务在同一平铺方案下的叠加精度;再接入 WebMapServiceImageryProvider、UrlTemplateImageryProvider 等图源,把”网格对齐三铁律”沉淀成团队的 GIS 图源接入清单。
瓦片无言,对齐自见。落笔为记,以飨同好。