Skip to content

Eine offene Welt im Browser erschaffen, Teil 21: Ein schnellerer Renderer, der nicht schneller war

Von Oleg Sidorkin, CTO und Mitgründer von Cinevva

Neu hier? Nutze den Serienleitfaden. Er erklärt, was ein Spike ist, und verlinkt alle Teile.

Teil 20 simulierte Oberflächentiefe auf einem flachen Quad. In diesem Teil geht es um eine Entscheidung zur Renderarchitektur – und um den Spike, bei dem sich die Lehrbuchantwort für unsere Hardware als falsch erwies. Die Frage lautete: Brauchen wir für Alpha-getestetes Gras in filmischer Dichte unter einer Third-Person-Kamera einen Visibility Buffer, bevor wir die Dichte auf 200 Spieler erhöhen? Die gängige Empfehlung lautet nachdrücklich: ja. Wir haben ihn gebaut und vermessen – und die Antwort war nein.

Die Technik, die alle empfehlen

Spike 40 in einem neuen Tab öffnen ↗ · Quellcode ansehen

Ein Visibility Buffer teilt das Rendering in zwei Durchläufe. Durchlauf 1 rastert die Geometrie und schreibt ausschließlich Dreiecks- und Instanz-IDs in ein kompaktes Integer-Target sowie Tiefenwerte – ganz ohne Shading. Durchlauf 2 ist ein Fullscreen-Pass, der an jedem abgedeckten Pixel die IDs liest, die Vertices des jeweiligen Dreiecks erneut abruft, die interpolierten Attribute rekonstruiert und jedes sichtbare Pixel exakt einmal schattiert. Das Versprechen lautet perfekte Overdraw-Unterdrückung: Der Tiefentest läuft für Fragmente, für die noch keine Shading-Arbeit ausgeführt wurde. Das teure Material wird daher nur für das berechnet, was tatsächlich sichtbar ist.

Der Spike führt beide Pfade auf demselben Canvas und demselben Gerät aus, sodass die einzige Variable darin besteht, wo das Shading stattfindet. Der Forward-Pfad verwendet ein normales MeshStandardNodeMaterial über three.js. Der Vis-Buffer-Pfad ist eine rohe, zweistufige WebGPU-Pipeline außerhalb von three.js. Sie liest die Grastextur von three.js direkt aus dem Backend, schreibt in Durchlauf 1 (instanceId, triId) in ein RG32Uint-Target und berechnet die Beleuchtung in Durchlauf 2. Beide verwenden dasselbe Referenz-Setup für die Beleuchtung.

Zwei Implementierungsdetails sind erwähnenswert. WebGPU besitzt in Fragment-Shadern weiterhin kein portables primitive_index-Builtin. Der Trick besteht daher darin, eine Dreiecks-ID pro Vertex in nicht indizierte Geometrie einzubetten und sie mit flat-Interpolation auszulesen. Das verdreifacht zwar die Vertex-Anzahl, ist bei einer Graskarte mit 12 Vertices jedoch vernachlässigbar. Auch die gemeinsame Nutzung des Canvas mit dem Renderer von three.js ist weitgehend problemlos, solange man weder den Kontext neu konfiguriert noch die Canvas-Abmessungen verändert – beides wird von three.js verwaltet. Das Timing des Forward-Pfads war der knifflige Teil, da three.js keinen Hook bereitstellt, um GPU-Timestamp-Queries in seinen Render-Pass einzufügen. Als Behelfslösung wird seine Arbeit von zwei wirkungslosen Timestamp-Passes eingerahmt, die davor und danach eingereicht werden und von der GPU in Einreichungsreihenfolge ausgeführt werden.

Die Zahlen entwickeln sich in die falsche Richtung

Auf einem Mac der M-Serie bei etwa 1080p, mit kreuzförmigen Grashalmen auf einem 80-Meter-Feld:

Bei 50.000 Instanzen gewann der Vis-Buffer-Pfad mit 25 %: 4,13 ms gegenüber 5,51 ms beim Forward-Pfad. Bei 100.000 herrschte Gleichstand. Bei 200.000 Instanzen gewann der Forward-Pfad mit 44 %: 5,44 ms gegenüber 7,80 ms beim Vis-Buffer. Der Vis-Buffer-Pfad wird mit steigender Dichte relativ schlechter. Das ist das genaue Gegenteil der verbreiteten Annahme, laut der er gerade bei starkem Overdraw gewinnen sollte.

Warum sich der Forward-Pfad so gut hält

