外观
自定义几何体与外观
约 1714 字大约 6 分钟
2026-10-11
本文面向"mars3d 封装的矢量类型满足不了需求"的场景,讲解如何在 mars3d 项目中直接使用 Cesium 原生的 Primitive(图元)体系。如果你只是想画点、线、面、模型,不要从本文开始,先看矢量数据。
1. 什么时候必须用 Primitive
mars3d 的矢量能力分三层,按下表从上往下选,能用上层就不往下层走:
| 方案 | 适用场景 | 动态属性 | 事件/Popup/标绘 | 海量数据 |
|---|---|---|---|---|
| Graphic(Entity 封装,如 PointEntity、ModelEntity) | 绝大多数业务场景,数据驱动 | 支持(Property 体系) | 全套支持 | 数千级以内 |
| 合并渲染类(BoxCombine、PolylineCombine、EllipsoidCombine 等) | 静态的大量同类对象 | 弱 | 图层级 | 万级~十万级 |
| 原生 Primitive | 自定义几何体、自定义着色、多几何体合批、极致性能调优 | 不支持,需手动更新 | 不支持,需自行拾取 | 上限最高 |
典型的"必须下探"场景:
- 需要 Cesium 内置 Graphic 没有覆盖的几何体或自定义顶点数据;
- 数万个静态对象需要合批成一次绘制(先评估合并渲染类是否已够用,如大量盒子(合并渲染 Primitive)、大量多边形面、大量像素点);
- 需要自定义 GLSL 外观、按实例着色;
- 集成第三方 Cesium 扩展中"带
update(frameState, commands)方法"的自定义 Primitive 类。
性能数据与更多优化手段见性能优化与调优。
2. Geometry、Appearance、GeometryInstance 与 Primitive 的关系
Geometry(几何体) 描述"形状":顶点、索引、包围球,与位置无关
└─ GeometryInstance(实例) 给形状赋予 modelMatrix(位置姿态)、逐实例属性(颜色/显示)、id
└─ 一个或多个 Instance 交给 Primitive
Appearance(外观) 描述"怎么画":材质、着色器、闭合/半透明等状态
└─ 与 Geometry 配对要求:顶点格式(vertexFormat)必须匹配
Primitive(图元) 负责把 Instance 编译成 WebGL 指令,管理更新、拾取与销毁要点:
- 配对约束:Geometry 创建时声明的
vertexFormat必须满足 Appearance 的要求。惯例是用 Appearance 自带的常量,例如PerInstanceColorAppearance.VERTEX_FORMAT、MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat、EllipsoidSurfaceAppearance.VERTEX_FORMAT。 - 合批:同一个 Primitive 可以接收一个 GeometryInstance 数组,相同外观的几何体只编译、绘制一次,这是 Primitive 性能优势的主要来源。
- 贴地:贴地形表面用 GroundPrimitive;贴 3DTiles 模型表面用带
classificationType的 ClassificationPrimitive。
3. 引入 Cesium
NPM 环境使用 mars3d 配套的 Cesium 包,不要同时安装 cesium 与 mars3d-cesium(版本对应关系见Cesium版本兼容):
// NPM 方式
import * as Cesium from "mars3d-cesium"
// CDN / script 标签方式
// const Cesium = mars3d.Cesium4. 快速上手:一个原生 Primitive
import * as Cesium from "mars3d-cesium"
// 1. 几何体:高度 1000 米的矩形
const geometryInstance = new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(116.0, 30.0, 116.2, 30.2),
height: 1000,
// 顶点格式与外观配对
vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat
})
})
// 2. 外观:半透明红色
const primitive = new Cesium.Primitive({
geometryInstances: geometryInstance,
appearance: new Cesium.MaterialAppearance({
material: Cesium.Material.fromType("Color", {
color: Cesium.Color.RED.withAlpha(0.5)
})
}),
asynchronous: true // 默认值:true,几何体在 Web Worker 中异步编译
})
// 3. 加入场景(map.scene.primitives 是 Cesium.PrimitiveCollection)
map.scene.primitives.add(primitive)
// 4. 移除:remove 默认会调用 primitive.destroy() 释放显存
map.scene.primitives.remove(primitive)贴地形表面的版本,换成 GroundPrimitive 即可(外观有默认值):
const groundPrimitive = new Cesium.GroundPrimitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(116.0, 30.0, 116.2, 30.2),
vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
})
})
})
map.scene.primitives.add(groundPrimitive)5. 批量实例化:一次绘制成千上万个对象
把相同形状、不同位置/颜色的几何体放进同一个 Primitive:
function createBoxInstance(lng, lat, alt, color) {
const h = 50 // 盒子半边长
return new Cesium.GeometryInstance({
geometry: new Cesium.BoxGeometry({
minimum: new Cesium.Cartesian3(-h, -h, -h),
maximum: new Cesium.Cartesian3(h, h, h),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
// 每个实例自己的位置姿态
modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(lng, lat, alt)),
// 每个实例自己的颜色(逐实例属性)
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.toValue(color)
},
// 业务标识,拾取时原样返回
id: { lng, lat, name: "业务ID" }
})
}
const primitive = new Cesium.Primitive({
geometryInstances: points.map((p) => createBoxInstance(p.lng, p.lat, p.alt, p.color)),
appearance: new Cesium.PerInstanceColorAppearance({
closed: true, // 盒子是封闭体,关闭背面剔除优化
flat: true
})
})
map.scene.primitives.add(primitive)注意:批量 Primitive 是"静态合批",位置增删需要重建 Primitive;频繁变化的动态对象不适合这种方式。
6. 拾取:原生 Primitive 与 mars3d 事件体系的差异
原生 Primitive 不会触发 GraphicLayer / Graphic 上的 click 等事件,需要在 map 事件中自行拾取判断:
map.on(mars3d.EventType.click, (event) => {
const picked = map.scene.pick(event.windowPosition)
// 命中我们创建的 Primitive 时,picked.primitive 就是它,picked.id 是实例的 id
if (Cesium.defined(picked) && picked.primitive === primitive) {
console.log("点中了实例", picked.id) // { lng, lat, name: '业务ID' }
}
})拾取坐标(scene.pickPosition、globe.pick 等)的选型见事件机制 - 主动拾取。
7. 更新
Primitive 不支持 Entity 那套 CallbackProperty 动态属性,更新分两种情况:
// 1. 逐实例属性(颜色、显示隐藏)可直接修改,无需重建
const attributes = primitive.getGeometryInstanceAttributes(instanceId)
attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.BLUE.withAlpha(0.8))
// 2. 材质 uniform(如颜色、透明度)实时生效
primitive.appearance.material.uniforms.color = Cesium.Color.YELLOW
// 3. 形状、位置、数量变化:移除旧 Primitive,重新创建8. 在 mars3d 中托管原生 Primitive 的三种方式
| 方式 | 做法 | 适用 |
|---|---|---|
| 直接挂载 | map.scene.primitives.add/remove 自行管理(第 4 节) | 一次性、与业务图层无关的图元 |
| 图层 + 自定义 Graphic 托管 | 继承 mars3d 的 Graphic 基类,在钩子里操作 this.primitiveCollection | 需要随图层 add/remove/show、销毁自动联动 |
| 注册为矢量类型 | GraphicUtil.register 注册后像内置 Graphic 一样 new | 需要在项目内复用、支持 JSON 配置 |
图层托管时,基类提供的 this.primitiveCollection 就是 Cesium.PrimitiveCollection,在 _addedHook 中加入、_removedHook 中移除即可:
class CamberRadar extends mars3d.graphic.BasePointPrimitive {
_addedHook() {
// CamberRadarPrimitive 是按 Cesium 规范定义的、带 update 方法的自定义 Primitive 类
this._primitive = this.primitiveCollection.add(
new CamberRadarPrimitive({
id: this.id,
modelMatrix: this.modelMatrix
})
)
}
_removedHook() {
if (this._primitive) {
this.primitiveCollection.remove(this._primitive)
delete this._primitive
}
}
}完整的自定义矢量注册流程、材质注册见自定义矢量对象与自定义材质。
9. 常见坑
- 顶点格式不匹配:外观渲染异常或全黑时,先检查 Geometry 的
vertexFormat是否取自所用 Appearance 的推荐格式。 - 封闭体要设
closed: true:盒子、椭球等封闭几何体配合PerInstanceColorAppearance({ closed: true }),否则背面被剔除。 - 线宽:WebGL 在多数 Windows 平台只支持 1px 线宽,需要粗线用
PolylineVolume或贴图线,这是平台限制。 - 内存释放:从
map.scene.primitives移除即销毁;自建 PrimitiveCollection 或手动remove(p, false)时,记得自行destroy()。 - 不要混用两个 Cesium:
cesium与mars3d-cesium同时存在会产生两套instanceof判定,几何体被判定为非预期类型。 - 贴地材质受限:GroundPrimitive 支持的材质类型少于普通 Primitive,自定义材质不生效时先确认该材质是否支持贴地图元。
