Skip to content

Eine offene Welt im Browser entwickeln, Teil 22: Beleuchtbare Wolken und Culling, das gefüttert werden muss

Von Oleg Sidorkin, CTO und Mitgründer von Cinevva

Neu hier? Nutze den Serienleitfaden. Dort wird erklärt, was ein Spike ist, und du findest Links zu allen Teilen.

In Teil 21 ging es um eine Rendering-Technik, die sich nicht ausgezahlt hat. Dieser Teil behandelt eine, bei der es sich gelohnt hat, und eine weitere, die erst nach einer sorgfältigen Korrektur überhaupt funktionierte. Spike 43 widmet sich dem Himmel: einer physikalisch basierten Atmosphäre und volumetrischen Wolken – der Grundlage, durch die eine Szene wie ein echter Ort und nicht wie eine Tech-Demo wirkt. Spike 44 behandelt Meshlet-artiges GPU-Culling. Die wichtigste Erkenntnis dabei war, dass der Occlusion-Test nur so gut ist wie die Occluder, mit denen man ihn füttert.

Ein Himmel aus Physik statt aus einem Farbverlauf

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

Fast jeder filmreife Wettereffekt hängt von zwei grundlegenden Komponenten ab: einer physikalisch basierten Atmosphäre, damit sich die Farben von Himmel und Sonne entsprechend der Tageszeit aus der Physik ergeben und nicht aus einem manuell abgestimmten Farbverlauf, und einem volumetrischen Wolkenvolumen, damit der Himmel eine 3D-Struktur statt einer vorgebackenen Cubemap erhält. Spike 43 baut auf dem bestehenden WebGPU- und TSL-Stack genau dieses Paar auf – nicht mehr –, denn sobald diese beiden Komponenten vorhanden sind, wird der Rest des Wetter-Stacks (Nebel, Lichtstrahlen, nasse Oberflächen, Schnee) zu einer Reihe kleinerer, bereits bekannter Folgeaufgaben.

Für die Atmosphäre kommt das Hillaire-Modell von 2020 zum Einsatz, das aus einer Reihe von Lookup-Tabellen besteht, die in WGSL-Compute-Shadern berechnet werden. Eine Transmittanztabelle integriert das Sonnenlicht durch die Dichteprofile von Rayleigh-Streuung, Mie-Streuung und Ozon und wird nur neu berechnet, wenn sich die Sonne bewegt. Eine Sky-View-Tabelle wird in jedem Frame neu gebacken, weil das günstig genug ist, dass sich eine bedingte Aktualisierung im Code nicht lohnt. Rund um den Horizont nutzt sie eine nichtlineare Parametrisierung, um Banding zu vermeiden. Für Mehrfachstreuung verwenden wir vorerst eine analytische Näherung statt der korrekten Tabelle. Die Sonnenuntergänge wirken trotzdem überzeugend, sodass sich diese Abkürzung gut versteckt. Die Wolken basieren auf einem Ray-March nach dem Schneider-Nubis-Prinzip durch eine horizontale Schicht. Ihre Form entsteht aus einer 128³-Perlin-Worley-Textur, die durch eine 32³-Worley-Textur erodiert wird. Beide werden beim Start per Compute gebacken, ohne Netzwerkabruf. Die Beleuchtung nutzt Extinktion nach dem Beerschen Gesetz, eine zweilappige Phasenfunktion und eine Powder-Näherung. Die entscheidende Kopplung besteht darin, dass die Sonnenfarbe der Wolken bei jedem Schritt aus derselben Transmittanztabelle abgetastet wird. Dadurch folgt die Wolkenbeleuchtung dem Sonnenuntergang, ohne dass ein zweiter Abstimmungsdurchgang nötig ist.

Auf einem M1 benötigt das gesamte System bei Wolken in halber Auflösung 1,1 bis 2,0 ms und bleibt damit deutlich unter dem Budget von 6 ms. Es belegt etwa 14 MB GPU-Speicher und läuft mit mehr als 100 FPS. Die beiden Kernthesen haben sich in der Praxis bestätigt. Der Sonnenuntergang ist die Vorzeigesequenz – der Moment, der den Renderer filmreif wirken lässt – und ergibt sich ohne tageszeitabhängige Feinabstimmung direkt aus der Physik. Dazu kam ein glücklicher Zufall: Wenn die Wolkenschicht zwischen 800 m und 4000 m parametrisiert wird, erscheinen entfernte Wolken am Horizont aus einer niedrigen Kameraperspektive wie dunkle Bergkämme. So erhält die Welt Hintergrundlandschaft, ohne dass jemand Hintergrundlandschaft modellieren muss.

