Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Shaders

Toon-Wasser für das Web erstellen: Teil 1

by
Difficulty:AdvancedLength:LongLanguages:
This post is part of a series called Creating Toon Water for the Web.
Creating Toon Water for the Web: Part 2

German (Deutsch) translation by Tatsiana Bochkareva (you can also view the original English article)

In meinem Anfängerleitfaden für Shader habe ich mich ausschließlich auf Fragment-Shader konzentriert, was für jeden 2D-Effekt und jedes ShaderToy-Beispiel ausreicht. Es gibt jedoch eine ganze Kategorie von Techniken, die Vertex-Shader erfordern. Dieses Tutorial führt Sie durch die Erstellung von stilisiertem Toon-Wasser, während Sie Vertex-Shader vorstellen. Ich werde auch den Tiefenpuffer vorstellen und wie man ihn verwendet, um mehr Informationen über Ihre Szene zu erhalten und Schaumlinien zu erstellen.

So muss der endgültige Effekt aussehen. Sie können hier eine Live-Demo ausprobieren (linke Maus zum Orbit, rechte Maus zum Schwenken, Scrollrad zum Zoomen).

Kayak and lighthouse in water

Dieser Effekt besteht aus:

  1. Einem unterteilten durchscheinenden Wassernetz mit verschobenen Eckpunkten, um Wellen zu erzeugen.
  2. Den statischen Wasserleitungen an der Oberfläche.
  3. Einem falschen Auftrieb auf den Booten.
  4. Einer dynamischen Schaumlinien um den Rand von Gegenständen im Wasser.
  5. Einer Nachbearbeitungsverzerrung von allem unter Wasser.

Was mir an diesem Effekt gefällt, ist, dass er viele verschiedene Konzepte in der Computergrafik berührt, sodass wir auf Ideen aus früheren Tutorials zurückgreifen und Techniken entwickeln können, die wir für viele zukünftige Effekte verwenden können.

Ich werde PlayCanvas dafür verwenden, nur weil es eine praktische kostenlose webbasierte IDE hat, aber alles sollte auf jede Umgebung anwendbar sein, in der WebGL ausgeführt wird. Eine Three.js-Version des Quellcodes finden Sie am Ende. Ich gehe davon aus, dass Sie mit Fragment-Shadern und der Navigation in der PlayCanvas-Oberfläche vertraut sind. Sie können hier die Shader auffrischen und hier ein Intro zu PlayCanvas überfliegen.

Umgebungs-Setup

Das Ziel dieses Abschnitts ist es, unser PlayCanvas-Projekt einzurichten und einige Umgebungsobjekte zu platzieren, gegen die das Wasser getestet werden soll.

Wenn Sie noch kein Konto bei PlayCanvas haben, melden Sie sich für eines an und erstellen Sie ein neues leeres Projekt. Standardmäßig sollten Sie einige Objekte, eine Kamera und ein Licht in Ihrer Szene haben.

A blank PlayCanvas project showing the objects the scene contains

Modelle einfügen

Das Poly-Projekt von Google ist eine wirklich großartige Ressource für 3D-Modelle für das Web. Hier ist das Bootsmodell, das ich verwendet habe. Sobald Sie das heruntergeladen und entpackt haben, sollten Sie eine .obj- und eine .png-Datei finden.

  1. Ziehen Sie beide Dateien in das Asset-Fenster Ihres PlayCanvas-Projekts.
  2. Wählen Sie das Material aus, das automatisch erstellt wurde, und legen Sie die diffuse Zuordnung auf die .png-Datei fest.
Click on the diffuse tab and select the boat image

Jetzt können Sie die Datei Tugboat.json in Ihre Szene ziehen und die Objekte Box und Plane löschen. Sie können das Boot vergrößern, wenn es zu klein aussieht (ich habe mein Boot auf 50 gesetzt).

You can scale the model up using the properties panel on the right once its selected

Sie können Ihrer Szene auf die gleiche Weise beliebige andere Modelle hinzufügen.

Orbit-Kamera

Um eine Orbit-Kamera einzurichten, kopieren wir ein Skript aus diesem PlayCanvas-Beispiel. Gehen Sie zu diesem Link und klicken Sie auf Editor, um das Projekt aufzurufen.

  1. Kopieren Sie den Inhalt von mouse-input.js und orbit-camera.js aus diesem Lernprogramm in die gleichnamigen Dateien in Ihrem eigenen Projekt.
  2. Fügen Sie Ihrer Kamera eine Skriptkomponente hinzu.
  3. Befestigen Sie die beiden Skripte an der Kamera.

