性能与实例化
一万棵树画不动,通常不是三角形太多,而是一万次绘制调用的开销。 把相同几何的重复对象合并成一次绘制,是最直接也最有效的优化。
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)。