Apple-Silicon-GPUs sind Tile-Based Deferred Renderer, und das verändert die gesamte Rechnung. Forward Shading auf einem TBDR verfügt über eine Hidden-Surface-Removal-Stufe, die vor dem Fragment-Shader ausgeführt wird: Der Rasterizer sammelt alle Fragmente, die einem Tile zugeordnet sind, sortiert sie nach Tiefe, und nur die nach dem Alpha-Test verbleibenden Fragmente erreichen überhaupt den Fragment-Shader. Der Forward-Pfad erhält damit den Großteil des Visibility-Buffer-Versprechens „jedes Pixel nur einmal schattieren“ kostenlos durch die Hardware. Wenn mehr Halme den Bildschirm füllen, werden vor Beginn des Shadings mehr Fragmente durch HSR verworfen. Dadurch bleibt der effektive Aufwand pro Pixel beim Forward-Pfad ungefähr konstant, anstatt mit dem Overdraw zu wachsen.

Durchlauf 1 des Vis-Buffer-Pfads profitiert vom selben TBDR-Vorteil. Das Problem liegt vollständig in Durchlauf 2. Dieser liest für jedes Pixel die Instanzmatrix aus einem Buffer, der bei 200.000 Instanzen 12,8 MB groß und damit deutlich größer als jeder GPU-Cache ist. Benachbarte Bildschirmpixel gehören gewöhnlich zu unterschiedlichen Grasinstanzen – die Verteilung basiert auf einem zufällig versetzten Raster, sodass benachbarte Halme beliebige Instanz-IDs besitzen. Daher verfehlt jede auf diesen Buffer zugreifende Wave den Cache auf divergente Weise. Dieser inkohärente Direktzugriff kostet allein ungefähr 4 ms pro Frame. Der Forward-Pfad vermeidet ihn vollständig, weil die Instanzmatrix zusammen mit dem Vertex über den instanzspezifischen Attributpfad eintrifft. Wenn der Fragment-Shader ausgeführt wird, liegen die transformierten Vertex-Daten daher bereits in lokalen Registern des Tiles vor, und es ist kein megabytegroßer zufälliger Lesezugriff erforderlich.

Genau diese Kosten soll Nanites Materialklassifizierungs-Pass amortisieren: Pixel werden nach Instanz gruppiert und sortierte Compute-Waves gestartet, damit die Lesezugriffe jeder Wave kohärent sind. Das haben wir nicht. Eine grobe Überschlagsrechnung ergibt, dass eine Sortierung der Pixel nach Instanz diese 4 ms womöglich auf 1,5 bis 2 ms reduzieren und den Schnittpunkt auf 400.000 bis 500.000 Instanzen verschieben würde. Doch damit würden wir weitere Optimierungen auf eine Architektur stapeln, die hier schon grundsätzlich nicht gewinnt.

Das ehrliche Fazit und die Prüfung, die es rechtfertigt

Für Alpha-getestetes, kreuzförmiges Laubwerk mit Apple-Silicon-WebGPU liegt der Forward-Pfad mit der TSL-Pipeline von three.js bereits auf oder unter den Kosten des Vis-Buffers. Die zusätzliche Vis-Buffer-Infrastruktur bringt bis weit über 200.000 Instanzen keinen sichtbaren Vorteil – und selbst dann nur, wenn zusätzlich ein Sortier- oder Binning-Pass implementiert wird. Die praktische Entscheidung für die Produktions-Engine lautet daher, den aus früheren Spikes stammenden Stack aus Forward Rendering, LOD und Impostern beizubehalten. In Vis-Buffer-Infrastruktur investieren wir erst, wenn entweder diskrete NVIDIA- oder AMD-GPUs zum primären Zielsystem werden – wo die Overdraw-Kosten linearer verlaufen – oder wir auf eine Meshlet-Architektur umsteigen, bei der der Vis-Buffer ohnehin die natürliche Ausgabe darstellt.