Eine erwähnenswerte Architekturentscheidung: Der naheliegende Ansatz besteht darin, zuerst den Himmel in die Swap Chain zu zeichnen und anschließend three.js mit autoClear = false die Geometrie darüber rendern zu lassen. Mit dem WebGPU-Renderer in r184 funktioniert das jedoch nicht, weil dieses Flag den Color-Load-Op nicht so steuert wie in WebGL. Dadurch überschreibt three.js den Himmel in jedem Frame. Die Lösung besteht darin, three.js in ein Offscreen-Target zu rendern und das finale Compositing (mix(skyCloud, scene, scene.alpha), danach ACES und anschließend sRGB) in einem eigenen Pass auszuführen, der die Swap Chain kontrolliert.

Culling, das nur so gut ist wie seine Occluder

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

Spike 44 vergleicht vier Rendering-Modi miteinander: einfaches Forward-Rendering, CPU-Cluster-Culling, GPU-Compute-Culling und einen Visibility Buffer mit Hi-Z-Occlusion-Culling. Der Hi-Z-Pfad ist der interessante Teil – und er enthielt einen unauffälligen Fehler: Das HUD zeigte an, dass Occlusion aktiviert war, doch der Zähler „Von Hi-Z verworfen“ blieb dauerhaft bei exakt 0,0 %. Frustum-Culling funktionierte, also war der vorgelagerte Teil des Culling-Shaders in Ordnung. Der Occlusion-Teil war wirkungslos, verursachte aber trotzdem die vollen Kosten.

Ein Hi-Z-Occlusion-Test projiziert die Bounding Box eines Clusters auf den Bildschirm, wählt eine Mip-Stufe der Tiefenpyramide so aus, dass das Bildschirmrechteck ungefähr 2×2 Texel groß ist, tastet die Tiefe des am weitesten entfernten Occluders in diesem Rechteck ab und verwirft den Cluster, wenn selbst sein nächster Punkt noch weiter entfernt ist als dieser Occluder. Die Tiefenpyramide wird in jedem Frame aufgebaut, indem Mip 0 aus einem Opaque-Depth-Pre-Pass befüllt und anschließend nach oben per Max-Reduktion verkleinert wird. Der Opaque-Pre-Pass enthält bewusst nur massive Occluder – den Boden und Proxy-Geometrie für die einzelnen Baumstämme –, weil Alpha-getestetes Blattwerk Lücken erzeugen würde, die eine Max-Reduktion täuschen.

Der Fehler war geometrischer, nicht logischer Natur. Der Stamm-Proxy war ein Quader mit den Maßen 0,5 m × 4 m × 0,5 m. Aus 30 m Entfernung nimmt er auf dem Bildschirm ungefähr 17 Pixel ein. Ein typischer 50-m-Grascluster wählt jedoch Mip 5 aus, wo jedes Texel 32 Quellpixel abdeckt. Ein 17 Pixel breiter Stamm bedeckt kein einziges Mip-5-Texel vollständig. Daher berührt jedes Texel, das den Stamm enthält, zugleich den umliegenden Boden. Schon die erste 2×2-Max-Reduktion wählt den größeren Tiefenwert – also den weiter entfernten Boden hinter dem Stamm – und die Tiefe des Stamms wird bei der ersten Reduktion gelöscht. In Mip 5 enthält die Pyramide dadurch fast überall die Bodentiefe. Der Cluster ist nie weiter entfernt als der Boden, und folglich wird nie etwas verdeckt.

