经纬志
首页 / Cesium / 教程

图元与批量渲染

先看结论

Entity 写起来顺手,但每个实体都是一次独立的绘制准备。 上万个构件时,要把它们合并成一次 GPU 调用,这就是 Primitive 与 GeometryInstance 的用途。

最后验证 2026-11-03 · CesiumJS 1.121 · 难度:进阶 · 核验 2026-10-29 · 引用本文

两套 API 的边界

维度Entity APIPrimitive 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)。