Weil dieses Ergebnis der Intuition widerspricht, ist die Schlussfolgerung nur dann etwas wert, wenn der Vergleich fair ist. Daher wurde der Spike vollständig geprüft. Dabei kamen mehrere echte Fehler ans Licht und wurden behoben: ein Regler für die Halmgröße, der die beiden Pfade unbemerkt voneinander entkoppelte; die Hälfte der Forward-Halme, die aufgrund antiparalleler Normalen fast schwarz gerendert wurde – behoben mit dem üblichen Trick aufwärts gerichteter Normalen für Laubwerk; und der Vis-Buffer, der wegen eines manuell gewählten Lambert-Faktors anstelle des energieerhaltenden 1/π, eines hartcodierten Umgebungslichtanteils und fehlenden Tone Mappings etwa doppelt so hell las. Die Korrektur übernahm die exakte filmische ACES-Kurve von three.js in WGSL und liest Farben sowie Intensitäten der Lichtquellen in jedem Frame aus den tatsächlichen Szenenlichtern. Die verbleibende bekannte Lücke – fehlende direkte Spiegelreflexionen in Durchlauf 2 – verzerrt den Vergleich zugunsten des Vis-Buffers. Das bedeutet, dass der Forward-Pfad pro Pixel eindeutig mehr Arbeit erledigt und bei hoher Dichte dennoch gewinnt. Dadurch ist die zentrale Schlussfolgerung konservativ und nicht optimistisch. Eine Einschränkung bleibt bestehen: All dies gilt spezifisch für die M-Serie. Auf einer diskreten Grafikkarte könnte sich der Schnittpunkt durchaus umkehren. Deshalb sollte der Test wiederholt werden, bevor wir uns für Nicht-Apple-Zielsysteme auf diesen Stack festlegen.

In diesem Kapitel behandelte Technologien

Visibility-Buffer-Rendering. Durchlauf 1 rastert die Geometrie und schreibt ausschließlich Dreiecks- und Instanz-IDs sowie Tiefenwerte, ohne Shading auszuführen. Durchlauf 2 ist ein Fullscreen-Resolve, das die IDs jedes abgedeckten Pixels liest, das Quelldreieck erneut abruft, perspektivisch korrekte baryzentrische Attribute rekonstruiert und jedes sichtbare Pixel einmal schattiert. Da WebGPU kein portables Fragment-primitive_index besitzt, wird die Dreiecks-ID als flat-interpoliertes Attribut pro Vertex in nicht indizierte Geometrie eingebettet.

TBDR-Hidden-Surface-Removal gegenüber Deferred Resolve. Auf einer Tile-Based Deferred GPU wie Apple Silicon verwirft Forward Shading verdeckte Fragmente bereits vor der Ausführung des Fragment-Shaders. Damit erhält es den Großteil des Visibility-Buffer-Vorteils, jedes Pixel nur einmal zu schattieren, kostenlos. Die Kosten pro Pixel bleiben mit zunehmendem Overdraw ungefähr konstant. Ein Vis-Buffer-Resolve-Pass bezahlt stattdessen für inkohärente zufällige Zugriffe auf einen großen instanzspezifischen Buffer – 12,8 MB bei 200.000 Instanzen. Bei hoher Dichte dominiert dieser Aufwand, sofern die Pixel nicht zuvor nach Instanz sortiert oder gruppiert werden, wie es Nanites Materialklassifizierung tut.

Gemeinsame Nutzung eines Canvas mit dem WebGPURenderer von three.js. Rohe WebGPU-Command-Buffer lassen sich in der gemeinsam genutzten Queue korrekt mit Einreichungen von three.js verschachteln, solange context.configure() nicht erneut aufgerufen und canvas.width/height nicht verändert wird – beides verwaltet der Renderer. Das GPU-Timing des Forward-Pfads, für das three.js keinen Hook bereitstellt, kann durch zwei wirkungslose Timestamp-Render-Passes eingerahmt werden, die vor und nach seinem Render-Aufruf eingereicht werden, da die GPU Command-Buffer in Einreichungsreihenfolge ausführt.

Validierung eines kontraintuitiven Benchmarks. Ein überraschendes Leistungsergebnis ist nur so vertrauenswürdig wie die Fairness des Vergleichs. Erst die Prüfung beider Pfade auf identische Szeneninhalte und identisches Shading – übereinstimmendes ACES-Tone-Mapping, energieerhaltendes Lambert-Modell, aus denselben Objekten gelesene Lichtquellen und identische Halmgröße – verwandelte „der Vis-Buffer ist langsamer“ von einem wahrscheinlichen Messartefakt in eine belastbare Schlussfolgerung. Die einzige verbleibende Asymmetrie verzerrt den Vergleich dabei in die konservative Richtung.


Teil 21 von 29. Zurück: Teil 20 – Simulierte Tiefe auf einer flachen Ebene Weiter: Teil 22 – Wolken, durch die man fliegen kann, und Culling, das sich auszahlt Serienleitfaden: /de/blog/2026-02-25-open-world-browser-series-guide