Performanceoptimierung Three.js

Grundsätzliche Probleme beim Darstellen vieler Objekte in Three.js

Um eine Three.js Szene flüssig auf dem Bildschirm anzuzeigen, müssen kontinuierlich neue Frames berechnet werden (am besten mindestens 50-60 Frames pro Sekunde). Ein Teil des Rechenaufwands wird dabei durch die CPU getragen. Der restliche Teil übernimmt die GPU. Damit die GPU weiß, was sie berechnen soll, sendet die CPU für die Berechnung eines einzelnen Frames zahlreiche Grafikbehle (sog. Draw-Calls) an die GPU.

In Three.js ist die Zahl der Draw-Calls pro Frame gleich der Menge an Geometrien, die der derzeit betrachtete Ausschnitt der Szene beinhaltet. Da jeder Draw-Call vergleichsweise viel Overhead mit sich bringt, ist die Kommunikation zwischen CPU und GPU sehr ineffizient. Genau diese ineffiziente Kommunikation ist verantwortlich dafür, dass komplexe Szenen mit mehreren hunderttausend Geometrien/Draw-Calls in Three.js oft so schlecht laufen.

Lösungsansätze

Dank einem ambitionierten Three.js User gibt es seit längerem die Mesh-Klasse „instancedMesh„. Mithilfe dieser lässt sich die Effizienz der Draw-Calls drastisch verbessern, wenn man in einer Szene sehr oft dasselbe Element darstellt -> z.B. ein Wald mit 100k identischen Bäumen.

Kurzversion der Erklärung:

  • Bei der Verwendung normaler Meshes muss die CPU der GPU 100k-mal via eines Draw-Calls sagen „bitte berechne mir die Shader-Informationen für die Kombination aus dieser Baumgeometrie und diesem Material“.
    • Das Einzige, was sich ziwschen Draw-Calls der einzelnen Bäumen unterscheiden würde. wäre die Position, an der der Baum im Endeffekt stehen soll. Dementsprechend gibt es viel redundante Informationen, die in allen 100k Draw-Calls gleich wären.
-> for (var i = 0; i++; i<100000){
let baumMesh = new THREE.Mesh( baumGeometry, baumMaterial );
}
//erstellt 100000 Bäume und erzeugt dabei 100000 Draw-Calls
  • Bei der Verwendung von instancedMeshs sagt die CPU der GPU stattdessen: „bitte berechne mir 100k-mal die Shader-Informationen für die Kombination aus dieser Baumgeometrie und diesem Material“. Das hat den Vorteil, dass die Infos über die Geometrie und das Material nur ein einziges Mal zur GPU gesendet wird.

    Beim verwenden von instanced Meshes muss man Eigenschaften, die einzelne Instanzen individuell haben sollen, mithilfe der Methoden setMatrixAt() (Transformation im 3D-Raum) und setColorAt() (Farbe) festlegen.

    // erstellt 100.000 Bäume und erzeugt dabei nur einen Draw-Call
    -> let baumMesh = new THREE.InstancedMesh( baumGeometry, baumMaterial, 100000);
    // ändert die Position des 42-ten Baumes gemäß der übergebenen Matrix

    -> baumMesh.setMatrixAt(42, <4x4-Transformationsmatrix>);

    Leider bin ich noch nicht 100% dahinter gestiegen, wie man andere Eigenschaften als die Farbe und die 3D-Transformation für einzelne Instanzen hinterlegen kann. Eine „unschöne“ Lösung wäre beispielsweise die individuellen Eigenschaften einer Instanz in der Transformations-Matrix als kleine numerische Werte zu speichern. Zum Beispiel: Das Alter eines Baums durch 10.000 dividieren und in einem unbenutzten Feld der 4×4 Transformations-Matrix via setMatrixAt() abspeichern und bei Bedarf via getMatrixAt() abrufen.

Den ausführlichen technischen Hintergrund gibts zum Beispiel hier.. nur

Ist nicht wirklich sinnvoll:

  • Beide Maßnahmen bringen wenig , wenn die FPS-Werte von vornherein sehr niedrig sind (<10 FPS ).
  • Verschlechtern das Aussehen der Szene deutlich
  • Wären mglw. eine Notlösung bei Three.js Szenen, die trotz des Einsatzes von allen anderen Mitteln immernoch schlecht laufen.

Das Zusammenfügen/Mergen von Geometrien vor dem Erstellen eines Meshes würde je nach Anwendungfall ähnlich viel bringen, wie das Verwenden von Instanced Meshes. Nach ausgiebiger Recherche konnte ich jedoch keinen Grund finden merged Geometries anstatt von instanced Meshes zu verwenden.

Wenn die Objekte einer Szene mgwl. so viele verschiedene Geometriearten (Sphere, Triangle etc.) haben, dass sich instancedMeshes für jede Gruppe einer Geometrieart nicht lohnen würde, könnte es vielleich sinnvoll sein, eine merged Geometrie zu nutzen…

Bringt je nach verbauter Hardware in einem Rechner unterschiedlich viel. Liegt eine eingermaßen performante Grafikkarte vor, ist der Zugewinn an Reaktionfreudigkeit einer komplexen Three.js-Szene immens. Wie man die Nutzung der dedizierten GPU in Chrome ermöglich zeigen wir hier.

Das nachträgliche Laden von Elementen in Three.js (z.B. erst nötige Meshes erstellen, wenn das Anzeigen der Elemente über ein Toggle ausgewählt wurde) sparte während unseres Testens lediglich einen Hauch Ladezeit. Der Wechsel von „normalen“ Meshes zu Instanced Meshes wiederum sparte im Vergleich dazu um einiges mehr an Ladezeit:

-2 Sekunden durch „On-demand“-Laden vs -25 Sekunden durch Instanced Meshes bei einer Szene mit etwa 400k Draw-Calls und einer ursprünglichen Ladezeit von fast einer Minute.

Bringt lediglich eventuell weniger Codezeilen beim Programmieren doch für die Performance macht es keinen Unterschied, wie hier zu lesen ist.