Die Lösung besteht darin, den Proxy groß genug zu machen, um die von ihm belegten Texel zu dominieren, und seine Größe an der Silhouette des Baums statt an seinem Holz auszurichten. Ein ungefähr 2 m × 6 m × 2 m großer Proxy ist noch immer kleiner als die tatsächliche Baumkrone, sodass durch Lücken sichtbare Blätter nicht übermäßig ausgeblendet werden. Gleichzeitig ist er groß genug, um die Max-Reduktion bis zu den relevanten Entfernungen zu überstehen. Der Occlusion-Zähler stieg sofort über null. Daraus ergibt sich eine allgemeine Regel für die Produktions-Engine: Alles, was als Hi-Z-Occluder dienen soll, muss entsprechend seiner Bildschirmsilhouette dimensioniert werden. Die Effektivität von Hi-Z in offenen Szenen mit Vegetation wird nämlich stärker von der Occluder-Abdeckung auf der relevanten Mip-Stufe bestimmt als von der Eleganz der Tiefentest-Mathematik. Gras-gegen-Gras-Occlusion kann ohnehin nicht greifen, weil ein Grashalm dieselbe Tiefe wie der Boden direkt unter ihm hat. Die echten Gewinne entstehen daher, wenn Bäume weiter entfernte Vegetation oder andere Bäume verdecken.

In diesem Kapitel erwähnte Technologien

Hillaire-2020-Atmosphären-LUTs. Eine Transmittanztabelle – für Sonnenlicht durch die Profile von Rayleigh-Streuung, Mie-Streuung und Ozon –, die nur bei einer Bewegung der Sonne neu berechnet wird, liefert zusammen mit einer pro Frame erzeugten Sky-View-Tabelle mit nichtlinearer Horizontparametrisierung physikalisch basierte Himmels- und Sonnenfarben, die der Tageszeit ohne manuell abgestimmten Farbverlauf folgen. Eine analytische Näherung für Mehrfachstreuung ersetzt die vollständige Tabelle, bis ein sichtbares Artefakt das korrekte Backen erforderlich macht. Der Sonnenuntergang ergibt sich ohne tageszeitabhängige Feinabstimmung direkt aus der Physik.

Volumetrische Wolken nach Schneider Nubis. Ein Ray-March durch eine horizontale Schicht, geformt durch eine beim Start gebackene 128³-Perlin-Worley-Textur, die von einer 32³-Worley-Textur erodiert wird. Die Beleuchtung nutzt Extinktion nach dem Beerschen Gesetz, eine zweilappige Phasenfunktion und einen Powder-Term. Wird die Sonnenfarbe der Wolken bei jedem Schritt aus derselben Transmittanztabelle abgetastet, folgt die Wolkenbeleuchtung Sonnenauf- und -untergang ohne zusätzliche Kosten. Ray-Marching in halber Auflösung ist ungefähr viermal günstiger als in voller Auflösung, ohne bei typischen Entfernungen sichtbar an Qualität zu verlieren – der übliche Kompromiss für den Produktionseinsatz.

Compositing von rohem WebGPU mit three.js in r184. Den Himmel in die Swap Chain zu zeichnen und die three.js-Geometrie mit autoClear = false darüber zu rendern, funktioniert nicht, weil dieses Flag den Color-Load-Op im WebGPU-Backend nicht steuert. Stattdessen wird three.js in ein Offscreen-RGBA16F-Target gerendert. Das abschließende mix, Tone-Mapping und die sRGB-Konvertierung erfolgen in einem Pass, der die Swap Chain kontrolliert.

Hi-Z-Occlusion-Culling und Dimensionierung der Occluder. Eine durch Max-Reduktion aufgebaute Tiefenpyramide ermöglicht es einem GPU-Culling-Pass, Cluster zu verwerfen, deren nächster Punkt hinter dem am weitesten entfernten Occluder in ihrem Bildschirmrechteck liegt. Der Test bleibt unbemerkt wirkungslos, wenn die Occluder zu klein sind, um auf der ausgewählten Mip-Stufe ein Texel zu dominieren. In diesem Fall ersetzt schon die erste Max-Reduktion die Tiefe des Occluders durch die des weiter entfernten Hintergrunds. Occluder müssen entsprechend ihrer Bildschirmsilhouette und nicht anhand ihres physischen Kerns dimensioniert werden. Siehe GPU-gesteuertes LOD.


Teil 22 von 29. Zurück: Teil 21 – Ein schnellerer Renderer, der nicht schneller war Weiter: Teil 23 – Fünfzig Avatare und eine Stimme im Raum Serienleitfaden: /blog/2026-02-25-open-world-browser-series-guide