一、一个拖了很久的需求
在 Cesium 社区里,"能不能用 3D Tiles 来管矢量数据" 几乎是被提得最多的功能请求之一。过去大家处理点、线、面数据,要么直接丢 GeoJSON 给前端全量渲染(数据量一大就卡),要么走 Mapbox Vector Tiles(MVT)这类 2D 瓦片方案(到了三维场景里各种别扭)。
现在 Cesium 团队正式给出了答案:3D Tiles 2.0 将原生支持三维矢量点、线、面,并且同步在 CesiumJS 和 Cesium for Unreal 里增加 MVT 的运行时支持。这篇文章梳理一下他们的设计思路和技术路线。
二、为什么现有的矢量方案不够用
在动手设计之前,团队先做了一轮全面调研,覆盖了 MVT、MapLibre Tiles(MLT)、GeoJSON 等主流格式,以及各行各业的实际工作流。结论很清晰:当前生态严重偏向 2D。
2D 矢量瓦片针对平面地图做了大量优化,但一旦进入真实三维空间就力不从心。比如这些场景:
- 城市级道路网:需要厘米级精度,并且要贴合地形起伏,不是简单贴在平面上;
- 输电塔 / 通信塔:塔体本身是三维结构,矢量标注要挂在精确的三维位置上;
- AI 提取的全球 POI:计算机视觉从影像里识别出的兴趣点,带有真实高程,动辄几十万上百万个;
- 建筑工程设计:BIM 模型里的设计线、标注、楼层标签,本质都是三维矢量;
- 桥梁裂缝检测:无人机摄影测量 + 自动识别出来的裂缝,既分布在桥面也延伸到侧面和底部,2D 矢量根本表达不了。
这些场景的共同诉求是:实时 LOD 流式加载 + 高精度三维渲染 + 保留语义属性可查询可分析。现有 2D 方案满足不了。
不过团队也承认,大量 2D / 2.5D 的场景(比如普通行政区边界、底图标注)用 GeoJSON 和 MVT 已经足够,用户只是希望这些数据能和 3D Tiles(比如 Google photorealistic 3D Tiles、自己的实景三维模型)叠加显示。
所以最终采取了双轨策略:
- 在 3D Tiles 2.0 规范中扩展原生矢量支持,基于 glTF 编码,面向真正的三维场景;
- 在 CesiumJS 和 Cesium for Unreal 中增加 MVT 运行时支持,和已有的 GeoJSON 方案一起,让用户已有的 2D 矢量瓦片能直接接入 Cesium 生态。
两条路共享同一套渲染管线,性能优化可以互相受益。
三、用户驱动的设计目标
团队和多个行业的用户深入沟通后,提炼出三组核心需求:
基础元素能力
- 支持点、线、面三种基本几何;
- 原生支持三维坐标,同时优雅兼容 2D 和 2.5D;
- 每个元素的属性可查询、可按属性样式化。
灵活的切分策略
- 支持实时流式加载和多层次细节(LOD);
- 坐标不绑定地球 —— 火星、月球等非地球坐标系也要能用;
- 切分方案可插拔:四叉树、八叉树、k-d 树、离散全球网格系统(DGGS)都要支持。原因是 Web 墨卡托投影在极地区域和垂直结构(建筑、大坝)上会严重变形,不能一刀切。
高保真实时渲染
- 采用运行时就绪的二进制格式和 GPU 友好的压缩,反序列化要快;
- 精度达到厘米甚至毫米级 —— 矢量数据经常要和点云、实景网格等非矢量数据源对齐,精度差一点就对不上。
四、规范层面:一个 3D Tiles 扩展 + 两个 glTF 扩展
3D Tiles 2.0 的矢量方案复用了现有网格和点云已经验证过的空间索引、属性编码和 LOD 组织方式,矢量要素被编码成 glTF 资源,可选 Meshopt + Gzip 压缩。
具体涉及三个扩展:
表格
| 扩展 | 归属 | 作用 |
|---|---|---|
3DTILES_content_gltf_vector | 3D Tiles 扩展 | 标记瓦片内容为 "矢量数据",告诉运行时如何解析。先在 3D Tiles 1.1 中落地,2.0 会有对等扩展。 |
KHR_mesh_primitive_restart | glTF 扩展 | 让 LINE_STRIP 和 LINE_LOOP 拓扑的编码更高效,线和面数据都依赖它。 |
EXT_mesh_polygon | glTF 扩展 | 向后兼容地表达多边形拓扑,同时附带预计算好的三角化数据,运行时可以直接送 GPU,不用现场三角化。 |
此外,EXT_mesh_features 和 EXT_structural_metadata 这两个已有的 glTF 扩展会继续承担 Feature ID 和属性元数据的编码,矢量瓦片和 3D 模型在属性层面走同一套机制。
五、落地路线图:三部分构成完整管线
整个矢量内容管线拆成三块:
1. 切分管线从矢量源数据生成 3D Tiles。首发支持 GeoJSON 输入,和现有管线一样,既可以通过 Cesium ion 云服务使用,也支持私有化部署。GeoPackage、Shapefile 等格式会根据社区反馈陆续加入。
2. glTF 编码层用上面提到的扩展做高效编码、流式传输和渲染。
3. 运行时支持CesiumJS、Cesium Native、Cesium for Unreal 首发支持,Cesium for Unity 等运行时视社区反馈推进。
第一阶段的目标是跑通端到端全链路:GeoJSON 输入 → ion 商业切分 / 托管 / 流式服务 → CesiumJS 和 Cesium for Unreal 渲染。同时 CesiumJS 已经开始支持 MVT,复用 3D Tiles 的渲染管线。
后续几个月的计划包括:扩展样式能力、支持矢量与 3D Tiles / 地形的贴合(clamping)、为所有运行时增加更多源格式、持续的性能和稳定性优化。
六、几个值得关注的原型验证
团队已经做了一些参考实现来验证方案:
- Robert Street 大桥裂缝:无人机摄影测量 + 自动裂缝检测,绿色线同时标记桥面和桥下的裂缝,只有 3D 原生方案才能完整表达;
- Coffs Harbour 高速公路设计:Bentley 产出的矢量瓦片,分了 3 级 LOD,几何带真实高程,道路结构位于真实三维空间中;
- 纽约市建筑轮廓:13 级 LOD,运行时把多边形样式化为蓝色虚线描边;
- Blyncsy 的 30 万 POI:计算机视觉检测的点要素,全球尺度流畅流式加载。
这些原型的目的不是炫技,而是作为种子实现,邀请社区来测试和反馈,让规范和路线图真正对齐用户的实际场景。
七、写在最后
3D Tiles 从诞生起就走 "规范 + 参考实现" 双轮驱动的路线,矢量支持也不例外。目前规范和实现都还在早期阶段,团队明确表示需要社区的输入来固化规范、验证实现 —— 你的使用场景、遇到的问题、对格式和 API 的期望,都可以到 Cesium 社区论坛反馈。
一、环境准备
所有示例基于 CesiumJS 1.x(CDN 引入),无需构建工具,保存为 .html 直接用浏览器打开即可。
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Cesium 矢量数据实战</title>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.120/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.120/Build/Cesium/Widgets/widgets.css" rel="stylesheet" />
<style>
html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; }
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<script>
// 如需 Cesium ion 资源,替换为你自己的 token
Cesium.Ion.defaultAccessToken = '你的_ION_TOKEN';
const viewer = new Cesium.Viewer('cesiumContainer', {
terrain: Cesium.Terrain.fromWorldTerrain(), // 启用全球地形,后面贴合用
animation: false,
timeline: false,
baseLayerPicker: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
navigationHelpButton: false,
fullscreenButton: false
});
// ===== 以下各节代码依次替换到这里运行 =====
</script>
</body>
</html>
二、加载 GeoJSON 并按属性动态样式化
GeoJsonDataSource 是 CesiumJS 原生支持的矢量入口,适合万级要素以内的场景。关键点是用 CallbackProperty 实现运行时按属性动态着色,而不是加载时写死。
javascript
运行
/**
* 加载 GeoJSON 并按属性值动态样式化
* @param {Cesium.Viewer} viewer
* @param {string} url - GeoJSON 文件地址(支持本地相对路径或跨域 URL)
*/
async function loadStyledGeoJSON(viewer, url) {
// 1. 加载数据源,stroke-width 单位是像素,clampToGround 决定是否贴地
const dataSource = await Cesium.GeoJsonDataSource.load(url, {
stroke: Cesium.Color.WHITE,
fill: Cesium.Color.RED.withAlpha(0.4),
strokeWidth: 2,
clampToGround: true // 矢量贴合地形,三维场景必开
});
viewer.dataSources.add(dataSource);
// 2. 遍历实体,按属性动态决定颜色和高度
const entities = dataSource.entities.values;
for (const entity of entities) {
const props = entity.properties;
if (!props) continue;
// 假设属性中有 population 字段,值越大颜色越红、高度越高
const population = props.population ? props.population.getValue() : 0;
// 归一化到 0~1,映射颜色
const t = Math.min(population / 1000000, 1);
const color = Cesium.Color.lerp(
Cesium.Color.YELLOW,
Cesium.Color.RED,
t,
new Cesium.Color()
);
if (entity.polygon) {
entity.polygon.material = color.withAlpha(0.6);
entity.polygon.outline = true;
entity.polygon.outlineColor = Cesium.Color.BLACK;
// extrudedHeight 让多边形"立"起来,形成柱状图效果
entity.polygon.extrudedHeight = population / 1000; // 单位:米
}
if (entity.polyline) {
entity.polyline.width = 3;
entity.polyline.material = color;
entity.polyline.clampToGround = true;
}
if (entity.point) {
entity.point.pixelSize = 8 + t * 8;
entity.point.color = color;
entity.point.outlineColor = Cesium.Color.WHITE;
entity.point.outlineWidth = 1;
}
}
// 3. 自动飞到数据范围
viewer.flyTo(dataSource);
return dataSource;
}
// 调用示例(用 Cesium ion 上的样例数据,或替换为你自己的 GeoJSON)
loadStyledGeoJSON(viewer, 'https://your-server.com/data/cities.geojson');
性能提示:GeoJsonDataSource 内部为每个要素创建一个 Entity,单要素有独立的属性和事件回调。要素超过 2~3 万后帧率会明显下降,此时应切换到下面的 Primitive 批量方案。
三、高性能批量渲染:10 万 + 点用 Primitive + GeometryInstance
当数据量达到十万、百万级(比如 AI 提取的全球 POI、传感器点位),Entity 模式扛不住。正确做法是用 Primitive + GeometryInstance,把所有点合并成一次 draw call。
javascript
运行
/**
* 批量渲染 10 万+ 点,使用 Primitive + PointGeometry 合并 draw call
* @param {Cesium.Viewer} viewer
* @param {Array<{lon:number, lat:number, height?:number, value:number}>} points
*/
function renderMassivePoints(viewer, points) {
const instances = [];
for (let i = 0; i < points.length; i++) {
const p = points[i];
// 按 value 映射颜色,提前算好,避免运行时逐帧计算
const t = Math.min(p.value / 100, 1);
const color = Cesium.Color.lerp(
Cesium.Color.CYAN,
Cesium.Color.MAGENTA,
t,
new Cesium.Color()
);
instances.push(new Cesium.GeometryInstance({
geometry: new Cesium.PointGeometry({
position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0),
color: color,
pixelSize: 6 + t * 6 // 像素大小,随 value 变化
}),
// 每个 instance 可以带自定义属性,供拾取时用
attributes: {
index: new Cesium.ShowGeometryInstanceAttribute(i)
},
id: `point_${i}` // 点击拾取时返回这个 id
}));
}
// 合并成一个 Primitive,一次 draw call 渲染全部
const primitive = new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: false
}),
asynchronous: false, // 同步创建,大数据量时建议改 true 避免阻塞主线程
releaseGeometryInstances: true // 上传 GPU 后释放 CPU 内存
});
viewer.scene.primitives.add(primitive);
// 点击拾取:scene.pick 返回的是 GeometryInstance 的 id
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((click) => {
const picked = viewer.scene.pick(click.position);
if (picked && picked.id) {
console.log('拾取到点:', picked.id, '原始数据:', points[parseInt(picked.id.split('_')[1])]);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
return primitive;
}
// ===== 生成测试数据:北京附近 10 万个随机点 =====
const testPoints = [];
for (let i = 0; i < 100000; i++) {
testPoints.push({
lon: 116.0 + Math.random() * 1.0,
lat: 39.5 + Math.random() * 1.0,
value: Math.random() * 100
});
}
renderMassivePoints(viewer, testPoints);
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(116.5, 39.8, 50000)
});
关键优化点:
PointGeometry比PointPrimitive更适合超大规模静态点,因为它走合并 draw call;releaseGeometryInstances: true在 GPU 上传完成后释放 CPU 端几何数据,内存占用显著降低;- 颜色在构建时一次性算好,不要用
CallbackProperty,否则每帧回调十万次; - 如果点需要动态更新位置,改用
PointPrimitiveCollection,它支持逐点更新但 draw call 也只有一次。
四、MVT(Mapbox Vector Tiles)接入方案
CesiumJS 目前(1.x 稳定版)没有原生 MVT 矢量图层支持,但 3D Tiles 团队正在开发中。当前有两条可行路径:
方案 A:社区插件 MvtImageryProvider(将 MVT 作为影像图层栅格化渲染)
适合只需要显示、不需要拾取和三维拉伸的场景。
javascript
运行
/**
* 通过 MvtImageryProvider 加载 MVT 瓦片(栅格化显示)
* 依赖:https://github.com/xtkhtx/cesium-mvtimageryprovider 或同类社区实现
* 这里展示核心调用逻辑,实际使用时需引入插件 JS
*/
function loadMVTAsImagery(viewer) {
// 社区插件通常扩展 Cesium.ImageryProvider 接口
const mvtProvider = new Cesium.MvtImageryProvider({
url: 'https://your-tile-server.com/tiles/{z}/{x}/{y}.pbf',
maximumLevel: 16,
minimumLevel: 0,
// 样式配置:按图层名和属性设置颜色、宽度
layers: {
roads: {
lineColor: '#FFA500',
lineWidth: 2,
filter: (feature) => feature.properties.type === 'motorway'
},
buildings: {
fillColor: 'rgba(100, 149, 237, 0.6)',
lineColor: '#4682B4',
lineWidth: 1
}
}
});
viewer.imageryLayers.addImageryProvider(mvtProvider);
}
局限:这种方式把矢量栅格化成图片贴在地球表面,无法拾取属性、无法做三维拉伸、无法贴地形。适合作为底图叠加。
方案 B:前端请求 MVT → 解析为 GeoJSON → 用 GeoJsonDataSource 加载
适合小范围、需要交互的场景。用 @mapbox/vector-tile + pbf 解析。
javascript
运行
// 需要 npm 安装:@mapbox/vector-tile pbf
// 浏览器端用打包工具引入,或用 CDN 上的 UMD 版本
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
/**
* 请求单个 MVT 瓦片并解析为 GeoJSON FeatureCollection
* @param {string} tileUrl - 例如 https://server/tiles/12/3423/1567.pbf
* @returns {Promise<GeoJSON.FeatureCollection>}
*/
async function fetchMvtTileAsGeoJSON(tileUrl) {
const response = await fetch(tileUrl);
const buffer = await response.arrayBuffer();
const tile = new VectorTile(new Protobuf(buffer));
const features = [];
for (const layerName in tile.layers) {
const layer = tile.layers[layerName];
for (let i = 0; i < layer.length; i++) {
// toGeoJSON 会把瓦片坐标转成经纬度(需要传入瓦片的 z/x/y)
const feature = layer.feature(i).toGeoJSON(
parseInt(tileUrl.match(//(\d+)/\d+/\d+.pbf/)[1]), // z
parseInt(tileUrl.match(//\d+/(\d+)/\d+.pbf/)[1]), // x
parseInt(tileUrl.match(//\d+/\d+/(\d+).pbf/)[1]) // y
);
features.push(feature);
}
}
return { type: 'FeatureCollection', features };
}
// 加载单个瓦片并渲染
async function loadSingleMvtTile(viewer, tileUrl) {
const geojson = await fetchMvtTileAsGeoJSON(tileUrl);
const dataSource = await Cesium.GeoJsonDataSource.load(geojson, {
clampToGround: true,
stroke: Cesium.Color.ORANGE,
strokeWidth: 2,
fill: Cesium.Color.ORANGE.withAlpha(0.3)
});
viewer.dataSources.add(dataSource);
viewer.flyTo(dataSource);
}
局限:手动管理瓦片请求、缓存、LOD 很麻烦,只适合演示或小范围。生产环境建议等 CesiumJS 原生 MVT 支持落地。
即将到来的原生 API(预览,非最终)
根据 Cesium 团队的路线图,未来 MVT 将复用 3D Tiles 的渲染管线,API 形态可能类似:
javascript
运行
// ⚠️ 以下为基于路线图的推测性 API,非当前可用
const vectorTileset = await Cesium.VectorTileset.fromUrl({
url: 'https://server/tiles/style.json', // TileJSON 或 style.json
style: {
layers: [{
id: 'roads',
source: 'openmaptiles',
'source-layer': 'transportation',
type: 'line',
paint: { 'line-color': '#ffa500', 'line-width': 2 }
}]
},
clampToGround: true
});
viewer.scene.primitives.add(vectorTileset);
五、矢量贴合地形:clampToGround 与高度偏移
三维场景中矢量不贴地形会 "飘" 在空中。Cesium 提供了多种贴合方式,适用场景不同:
javascript
运行
/**
* 三种矢量贴地方式对比
*/
function vectorClampingDemo(viewer) {
const positions = Cesium.Cartesian3.fromDegreesArray([
116.0, 39.5,
116.5, 39.8,
117.0, 39.6
]);
// 方式 1:Entity polyline + clampToGround(最简单,适合少量线)
viewer.entities.add({
name: '贴地线 - Entity',
polyline: {
positions: positions,
width: 5,
material: Cesium.Color.RED,
clampToGround: true, // 关键:自动采样地形高度
classificationType: Cesium.ClassificationType.BOTH // 同时贴地形和3D Tiles
}
});
// 方式 2:GroundPrimitive(高性能,适合大量多边形/线贴地)
const groundPolygon = new Cesium.GroundPrimitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(positions),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.BLUE.withAlpha(0.5)
)
}
}),
classificationType: Cesium.ClassificationType.TERRAIN // 只贴地形
});
viewer.scene.primitives.add(groundPolygon);
// 方式 3:Corridor 走廊(带宽度的贴地线,适合道路)
viewer.entities.add({
name: '贴地走廊 - Corridor',
corridor: {
positions: positions,
width: 100, // 单位:米
material: Cesium.Color.YELLOW.withAlpha(0.6),
height: 0,
extrudedHeight: 5 // 向上挤出 5 米,形成"路沿"效果
}
});
}
vectorClampingDemo(viewer);
选型建议:
表格
| 方式 | 适用场景 | 性能 | 拾取 |
|---|---|---|---|
| Entity + clampToGround | 少量线 / 面,需要动态更新 | 一般 | 支持 |
| GroundPrimitive | 大量静态多边形贴地 | 好 | 支持 |
| Corridor | 道路、河流等带宽度的线 | 好 | 支持 |
六、手动构建三维矢量:带真实高程的线
3D Tiles 2.0 矢量的核心卖点之一是原生三维坐标。在当前 API 下,我们可以手动构建带高程的线来模拟这一能力 —— 比如桥梁、隧道、管线。
javascript
运行
/**
* 构建一条带真实高程的三维折线(模拟桥梁/管线)
* 并添加"垂直投影到地面"的辅助线,增强空间感知
*/
function build3DVectorLine(viewer) {
// 一条从山脚到山顶的路径,每个点带真实高程
const path = [
{ lon: 116.20, lat: 39.90, height: 50 }, // 起点:海拔50米
{ lon: 116.25, lat: 39.92, height: 200 },
{ lon: 116.30, lat: 39.94, height: 500 },
{ lon: 116.35, lat: 39.96, height: 1200 }, // 高点
{ lon: 116.40, lat: 39.98, height: 800 },
{ lon: 116.45, lat: 40.00, height: 300 } // 终点
];
// 转为 Cartesian3(带高度)
const positions3D = path.map(p =>
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height)
);
// 主线:三维空间中的线,不贴地
viewer.entities.add({
name: '三维管线',
polyline: {
positions: positions3D,
width: 4,
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.25,
color: Cesium.Color.CYAN
}),
clampToGround: false // 关键:关闭贴地,使用自身高程
}
});
// 每个点的位置标记 + 垂直投影线
for (let i = 0; i < path.length; i++) {
const p = path[i];
// 节点标记
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height),
point: {
pixelSize: 10,
color: Cesium.Color.CYAN,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见,不被遮挡
},
label: {
text: `P${i} ${p.height}m`,
font: '12px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -16),
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
// 垂直投影线:从点的位置垂直到地面(height=0)
viewer.entities.add({
polyline: {
positions: [
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height),
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, 0)
],
width: 1,
material: Cesium.Color.CYAN.withAlpha(0.3),
clampToGround: false
}
});
}
// 飞到合适视角
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(116.32, 39.95, 8000),
orientation: {
pitch: Cesium.Math.toRadians(-30)
}
});
}
build3DVectorLine(viewer);
这段代码展示的正是 3D Tiles 2.0 矢量要标准化的能力:矢量要素自带三维坐标,在真实空间中渲染,而不是被拍平在地球表面。桥梁的裂缝、建筑的标注线、地下管线,都需要这种能力。
七、性能优化清单
最后汇总一份矢量渲染性能优化 checklist,代码中已体现的不再展开:
- 数据量分级:<1 万用 Entity / GeoJsonDataSource;1~10 万考虑 Primitive 合并;> 10 万必须走瓦片化(3D Tiles 或 MVT)。
- 颜色预计算:静态数据的颜色、大小在构建时算好,不要用
CallbackProperty逐帧计算。 - 释放 CPU 几何:
Primitive设置releaseGeometryInstances: true。 - 贴地开销:
clampToGround需要每帧采样地形深度,线越多越慢;静态数据优先用GroundPrimitive。 - 关闭深度测试的代价:
disableDepthTestDistance会让物体永远画在最上层,大量使用时排序开销增大。 - Label / Billboard 合批:Cesium 内部会自动合批,但如果每个 label 用了不同的 font /outline,合批会失效,尽量统一样式。
- 瓦片化是终极方案:无论当前用什么技巧,超大规模矢量的最终归宿都是 3D Tiles 2.0 原生矢量瓦片 —— 这也正是 Cesium 团队在推的方向。