InstancedMesh zmienił wszystko — z 5 FPS do 60 FPS

Renderowanie tysięcy identycznych obiektów to najczęstsza pułapka wydajnościowa w Three.js. Jedno wywołanie API — InstancedMesh — rozwiązuje ten problem całkowicie. W środku benchmarki i kod.

Problem: wywołania rysowania

Kiedy tworzysz 10 000 obiektów Mesh w Three.js i dodajesz je do sceny, każdy z nich generuje przy renderowaniu osobne wywołanie rysowania (draw call). Wywołanie rysowania to polecenie CPU wysyłane do GPU, które mówi: podepnij ten bufor, zastosuj tę macierz transformacji, narysuj tę geometrię.

GPU potrafi przetwarzać miliony poligonów na sekundę — ale jest w stanie obsłużyć jedynie kilka tysięcy zmian stanu wywołań rysowania na sekundę. Wąskie gardło CPU zabija liczbę klatek na długo zanim zabraknie ci złożoności geometrycznej.

Liczba obiektów Metoda Wywołania rysowania 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

Rozwiązanie: InstancedMesh

InstancedMesh pakuje wszystkie transformacje instancji do jednego bufora GPU i renderuje wszystko w jednym wywołaniu rysowania. Każda instancja może mieć niezależną pozycję, rotację, skalę i kolor — ale wszystkie dzielą jedną geometrię i jeden materiał.

// Przed: 10 000 wywołań rysowania
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);
}

// Po: 1 wywołanie rysowania
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);

Aktualizacja transformacji instancji w każdej klatce

Dla animowanych instancji aktualizuj macierz w każdej klatce i oznacz ją jako „brudną":

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; // kluczowe!
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

Pułapka: zapomnienie o needsUpdate = true oznacza, że GPU nigdy nie zobaczy nowych macierzy. Instancje będą wyglądać na zamrożone, mimo że pętla JavaScript działa. To najczęstszy błąd związany z InstancedMesh.

Kolor na instancję

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;

Kiedy NIE używać InstancedMesh