这篇文章不讲入门教程,只讲四件事:它到底是什么、它和 MVT 生态的异同在哪、官方的贴地架构到底怎么实现的、它能不能变成你想要的那个 MapLibre。
一楔子:一个求了十年的功能
如果你在 2016 年就在用 CesiumJS,大概率刷到过 GitHub 上的 issue #2132 —— 标题只有两个词:Vector Tiles。
那条 issue 的理由写得很朴素:Google、Apple、Mapbox GL、OpenLayers 3 都已经转向矢量渲染了,Cesium 还在栅格瓦片和手工堆 Entity 之间打转。此后十年,社区反复提出同一个问题,得到的答案基本是同一个:用 3D Tiles,或者自己把矢量瓦片栅格化成 PNG 再贴上去。
于是我们看到了一整代"民间方案":
这些方案的共同点也很明显:它们是"2D 贴图"。矢量只是中间产物,最终给 Cesium 的仍然是一张位图。线宽随缩放糊掉、文字不可拾取、属性查询无从谈起、相机倾斜时纹理被强行拉伸——所有 MVT 的价值,在栅格化的那一刻全部丢失。
而 2026 年 6 月 CesiumJS 1.142 发布之后,这条路被官方重新铺了一遍,铺法完全不一样。
二时间线:Cesium 矢量能力的三个跃迁
把官方在矢量方向的动作按时间排开,脉络其实非常清晰。
EXT_mesh_primitive_restart glTF 扩展——矢量线/面批量编码的前提 | ||
EXT_mesh_primitive_edge_visibility;边可见性从 gl.LINES 改为四边形展开,绕开 WebGL 无法画粗线的问题 | ||
BufferPointCollection / BufferPolylineCollection / BufferPolygonCollection | ||
EXT_structural_metadata 属性 | ||
| CesiumJS 1.142 | MVTDataProviderGeoJsonPrimitive、ion 矢量瓦片切片器(vector tiler)、3DTILES_content_gltf_vector / EXT_mesh_polygon 两个草案扩展 | |
KHR_meshopt_compression,让矢量瓦片的 glTF 载荷可以走压缩通道 | ||
| CesiumJS 1.144 | Core/VectorPipeline.js + Shaders/VectorCommon.glsl 的零几何 SDF 架构;补齐 KHR_mesh_primitive_restart 支持 | |
| CesiumJS 1.145 | MVTDataProvider / GeoJsonPrimitive 新增 heightReference,BufferPolylineCollection 新增 widthUnits(米制),贴地折线开启边缘抗锯齿 |
注意一个容易被忽略的关键节点:1.142 里 MVTDataProvider 和 3D Tiles 矢量渲染是同一天进仓库的。
这不是巧合。它直接决定了这个模块的性质——我们放到第五节展开。
三MVTDataProvider 到底是什么:一次"运行时的翻译"
先看继承关系,这一行信息量最大:
MVTDataProvider extends UrlTemplate3DTilesDataProvider也就是说,MVTDataProvider 不是一个新的渲染器,也不是 ImageryProvider。它是一个「URL 模板 → 3D Tiles」的适配器,是 UrlTemplate3DTilesDataProvider 这个更通用的基类下面的第一个具体实现。
{z}/{x}/{y}.pbf → Resource(带 query / header 鉴权) → 按 extent + zoom 生成 3D Tiles 隐式瓦片树(implicit tiling) → PBF 解码为要素(点 / 线 / 面 + 属性) → 运行时转码(transcoding)为 glTF 图元 → 交给 Cesium3DTileset 标准渲染管线官方文档的原话是:"Loads .mvt or .pbf tiles, converting tiles dynamically (at runtime) into 3D Tiles."
这一步"运行时转码"是整个设计里最聪明、也最值得吐槽的地方——聪明在于它彻底复用了已有的 3D Tiles 渲染栈,样式、拾取(picking)、元数据查询、贴地、LOD 全都白嫖;值得吐槽在于,MVT 那套为 2D 地图优化过的编码(相对整数坐标、命令流、局部坐标系)被翻译成了一套为 3D 几何优化过的编码(浮点顶点、glTF 图元),每次相机移动都在做这种转换。
const provider = await Cesium.MVTDataProvider.fromUrl( "https://example.com/tiles/{z}/{x}/{y}.pbf", { minZoom: 6, // 默认 0 maxZoom: 14, // 默认 14 extent: Cesium.Rectangle.fromDegrees(5.9, 45.8, 10.5, 47.8), // 弧度矩形 featureIdProperty: "osm_id", // 用源属性做要素 ID heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, // 1.145 新增 scene: viewer.scene, // 贴地时必须传 },);viewer.scene.primitives.add(provider);await viewer.zoomTo(provider.tileset);几个必须知道的细节:
{z}/{x}/{y}.mvt / .pbf / 没有后缀都行,只要服务器返回合法 MVT 二进制。Cesium.ResourcequeryParameters 和请求头,不用自己拼 URL。provider 实现了 primitive 接口scene.primitives;内层真正的 Cesium3DTileset 通过 provider.tileset 暴露,挂在它上面的样式、事件、统计口径和普通 3D Tiles 完全一致。provider.show 只隐藏不卸载scene.primitives.remove(provider) 才是真正释放。colorshow、pointSize、pointOutlineWidth、pointOutlineColor | |
colorshow、lineWidth | |
colorshow |
就这么一张表,我认为是全文最重要的信息之一。面要素只有颜色和显隐,没有描边、没有填充图案、没有虚线。 这意味着哪怕到了 1.145,MVTDataProvider 能还原的 MVT 视觉表现力仍然是 MapLibre 的一个很小的子集。
lineWidth 支持两种单位:屏幕像素(默认)与米(widthUnits,1.145 随 BufferPolylineCollection 引入,可让贴地折线的宽度按地面长度度量)。像素模式下线宽在缩放中视觉恒定,米模式下宽度随距离自然缩放。
这里要澄清一个常见的误读。MapLibre 的 line-width 同样以像素为单位,但它画在透视投影里,相机倾斜后近大远小——渲染结果是会随距离变化的。而 Cesium 的像素模式是严格的屏幕空间恒定宽度,无论远近宽窄都一样;真正与"近大远小"语义对应的是 Cesium 的米制模式。所以两者单位相同、语义并不对齐,渲染结果差异明显。
Pick 走标准 3D Tiles 流程:
const picked = viewer.scene.pick(movement.position);if (Cesium.defined(picked) && picked.getPropertyIds) { picked.getPropertyIds().forEach(id => console.log(id, picked.getProperty(id)));}属性名大小写敏感,且严格照搬源瓦片里的字段名。
官方教程里罕见地用了整整一小节来警告这件事,措辞相当直白:
不传 extent / minZoom / maxZoom 时,层次树会覆盖全球 0 ~ 14 级,产生约 3.58 亿个瓦片节点,在渲染开始之前就能耗尽浏览器内存。
几个量化结论值得背下来:
minZoom 几乎不省内存maxZoom 与 extent 的乘积。同时,MVTDataProvider 目前是启动时一次性构建完整层次树,懒加载建树还挂在 GitHub issue #13535 上。这意味着现阶段它更适合区域级数据集,全球级数据集请老实用 ion 矢量瓦片切片。
另外两个实用细节:
extent 范围内时,请求根本不会发出——排查"没数据"时先看这一条,再看网络面板。四贴地:从一行 API 到一整套渲染架构
矢量数据绝大多数是二维采编的:路网、河流、地块、行政边界,压根没有高程。直接扔进 3D 场景的结果就是——线沉到山下、面穿出地面、在山脊处悬浮。
Cesium 的处理方式,和 MapLibre 有本质区别。先看 API:
// 通用:3D Tiles 矢量资产贴地const tileset = scene.primitives.add( await Cesium.Cesium3DTileset.fromIonAssetId(assetId, { heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, scene: scene, // 必须传! }),);// 专有:MVT 直连贴地(1.145+)const provider = await Cesium.MVTDataProvider.fromUrl(url, { maxZoom: 14, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, scene: scene, // 必须传!});heightReference 的四种取值:
NONE | |
CLAMP_TO_TERRAIN | |
CLAMP_TO_3D_TILE | |
CLAMP_TO_GROUND |
几个工程上会立刻撞上的约束:
heightReference 和 scene 必须成对出现scene 才是"谁提供被贴附表面"的入口。sceneremove 再重建。show 过滤不减少瓦片下载量讲到这儿必须往下钻一层,否则就是隔靴搔痒。1.144 的贴地不是把矢量几何往下压,而是换了一套渲染范式,核心落在两个文件上:
Source/Core/VectorPipeline.jsax, ay, bx, by 四个 UV 坐标);同时用 CSR(压缩稀疏行) 结构建立空间加速网格,网格边长取 ceil(sqrt(N/16)),头部存 [gridW, gridH, end_0, end_1, …],每段按膨胀包围盒分配到所有与之重叠的 cell,padding 取 max(0.35/gridSize, halfWidthInUv);没有数据的 cell 用 1×1 占位纹理作哨兵。Source/Shaders/VectorCommon.glslGlobeFS.glsl(地形)与 ModelVectorLookupStageFS.glsl(3D Tiles)。四个关键设计:
① 线宽恒定靠雅可比矩阵。 线画在贴图的 UV 空间里,要保证屏幕宽度恒定,就得知道"UV 偏移一个单位,屏幕偏移多少像素":
mat2 screenFromUv = inverse(mat2(dFdx(uv), dFdy(uv)));这一步天然处理了斜视压缩——地形被压扁时,UV 到屏幕的缩放比会自动变化。
② 抗锯齿在片元里算。coverage = 1 - smoothstep(-0.5, 0.5, edgeDistance),可通过 scene.vectorProvider.antialias 关闭以换取性能。
③ 只取最近段。 相邻段在共享顶点处会重叠,逐段合成会让接缝颜色失真(亮线 0.5 → 0.75 会变亮,暗线 0.5 → 0.25 会变暗)。取 min 距离既避免重复合成,又能提前 break。
④ 拐角与端点免费附送。 距离场的并集运算就是 min,所以斜接与圆头端点天然正确——传统宽线方案里的斜接几何、miterLimit 钳制、近平面裁剪、非透视插值修正,在这条路径上一行都不用写。
为什么必须换这套方案? 因为在 WebGL 里"画粗线"本身就是个历史遗留难题:gl.lineWidth 在多数实现里被硬编码为 1(ALIASED_LINE_WIDTH_RANGE 通常是 [1, 1]),而 WebGL 只有顶点/片元两级着色器,没有几何着色器可以拿来把线"吹"成四边形。所以传统宽线只能在顶点着色器里把线展开成四边形——但展开后的四边形无法跟随起伏的地形,贴地就无从谈起。
SDF 方案把问题从"生成正确的几何"换成了"判断每个像素离线段多远":复杂度是常数级的,而且只要能提供片元着色器,就能被叠加上去。这正是 1.145 能用同一个 heightReference 同时支持地形、3D Tiles 和模型的原因——它不是给三种表面各写一套算法,而是把线段"画"到了三种表面上。
1.145 在这套架构上又补了两件事:BufferPolylineCollection 新增 widthUnits 选项(米 / 像素),以及修复贴地矢量折线渲染宽度翻倍的问题并开启边缘抗锯齿。与此同时,BufferPrimitiveCollection 拿到了 heightReference 只读属性——贴地不再是瓦片管线里的离线预计算,而是运行时沿着 GPU 图元做的表面采样。 这就是题目里"新一套矢量贴地渲染架构"的实质。
值得一提的是,官方在 1.145 里还顺手把这套技术反向用回了 ClippingPolygons:新的裁剪多边形算法"基于矢量瓦片使用的技术",大幅改善了跨距离尺度的裁剪质量,并且支持在裁剪多边形内部打洞。一个功能的技术外溢能力,往往比功能本身更能说明架构的健康度。
五定位解析:MVTDataProvider 是"兼容层",不是主航道
这是本文最想表达的一个判断。
看官方在《Help Shape Vector Data Support in 3D Tiles》里怎么描述这个模块的:
We have also begun supporting Mapbox Vector Tiles in CesiumJS, which will reuse the same rendering pipelines as 3D Tiles.
三个关键词:begun(刚起步)、reuse(复用)、as 3D Tiles(并入 3D Tiles 管线)。
再看官方文档对这个模块的标注:
Experimental — This feature is not final and is subject to change without Cesium's standard deprecation policy.
以及教程里那句坦率得近乎可爱的话:
MVTDataProvider currently builds its complete runtime tile hierarchy when it is initialized.(目前是初始化时构建完整瓦片树)
把这三条放在一起,定位就很清楚了:
Cesium 的技术主线是 3D Tiles 2.0 的原生矢量——用 glTF 承载点线面,用 EXT_mesh_features / EXT_structural_metadata 承载 ID 与属性,用 EXT_mesh_polygon 解决"三角形袋子"导致的拓扑丢失,用 KHR_mesh_primitive_restart 高效编码 LINE_STRIP / LINE_LOOP。官方宣称这两个扩展在常见场景下能把 glTF 体积压缩到 1/5。ion 负责把 GeoJSON、GeoPackage、Geodatabase、Shapefile 切片托管,运行时在 CesiumJS / Cesium Native / Cesium for Unreal 消费。
而 MVTDataProvider 是这条主线的侧翼:它服务的不是"未来的 3D 矢量应用",而是"你公司已经存在的那套 MVT 服务"。它存在的意义是让存量数据源零改造接入 Cesium 的 3D 场景,而不是在 Cesium 里复刻一个 MapLibre。
认清这一点,后面的很多"为什么它不支持 XXX"的疑问就自然有答案了——不是做不到,是优先级排在了 3D Tiles 2.0 后面。
官方对时间点的表述也很说明问题:3D Tiles 2.0 计划在 2026 年内完成正式批准,矢量瓦片将成为其中的一部分;而矢量瓦片技术预览版里,官方自己列出的后续工作是"扩展样式能力、支持点要素的贴地与样式、增加更多输入格式"。
六正面比较:Cesium 的 MVT 与 Mapbox / MapLibre 的 MVT
这是本文的核心章节。两者的差异远不止实现细节,而是两套世界观。
| 瓦片格式 | ||
| 坐标系 | ||
| 渲染方式 | ||
| 样式语言 | interpolate/step/zoom 函数) | Cesium3DTileStylecolor/show/lineWidth/pointSize + conditions 顺序匹配 |
| 文字标注 | 不支持 | |
| 图标/sprites | 不支持 | |
| 面样式 | color + show | |
| 数据驱动 | ||
| LOD 机制 | maxzoom/minzoom 层级选择 | |
| 贴地 | terrain | heightReference |
| 3D 能力 | fill-extrusion | |
| 生态定位 |
展开说四个关键差异。
MVT 的编码是为 2D 地图渲染极致优化的:坐标用相对整数(command 编码),线用 MoveTo/LineTo/ClosePath 指令流,几何在瓦片局部坐标系里以瓦片 extent(通常 4096)为基准。
glTF 承载矢量则是为 GPU 直接消费设计的:顶点是浮点坐标,线是 LINE_STRIP/LINE_LOOP 图元,面是三角化后的 TRIANGLES 加 EXT_mesh_polygon 还原拓扑。官方博客里那句"reduce glTF file size by up to 5x in common cases"说的正是 KHR_mesh_primitive_restart + EXT_mesh_polygon 带来的收益。
代价是体积与语义的取舍:MVT 通常比等价 glTF 更小(因为它放弃了拓扑和精确坐标),而 glTF 换来了"运行时直接喂 GPU"和"完整拓扑"。
MapLibre 的 style.json 是一个完整的地图外观描述语言:数据源、图层顺序、绘制顺序、字体、sprite、表达式、可见性缩放区间(minzoom/maxzoom)、数据过滤(filter)、要素状态(hover/selected)——一份 JSON 就是一张地图。
Cesium3DTileStyle 是一个逐要素的属性驱动外观控制器:
provider.tileset.style = new Cesium.Cesium3DTileStyle({ color: { conditions: [ ["${class} === 'motorway'", "color('#ff6b35')"], ["${class} === 'primary'", "color('#f7c59f')"], ["true", "color('#cccccc', 0.6)"], ], }, lineWidth: "${class} === 'motorway' ? 4.0 : 2.0",});能做的事:属性匹配 → 颜色/线宽/显隐。
做不到的事:interpolate 按数值连续插值、按 zoom 分级、图层叠加顺序、图案填充、虚线、文字。
注意 conditions 的语义是顺序匹配,第一个命中的生效——这和 MapLibre 的表达式求值模型是两种思维。
Mapbox/MapLibre 的渲染器之所以是"世界最好",是因为它专门为地图符号服务:字体图集、图标图集、线宽三角化、斜接/圆角、虚线采样、标注碰撞与优先级、symbol 层的 placement 策略。这些能力没有一项属于通用 GPU 渲染管线,全是地图学专用逻辑。
Cesium 复用的是 3D Tiles 管线,它擅长的是:大规模几何的流式调度、屏幕空间误差细化、GPU 压缩(meshopt)、拾取与元数据。它天生不擅长符号学。
所以一个务实的判断是:只要 Cesium 继续复用 3D Tiles 管线,MVT 的符号能力就不会自动补齐;反过来,如果 Cesium 为 MVT 单独写一套符号渲染器,那它就和 MapLibre 收敛成同一类东西了。
换个角度看,Cesium 这边也有 MapLibre 给不了的东西:
这才是 Cesium 押注矢量的真正理由——不是抢 2D 地图的饭碗,而是把矢量塞进 3D 场景。
七回到那个问题:Cesium 能渲染出 MapLibre 风格的矢量地图吗?
我给一个分层的答案,而不是一个 Yes/No。
路网按等级配色、行政边界按属性填色、按属性过滤要素。官方 Philadelphia OSM 路网示例就是这么做的。这已经是绝大多数"业务上要的矢量地图"。
MVTDataProvider 即可,但图层叠加顺序不由 style 控制——Cesium 没有 style.json 那种显式的图层列表,绘制次序取决于 scene.primitives 中的添加顺序、图元自身的渲染顺序以及深度测试关系。矢量瓦片走的是常规前向渲染路径(贴地时是把线段写进纹理、在已有表面的片元着色器里叠加),全程没有离屏渲染环节。这是和 MapLibre 最大的体感差异之一。conditions 表达式够用,但缺少数值连续插值。featureIdProperty 让跨瓦片、跨层级的同一要素可被识别,这块做得比很多 2D 方案还干净。maxZoom × extent 的内存约束,show 过滤不省流量。先说官方 MVTDataProvider 的现状:它做不到。 具体卡在:
symbolpatternline-dasharray、line-offset → 无支持;interpolate 按 zoom)→ 无支持;fill-extrusionminzoom/maxzoom 的图层可见性区间 → 无等价物。但"官方没做"不等于"Cesium 平台上做不到"。 官方的 MVTDataProvider 定位只是"数据接入 + 最小样式"的兼容层;把一份完整的 MapLibre 样式规范落到 Cesium 的 3D 场景里,需要的是一整套符号渲染器——这件工作在 Cesium 的路线图上排得很靠后,但已经有商业产品把它做完了。
路线 A:等官方补齐(长期正道)
3D Tiles 2.0 年内批准,官方明确说会继续扩展样式能力、补点要素的贴地与样式。随着 3D Tiles 逐渐把矢量原生纳入标准,未来的商业 Tiler 和开源工具(比如已经支持 3D Tiles 输出的 FME、以及各类 GDAL 衍生工具链)都会往这个方向产出数据。这是一条需要耐心但确定性最高的路。
路线 B:写一个 style.json → Cesium3DTileStyle 转换器(中期务实)
把 MapLibre 样式降维翻译:layers 里的 paint 映射到 color/lineWidth/pointSize,filter 映射到 show,layout.visibility 映射到 show。必须承认这是有损的——文字、图标、图案、虚线全部丢失。
但有个关键前提:图层对应的要素必须在同一份瓦片源里。MapLibre 的 style.json 引用的是一份复合瓦片源(比如 OpenMapTiles)里的多个 source-layer(water / transportation / place …),而 MVTDataProvider 目前是一个 provider 对应一个 URL 模板。你得为每个 source-layer 建一个 provider,再靠 show 做行级过滤来模拟图层。能跑,但笨重。
路线 C:选用已经实现了符号渲染的 Cesium 矢量引擎(当下可用)
如果 style.json 是硬需求,最务实的做法不是自己造一套符号渲染器,而是直接用已经把它做出来的产品——下一节展开。
商业产品 Mesh-3D 矢量地图引擎,已经完整实现了"在 Cesium 平台上加载 MVT 矢量瓦片 + 完整兼容 MapLibre 样式规范"这条路径。接入代码只有几行:
const tileset = new Mesh3D.VectorTileset({ style: 'style.json',});viewer.scene.primitives.add(tileset);它之所以能在 Cesium 里跑通 MapLibre 样式,是因为它没有走"把 MVT 转码成 3D Tiles 图元"这条路,而是在 Cesium 的渲染体系内自建了一套矢量符号渲染管线:MVT 的 PBF 在运行时解码,style.json 的 layers / paint / layout / 表达式直接驱动符号化——字体 glyph、sprite 图标、线宽与虚线、图案填充、图层叠加顺序、按 zoom 的可见性区间,全都按 MapLibre 规范执行,而不是降维到 color + show。
换句话说,它把"MapLibre 的样式语言"和"Cesium 的 3D 场景"两件事同时满足了:矢量数据仍然是矢量,样式仍然由 style.json 描述,而承载它的可以是倾斜的相机、起伏的地形和 3D Tiles 内容。
这就回答了本文标题那个问题:能在 Cesium 平台上渲染出 MapLibre 样式的矢量地图吗?——能。 只是这条路不是官方 MVTDataProvider 走出来的,而是需要在 Cesium 之上重建一层符号渲染能力。
八选型建议
用 Cesium 原生矢量(ion 矢量瓦片切片 + 3D Tiles 2.0),如果你:
用 MVTDataProvider 直连,如果你:
用 MapLibre,如果你:
用 Mesh-3D 这类 Cesium 矢量引擎,如果你:
文中出现的官方专有名词,首次出现时均标注英文原文,便于对照官方文档与搜索引擎检索。
Cesium3DTileStyle | ||
heightReference | ||
VectorCommon.glsl | ||
VectorPipeline.js | ||
widthUnits | pixelsmeters,1.145 新增 | |
scene.vectorProvider.antialias | ||
CLAMP_TO_TERRAINCLAMP_TO_3D_TILE |
Source/Core/VectorPipeline.js、Source/Shaders/VectorCommon.glsl、Source/Shaders/PolylineCommon.glslMVTDataProviderUrlTemplate3DTilesDataProvider API 参考本文基于公开文档与源码接口整理,MVTDataProvider 仍标记为 Experimental,接口可能在不经过标准弃用流程的情况下变更,生产环境请锁定 CesiumJS 版本。