Verwenden von Displacement-Shadern zum Erstellen eines Unterwassereffekts

Trotz ihrer Bekanntheit ist das Erstellen von Wasserständen eine altehrwürdige Tradition in der Geschichte der Videospiele, sei es, um die Spielmechanik zu rütteln oder einfach weil Wasser so schön anzusehen ist. Es gibt verschiedene Möglichkeiten, um ein Unterwassergefühl zu erzeugen, von einfachen visuellen Elementen (z. B. dem Bildschirm blau färben) bis hin zu Mechanik (z. B. langsame Bewegung und schwache Schwerkraft).. 

Wir betrachten Verzerrung als eine Möglichkeit, die Anwesenheit von Wasser visuell zu kommunizieren (stellen Sie sich vor, Sie stehen am Rand eines Beckens und schauen auf Dinge im Inneren - das ist die Art von Effekt, die wir wiederherstellen möchten). Sie können eine Demo des endgültigen Looks hier auf CodePen nachlesen.

Ich werde Shadertoy im gesamten Tutorial verwenden, damit Sie direkt in Ihrem Browser mitlesen können. Ich werde versuchen, es ziemlich plattformunabhängig zu halten, sodass Sie das, was Sie hier lernen, in jeder Umgebung implementieren können, die Grafik-Shader unterstützt. Am Ende werde ich ein paar Tipps zur Implementierung sowie den JavaScript-Code geben, den ich verwendet habe, um das obige Beispiel mit der Phaser-Bibliothek zu implementieren.

Es mag etwas kompliziert aussehen, aber der Effekt selbst besteht nur aus ein paar Zeilen Code! Es ist nicht mehr als verschiedene zusammengesetzte Verschiebungseffekte. Wir fangen bei Null an und sehen genau, was das bedeutet.

Ein einfaches Bild rendern

Gehen Sie zu Shadertoy und erstellen Sie einen neuen Shader. Bevor wir eine Verzerrung anwenden können, müssen wir ein Bild rendern. Wir wissen aus vorherigen Tutorials, dass wir nur ein Bild in einem der unteren Kanäle der Seite auswählen und es mit dem Bildschirm verknüpfen müssen texture2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Erhalte die normalisierte Position des aktuellen Pixels fragColor = texture2D (iChannel0, uv); // Rufe die Farbe des aktuellen Pixels in der Textur ab und stelle die Farbe auf dem Bildschirm ein

Folgendes habe ich ausgewählt:

Unser erster Weg

Was passiert nun, wenn nicht nur das Pixel an der Position gerendert wird? uv, Wir rendern das Pixel bei uv + vec2 (0,1,0,0)?

Es ist immer am einfachsten zu denken, was bei der Arbeit mit Shader auf einem einzelnen Pixel geschieht. Bei jeder Position auf dem Bildschirm wird nicht die ursprüngliche Farbe in die Textur gezeichnet, sondern die Farbe eines Pixels rechts davon. Das heißt, visuell wird alles verschoben links. Versuch es!

Standardmäßig setzt Shadertoy den Umbruchmodus für alle Texturen auf wiederholen. Wenn Sie also versuchen, ein Pixel rechts neben dem ganz rechten Pixel abzutasten, wird es einfach umgebrochen. Hier habe ich es geändert Klemme (was Sie über das Zahnradsymbol auf der Box tun können, in der Sie die Textur ausgewählt haben).

Herausforderung: Können Sie das gesamte Bild langsam nach rechts bewegen? Wie wäre es, sich hin und her zu bewegen? Was ist im Kreis?? 

Hinweis: Shadertoy gibt eine Laufzeitvariable namens an iGlobalTime.

Ungleichmäßige Verdrängung

Das Verschieben eines ganzen Bildes ist nicht sehr aufregend und erfordert nicht die parallele Leistung der GPU. Was wäre, wenn wir, anstatt jede Position um einen festen Betrag (z. B. 0,1) zu verschieben, unterschiedliche Pixel um unterschiedliche Beträge verschieben?

