经纬志
首页 / three.js / 教程

性能与实例化

先看结论

一万棵树画不动,通常不是三角形太多,而是一万次绘制调用的开销。 把相同几何的重复对象合并成一次绘制,是最直接也最有效的优化。

最后验证 2026-11-03 · three.js 0.160.0 · 难度:进阶 · 核验 2026-10-29 · 引用本文

InstancedMesh:一次绘制 N 个

InstancedMesh 共享一份几何与材质,每个实例只携带一个变换矩阵。 构造时确定实例数量,之后用 setMatrixAt 逐个摆放。

const COUNT = 8000;

const trunk = new THREE.CylinderGeometry(0.6, 0.9, 12, 6);
const leaf = new THREE.SphereGeometry(3.4, 8, 6);

const trunkMesh = new THREE.InstancedMesh(trunk, trunkMaterial, COUNT);
const leafMesh = new THREE.InstancedMesh(leaf, leafMaterial, COUNT);

const dummy = new THREE.Object3D();
const color = new THREE.Color();

for (let i = 0; i < COUNT; i++) {
  const x = (Math.random() - 0.5) * 1600;
  const z = (Math.random() - 0.5) * 1600;
  const s = 0.7 + Math.random() * 0.6;

  dummy.position.set(x, 6 * s, z);
  dummy.rotation.y = Math.random() * Math.PI * 2;
  dummy.scale.set(s, s, s);
  dummy.updateMatrix();
  trunkMesh.setMatrixAt(i, dummy.matrix);

  dummy.position.y = 12 * s + 2.4 * s;
  dummy.scale.setScalar(s);
  dummy.updateMatrix();
  leafMesh.setMatrixAt(i, dummy.matrix);

  color.setHSL(0.28 + Math.random() * 0.08, 0.45, 0.32 + Math.random() * 0.12);
  leafMesh.setColorAt(i, color);
}

trunkMesh.instanceMatrix.needsUpdate = true;
leafMesh.instanceMatrix.needsUpdate = true;
leafMesh.instanceColor.needsUpdate = true;

scene.add(trunkMesh, leafMesh);

改完矩阵必须设置 instanceMatrix.needsUpdate = true, 否则 GPU 上仍是初始的全零矩阵,所有实例叠在原点。 instanceColor 同理,且只有调用过 setColorAt 后才存在。

实例数量在构造时固定。超出的 setMatrixAt 会写入越界位置而不报错。需要动态增减时,按最大值分配,用 mesh.count 控制实际绘制数量。

合并几何:不同形状也能一次画

形状不重复的静态内容(建筑群、栏杆、道路段),可以用 BufferGeometryUtils.mergeGeometries 拼成一份几何。 合并前要把各自的变换烘焙进顶点。

import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const parts = [];

buildings.forEach(function (b) {
  const geo = new THREE.BoxGeometry(b.w, b.h, b.d);
  geo.translate(b.x, b.h / 2, b.z);
  geo.deleteAttribute('uv');          // 属性不一致会导致合并失败
  parts.push(geo);
});

const merged = mergeGeometries(parts, false);
const mesh = new THREE.Mesh(merged, new THREE.MeshStandardMaterial({
  color: 0x9aa7b4,
  roughness: 0.9
}));

scene.add(mesh);

所有参与合并的几何必须有一致的属性集合。带 uv 的和不带 uv 的混在一起会返回 null,控制台只有一条警告,容易被忽略。

手段减少的开销适用限制
InstancedMesh绘制调用从 N 降到 1同几何、大量重复每个实例只能改矩阵与颜色
几何合并绘制调用从 N 降到 1静态、不同形状合并后不能单独移动
LOD远处用低面数模型建筑、设备、树木需要准备多级模型
视锥剔除跳过视野外对象分散的大场景依赖包围球准确
纹理图集减少材质切换多种小贴图需要重排 UV

远近分级

LOD 按相机距离切换不同精度的模型,适合形状不重复但数量多的对象。 远处用几段面片代替完整模型,能显著降低顶点处理压力。

const lod = new THREE.LOD();

lod.addLevel(new THREE.Mesh(highDetail, detailMaterial), 0);
lod.addLevel(new THREE.Mesh(midDetail, detailMaterial), 120);
lod.addLevel(new THREE.Mesh(lowDetail, detailMaterial), 400);

lod.position.set(260, 0, 180);
scene.add(lod);

// 每帧更新层级,渲染器不会自动根据距离切换
renderer.setAnimationLoop(function () {
  lod.update(camera);
  renderer.render(scene, camera);
});

注意事项

  • InstancedMesh 的拾取需要遍历实例,数量大时先关闭 raycast,改用空间索引筛选。
  • 实例矩阵是 16 个浮点数,八万个实例约占 5 MB 显存,规模再大要考虑分块加载。
  • 合并几何后包围球会覆盖整个范围,视锥剔除的效果变差,可按区块分组合并。
  • LOD.update 每帧都要调用,放在渲染循环内而不是事件回调里。
  • 实例化对象与非实例化对象不能共用同一个材质实例时,注意分别 dispose。

先看绘制调用数。打开 renderer.info.render.calls, 这个值远大于场景中对象数量时,合并或实例化才有意义; 数值不高但帧率低,瓶颈可能在着色器或填充率。

引用本文

经纬志(2026)。《性能与实例化》。https://www.jingweizhi.com/three/tutorial/instancing.html(访问日期 2026-11-03)。