Skip to content

Eine offene Welt im Browser bauen, Teil 28: Gras bis zum Horizont und ein Boden, der sich selbst verbirgt

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 27 baute die Insel und gab ihr einen Boden, der nicht gekachelt aussieht. In diesem Teil geht es um die beiden Dinge, durch die sich Terrain bewohnt statt leer anfühlt. Spike 56 behandelt Gras – das Oberflächendetail, das einen texturierten Hang in einen Ort verwandelt, durch den man laufen möchte – und die Herausforderung, ein Feld wie ein Feld statt wie verstreute Streifen wirken zu lassen. Spike 57 befasst sich mit dem Gegenteil von mehr Zeichnen: Es geht darum, nicht zu zeichnen, was ein Hügel ohnehin bereits verdeckt. Das interessante Ergebnis ist, dass sich die schnelle Prüfmethode zugleich als die korrekte erwies.

Gras, das wie ein Feld wirkt, nicht wie Konfetti

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

Die grundlegende Entscheidung betrifft die Geometrie. Ein einzelner dünner, sich verjüngender Grashalm ist aus den meisten Kamerawinkeln kleiner als ein Pixel, sodass eine halbe Million flacher Halme eher wie verstreutes grünes Konfetti als wie ein Feld wirkt. Die Lösung ist ein Cross-Quad-Büschel: drei sich verjüngende Quads, die um jeweils sechzig Grad versetzt um die lokale Hochachse gedreht sind. Dadurch steht aus jeder Blickrichtung mindestens ein Quad annähernd senkrecht zur Kamera, und jede Instanz deckt auf dem Bildschirm ungefähr die tatsächliche Fläche von drei Halmstärken ab. Das ist der Unterschied zwischen grünen Streifen und erkennbarem Gras.

Ein WebGPU-Compute-Kernel platziert beim Initialisieren jedes Büschel genau einmal. Er hasht den Instanzindex in fünf dekorrelierte Zufallsströme, wählt eine XZ-Position innerhalb des Bereichs, liest die Bodenhöhe aus demselben FBM aus, das auch das CPU-Bodennetz verwendet – eine TSL-Portierung mit vorzeichenerhaltendem Modulo, damit die Werte exakt übereinstimmen und die Halme auf der Oberfläche stehen, statt darüber zu schweben –, berechnet eine Normale über zentrale Differenzen und bestimmt Breite, Höhe und Farbton jedes Büschels. Auf der Renderseite umgeht ein TSL-Vertexgraph die Instanzmatrix vollständig und schreibt direkt in den Clip-Space: Er skaliert das Cross-Quad in Einheitsgröße, dreht die lokale Hochachse mit der Rodrigues-Formel auf die Bodennormale und verschiebt es an die Position des Büschels. Entfernungsabhängiges LOD gibt es ohne zusätzlichen Culling-Durchlauf, weil der Vertexgraph die Höhe des Büschels mit 1smoothstep(fadeNear,fadeFar,dist) multipliziert. Weit entfernte Büschel kollabieren dadurch flach auf eine Höhe von null und verursachen keinen Fill-Aufwand mehr. Der Wind besteht aus zwei Oktaven von Sinus und Kosinus über der Welt-XZ-Position des Büschels plus Zeit. Er wird horizontal angewendet und durch den Höhenanteil begrenzt, sodass die Basis verankert bleibt, während sich die Spitzen bewegen. Eine minimale, kameraabhängige Neigung kippt jedes Büschel zum Betrachter, damit es sich perspektivisch öffnet, statt wie eine flache Karte zu wirken.

Das Farbrezept stammt aus dem Breath-of-the-Wild-URP-Shader von NedMakesGames: Flat Shading, bei dem die Halmfarbe entlang des Höhenanteils von einem Wurzelton zu einem Spitzenton interpoliert wird. Wurzel- und Spitzenton werden ihrerseits anhand des Farbtonwerts jedes Büschels zwischen zwei Paletten interpoliert. So entsteht der gesprenkelte Zweifarb-Look eines echten BotW-Grasfelds. Für die diffuse Beleuchtung wird die Bodennormale gegen die Sonne mit einem Mindestwert für das Umgebungslicht verwendet, da die einzelnen Quad-Normalen des Cross-Quads für einen stilisierten Look zu unruhig wären, wenn sie separat schattiert würden. Das gesamte Feld wird mit einem einzigen Draw Call gezeichnet und vollständig auf der GPU platziert und animiert.