Tipp: Sie können den Ordner im Asset-Fenster erstellen, um die Organisation zu gewährleisten. Ich habe diese beiden Kameraskripte unter Scripts/Camera/, mein Modell unter Models/ und mein Material unter Materials/ abgelegt.

Wenn Sie jetzt das Spiel starten (Wiedergabetaste oben rechts in der Szenenansicht), sollten Sie in der Lage sein, Ihr Boot zu sehen und es mit der Maus zu umkreisen.

Unterteilte Wasseroberfläche

Das Ziel dieses Abschnitts ist es, ein unterteiltes Netz zu erzeugen, das als Wasseroberfläche verwendet werden kann.

Um die Wasseroberfläche zu generieren, werden wir einen Code aus diesem Tutorial zur Geländegenerierung anpassen. Erstellen Sie eine neue Skriptdatei mit dem Namen Water.js. Bearbeiten Sie dieses Skript und erstellen Sie eine neue Funktion namens GeneratePlaneMesh, die folgendermaßen aussieht:

Jetzt können Sie dies in der initialize funktion aufrufen:

Sie sollten nur ein flaches Flugzeug sehen, wenn Sie das Spiel jetzt starten. Dies ist jedoch nicht nur ein flaches Flugzeug. Es ist ein Netz aus vielen Eckpunkten. Versuchen Sie als Herausforderung, dies zu überprüfen (dies ist eine gute Ausrede, um den gerade kopierten Code durchzulesen).

Herausforderung #1: Verschieben Sie die Y-Koordinate jedes Scheitelpunkts um einen zufälligen Betrag, damit die Ebene ungefähr so aussieht, wie im Bild unten.
A subdivided plane with displaced vertices

Wellen

Das Ziel dieses Abschnitts ist es, der Wasseroberfläche ein benutzerdefiniertes Material zu geben und animierte Wellen zu erzeugen.

Um die gewünschten Effekte zu erzielen, müssen wir ein benutzerdefiniertes Material einrichten. Die meisten 3D-Engines verfügen über vordefinierte Shader zum Rendern von Objekten und zum Überschreiben. Hier ist eine gute Referenz dafür in PlayCanvas.

Anbringen eines Shaders

Erstellen wir eine neue Funktion namens CreateWaterMaterial, die ein neues Material mit einem benutzerdefinierten Shader definiert und zurückgibt:

Diese Funktion erfasst den Vertex- und Fragment-Shader-Code aus den Skriptattributen. Definieren wir also die oben in der Datei (nach der Zeile pc.createScript):

Jetzt können wir diese Shader-Dateien erstellen und an unser Skript anhängen. Kehren Sie zum Editor zurück und erstellen Sie zwei neue Shader-Dateien: Water.frag und Water.vert. Fügen Sie diese Shader wie unten gezeigt an Ihr Skript an.

Watervert and Waterfrag are attached to WaterInit

Wenn die neuen Attribute nicht im Editor angezeigt werden, klicken Sie auf die Schaltfläche Parse, um das Skript zu aktualisieren.

Fügen Sie nun diesen grundlegenden Shader in Water.frag ein:

Und das in Water.vert:

Kehren Sie schließlich zu Water.js zurück und verwenden Sie unser neues benutzerdefiniertes Material anstelle des Standardmaterials. Also statt:

Tun:

Wenn Sie das Spiel starten, sollte das Flugzeug jetzt blau sein.

The shader we wrote renders the plane as blue

Hot Reloading

Bisher haben wir gerade einige Dummy-Shader für unser neues Material eingerichtet. Bevor wir mit dem Schreiben der tatsächlichen Effekte beginnen, möchte ich als letztes das automatische Neuladen von Code einrichten.

Das Kommentieren der swap-Funktion in einer Skriptdatei (z. B. Water.js) ermöglicht das Hot-Reloading. Wir werden später sehen, wie Sie dies verwenden können, um den Status beizubehalten, selbst wenn wir den Code in Echtzeit aktualisieren. Im Moment möchten wir die Shader jedoch erneut anwenden, sobald wir eine Änderung festgestellt haben. Shader werden kompiliert, bevor sie in WebGL ausgeführt werden. Daher müssen wir das benutzerdefinierte Material neu erstellen, um dies auszulösen.

Wir werden prüfen, ob der Inhalt unseres Shader-Codes aktualisiert wurde, und in diesem Fall das Material neu erstellen. Speichern Sie zunächst die aktuellen Shader in der Initialisierung:

Überprüfen Sie im Update, ob Änderungen vorgenommen wurden:

Um dies zu bestätigen, starten Sie das Spiel und ändern Sie die Farbe des Flugzeugs in Water.frag in ein geschmackvolleres Blau. Sobald Sie die Datei gespeichert haben, sollte sie aktualisiert werden, ohne dass sie aktualisiert oder neu gestartet werden muss! Dies war die Farbe, die ich gewählt habe:

