Im
zweiten Teil haben wir Auftriebs- und Schaumlinien untersucht. In diesem letzten Teil wenden wir Unterwasserverzerrungen als Nachbearbeitungseffekt an.
Brechungs- und Nachbearbeitungseffekte
Unser Ziel ist es, die Lichtbrechung im Wasser visuell zu vermitteln. Wir haben bereits
darüber gesprochen, wie diese Art von Verzerrung in einem Fragment-Shader für eine 2D-Szene erzeugt werden kann. Hier besteht der einzige Unterschied darin, dass wir verstehen müssen, welcher Bereich des Bildschirms sich unter Wasser befindet, und nur Verzerrungen anwenden müssen.
Nachbearbeitung
Im allgemeinen Fall ist der Nachbearbeitungseffekt ein Effekt, der nach dem Rendern auf die gesamte Szene angewendet wird, z. B. Farbtöne oder der
Effekt eines alten CRT-Bildschirms . Anstatt die Szene direkt auf dem Bildschirm zu rendern, rendern wir sie zuerst in den Puffer oder in die Textur, und dann rendern wir die Szene durch unseren Shader auf den Bildschirm.
In PlayCanvas können Sie diesen Nachbearbeitungseffekt anpassen, indem Sie ein neues Skript erstellen. Nennen wir es
Refraction.js und kopieren Sie diese Vorlage als Leerzeichen hinein:
Dies ähnelt einem normalen Skript, wir definieren jedoch eine
RefractionPostEffect
Klasse, die auf die Kamera angewendet werden kann. Zum Rendern werden Vertex- und Fragment-Shader benötigt. Die Attribute sind bereits konfiguriert. Erstellen Sie daher
Refraction.frag mit den folgenden Inhalten:
precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; }
Und
Refraction.vert mit einem einfachen Vertex-Shader:
attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; }
Hängen Sie nun das Skript
Refraction.js an die Kamera an und weisen Sie den Shadern die entsprechenden Attribute zu. Wenn Sie das Spiel starten, sehen Sie die Szene auf die gleiche Weise wie zuvor. Dies ist ein leerer Post-Effekt, der die Szene einfach neu rendert. Um sicherzustellen, dass es funktioniert, versuchen wir, der Szene einen roten Farbton zu geben.
Versuchen Sie, die rote Komponente auf 1.0 zu setzen, anstatt die Farbe einfach auf Refraction.frag zurückzugeben. Dadurch sollte das Bild das unten gezeigte Bild erhalten.
Verzerrungs-Shader
Um eine animierte Verzerrung zu erstellen, müssen wir eine einheitliche Zeitvariable hinzufügen. Erstellen wir sie also in diesem Post-Effekt-Konstruktor in Refraction.js:
var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs;
Jetzt übergeben wir es innerhalb der Renderfunktion an unseren Shader, um es zu erhöhen:
RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, {
Jetzt können wir denselben Shader-Code aus dem Tutorial zur Wasserverzerrung verwenden und unseren vollständigen Fragment-Shader wie folgt umwandeln:
precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying vec2 vUv0; void main() { vec2 pos = vUv0; float X = pos.x*15.+uTime*0.5; float Y = pos.y*15.+uTime*0.5; pos.y += cos(X+Y)*0.01*cos(Y); pos.x += sin(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; }
Wenn alles richtig gemacht ist, sollte das ganze Bild so aussehen, als ob es vollständig unter Wasser wäre.
Aufgabe 1: Stellen Sie sicher, dass die Verzerrung nur am unteren Bildschirmrand auftritt.
Kameramasken
Wir sind fast fertig. Es bleibt uns überlassen, diesen Verzerrungseffekt auf den Unterwasserteil des Bildschirms anzuwenden. Der einfachste Weg, an den ich gedacht habe, besteht darin, die Szene mit der Oberfläche des Wasser-Renderings in festem Weiß neu zu rendern, wie in der folgenden Abbildung gezeigt.
Es wird in die Textur gerendert, die wir als Maske verwenden. Dann übertragen wir diese Textur auf unseren Refraktions-Shader, der das Pixel im fertigen Bild nur dann verzerrt, wenn das entsprechende Pixel in der Maske weiß ist.
Fügen wir der Wasseroberfläche ein boolesches Attribut hinzu, um festzustellen, ob es als Maske verwendet wird. Fügen Sie Water.js Folgendes hinzu:
Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"});
Dann können wir es wie üblich mit
material.setParameter('isMask',this.isMask);
an den Shader
material.setParameter('isMask',this.isMask);
. Deklarieren Sie es dann in Water.frag und färben Sie das Pixel weiß, wenn das Attribut wahr ist.
Stellen Sie sicher, dass dies funktioniert, indem Sie die Eigenschaft "Ist Maske?" Aktivieren. im Editor und Neustart des Spiels. Es sollte weiß aussehen, wie im Bild oben.
Um die Szene neu zu rendern, benötigen wir eine zweite Kamera. Erstellen Sie im Editor eine neue Kamera und nennen Sie sie
CameraMask . Wir duplizieren auch die Water-Entität im Editor und benennen die doppelte
WaterMask . Stellen Sie sicher, dass für die Entität "Wasser ist Maske?" ist falsch und WaterMask ist wahr.
Erstellen Sie ein neues
CameraMask.js- Skript und hängen Sie es an die neue Kamera an, damit eine neue Kamera nicht auf einem Bildschirm, sondern auf einer Textur
gerendert wird . Wir erstellen ein
RenderTarget , um die Ausgabe dieser Kamera zu erfassen:
Nachdem Sie die Anwendung gestartet haben, werden Sie feststellen, dass diese Kamera nicht mehr auf dem Bildschirm angezeigt wird. Wir können die Ausgabe des Ziel-Renderings in
Refraction.js wie folgt
abrufen :
Refraction.prototype.initialize = function() { var cameraMask = this.app.root.findByName('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer);
Beachten Sie, dass ich diese Maskentextur als Argument an den Post-Effekt-Konstruktor übergebe. Wir müssen in unserem Konstruktor einen Link dazu erstellen, damit es so aussieht:
Schließlich übergeben wir in der Renderfunktion den Puffer an unseren Shader:
scope.resolve("uMaskBuffer").setValue(this.buffer);
Um sicherzustellen, dass dies alles funktioniert, überlasse ich es Ihnen als Aufgabe.
Aufgabe 2: Rendern Sie den uMaskBuffer auf dem Bildschirm, um sicherzustellen, dass er die Ausgabe der zweiten Kamera ist.
Folgendes sollte berücksichtigt werden: Das Ziel-Rendering wird bei der Initialisierung des Skripts CameraMask.js konfiguriert und sollte zum Zeitpunkt des Aufrufs von Refraction.js bereit sein. Wenn die Skripte anders funktionieren, erhalten wir eine Fehlermeldung. Um sicherzustellen, dass sie in der richtigen Reihenfolge funktionieren, ziehen Sie CameraMask wie unten gezeigt an den Anfang der Entitätsliste im Editor.
Die zweite Kamera sollte immer mit der gleichen Ansicht wie die ursprüngliche aussehen. Lassen Sie sie daher immer der Position und Drehung des Skripts CameraMask.js im Update folgen:
CameraMask.prototype.update = function(dt) { var pos = this.CameraToFollow.getPosition(); var rot = this.CameraToFollow.getRotation(); this.entity.setPosition(pos.x,pos.y,pos.z); this.entity.setRotation(rot); };
Definieren
CameraToFollow
Initialisieren
CameraToFollow
:
this.CameraToFollow = this.app.root.findByName('Camera');
Schnittmasken
Beide Kameras rendern jetzt dasselbe. Wir möchten, dass die Maskenkamera alles außer echtem Wasser rendert und die echte Kamera alles außer Maskenwasser rendert.
Dazu können wir die Bit-Clipping-Maske der Kamera verwenden. Es funktioniert ähnlich
wie Kollisionsmasken . Ein Objekt wird abgeschnitten (d. H. Nicht gerendert), wenn das Ergebnis des bitweisen
AND
zwischen seiner Maske und der Kameramaske 1 ist.
Angenommen, Wasser hat Bit 2, WaterMask hat Bit 3. Alle Bits außer 3 sollten für eine echte Kamera und alle Bits außer 2 für eine Maskenkamera gesetzt werden. Der einfachste Weg, „alle Bits außer N“ zu sagen, ist wie folgt Weg:
~(1 << N) >>> 0
Lesen Sie hier mehr über bitweise Operationen.
Um Kamera-Clipping-Masken zu konfigurieren, können Sie am Ende der Initialisierung des
CameraMask.js- Skripts Folgendes
einfügen :
Jetzt setzen wir in Water.js Bit 2 der Maske des Wassernetzes und die Maskenversion davon auf Bit 3:
Jetzt wird eine Art mit klarem Wasser und die zweite mit festem Weißwasser sein. Das Bild links zeigt die Ansicht der Originalkamera und rechts die Ansicht der Maskenkamera.
Maskenapplikation
Und jetzt der letzte Schritt! Wir wissen, dass Unterwasserbereiche mit weißen Pixeln markiert sind. Wir müssen nur überprüfen, ob wir uns in einem weißen Pixel befinden. Wenn nicht, deaktivieren Sie die Verzerrung in
Refraction.frag :
Und das sollte unser Problem lösen!
Es ist auch erwähnenswert, dass die Textur für die Maske beim Start initialisiert wird und beim Ändern der Fenstergröße zur Laufzeit nicht mehr der Bildschirmgröße entspricht.Glätten
Möglicherweise stellen Sie fest, dass die Ränder der Szene jetzt etwas scharf aussehen. Dies geschah, weil wir nach dem Anwenden des Nacheffekts die Glättung verloren haben.
Als weiteren Nacheffekt können wir zusätzlich zu unserem Effekt eine zusätzliche Glättung anwenden. Glücklicherweise gibt es im
PlayCanvas-Store eine weitere Variable, die wir verwenden können. Gehen Sie zur
Seite mit den
Skript-Assets , klicken Sie auf die große grüne Schaltfläche zum Herunterladen und wählen Sie Ihr Projekt aus der angezeigten Liste aus. Das Skript wird im Stammverzeichnis des Assets-Fensters als
posteffect-fxaa.js angezeigt . Hängen Sie es einfach an die Kamera-Entität an und Ihre Szene wird viel besser aussehen!
Gedanken zum Schluss
Wenn Sie hierher kommen, können Sie sich selbst loben! In diesem Tutorial haben wir einige Techniken behandelt. Jetzt müssen Sie sich sicher fühlen, wenn Sie mit Vertex-Shadern arbeiten, Texturen rendern, Nachbearbeitungseffekte anwenden, Objekte selektiv ausschneiden, den Tiefenpuffer verwenden und mit Überblendung und Transparenz arbeiten. Obwohl wir all dies in PlayCanvas implementiert haben, können Sie all diese allgemeinen Konzepte der Computergrafik in der einen oder anderen Form auf jeder Plattform erfüllen.
Alle diese Techniken sind auch auf viele andere Effekte anwendbar. Eine besonders interessante Anwendung für Vertex-Shader fand ich im
Bericht über das Abzu-Diagramm , in dem Entwickler erklären, wie sie mit Vertex-Shadern Zehntausende von Fischen auf dem Bildschirm effektiv animieren.
Jetzt haben Sie einen schönen Wassereffekt, den Sie in Ihren Spielen anwenden können! Sie können es anpassen und Ihre eigenen Details hinzufügen. Mit Wasser kann noch viel mehr getan werden (ich habe nicht einmal eine der Arten von Reflexionen erwähnt). Im Folgenden finden Sie einige Ideen.
Geräuschwellen
Anstatt die Wellen nur mit einer Kombination aus Cosinus und Sinus zu animieren, können Sie die Textur so abtasten, dass die Wellen etwas natürlicher und weniger vorhersehbar aussehen.
Dynamische Schaumspuren
Anstelle von vollständig statischen Wasserlinien auf der Oberfläche können Sie beim Bewegen von Objekten in die Textur zeichnen, um dynamische Schaumspuren zu erzeugen. Dies kann auf viele verschiedene Arten geschehen, so dass diese Aufgabe an sich zu einem Projekt werden kann.
Quellcode
Das fertige PlayCanvas-Projekt finden Sie
hier . Unser Repository hat auch einen
Projektport unter Three.js .