Wir brauchen eine Variable, die für jedes Pixel einzigartig ist. Jede von Ihnen deklarierte Variable oder Uniform, die Sie übergeben, variiert nicht zwischen den Pixeln. Zum Glück haben wir bereits etwas, das so variiert: das eigene Pixel x und y. Versuche dies:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Verschiebe das Y um das aktuelle Pixel x fragColor = texture2D (iChannel0, uv);

Wir verschieben jedes Pixel vertikal um seinen x-Wert. Die Pixel ganz links erhalten den kleinsten Versatz (0), während ganz rechts der maximale Versatz (1) angezeigt wird..

Jetzt haben wir einen Wert, der im gesamten Bild von 0 bis 1 variiert. Wir verwenden dies, um die Pixel nach unten zu drücken, sodass wir diese Neigung erhalten. Nun zur nächsten Herausforderung!

Herausforderung: Kannst du damit eine Welle kreieren? (Wie unten abgebildet)

Hinweis: Ihre Offset-Variable geht von 0 bis 1. Sie möchten stattdessen von -1 auf 1 wechseln. Die Cosinus / Sinus-Funktion ist dafür die perfekte Wahl.

Zeit hinzufügen

Wenn Sie den Welleneffekt herausgefunden haben, versuchen Sie, ihn hin und her zu bewegen, indem Sie ihn mit unserer Zeitvariablen multiplizieren! Hier ist mein bisheriger Versuch:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25.) * 0,06 * cos (iGlobalTime); fragColor = texture2D (iChannel0, uv);

Ich multipliziere uv.x durch eine große Zahl (25), um die Frequenz der Welle zu steuern. Ich verkleinere es dann, indem ich es mit 0,06 multipliziere, das ist also die maximale Amplitude. Zum Schluss multipliziere ich mich mit dem Cosinus der Zeit, um ihn periodisch hin und her zu bewegen.

Hinweis: Wenn Sie wirklich bestätigen möchten, dass unsere Verzerrung einer Sinuswelle folgt, ändern Sie den Wert 0,06 in 1,0 und achten Sie darauf, dass die Verzerrung maximal ist!

Herausforderung: Können Sie herausfinden, wie man es schneller wackeln lässt??

Hinweis: Es ist das gleiche Konzept, mit dem wir die Frequenz der Welle räumlich erhöhen.

Wenn Sie gerade dabei sind, können Sie auch das Gleiche anwenden uv.x auch, so ist es sowohl auf dem x als auch auf dem y verzerrt (und vielleicht die cos's für die Sünden austauschen).

Jetzt das ist wackeln in einer Wellenbewegung, aber etwas ist aus. So verhält sich Wasser nicht ganz…

Eine andere Art, Zeit hinzuzufügen

Wasser muss so aussehen, als würde es fließen. Was wir jetzt haben, ist nur hin und her zu gehen. Untersuchen wir noch einmal unsere Gleichung:

Unsere Frequenz ändert sich nicht, was im Moment gut ist, aber wir möchten auch nicht, dass sich unsere Amplitude ändert. Wir wollen, dass die Welle die gleiche Form hat, aber dazu Bewegung über den Bildschirm.

Um zu sehen, wo wir in unserer Gleichung versetzen möchten, überlegen Sie, was bestimmt, wo die Welle beginnt und endet. uv.x ist die abhängige Variable in diesem Sinne. Wo auch immer uv.x ist pi / 2, es wird keine Verschiebung geben (da cos (pi / 2) = 0) und wo uv.x Ist in der Gegend pi / 2, das wird maximale Verschiebung sein.

Lassen Sie uns unsere Gleichung ein wenig anpassen:

Jetzt sind sowohl unsere Amplitude als auch unsere Frequenz festgelegt, und die einzige Veränderung, die variiert, ist die Position der Welle selbst. Mit diesem bisschen Theorie aus dem Weg, Zeit für eine Herausforderung!

Herausforderung: Implementieren Sie diese neue Gleichung und passen Sie die Koeffizienten an, um eine schöne Wellenbewegung zu erhalten.

Alles zusammenfügen

Hier ist mein Code für das, was wir bisher haben:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25 + iGlobalTime) * 0,01; uv.x + = cos (uv.y * 25 + iGlobalTime) * 0,01; fragColor = texture2D (iChannel0, uv);