Vertex Shader

Um Wellen zu erzeugen, müssen wir jeden Scheitelpunkt in unserem Netz in jedem Frame verschieben. Das hört sich so an, als wäre es sehr ineffizient, aber jeder Scheitelpunkt jedes Modells wird bereits in jedem von uns gerenderten Frame transformiert. Dies ist, was der Vertex-Shader tut.

Wenn Sie sich einen Fragment-Shader als eine Funktion vorstellen, die auf jedem Pixel ausgeführt wird, eine Position einnimmt und eine Farbe zurückgibt, ist ein Vertex-Shader eine Funktion, die auf jedem Vertex ausgeführt wird, eine Position einnimmt und eine Position zurückgibt.

Der Standard-Vertex-Shader übernimmt die Weltposition eines bestimmten Modells und gibt die Bildschirmposition zurück. Unsere 3D-Szene wird in Form von x, y und z definiert, aber Ihr Monitor ist eine flache zweidimensionale Ebene, sodass wir unsere 3D-Welt auf unseren 2D-Bildschirm projizieren. Diese Projektion ist das, worum sich die Ansichts-, Projektions- und Modellmatrizen kümmern und liegt außerhalb des Bereichs dieses Tutorials. Wenn Sie jedoch genau erfahren möchten, was in diesem Schritt passiert, finden Sie hier eine sehr schöne Anleitung.

Also diese Zeile:

Nimmt aPosition als 3D-Weltposition eines bestimmten Scheitelpunkts und wandelt sie in gl_Position um, die die endgültige 2D-Bildschirmposition darstellt. Das Präfix 'a' in aPosition bedeutet, dass dieser Wert ein Attribut ist. Denken Sie daran, dass eine uniform Variable ein Wert ist, den wir auf der CPU definieren können, um ihn an einen Shader zu übergeben, der über alle Pixel / Eckpunkte hinweg den gleichen Wert beibehält. Der Wert eines Attributs stammt dagegen von einem in der CPU definierten Array. Der Vertex-Shader wird für jeden Wert in diesem Attributarray einmal aufgerufen.

Sie können sehen, dass diese Attribute in der Shader-Definition eingerichtet sind, die wir in Water.js eingerichtet haben:

PlayCanvas kümmert sich um das Einrichten und Übergeben eines Arrays von Scheitelpunktpositionen für aPosition, wenn wir diese Aufzählung übergeben. Im Allgemeinen können Sie jedoch jedes Array von Daten an den Scheitelpunkt-Shader übergeben.

Verschieben der Scheitelpunkte

Angenommen, Sie möchten die Ebene zerquetschen, indem Sie alle x-Werte mit der Hälfte multiplizieren. Sollten Sie aPosition oder gl_Position ändern?

Versuchen wir zuerst aPosition. Wir können ein Attribut nicht direkt ändern, aber wir können eine Kopie erstellen:

Die Ebene sollte jetzt rechteckiger aussehen. Da ist nichts Seltsames. Was passiert nun, wenn wir stattdessen versuchen, gl_Position zu ändern?

Es könnte genauso aussehen, bis Sie anfangen, die Kamera zu drehen. Wir ändern die Koordinaten des Bildschirmbereichs, was bedeutet, dass sie je nach Betrachtung unterschiedlich aussehen.

Auf diese Weise können Sie die Scheitelpunkte verschieben, und es ist wichtig, diese Unterscheidung zwischen Welt- und Bildschirmbereich zu treffen.

Herausforderung #2: Können Sie die gesamte ebene Fläche im Vertex-Shader um einige Einheiten (entlang der Y-Achse) nach oben bewegen, ohne ihre Form zu verzerren?
Herausforderung #3: Ich sagte, gl_Position ist 2D, aber gl_Position.z existiert. Können Sie einige Tests durchführen, um festzustellen, ob sich dieser Wert auf etwas auswirkt, und wenn ja, wofür er verwendet wird?

Zeit hinzufügen

Eine letzte Sache, die wir brauchen, bevor wir uns bewegende Wellen erzeugen können, ist eine einheitliche Variable, die als Zeit verwendet werden kann. Deklarieren Sie eine Uniform in Ihrem Vertex-Shader:

Um dies an unseren Shader zu übergeben, kehren Sie zu Water.js zurück und definieren Sie eine Zeitvariable in der Initialisierung:

Um dies an unseren Shader zu übergeben, verwenden wir material.setParameter. Zuerst setzen wir einen Anfangswert am Ende der CreateWaterMaterial-Funktion:

Jetzt können wir in der update funktion die Zeit erhöhen und auf das Material zugreifen, indem wir die Referenz verwenden, die wir dafür erstellt haben:

Kopieren Sie als letzten Schritt in der Swap-Funktion den alten Zeitwert, sodass der Code auch dann weiter erhöht wird, wenn Sie ihn ändern, ohne ihn auf 0 zurückzusetzen.

Jetzt ist alles fertig. Starten Sie das Spiel, um sicherzustellen, dass keine Fehler vorliegen. Bewegen wir nun unser Flugzeug in Water.vert um eine Funktion der Zeit:

Und Ihr Flugzeug sollte sich jetzt auf und ab bewegen! Da wir jetzt eine Swap-Funktion haben, können Sie Water.js auch aktualisieren, ohne neu starten zu müssen. Versuchen Sie, die Zeit schneller oder langsamer zu erhöhen, um zu bestätigen, dass dies funktioniert.

Moving the plane up and down with a vertex shader
Herausforderung #4: Können Sie die Eckpunkte so verschieben, dass sie wie die Welle unten aussehen?

Als Hinweis habe ich ausführlich über verschiedene Möglichkeiten gesprochen, hier Wellen zu erzeugen. Das war in 2D, aber hier gilt die gleiche Mathematik. Wenn Sie lieber nur einen Blick auf die Lösung werfen möchten, finden Sie hier das Wesentliche.

Transluzenz

Ziel dieses Abschnitts ist es, die Wasseroberfläche durchscheinend zu machen.

Möglicherweise haben Sie bemerkt, dass die Farbe, die wir in Water.frag zurückgeben, einen Alpha-Wert von 0,5 hat, die Oberfläche jedoch immer noch vollständig undurchsichtig ist. Transparenz ist in vielerlei Hinsicht immer noch ein offenes Problem in der Computergrafik. Ein billiger Weg, dies zu erreichen, ist die Verwendung von Blending.

Wenn ein Pixel gezeichnet werden soll, vergleicht es normalerweise den Wert im Tiefenpuffer mit seinem eigenen Tiefenwert (seiner Position entlang der Z-Achse), um festzustellen, ob das aktuelle Pixel auf dem Bildschirm überschrieben oder selbst verworfen werden soll. Auf diese Weise können Sie eine Szene korrekt rendern, ohne Objekte von hinten nach vorne sortieren zu müssen.

Beim Mischen können wir die Farbe des bereits gezeichneten Pixels (das Ziel) mit dem Pixel, das gezeichnet werden soll (die Quelle), kombinieren, anstatt es einfach zu verwerfen oder zu überschreiben. Sie können alle verfügbaren Mischfunktionen in WebGL hier sehen.

Damit das Alpha so funktioniert, wie wir es erwarten, möchten wir, dass die kombinierte Farbe des Ergebnisses die Quelle multipliziert mit dem Alpha plus das Ziel multipliziert mit eins minus dem Alpha ist. Mit anderen Worten, wenn das Alpha 0,4 beträgt, sollte die endgültige Farbe sein:

In PlayCanvas erledigt die Option pc.BLEND_NORMAL genau dies.

Um dies zu aktivieren, legen Sie einfach die Eigenschaft für das Material in CreateWaterMaterial fest:

Wenn Sie das Spiel jetzt starten, ist das Wasser durchscheinend! Dies ist jedoch nicht perfekt. Ein Problem tritt auf, wenn sich die durchscheinende Oberfläche mit sich selbst überlappt, wie unten gezeigt.

Artifacts arise when a translucent surface overlaps with itself

Wir können dies beheben, indem wir Alpha to Coverage verwenden. Dies ist eine Multi-Sampling-Technik, um Transparenz zu erzielen, anstatt sie zu mischen:

Dies ist jedoch nur in WebGL 2 verfügbar. Für den Rest dieses Tutorials werde ich Blending verwenden, um es einfach zu halten.

Abschluss

Bisher haben wir unsere Umgebung eingerichtet und unsere durchscheinende Wasseroberfläche mit animierten Wellen aus unserem Vertex-Shader erstellt. Der zweite Teil behandelt das Aufbringen von Auftrieb auf Objekte, das Hinzufügen von Wasserlinien zur Oberfläche und das Erstellen der Schaumlinien um die Kanten von Objekten, die die Oberfläche schneiden.

Der letzte Teil behandelt die Anwendung des Unterwasser-Post-Process-Verzerrungseffekts und einige Ideen, wie es weitergehen soll.

Quellcode

Das fertige gehostete PlayCanvas-Projekt finden Sie hier. In diesem Repository ist auch ein Three.js-Port verfügbar.

Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.