Vier Methoden, um auszublenden, was ein Hügel verbirgt

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

Spike 57 vergleicht vier Culling-Pfade über derselben prozeduralen Welt, die der Produktionsclient streamt. Sie ist vertikal um den Faktor 1,8 skaliert, damit Hügel Vegetation tatsächlich verdecken. T0 berücksichtigt nur die Entfernung und entspricht damit der bereits im ausgelieferten Client verwendeten Chunk-Sichtbarkeitsprüfung. T1 ergänzt Frustum Culling – die mit Abstand größte günstige Optimierung –, das alles außerhalb des Sichtkegels entfernt. T2 fügt einen terrainabhängigen Horizonttest hinzu: Von der Kameraposition zu jeder Instanz wird ein Strahl abgetastet. Die Instanz wird verworfen, wenn die Heightmap irgendwo entlang des Wegs über den Strahl steigt. So wird ein Baum hinter einem Bergrücken ausgeblendet, obwohl er sich innerhalb des Frustums befindet. T3 behält denselben Horizonttest bei, beschleunigt ihn aber mit einer Max-Height-Pyramide. T4 portiert den gesamten Entfernungs-, Frustum- und Horizonttest in einen TSL-Compute-Kernel, der für jede Instanz einen Sichtbarkeitsskalierungswert schreibt. Das Vegetationsmaterial liest diesen Wert und lässt die Vertices verborgener Instanzen kollabieren. Ein zweiframeslanger Sichtbarkeitszähler glättet das kurzzeitige Flackern, das entsteht, wenn ein Abtastpunkt bei kleinen Kamerabewegungen knapp innerhalb oder außerhalb eines Texels landet. Er bleibt absichtlich kurz, denn eine längere Dauer würde algorithmische Fehler kaschieren, statt sie zu beheben.

Die Pyramide ist der entscheidende Teil dieses Spikes, und die Lehre daraus lautet, den Test an das anzupassen, was tatsächlich auf dem Bildschirm dargestellt wird. Das gerenderte Terrain ist ein Dreiecksnetz mit Vertices auf einem festen Zwei-Meter-Raster. Zwischen den Vertices interpoliert der Rasterizer linear, daher entspricht die tatsächliche gerenderte Höhe innerhalb eines Rechtecks dem Maximum der darin enthaltenen Vertices – niemals einer Spitze des kontinuierlichen Rauschens zwischen ihnen. Wenn die Verdeckungspyramide das Rauschen auf einem feineren Raster abtastet, findet sie Phantomspitzen, die das Mesh nie darstellt, und blockiert Strahlen, durch deren Verlauf die Kamera offensichtlich sehen kann. Deshalb liegen die Basis-Texel der Pyramide exakt auf dem Vertexraster, wobei jedes das Maximum seiner vier Eck-Vertices speichert. Die höheren Ebenen entstehen durch klassische 2x2-Maximumsreduktionen, wodurch die Pyramide in Bezug auf das gerenderte Mesh exakt ist. Beim schrittweisen Strahltest tastet der Code das Mesh bewusst punktweise mit bilinearer Interpolation ab, statt die Pyramide abzufragen. Eine AABB-Abfrage unterhalb der Texelgröße liefert nämlich das Maximum des gesamten Texels und erhöht die angenommene Höhe an steilen Bergrücken um mehrere Meter. Genau diese Überverdeckung würde sichtbare Requisiten verbergen.

