Eine offene Welt im Browser bauen, Teil 14: Die Welt wird lebendig
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.
Nach Teil 13 konnten wir das Terrain modellieren. Gelände anheben, Höhlen graben, Klippen glätten – alles in Echtzeit und mit intakten Übergängen. Doch die Welt sah wie eine Tech-Demo aus. Flache Schattierung in Debug-Farben, Drahtgitter-Overlays, graue Geometrie mit LOD-Farben zur Unterscheidung der Chunks. Man konnte sie bearbeiten. Aber man konnte sie nicht spüren.
Drei Spikes änderten das. Nicht die Architektur. Es war exakt dieselbe Pipeline mit denselben Puffern und derselben Übergangsverknüpfung wie in Teil 13. Wir fügten lediglich die Ebenen hinzu, durch die sich Terrain wie ein Ort anfühlt: Oberflächen, die auf ihre Form reagieren, Leben, das auf diesen Oberflächen wächst, und einen Körper, der sich auf ihnen bewegt.
Der Unterschied zwischen „funktioniert technisch“ und „hier möchte ich bleiben“ erwies sich als überraschend klein.
Eine Klippe modellieren und zusehen, wie sie zu Fels wird
Spike 28 stellte eine eng umrissene Frage: Kann ein Material mit vier Ebenen und triplanarer Projektion auf Compute-generiertem Terrain laufen, ohne das Frame-Budget zu sprengen? Die Antwort lautete ja, doch das Interessante war, was danach geschah.
Spike 28 in einem neuen Tab öffnen ↗ · Quellcode ansehen
Vier prozedurale Texturen (Gras, Fels, Sand, Schnee), die beim Start aus FBM-Rauschen erzeugt werden. Keine externen Dateien, keine Asset-Pipeline, nur Mathematik und eine DataTexture. Die Materialgewichtungen ergeben sich aus der Oberfläche selbst: Neigung und Höhe. Flache Bereiche unterhalb der Baumgrenze erhalten Gras. Steile Flächen erhalten Fels. Tief gelegenes Gelände erhält Sand. Hohe Gipfel erhalten Schnee. Jede Rohgewichtung ergibt sich aus einem smoothstep für Neigung und Höhe. Anschließend werden sie pro Fragment normalisiert, sodass
Der Moment, der mich überzeugte: Hebt man das Terrain mit dem Pinsel an, um eine steile Klippe zu erschaffen, erscheint noch im selben Frame eine Felstextur auf der neuen Fläche. Glättet man sie wieder, erobert das Gras die Oberfläche zurück. Das Material weiß nichts über den Pinsel. Es liest lediglich Weltposition und Oberflächennormale – dieselben Daten, aus denen die Geometrie erstellt wurde. Die visuelle Rückkopplung auf die Modellierung erfolgt unmittelbar und ohne Skriptlogik.
Spike 8 testete vor anderthalb Monaten die Kosten des Terrainmaterials mit einem statischen Mesh unter WebGL. Spike 28 beweist, dass es auch auf dynamischem, Compute-generiertem Terrain mit der vollständigen Modellierungspipeline darunter funktioniert. Wir hatten dies einkalkuliert, aber alles bei laufenden 60 fps zu sehen, war dennoch eine Erleichterung.
80.000 Grasbüschel und eine Höhlendecke
Bei Spike 29 übernahmen die Instinkte der Spieleentwicklung.
Spike 29 in einem neuen Tab öffnen ↗ · Quellcode ansehen
Wir wollten Gras wie in Breath of the Wild. Nicht die Polygonzahl, sondern das Gefühl. Gras, das die richtigen Stellen bedeckt, sich im Wind bewegt und dazu einlädt, hindurchzulaufen.
Jedes Grasbüschel besteht aus drei sich in 60-Grad-Winkeln kreuzenden Quads mit jeweils vier vertikalen Segmenten zum Biegen. Diese Kreuzform wirkt aus jedem Blickwinkel räumlich, ganz ohne Billboard-Tricks. Ein InstancedMesh pro Chunk, 80.000 Büschel in der gesamten Welt und insgesamt ungefähr 2,4 Millionen Gras-Vertices.
Entscheidend ist die Platzierung. Die CPU durchläuft ein versetztes Raster über jeden Chunk und wertet dieselbe Neigungs- und Höhenlogik aus, die auch der GPU-Material-Shader verwendet. Wo das Materialsystem „Gras“ sagt, wächst Gras. Wo Fels oder Sand dominieren, sinkt die Dichte auf null. Der Übergang ist weich, weil die zugrunde liegenden smoothstep-Gewichtungen an den Biomgrenzen kontinuierliche Verläufe erzeugen. Man bemerkt keine Grenze, weil es keine gibt.
Dann probierten wir etwas aus, von dem ich nicht sicher war, ob es funktionieren würde: Gras auf SDF-Oberflächen. Die Verteilungsfunktion durchläuft jede Säule des SDF-Volumens und findet Nulldurchgänge zwischen benachbarten Voxeln. Wo die SDF von negativ zu positiv wechselt, befindet sich eine Oberfläche. Die Oberflächennormale ist einfach der normalisierte Gradient des Feldes,
Grabe in Spike 27 mit dem SDF-Pinsel eine Höhle. Kehre zu Spike 29 zurück, und auf der Oberseite der Höhlendecke wächst Gras. Der Verteilungscode weiß nicht, was eine Höhle ist. Er erkennt lediglich eine Oberfläche mit der richtigen Neigung auf der richtigen Höhe. Genau dieses emergente Verhalten macht die Entwicklung von Open-World-Systemen so befriedigend.
Der Wind ist eine Sinuswelle im TSL-Vertex-Shader, moduliert durch die V-Koordinate des Halms, sodass sich die Spitzen wiegen, während die Wurzeln an Ort und Stelle bleiben. Mit V wechselt man zwischen sanftem, starkem und deaktiviertem Wind. Der Wind wirkt gleichzeitig auf alle 80.000 Büschel, ohne CPU-Kosten zu verursachen, da er vollständig im Vertex-Shader berechnet wird.
In Spike 7 testeten wir 50.000 Grasinstanzen und waren nervös, an die Grenze zu stoßen. Spike 29 schafft 80.000 – zusätzlich zu Compute-Terrain, Übergangsverknüpfung, Multi-Material-Texturierung und Pinseln. Das Bündeln in weniger InstancedMesh-Draw-Calls ist weiterhin wichtiger als eine Reduzierung der Vertex-Anzahl pro Halm. Die Erkenntnis aus Spike 7 hat sich bestätigt.
Eine Sache haben wir vertagt: Das Gras aktualisiert sich nicht, wenn man das Terrain darunter modelliert. Die Instanzmatrizen werden beim Verteilen festgelegt. Formt man einen Hügel zu einem Tal um, schwebt das Gras in der Luft, bis man mit G eine erneute Verteilung auslöst. Für einen Spike reicht das. In der Produktion benötigen wir eine erneute Verteilung für geänderte Chunks.
Der erste Schritt
Spike 30 ist derjenige, zu dem ich immer wieder zurückkehre.
Spike 30 in einem neuen Tab öffnen ↗ · Quellcode ansehen
Keine Physikbibliothek. Eine eigene Kapsel mit einem festen 120-Hz-Zeitschritt. Die Produktionscodebasis verwendet Rapier in einem Web Worker (Spike 2 hat bewiesen, dass die Latenz unproblematisch ist), aber dieser Spike sollte die Kollisionsabfragen selbst überprüfen. Kann eine Figur über Heightmap-Terrain laufen, auf modelliertes SDF-Terrain treten und dabei nicht hindurchfallen?
Das Herzstück ist eine Funktion namens terrainQuery(x, y, z). Sie prüft, ob die Position innerhalb eines auf MC festgelegten Chunks liegt. Ist das der Fall, interpoliert sie die CPU-Spiegelung der SDF trilinear und gibt den Gradienten als Oberflächennormale zurück. Andernfalls erfolgt eine Heightmap-Abfrage mit einer durch zentrale Differenzen berechneten Normalen. Die Figur weiß nicht, auf welchem Terrainsystem sie steht. Sie fragt lediglich nach dem Boden und erhält eine Antwort.
Die SDF-Kollision war der Teil, den ich für schwierig hielt. Sieben Prüfpunkte rund um die Kapsel (unten, Mitte, oben und vier seitliche Versätze). Ein Prüfpunkt an der Position
Übrig bleibt nur die Komponente, die entlang der Oberfläche gleitet. Das ist keine Physik-Engine. Es sind Geometrieabfragen und einfache Reaktionen. Trotzdem verarbeitet das System Höhlen, Überhänge und modellierte Tunnel, ohne Sonderfallcode für einzelne Formen. Man betritt eine Höhle, die man zehn Sekunden zuvor gegraben hat, und die Kapsel folgt exakt ihrer Deckenkontur.
Das Bewegungsmodell begann zweckmäßig und wurde dann unterhaltsam. Gehen, laufen, sprinten, springen und Rutschen auf Hängen mit mehr als 45 Grad Neigung. Dann fügte ich einen BotW-Gleitschirm hinzu, und der Spike wurde zu etwas, das ich nicht mehr schließen wollte.
Drücke in der Luft die Leertaste. Die Schwerkraft sinkt von -30 auf -4. Die Fallgeschwindigkeit wird auf -3 begrenzt. Ein Deltaflügel-Mesh entfaltet sich mit einer Skalierungsinterpolation aus der Kapsel. Beim Steuern nach links neigt sich der Flügel. Die Blickrichtung der Kapsel richtet sich automatisch nach dem Geschwindigkeitsvektor aus, sodass man immer dorthin schaut, wohin man sich bewegt. Lässt man die Leertaste los, fällt man. Nach der Landung läuft man wieder.
Die Kamera fährt in die Third-Person-Perspektive zurück (umschaltbar mit P). Sie folgt der Spielfigur mit geglätteter Gierbewegung. Ein Raymarch vom Spieler zur Kamera testet das Terrain in 20 Schritten. Fliegt man in eine Höhle, verkürzt sich der Kameraarm sanft, statt durch den Fels zu clippen. Auf der anderen Seite angekommen, fährt er wieder aus.
Dieser Moment machte den Spike lohnenswert: Mit dem Heightmap-Pinsel eine hohe Klippe modellieren. Zur Third-Person-Kamera wechseln. Zur Kante laufen. Springen. Den Gleitschirm entfalten. Nach links über das gerade modellierte Terrain gleiten, während sich darunter das in Spike 29 gewachsene Gras im Wind wiegt, die Felstextur aus Spike 28 die Klippenwand bedeckt und die Transvoxel-Übergänge an jeder Chunk-Grenze halten. Auf der anderen Seite landen. Alles arbeitet gemeinsam in einem einzigen Browser-Tab.
Das Terrain unter den eigenen Füßen modellieren
Bei einer Sache war ich mir nicht sicher: Was passiert, wenn man das Terrain modelliert, auf dem die Figur gerade steht? Änderungen an der Heightmap werden unmittelbar über terrainQuery() übernommen, weil die Funktion den CPU-Puffer direkt liest. SDF-Änderungen werden über die CPU-Spiegelung der SDF übernommen. Die Kapsel löst die Durchdringung beim nächsten Physik-Tick auf, was bei 120 Hz innerhalb von
Es funktioniert einfach. Hebt man den Boden unter der Spielfigur an, wird sie mit angehoben. Gräbt man den Boden weg, fällt sie. Keine Sonderbehandlung. Die Physik läuft schnell genug, dass Terrainänderungen innerhalb eines einzelnen Frames niemals zu tiefen Durchdringungen führen. Das war ein glücklicher Nebeneffekt unserer Entscheidung für einen Zeitschritt von 120 Hz. Wir hatten ihn für flüssige Bewegungen gewählt und erhielten damit kostenlos eine sichere Terrainbearbeitung.
30 Spikes später
Wir begannen diese Serie mit einer flachen Heightmap und 500 Würfeln. Jetzt haben wir modelliertes Terrain mit volumetrischen Höhlen, Transvoxel-Übergangsverknüpfung, eine auf die Oberflächenform reagierende Multi-Material-Texturierung, 80.000 im Wind wogende Grasbüschel und eine Figur, die über all das gehen, sprinten, springen und gleiten kann.
Nichts davon ist bereits in Produktion. Die Codebasis unter world/client/ läuft weiterhin mit WebGL und einfachen Heightmap-Chunks. Alles aus diesen 30 Spikes befindet sich in eigenständigen HTML-Seiten. Als Nächstes folgt die Integration: die Migration zu WebGPURenderer, das Einbinden der hybriden HM/MC-Strategie in den Chunk-Manager und die Anbindung der Pinsel- und Materialsysteme an den Mehrspielermodus.
Das Rendering-System ist jedoch nicht länger das Risiko. Die offenen Fragen betreffen den Datenfluss: dauerhafte Speicherung von Bearbeitungen, Netzwerksynchronisierung der Pinselstriche und gemeinsames Modellieren. Also das, was zwischen Spielern geschieht, nicht zwischen Dreiecken.
Wenn du diese Serie seit Teil 1 verfolgt hast, danke, dass du auch bei den chaotischen Abschnitten dabeigeblieben bist. Falls du sie gerade erst entdeckt hast, beginne bei Teil 1. In den Irrwegen stecken die Lektionen.
In diesem Kapitel erwähnte Technologien
TSL (Three Shading Language). Das knotenbasierte Shader-System von Three.js für den WebGPU-Renderer. Materialien werden mithilfe von Funktionskomposition in JavaScript aus Knoten (positionWorld, normalWorld, smoothstep, triplanarTexture) zusammengesetzt. Der Shader-Graph wird zur Laufzeit in WGSL kompiliert. TSL ersetzt das direkte GLSL-ShaderMaterial für WebGPU-Ziele und ermöglicht die Zusammenarbeit zwischen Standardfunktionen von Three.js-Materialien (Licht, Schatten, Nebel) und benutzerdefinierter Logik pro Fragment.
Triplanare Projektion. Eine Texturprojektionstechnik, die eine Textur dreimal abtastet (auf den Ebenen XY, XZ und YZ) und die Ergebnisse anhand der Richtung der Oberflächennormalen mischt. Dadurch wird die UV-Verzerrung auf beliebiger Mesh-Geometrie vermieden. Das ist für die Ausgabe von Marching Cubes entscheidend, da deren Dreiecke keine sinnvollen UV-Koordinaten besitzen. TSL stellt triplanarTexture() als integrierten Knoten bereit.
Instanzierte Vegetation mit kreuzförmigen Quad-Halmen. Jedes Grasbüschel besteht aus drei sich in 60-Grad-Winkeln kreuzenden Quads und wirkt dadurch aus jeder Blickrichtung räumlich. Vier vertikale Segmente pro Quad ermöglichen ein sanftes Biegen für die Windanimation. Das gesamte Feld wird als ein einziges InstancedMesh pro Chunk gerendert. Die Kapazität wird um 25 % überdimensioniert, sodass neue Instanzen bei Terrainbearbeitungen reservierte Plätze belegen können, ohne dass der GPU-Puffer neu zugewiesen werden muss. Siehe unseren Leitfaden zur Landschaftsgenerierung über Vegetation. Kapsel-gegen-SDF-Kollision. Charakterkollision mit volumetrischem Terrain ohne Physik-Engine. Die Kapsel wird an mehreren Punkten gegen das SDF geprüft. Ist der Feldwert kleiner als der Kapselradius, liefert der Gradient die nach außen gerichtete Normale und die Differenz die Eindringtiefe. So werden Höhlen, Überhänge und Tunnel ohne formspezifischen Code unterstützt. Siehe SDF-Terrainkollisionen.
Charakter-Controller mit festem Zeitschritt. Die Physik wird unabhängig von der Bildrate mit 120 Hz berechnet, wobei die reale Zeit akkumuliert und in Schritten fester Größe verarbeitet wird. Eine maximale Anzahl von Teilschritten verhindert bei langsamen Frames eine Todesspirale. Boden-Snapping hält die Kapsel beim Überqueren von Hängen am Boden. Der feste Zeitschritt gewährleistet deterministisches Verhalten für spätere Multiplayer-Replays.
Teil 14 von 29. Zurück: Teil 13 – Terrain-Modellierung und das Ende der mathematischen Funktion Weiter: Teil 15 – Die Basis ersetzen und anschließend synchronisieren Serienleitfaden: /de/blog/2026-02-25-open-world-browser-series-guide