Erstellung eines Voxel-Wasser/Lava-Shaders mit Unity

Seite wird geladen

In unserem ersten Blogbeitrag aus dem "Tagebuch eines Entwicklers" stellen wir Ihnen die Erstellung eines Voxel-Wasser-Shaders mit der Game-Engine Unity vor. 
Unser Entwickler Daniel Fischer erklärt Ihnen in einzelnen Arbeitsschritten mit anschaulichen Beispielen seine persönliche Herangehensweise bei der Umsetzung.

Es geht im Folgenden also darum, wie wir mit einem Shader ein langweiliges Rechteck in dieses spannende Ergebnis verwandeln können.

Shader sind kurz gesagt Programme, die nur auf einer GPU-Grafikkarte laufen und ursprünglich für die "Schattierung" von 3D-Szenen benutzt wurden.

Mittlerweile werden sie auch für komplexe Spezialeffekte, Nachbearbeitungsarbeiten

und sogar Funktionen eingesetzt, die gar nichts mehr mit einer grafischen Darstellung zu tun haben.

Durch die Parallelisierung einer Grafikkarte sind Shader extrem schnell und performant. So lassen sich auch rechenintensive Probleme wie z. B. künstliche Intelligenz mit Pathfinding auf die Grafikkarte übertragen.

Die zwei häufigsten Shader sind Fragment-/Pixel- Shader und Vertex-Shader. Des Weiteren gibt es noch Compute-Shader für rechenintensive Anwendungen, Geometrie-Shader, Tessellation-Shader und Raytracing-Shader.

Ich beziehe mich hier allerdings nur auf Fragment-/Pixel- und Vertex-Shader.

Fragment-Shader oder auch Pixel-Shader sind zuständig für die Farbe eines "Fragments" bzw. Pixel. Hiermit werden bspw. Texturen auf Objekten dargestellt.

Fragment- bzw. Pixel-Shader laufen für jeden einzelnen Pixel auf dem Bildschirm. Bei einer HD-Auflösung von 1920 x 1080 Pixel bedeutet dies exakt 2,073,600 mal.

Vertex-Shader werden benutzt, um die "Vertices" eines 3D-Objekts zu verändern.

Gemeint sind hier die einzelnen Eckpunkte, aus denen ein 3D-Objekt besteht (s. grüne Markierung).

Der Trick hinter dem Welleneffekt

Da wir nun wissen, dass wir mit einem Vertex-Shader die einzelnen Punkte beliebig verschieben können, müssen wir dies nur noch dem Shader mitteilen.

Mit Shadergraph geht das ganz leicht, indem wir zur aktuellen Position der Vertices einfach etwas hinzufügen und diesen Wert dann für das Objekt umwandeln.

Wir haben unser Objekt nun um drei Einheiten nach oben verschoben.
Insgesamt ist dieser Effekt jedoch noch recht unspektakulär.

Für tatsächliche Wellenbewegungen brauchen wir "Noise"!

Noise, zu Deutsch "Rauschen", ist im Prinzip genau das.

Es gibt eine Funktion, mit der wir solch ein Rauschen mit einem wiedererkennbaren Muster erzeugen können.

Die Eingabewerte heißen hier "Scale" (Skalierung) und "Cell Density" (Zellen-Dichte). Wenn wir diese jeweils mit Zahlen füllen, erzeugen wir genau so einen Rauscheffekt.

Mehrere Noise-Ebenen überlagern

Wir können zwar direkt ein einzelnes Noise-Muster für unsere Wellen benutzen, das Ergebnis ist jedoch immer noch nicht so richtig beeindruckend.

Der Trick liegt darin, die verschiedenen Muster zu überlagern. Für dieses Beispiel benutzen wir drei Ebenen.

Zwei Gradient-Noise-Muster mit verschiedenen Größen und ein Vornoi-Noise-Muster, das später die weißen Kanten von bspw. brechenden Wellen darstellt.

Um fortzufahren, müssen wir den Begriff "UV" klären. "U" und "V" sind die Texturkoordinaten. Diese bestimmen, welcher Teil einer Textur wo auf das 3D-Modell "gemalt", also projiziert, werden soll. U und V haben hier keine sprachliche Bedeutung.

UV-Koordinaten beginnen unten links auf einer Textur mit den Koordinaten "0, 0" und enden oben rechts mit den Koordinaten "1, 1".

Da Wellen sich bewegen, müssen wir auch die UVs unserer Noise-Muster bewegen. Das passiert mit "Tiling and Offset" (s. o. Screenshot).

Hier werden die Texturkoordinaten mit einer Zeitangabe und einem Wert multipliziert, um den Bewegungseffekt zu erzeugen.

Normalerweise würden Texturen grob gesagt "verschwinden", sobald wir die UVs zu weit bewegen.