Nun ist dies im Wesentlichen das Herzstück der Wirkung. Wir können jedoch die Dinge weiter optimieren, damit es noch besser aussieht. Es gibt zum Beispiel keinen Grund, warum Sie die Welle nur um die x- oder y-Koordinate variieren müssen. Sie können beide ändern, also variiert sie diagonal! Hier ist ein Beispiel:

Float X = uv.x * 25. + iGlobalTime; Float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01; uv.x + = sin (X-Y) * 0,01;

Es sah ein wenig repetitiv aus, also wechselte ich den zweiten Cos für eine Sünde, um das zu beheben. Wenn wir gerade dabei sind, können wir auch versuchen, die Amplitude etwas zu variieren:

Float X = uv.x * 25. + iGlobalTime; Float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01 * cos (Y); uv.x + = sin (X-Y) * 0,01 * sin (Y);

Und das ist ungefähr so ​​weit wie ich gekommen bin, aber Sie können immer mehr Funktionen kombinieren und kombinieren, um andere Ergebnisse zu erzielen!

Anwenden auf einen Bildschirmbereich

Das letzte, was ich im Shader erwähnen möchte, ist, dass Sie den Effekt in den meisten Fällen wahrscheinlich nur auf einen Teil des Bildschirms anwenden müssen, anstatt auf das Ganze. Eine einfache Möglichkeit, dies zu tun, besteht darin, eine Maske zu übergeben. Dies wäre ein Bild, das darstellt, welche Bereiche des Bildschirms betroffen sein sollten. Diejenigen, die transparent (oder weiß) sind, können nicht beeinflusst werden, und die undurchsichtigen (oder schwarzen) Pixel können die volle Wirkung haben.

In Shadertoy können Sie nicht beliebige Bilder hochladen, aber Sie können in einen separaten Puffer rendern und diesen als Textur übergeben. Hier ist ein Shadertoy-Link, bei dem ich den Effekt oben nur auf die untere Hälfte des Bildschirms anwende.

Die Maske, die Sie übergeben, muss kein statisches Bild sein. Es kann eine völlig dynamische Sache sein; Solange Sie es in Echtzeit rendern und an den Shader übergeben können, kann sich Ihr Wasser nahtlos durch den Bildschirm bewegen.

Implementierung in JavaScript

Ich habe Phaser.js verwendet, um diesen Shader zu implementieren. Sie können die Quelle in diesem Live-CodePen auschecken oder eine lokale Kopie von diesem Repository herunterladen.

Man kann sehen, wie ich die Bilder manuell als Uniformen übergebe, und ich muss auch die Zeitvariable selbst aktualisieren.

Das größte Detail der Implementierung, an das Sie denken müssen, ist, worauf Sie diesen Shader anwenden. Sowohl im Shadertoy-Beispiel als auch in meinem JavaScript-Beispiel habe ich nur ein Bild auf der Welt. In einem Spiel werden Sie wahrscheinlich mehr haben.

Mit Phaser können Sie Shader auf einzelne Objekte anwenden. Sie können sie jedoch auch auf das Weltobjekt anwenden, was wesentlich effizienter ist. In ähnlicher Weise ist es möglicherweise eine gute Idee, auf einer anderen Plattform alle Objekte auf einen Puffer zu rendern und durch den Wasser-Shader zu leiten, anstatt ihn auf jedes einzelne Objekt anzuwenden. Auf diese Weise wirkt es als Nachbearbeitungseffekt.

Fazit

Ich hoffe, dass Sie diesen Shader von Grund auf neu komponiert haben, um Ihnen einen guten Einblick zu geben, wie viele komplexe Effekte aufgebaut werden, indem Sie all diese verschiedenen kleinen Verschiebungen übereinander legen!

Als letzte Herausforderung gibt es hier eine Art Wasserwellen-Shader, der auf denselben Verdrängungsideen beruht, die wir gesehen haben. Sie könnten versuchen, es auseinander zu nehmen, die Schichten zu entfalten und herauszufinden, was jedes Stück tut!