Das überraschende Ergebnis ist, dass T2, die Brute-Force-Referenz, falsch liegt. Da T2 das kontinuierliche Rauschen direkt punktweise abtastet, erfasst es Spitzen zwischen den Mesh-Vertices, die gar nicht gerendert werden. Dadurch verdeckt es geringfügig zu viel und blendet Vegetation aus, die der Spieler tatsächlich sehen kann. Der Pyramidenpfad ist sowohl schneller – dank der O(logN)-AABB-Reduktion für Tests auf Chunk-Ebene – als auch geometrisch korrekter, weil er ausschließlich Höhen zurückgeben kann, die das Mesh wirklich darstellt. Das ist die zentrale Aussage des Spikes: Die beschleunigte Struktur ist kein Qualitätskompromiss zugunsten der Geschwindigkeit, sondern die Version, die der Realität entspricht. Das Chunk-Culling führt pro Chunk einen Fünfpunktetest aus – die vier oberen Ecken plus die Mitte auf der Maximalhöhe des Chunks – und schließt die Grundfläche des jeweiligen Chunks aus der Menge der Verdeckungsobjekte aus, damit sich ein Chunk niemals selbst verdeckt. Für die Produktion wird T4 empfohlen: Instanzmetadaten und Höhenfeld werden in Storage Buffers übertragen, und dieselbe Schleife läuft in einem Compute-Shader mit indirektem Draw, zumal der Renderer ohnehin auf WebGPU umgestellt wird.

In diesem Kapitel erwähnte Technologie

Cross-Quad-Gras auf der GPU. Drei sich verjüngende Quads, die pro Büschel um jeweils sechzig Grad versetzt sind, garantieren aus jedem Winkel ein annähernd senkrecht zur Kamera stehendes Quad. Dadurch wirken eine halbe Million Instanzen wie ein Feld statt wie Subpixel-Konfetti. Ein Compute-Kernel platziert jedes Büschel – FBM-Bodenhöhe, abgetastet mit demselben vorzeichenerhaltenden Modulo wie beim CPU-Mesh, Normale aus zentralen Differenzen sowie individuelle Größe und Farbton je Büschel. Ein TSL-Vertexgraph umgeht die Instanzmatrix, um zu skalieren, mit der Rodrigues-Formel auf die Normale zu drehen, zu verschieben und höhenabhängig begrenzten Wind anzuwenden. Entfernungsabhängiges LOD ist kostenlos: Büschel kollabieren durch 1smoothstep(fadeNear,fadeFar,dist) auf eine Höhe von null. Die Farbe folgt dem BotW-Rezept von NedMakesGames: ein Verlauf von der Wurzel zur Spitze über zwei anhand des Farbtons gemischte Paletten. Siehe GPU-gesteuertes LOD.

Terrain-Horizont-Occlusion-Culling. Vier verglichene Pfade über der Produktionswelt: Entfernung, zusätzlich Frustum, zusätzlich ein Heightmap-Strahltest, der Instanzen hinter einem Bergrücken verwirft, zusätzlich eine beschleunigende Max-Height-Pyramide sowie eine Portierung in einen TSL-Compute-Kernel. Die Pyramide tastet exakt auf dem Zwei-Meter-Vertexraster ab, auf dem das Mesh tesselliert wird – Basis-Texel = Maximum der vier Eck-Vertices, darüber schrittweise 2x2-Maximumsreduktionen. Dadurch liefert sie ausschließlich Höhen, die der Rasterizer tatsächlich darstellt.

Beschleunigt und korrekt, kein Kompromiss. Das punktweise Abtasten kontinuierlichen Rauschens – der Brute-Force-Pfad T2 – findet Spitzen zwischen Mesh-Vertices, die nie gerendert werden, und verdeckt dadurch sichtbare Vegetation übermäßig. Die Vertexraster-Pyramide ist sowohl schneller – O(logN)-AABB-Reduktion – als auch geometrisch exakt. Deshalb tasten die schrittweisen Strahltests das Mesh bilinear ab, statt das texelweise Maximum der Pyramide abzufragen, das die Höhen an steilen Bergrücken um mehrere Meter vergrößern würde. Das Chunk-Culling nutzt einen Fünfpunktetest und schließt die Grundfläche des jeweiligen Chunks aus, damit dieser sich niemals selbst verdeckt. Im Produktionspfad werden Metadaten und Höhenfeld in Storage Buffers übertragen, um das Culling per Compute-Shader mit indirektem Draw auszuführen.


Teil 28 von 29. Zurück: Teil 27 – Eine Insel aus Rauschen und Boden, der wie Boden aussieht Weiter: Teil 29 – Ein Controller für jeden Körper Serienleitfaden: /de/blog/2026-02-25-open-world-browser-series-guide