Jedoch haben unsere Noise-Muster den Vorteil, dass sie wiedererkennbar sind und sich ihre UVs beliebig weit bewegen lassen.

Nun heißt es alles zusammenmischen

In Shadergraph gibt es die Funktion "Blend", mit der wir unsere Noise-Muster vermischen können. Wenn wir unsere beiden Muster eingeben, vermischt die Blend-Funktion diese um dem Wert einer Zahl.

Wir mixen nun zuerst die zwei Gradient-Noise-Muster und vermischen dieses neue Muster dann mit dem Voronoi-Muster.

Nun können wir die Vertices verändern

Da wir jetzt ein schönes bewegtes Wellenmuster haben, können wir damit die Vertices verändern.

Hierzu werden die Werte unseres finalen Noise-Musters normalisiert. Das heißt lediglich, dass die Werte dort nur noch zwischen "0" und "1" liegen.

Dann können wir dieses Muster für die Angabe der Wellenhöhe mit einer Zahl multiplizieren und zu unserer Position hinzufügen.

Hier ist es wichtig, dass wir nur den Wert für die Höhe bearbeiten. Sonst bewegt sich unser Objekt nach links und rechts, was unnatürlich wirken würde.

Daher teilen wir die Position auf, sodass nur der Wert für die Höhe das Noise-Muster bekommt.

Da wir hier direkt mit der "Objekt"-Position arbeiten, müssen wir diese nicht umwandeln wie zu Beginn des Blogs. Allerdings bedeutet dies, dass wir die Z-Koordinate anstatt die Y-Koordinate für die Höhe nutzen müssen. Im Screenshot stehen "R, G, B, A" für die Koordinaten "X, Y, Z, W".

Zwischenstand

Wenn wir alles richtig gemacht haben, sieht unser 3D-Modell jetzt so aus:

Hier ist gut erkennbar, wie die einzelnen Vertices basierend auf unserem Muster nach oben oder unten verschoben werden.


UVs "verpixeln"

Um den Effekt zu erzeugen, dass unser Wasser aus Voxel (3D-Pixeln oder auch Würfeln) besteht, können wir die Texturkoordinaten vor dem Übergang in die Noise-Muster kacheln. .

Alternativ kann dieser Teil auch entfallen, z. B. wenn Sie "normales" Wasser lieber mögen.

Water Resolution hat jeweils eine X- und eine Y- Koordinate. Hier können wir Auflösungen wie "32 x 32, 64 x 64" oder auch "2048 x 2048" eintragen.

Jetzt fehlt noch Farbe!

Jedes Noise-Muster erhält nun seine eigene Farbe.
Farben werden in Shadergraph mit "R, G, B-Werten" von "0" bis "255" angegeben.

Dazu packen wir lediglich den Output jedes einzelnen Noise-Musters in eine "Multiply"-Funktion. Diese multipliziert zwei Eingaben miteinander. Die zweite Eingabe ist dann die jeweilige Farbe für das Noise-Muster.

Am Ende addieren wir alle Farben zusammen und verbinden den Output mit unserem Fragment-Shader.

Hier das Ergebnis!

Um unser Wasser noch etwas zu optimieren, eignen sich Reflexionen sehr gut.


Reflexionen für den letzten Feinschliff

Um unser Wasser noch etwas zu optimieren, eignen sich Reflexionen sehr gut.

Normal Maps 

Normal Maps sind Texturen, die die Lichtberechnung beeinflussen. Sie dienen dazu, einem Objekt mehr Details zu verleihen, ohne jedoch die Anzahl an Polygonen zu erhöhen.

Polygone sind die Dreiecke zwischen unseren Vertices. Je mehr wir davon haben, desto höher wird die Auflösung, aber auch der Rechenaufwand.

 

Normal Maps erzeugen mit Shadergraph

Um nun passende Reflexionen zu erzeugen, kopieren wir einfach den gesamten Teil der Noise-Muster-Berechnung ohne das "Verpixeln" und packen den Output in die "Normal From Height"-Funktion.

Der Shader ist fast fertig!

Jetzt schieben wir das Ganze noch in den "Normal"-Input unseres Fragment-Shaders. Dort können wir den Wert "Smoothness" verändern. Dieser gibt an, wie viel unsere Oberfläche reflektieren kann.

Level up! Der Shader ist fertig!

Hier sehen Sie zwei weitere Beispiele, welche Shader-Effekte noch möglich sind. Eine Lava-Variante und einen blubbernden Gift-See.

Wir hoffen, unser Blogbeitrag zur Erstellung eines Voxel-Wasser-Shaders war interessant für Sie. Vielleicht haben Sie ja auch nützliche Anregungen für Ihre eigene Arbeitspraxis gewonnen.


Kommentar schreiben

* Diese Felder sind erforderlich

Kommentare

Keine Kommentare