图元与批量渲染
Entity 写起来顺手,但每个实体都是一次独立的绘制准备。 上万个构件时,要把它们合并成一次 GPU 调用,这就是 Primitive 与 GeometryInstance 的用途。
两套 API 的边界
| 维度 | Entity API | Primitive API |
|---|---|---|
| 写法 | 声明式,属性可以是常量或随时间变化的 Property | 命令式,直接给几何、外观、矩阵 |
| 数量级 | 几千个以内较舒适 | 十万级几何体合批 |
| 时间动画 | 原生支持 | 需要手动更新 modelMatrix |
| 拾取 | 自动,返回 Entity | 需通过 id 手动关联业务对象 |
| 灵活性 | 受内置图形类型限制 | 可自定义几何、着色器、外观 |
两者可以混用。业务交互走 Entity,海量静态构件走 Primitive,
各自加到 viewer.entities 与 viewer.scene.primitives。
把一万个盒子合成一个图元
合批的关键是 GeometryInstance:共用同一份几何数据,
用 modelMatrix 摆到不同位置,用 attributes 给不同颜色。
一个 Primitive 内部的所有实例只产生一次绘制调用。
var origin = Cesium.Cartesian3.fromDegrees(116.391, 39.907, 0);
var enu = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
var box = Cesium.BoxGeometry.fromDimensions({
dimensions: new Cesium.Cartesian3(18, 18, 30),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
});
var instances = [];
for (var i = 0; i < 40; i++) {
for (var j = 0; j < 40; j++) {
var offset = new Cesium.Cartesian3(i * 24 - 480, j * 24 - 480, 15);
instances.push(new Cesium.GeometryInstance({
geometry: box,
modelMatrix: Cesium.Matrix4.multiplyByTranslation(enu, offset, new Cesium.Matrix4()),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.fromHsl((i + j) / 80, 0.55, 0.55, 0.9)
)
},
id: { row: i, col: j }
}));
}
}
var primitive = new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: false
}),
asynchronous: false,
allowPicking: true
});
viewer.scene.primitives.add(primitive);
asynchronous: false 让几何在主线程同步创建,首帧会有一次明显停顿,
但不会出现「添加后几秒才显示」的现象。数据量大时保留默认的异步创建,
并用 primitive.readyPromise 处理加载完成后的逻辑。
几何必须共用。在循环里 new Cesium.BoxGeometry(...)
会让合批失效,内存也会成倍增长。所有实例的 geometry
应该指向同一个对象。
点、广告牌与标签的集合
点类内容不需要走 GeometryInstance,分别有专用的集合类型。 集合本身是一个图元,内部按批次管理,比逐个 Entity 轻得多。
var points = new Cesium.PointPrimitiveCollection();
for (var i = 0; i < 5000; i++) {
points.add({
position: Cesium.Cartesian3.fromDegrees(
116.20 + Math.random() * 0.40,
39.80 + Math.random() * 0.24,
Math.random() * 120
),
color: Cesium.Color.fromCssColorString('#4d9bff').withAlpha(0.85),
pixelSize: 4 + Math.random() * 4
});
}
viewer.scene.primitives.add(points);
var billboards = new Cesium.BillboardCollection({ scene: viewer.scene });
billboards.add({
position: Cesium.Cartesian3.fromDegrees(116.391, 39.907, 60),
image: '/images/facility.png',
scale: 0.6,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
scaleByDistance: new Cesium.NearFarScalar(800, 1.0, 60000, 0.35),
id: { code: 'BJ-001' }
});
viewer.scene.primitives.add(billboards);
BillboardCollection 构造时传 scene 才能使用贴地、
距离缩放这类依赖场景的功能。不传也能画,但部分特性静默失效。
动态更新只改属性
Primitive 创建后不能换几何,但可以改实例属性。大量位置变化时,
逐个改 modelMatrix 不如重建一次图元快,取决于更新比例。
// 少量实例变色:直接改属性
var attr = new Cesium.ColorGeometryInstanceAttribute(1.0, 0.3, 0.2, 0.9);
primitive.getGeometryInstanceAttributes({ row: 5, col: 5 }).color = attr;
// 全部实例移动:销毁重建,避免逐实例矩阵写入
viewer.scene.primitives.remove(primitive);
primitive.destroy();
primitive = new Cesium.Primitive({
geometryInstances: rebuildInstances(newOffset),
appearance: new Cesium.PerInstanceColorAppearance({ flat: true })
});
viewer.scene.primitives.add(primitive);
注意事项
- Primitive 的
id在拾取时通过picked.id取回,不会自动变成 Entity。 - 合批要求所有实例使用相同的
vertexFormat与外观,混用颜色与法线会拆批。 - 关闭
allowPicking能省掉拾取缓冲的额外渲染,纯展示场景建议关闭。 - 删除图元要同时从
scene.primitives移除并调用destroy(),只移除不销毁会泄漏显存。 - 半透明实例在合批中按实例排序,重叠时仍可能出现前后关系错误,必要时拆成两个图元分别控制。
先量化再切换。把 Entity 换成 Primitive 的收益与数量强相关。
先用 viewer.scene.primitives.length 与帧时间确认瓶颈在绘制调用,
再动手改写,否则只是增加了维护成本。
经纬志(2026)。《图元与批量渲染》。https://www.jingweizhi.com/cesium/tutorial/primitives.html(访问日期 2026-11-03)。