InstancedMesh змінив усе — з 5 до 60 кадрів на секунду

Рендеринг тисяч однакових об'єктів — найпоширеніша пастка продуктивності в Three.js. Один виклик API — InstancedMesh — вирішує це повністю. З бенчмарками та кодом.

Проблема: виклики малювання (draw calls)

Коли ви створюєте 10 000 об'єктів Mesh у Three.js і додаєте їх на сцену, кожен з них під час рендерингу породжує окремий виклик малювання. Виклик малювання — це команда процесора, надіслана відеокарті, яка каже: прив'яжи цей буфер, застосуй цю матрицю трансформації, намалюй цю геометрію.

Відеокарта здатна обробляти мільйони полігонів за секунду — але вона може обробити лише кілька тисяч змін стану викликів малювання за секунду. Вузьке місце на боці процесора вбиває частоту кадрів задовго до того, як ви вичерпаєте запас геометричної складності.

К-сть об'єктів Метод Виклики малювання FPS
1 000 Mesh ×1000 1 000 55
10 000 Mesh ×10000 10 000 12
100 000 Mesh ×100000 100 000 1
100 000 InstancedMesh 1 60

Рішення: InstancedMesh

InstancedMesh пакує всі трансформації екземплярів в один буфер відеокарти й рендерить усе за один виклик малювання. Кожен екземпляр може мати незалежну позицію, обертання, масштаб і колір — але всі вони спільно використовують одну геометрію та один матеріал.

// До: 10 000 викликів малювання
for (let i = 0; i < 10000; i++) {
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(Math.random() * 100, 0, Math.random() * 100);
  scene.add(mesh);
}

// Після: 1 виклик малювання
const instancedMesh = new THREE.InstancedMesh(geometry, material, 10000);
const dummy = new THREE.Object3D();

for (let i = 0; i < 10000; i++) {
  dummy.position.set(Math.random() * 100, 0, Math.random() * 100);
  dummy.updateMatrix();
  instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);

Оновлення трансформацій екземплярів щокадру

Для анімованих екземплярів оновлюйте матрицю щокадру і позначайте її «брудною»:

function animate() {
  for (let i = 0; i < COUNT; i++) {
    dummy.position.x = positions[i].x + Math.sin(time + i) * 0.1;
    dummy.updateMatrix();
    instancedMesh.setMatrixAt(i, dummy.matrix);
  }
  instancedMesh.instanceMatrix.needsUpdate = true; // критично важливо!
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

Пастка: Якщо забути needsUpdate = true, відеокарта ніколи не побачить нові матриці. Екземпляри виглядатимуть застиглими, навіть якщо ваш цикл JavaScript виконується. Це найпоширеніша помилка при роботі з InstancedMesh.

Колір для кожного екземпляра окремо

const color = new THREE.Color();
instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(
  new Float32Array(COUNT * 3), 3
);

for (let i = 0; i < COUNT; i++) {
  color.setHSL(i / COUNT, 1.0, 0.5);
  instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceColor.needsUpdate = true;

Коли НЕ варто використовувати InstancedMesh