Willkommen bei dieser dreiteiligen Serie zum Erstellen von stilisiertem Toon-Wasser in PlayCanvas mithilfe von Vertex-Shadern. In Teil 1 haben wir uns mit der Einrichtung unserer Umwelt und Wasseroberfläche beschäftigt. Dieser Teil behandelt das Auftragen von Objekten, das Hinzufügen von Wasserlinien zur Oberfläche und das Erstellen der Schaumlinien mit Tiefenpuffer um die Kanten von Objekten, die die Oberfläche schneiden.
Ich habe ein paar kleine Änderungen an meiner Szene vorgenommen, damit sie ein bisschen schöner aussieht. Sie können Ihre Szene beliebig anpassen, aber ich habe folgendes getan:
# FFA457
. # 6CC8FF
.# FFC480
(Sie finden dies in den Szeneneinstellungen).Unten sieht mein Ausgangspunkt jetzt aus.
Die einfachste Möglichkeit, Auftrieb zu erstellen, besteht darin, ein Skript zu erstellen, das Objekte nach oben und unten schieben kann. Erstellen Sie ein neues Skript namens Auftrieb.js und setze seine Initialisierung auf:
Buoyancy.prototype.initialize = function () this.initialPosition = this.entity.getPosition (). Clone (); this.initialRotation = this.entity.getEulerAngles (). clone (); // Die Anfangszeit ist auf einen zufälligen Wert gesetzt, damit // wenn dieses Skript an mehrere Objekte angehängt wird, // sie sich nicht alle auf dieselbe Weise bewegen. This.time = Math.random () * 2 * Math.PI; ;
Im Update erhöhen wir nun die Zeit und drehen das Objekt:
Buoyancy.prototype.update = Funktion (dt) this.time + = 0,1; // Objekt nach oben und unten verschieben var pos = this.entity.getPosition (). Clone (); pos.y = this.initialPosition.y + Math.cos (this.time) * 0,07; this.entity.setPosition (pos.x, pos.y, pos.z); // Objekt leicht drehen var rot = this.entity.getEulerAngles (). Clone (); rot.x = this.initialRotation.x + Math.cos (this.time * 0,25) * 1; rot.z = this.initialRotation.z + Math.sin (diese Zeit * 0,5) * 2; this.entity.setLocalEulerAngles (rot.x, rot.y, rot.z); ;
Wenden Sie dieses Skript auf Ihr Boot an und beobachten Sie, wie es im Wasser auf und ab rast! Sie können dieses Skript auf mehrere Objekte anwenden (einschließlich der Kamera).!
Im Moment können Sie die Wellen nur durch das Betrachten der Ränder der Wasseroberfläche sehen. Das Hinzufügen einer Textur hilft, die Bewegung auf der Oberfläche sichtbarer zu machen, und ist eine kostengünstige Möglichkeit, Reflexionen und Kaustiken zu simulieren.
Sie können versuchen, eine Caustics-Textur zu finden oder Ihre eigene zu machen. Hier ist eine, die ich in Gimp gezeichnet habe, die Sie frei verwenden können. Jede Textur funktioniert so lange, wie sie nahtlos gefliest werden kann.
Wenn Sie eine gewünschte Textur gefunden haben, ziehen Sie sie in das Asset-Fenster Ihres Projekts. Wir müssen diese Textur in unserem Water.js-Skript referenzieren. Erstellen Sie daher ein Attribut dafür:
Water.attributes.add ('surfaceTexture', type: 'asset', assetType: 'texture', Titel: 'Surface Texture');
Und dann ordnen Sie es im Editor zu:
Jetzt müssen wir es an unseren Shader übergeben. Gehe zu Wasser.js und setzen Sie einen neuen Parameter in der CreateWaterMaterial
Funktion:
material.setParameter ('uSurfaceTexture', this.surfaceTexture.resource);
Jetzt geh hinein Wasserfrosch und erkläre unsere neue Uniform:
einheitliche sampler2D uSurfaceTexture;
Wir sind fast da. Um die Textur auf die Ebene zu rendern, müssen wir wissen, wo sich jedes Pixel entlang des Netzes befindet. Das bedeutet, dass wir einige Daten vom Vertex-Shader an den Fragment-Shader übergeben müssen.
EIN variierendMit Variable können Sie Daten vom Vertex-Shader an den Fragment-Shader übergeben. Dies ist der dritte Typ einer speziellen Variablen, die Sie in einem Shader haben können (die anderen beiden sind dies) Uniformund Attribut). Sie ist für jeden Scheitelpunkt definiert und für jedes Pixel zugänglich. Da es viel mehr Pixel als Scheitelpunkte gibt, wird der Wert zwischen Scheitelpunkten interpoliert (hier kommt der Name "variierend" - er unterscheidet sich von den Werten, die Sie ihm geben)..
Um dies auszuprobieren, deklarieren Sie eine neue Variable in Wasser.vert als variierende:
variierende vec2 ScreenPosition;
Und dann auf einstellen gl_Position
nachdem es berechnet wurde:
ScreenPosition = gl_Position.xyz;
Nun geh zurück zu Wasserfrosch und deklarieren Sie dieselbe Variable. Es gibt keine Möglichkeit, einige Debug-Ausgaben aus einem Shader heraus zu erhalten, aber wir können Farbe verwenden, um visuell zu debuggen. Hier ist eine Möglichkeit, dies zu tun:
einheitliche sampler2D uSurfaceTexture; variierende vec3 ScreenPosition; void main (void) Farbe vec4 = vec4 (0,0,0,7,1,0,0,5); // Testen Sie unsere neue variierende Variable color = vec4 (vec3 (ScreenPosition.x), 1.0); gl_FragColor = Farbe;
Das Flugzeug sollte jetzt schwarz und weiß aussehen, wo sich die Trennlinie befindet ScreenPosition.x
ist 0. Farbwerte gehen nur von 0 bis 1, aber die Werte in ScreenPosition
kann außerhalb dieses Bereichs liegen. Sie werden automatisch geklemmt. Wenn Sie also Schwarz sehen, kann dies 0 oder negativ sein.
Was wir gerade getan haben, ist, die Bildschirmposition jedes Scheitelpunkts an jedes Pixel zu übergeben. Sie können sehen, dass sich die Linie zwischen den schwarzen und weißen Seiten immer in der Mitte des Bildschirms befindet, unabhängig davon, wo sich die Oberfläche tatsächlich auf der Welt befindet.
Herausforderung Nr. 1: Erstellen Sie eine neue variierende Variable, um die Weltposition anstelle der Bildschirmposition zu überschreiten. Visualisieren Sie es auf die gleiche Weise wie oben. Wenn sich die Farbe mit der Kamera nicht ändert, haben Sie dies richtig gemacht.
Die UV-Werte sind die 2D-Koordinaten für jeden Scheitelpunkt entlang des Gitters, normalisiert von 0 auf 1. Dies ist genau das, was wir benötigen, um die Textur korrekt auf die Ebene abzutasten, und sie sollte bereits vom vorherigen Teil aus eingerichtet sein.
Deklarieren Sie ein neues Attribut in Wasser.vert (Dieser Name stammt von der Shader-Definition in Water.js):
Attribut vec2 aUv0;
Alles, was wir tun müssen, ist es an den Fragment-Shader zu übergeben. Erstellen Sie also einfach eine Variation und setzen Sie sie auf das Attribut:
// In Water.vert // Wir deklarieren dies zusammen mit unseren anderen Variablen oben vec2 vUv0; //… // In der Hauptfunktion speichern wir den Wert des Attributs // in der Variation, damit der Frag-Shader darauf zugreifen kann vUv0 = aUv0;
Nun erklären wir dasselbe im Fragment-Shader. Um zu überprüfen, dass es funktioniert, können wir es wie zuvor visualisieren, so dass Water.frag jetzt so aussieht:
einheitliche sampler2D uSurfaceTexture; variierendes vec2 vUv0; void main (void) Farbe vec4 = vec4 (0,0,0,7,1,0,0,5); // Bestätige UVs color = vec4 (vec3 (vUv0.x), 1.0); gl_FragColor = Farbe;
Sie sollten einen Verlauf sehen, der bestätigt, dass wir an einem Ende den Wert 0 und am anderen Ende 1 haben. Um unsere Textur tatsächlich zu testen, müssen wir nur noch Folgendes tun:
color = texture2D (uSurfaceTexture, vUv0);
Und du solltest die Textur auf der Oberfläche sehen:
Anstatt die Textur nur als neue Farbe festzulegen, kombinieren wir sie mit dem Blau, das wir hatten:
einheitliche sampler2D uSurfaceTexture; variierendes vec2 vUv0; void main (void) Farbe vec4 = vec4 (0,0,0,7,1,0,0,5); vec4 WaterLines = texture2D (uSurfaceTexture, vUv0); color.rgba + = WaterLines.r; gl_FragColor = Farbe;
Dies funktioniert, weil die Farbe der Textur mit Ausnahme der Wasserlinien überall schwarz ist (0). Durch das Hinzufügen ändern wir nicht die ursprüngliche blaue Farbe, außer an den Stellen, an denen sich Linien befinden, an denen es heller wird.
Dies ist jedoch nicht die einzige Möglichkeit, die Farben zu kombinieren.
Herausforderung Nr. 2: Können Sie die Farben so kombinieren, dass der unten dargestellte subtilere Effekt erzielt wird??
Als Endeffekt möchten wir, dass sich die Linien entlang der Oberfläche bewegen, sodass sie nicht so statisch wirken. Um dies zu tun, nutzen wir die Tatsache, dass jeder Wert, den der texture2D
Funktion außerhalb des Bereichs von 0 bis 1 wird umgebrochen (so dass 1,5 und 2,5 beide zu 0,5 werden). So können wir unsere Position um die bereits einheitliche Zeitvariable erhöhen und die Position multiplizieren, um die Dichte der Linien in unserer Oberfläche zu erhöhen oder zu verringern, sodass der endgültige Frag-Shader folgendermaßen aussieht:
einheitliche sampler2D uSurfaceTexture; Uniform Float uTime; variierendes vec2 vUv0; void main (void) Farbe vec4 = vec4 (0,0,0,7,1,0,0,5); vec2 pos = vUv0; // Multiplizieren mit einer Zahl größer als 1 führt dazu, dass sich die // Textur öfter wiederholt. Pos * = 2.0; // Verschieben der gesamten Textur, sodass sie sich entlang der Oberfläche bewegt pos.y + = uTime * 0.02; vec4 WaterLines = texture2D (uSurfaceTexture, pos); color.rgba + = WaterLines.r; gl_FragColor = Farbe;
Das Rendern von Schaumlinien um Objekte im Wasser macht es viel einfacher zu erkennen, wie Objekte eingetaucht werden und wo sie die Oberfläche schneiden. Es macht auch unser Wasser glaubwürdiger. Um dies zu erreichen, müssen wir irgendwie herausfinden, wo sich die Kanten an jedem Objekt befinden, und dies effizient tun.
Was wir wollen, ist, an einem Pixel auf der Wasseroberfläche erkennen zu können, ob es sich in der Nähe eines Objekts befindet. Wenn ja, können wir es als Schaum einfärben. Es gibt keinen einfachen Weg, dies zu tun (von dem ich weiß). Um dies herauszufinden, verwenden wir eine hilfreiche Problemlösungsmethode: Stellen Sie sich ein Beispiel vor, zu dem wir die Antwort wissen, und sehen Sie, ob wir es verallgemeinern können.
Betrachten Sie die Ansicht unten.
Welche Pixel sollten Teil des Schaums sein? Wir wissen, dass es so aussehen sollte:
Überlegen wir uns also zwei bestimmte Pixel. Ich habe zwei mit Sternen markiert. Der Schwarze ist im Schaum. Der rote ist nicht. Wie können wir sie in einem Shader unterscheiden??
Was wir wissen ist, dass diese beiden Pixel zwar nahe beieinander liegen (beide werden direkt auf dem Leuchtturm dargestellt), aber im Weltraum weit auseinander liegen. Wir können dies überprüfen, indem wir dieselbe Szene aus einem anderen Blickwinkel betrachten, wie unten gezeigt.
Beachten Sie, dass sich der rote Stern nicht auf dem Leuchtturm befindet, wie er erschien, der schwarze Stern ist es jedoch. Wir können sie anhand der Entfernung zur Kamera unterscheiden, die üblicherweise als "Tiefe" bezeichnet wird. Eine Tiefe von 1 bedeutet, dass sie sich sehr nahe an der Kamera befindet und eine Tiefe von 0 bedeutet, dass sie sehr weit ist. Es ist jedoch nicht nur eine Frage der absoluten Weltentfernung oder -tiefe zur Kamera. Es ist die Tiefe verglichen mit dem Pixel dahinter.
Rückblick auf die erste Ansicht. Nehmen wir an, der Leuchtturm hat einen Tiefenwert von 0,5. Die Tiefe des schwarzen Sterns wäre sehr nahe bei 0,5. Also haben es und die Pixel dahinter ähnliche Tiefenwerte. Der rote Stern dagegen hätte eine viel größere Tiefe, weil er näher an der Kamera wäre, sagen wir 0,7. Und dennoch hat das Pixel dahinter, immer noch auf dem Leuchtturm, einen Tiefenwert von 0,5, daher gibt es dort einen größeren Unterschied.
Das ist der Trick. Wenn die Tiefe des Pixels auf der Wasseroberfläche nahe genug an der Tiefe des Pixels liegt, sind wir ziemlich nahe am Rand von etwas, und wir können es als Schaum machen.
Wir brauchen also mehr Informationen, als in einem bestimmten Pixel verfügbar sind. Wir müssen irgendwie die Tiefe des Pixels wissen, auf das es gerade gezeichnet wird. Hier kommt der Tiefenpuffer ins Spiel.
Sie können sich einen Puffer oder einen Framebuffer als nur ein Render-Ziel außerhalb des Bildschirms oder eine Textur vorstellen. Sie möchten außerhalb des Bildschirms rendern, wenn Sie versuchen, Daten zurückzulesen, eine Technik, die dieser Raucheffekt verwendet.
Der Tiefenpuffer ist ein spezielles Renderziel, das Informationen zu den Tiefenwerten an jedem Pixel enthält. Denken Sie daran, dass der Wert in gl_Position
Im Vertex-Shader wurde ein Bildschirmraumwert berechnet, der aber auch eine dritte Koordinate, einen Z-Wert, hatte. Dieser Z-Wert wird verwendet, um die Tiefe zu berechnen, die in den Tiefenpuffer geschrieben wird.
Der Tiefenpuffer dient dazu, unsere Szene korrekt zu zeichnen, ohne dass Objekte nach vorne sortiert werden müssen. Jedes Pixel, das als erstes gezeichnet werden soll, konsultiert den Tiefenpuffer. Ist der Tiefenwert größer als der Wert im Puffer, wird dieser gezeichnet und der eigene Wert überschreibt den im Puffer befindlichen Wert. Andernfalls wird es verworfen (weil ein anderes Objekt davor steht)..
Sie können das Schreiben in den Tiefenpuffer tatsächlich deaktivieren, um zu sehen, wie die Dinge ohne sie aussehen würden. Sie können dies in Water.js versuchen:
material.depthTest = false;
Sie werden sehen, wie das Wasser immer oben dargestellt wird, auch wenn es hinter undurchsichtigen Objekten liegt.
Fügen wir eine Möglichkeit hinzu, den Tiefenpuffer für Debugging-Zwecke zu visualisieren. Erstellen Sie ein neues Skript namens DepthVisualize.js. Befestigen Sie dies an Ihrer Kamera.
Um auf den Tiefenpuffer in PlayCanvas zugreifen zu können, müssen wir nur Folgendes sagen:
this.entity.camera.camera.requestDepthMap ();
Dadurch wird automatisch eine Uniform in alle unsere Shader eingefügt, die wir verwenden können, indem wir sie als deklarieren:
einheitlicher sampler2D uDepthMap;
Nachfolgend finden Sie ein Beispielskript, das die Tiefenkarte anfordert und über unserer Szene darstellt. Es ist für Hot-Reloading eingerichtet.
var DepthVisualize = pc.createScript ('depthVisualize'); // einmal pro Entity aufgerufenen Code initialisieren DepthVisualize.prototype.initialize = function () this.entity.camera.camera.requestDepthMap (); this.antiCacheCount = 0; // Um zu verhindern, dass die Engine unseren Shader zwischenspeichert, können wir ihn live-update this.etupDepthViz (); ; DepthVisualize.prototype.SetupDepthViz = function () var device = this.app.graphicsDevice; var chunks = pc.shaderChunks; this.fs = "; this.fs + = 'variierendes vec2 vUv0;'; this.fs + = 'uniform sampler2D uDepthMap;'; this.fs + ="; this.fs + = 'float unpackFloat (vec4 rgbaDepth) '; this.fs + = 'const vec4 bitShift = vec4 (1,0 / (256,0 * 256,0 * 256,0), 1,0 / (256,0 * 256,0), 1,0 / 256,0, 1,0);'; this.fs + = 'float tiefe = punkt (rgbaDepth, bitShift);'; this.fs + = 'Rücktiefe;'; this.fs + = ''; this.fs + = "; this.fs + = 'void main (void) '; this.fs + = 'float tiefe = unpackFloat (texture2D (uDepthMap, vUv0)) * 30.0;'; this.fs + = ' gl_FragColor = vec4 (vec3 (Tiefe), 1.0); '; this.fs + =' '; this.shader = chunks.createShaderFromCode (device, chunks.fullscreenQuadVS, this.fs, "renderDepth" + this.antiCacheCount); this.antiCacheCount ++; // Wir erstellen manuell einen Draw-Aufruf, um die Tiefenkarte über alles darzustellen. this.command = new pc.Command (pc.LAYER_FX, pc.BLEND_NONE, function () pc.drawQuadWithShader (device, null, this.shader); .bind (this)); this.command.isDepthViz = true; // Markieren Sie es, damit wir es später entfernen können. this.app.scene.drawCalls.push (this.command); // Aktualisierungscode, der jeden Frame aufgerufen wird DepthVisualize.prototype.update = Funktion (dt) ; // Auswechselmethode, die für das Hot-Reloading des Skripts aufgerufen wird, // erben Sie hier Ihren Skriptstatus. DepthVisualize.prototype.swap = Funktion (alt) this .antiCacheCount = old.antiCacheCount; // Entferne den Aufruf für die Tiefenansicht durch (var i = 0; iVersuchen Sie, das in zu kopieren, und kommentieren Sie die Zeile
this.app.scene.drawCalls.push (this.command);
um die Tiefenwiedergabe umzuschalten. Es sollte ungefähr so aussehen wie das Bild unten.Herausforderung 3: Die Wasseroberfläche wird nicht in den Tiefenpuffer gezogen. Die PlayCanvas-Engine macht dies absichtlich. Kannst du herausfinden warum? Was ist das Besondere an dem Wassermaterial? Anders ausgedrückt, basierend auf unseren Tiefenprüfungsregeln, was passieren würde, wenn die Wasserpixel in den Tiefenpuffer schreiben würden?Hinweis: In Water.js können Sie eine Zeile ändern, durch die das Wasser in den Tiefenpuffer geschrieben wird.
Beachten Sie auch, dass ich den Tiefenwert im eingebetteten Shader in der Initialisierungsfunktion mit 30 multipliziere. Dies ist nur, um es klar erkennen zu können, da der Wertebereich ansonsten zu klein ist, um Farbschattierungen zu erkennen.
Den Trick umsetzen
Die PlayCanvas-Engine enthält eine Reihe von Hilfsfunktionen, um mit Tiefenwerten zu arbeiten, aber zum Zeitpunkt des Schreibens werden sie nicht in der Produktion veröffentlicht. Daher werden wir diese nur selbst einrichten.
Definieren Sie die folgenden Uniformen zu Wasserfrosch:
// Diese Uniformen werden automatisch von PlayCanvas uniform sampler2D eingefügt. UDepthMap; einheitliche vec4 uScreenSize; einheitliche mat4 Matrixansicht; // Wir müssen uns selbst gleich einrichten vec4 camera_params;Definieren Sie diese Hilfsfunktionen oberhalb der Hauptfunktion:
#ifdef GL2 float linearizeDepth (float z) z = z * 2,0 - 1,0; return 1.0 / (camera_params.z * z + camera_params.w); #else #ifndef UNPACKFLOAT #define UNPACKFLOAT Float UnpackFloat (vec4 rgbaDepth) const vec4 bitShift = vec4 (1,0 / (256,0 * 256,0 * 256,0), 1,0 / (256,0 * 256,0), 1,0 / 256,0, 1,0); Rückgabepunkt (rgbaDepth, BitShift); #endif #endif float getLinearScreenDepth (vec2 uv) #ifdef GL2 return linearizeDepth (texture2D (uDepthMap, uv) .r) * camera_params.y; #else return unpackFloat (texture2D (uDepthMap, uv)) * camera_params.y; #endif float getLinearDepth (vec3 pos) return - (matrix_view * vec4 (pos, 1.0)). z; float getLinearScreenDepth () vec2 uv = gl_FragCoord.xy * uScreenSize.zw; return getLinearScreenDepth (uv);Geben Sie einige Informationen über die Kamera an den Shader in weiter Wasser.js. Setzen Sie dies an die Stelle, an der Sie andere Uniformen wie uTime passieren:
if (! this.camera) this.camera = this.app.root.findByName ("Camera"). Kamera; var camera = this.camera; var n = camera.nearClip; var f = camera.farClip; var camera_params = [1 / f, f, (1-f / n) / 2, (1 + f / n) / 2]; material.setParameter ('camera_params', camera_params);Schließlich benötigen wir die Weltposition für jedes Pixel in unserem Frag-Shader. Wir müssen dies vom Vertex-Shader abrufen. Definieren Sie also eine Abweichung in Wasserfrosch:
variierende vec3 WorldPosition;Definiere dasselbe in Wasser.vert. Setzen Sie ihn dann im Vertex-Shader auf die verzerrte Position, damit der vollständige Code folgendermaßen aussehen würde:
Attribut vec3 aPosition; Attribut vec2 aUv0; variierendes vec2 vUv0; variierende vec3 WorldPosition; einheitliches mat4 matrix_model; einheitliche mat4 matrix_viewProjection; Uniform Float uTime; void main (void) vUv0 = aUv0; vec3 pos = aPosition; pos.y + = cos (pos.z * 5,0 + uTime) * 0,1 * sin (pos.x * 5,0 + uTime); gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0); WorldPosition = pos;Den Trick tatsächlich umsetzen
Nun können wir endlich die am Anfang dieses Abschnitts beschriebene Technik implementieren. Wir möchten die Tiefe des Pixels, mit dem wir uns gerade befinden, mit der Tiefe des Pixels dahinter vergleichen. Das Pixel, an dem wir uns befinden, stammt von der Weltposition und das Pixel dahinter von der Bildschirmposition. Also schnapp dir diese zwei Tiefen:
float worldDepth = getLinearDepth (WorldPosition); float screenDepth = getLinearScreenDepth ();Herausforderung Nr. 4: Einer dieser Werte ist niemals größer als der andere (vorausgesetzt, depthTest = true). Kannst du was ableiten??Wir wissen, dass der Schaum dort ist, wo der Abstand zwischen diesen beiden Werten klein ist. Lassen Sie uns diesen Unterschied bei jedem Pixel rendern. Fügen Sie dies am unteren Rand Ihres Shatters ein (und stellen Sie sicher, dass das Skript zur Tiefenvisualisierung aus dem vorherigen Abschnitt deaktiviert ist):
color = vec4 (vec3 (screenDepth - worldDepth), 1,0); gl_FragColor = Farbe;Was sollte ungefähr so aussehen:
Mit welcher die Ränder eines in Wasser getauchten Objekts in Echtzeit richtig herausgegriffen werden können! Sie können diesen Unterschied natürlich skalieren, damit der Schaum dicker / dünner wirkt.
Es gibt jetzt viele Möglichkeiten, diese Ausgabe mit der Wasseroberflächenfarbe zu kombinieren, um schön aussehende Schaumlinien zu erhalten. Sie können es als Farbverlauf beibehalten, mit einer anderen Textur abtasten oder eine bestimmte Farbe festlegen, wenn der Unterschied kleiner oder gleich einem bestimmten Schwellenwert ist.
Mein Lieblingslook ist eine Farbe, die der der statischen Wasserlinien ähnelt, so dass meine letzte Hauptfunktion so aussieht:
void main (void) Farbe vec4 = vec4 (0,0,0,7,1,0,0,5); vec2 pos = vUv0 * 2,0; pos.y + = uTime * 0,02; vec4 WaterLines = texture2D (uSurfaceTexture, pos); color.rgba + = WaterLines.r * 0,1; float worldDepth = getLinearDepth (WorldPosition); float screenDepth = getLinearScreenDepth (); float foamLine = Klemme ((screenDepth - worldDepth), 0.0,1.0); wenn (foamLine < 0.7) color.rgba += 0.2; gl_FragColor = color;Zusammenfassung
Wir haben Auftrieb für im Wasser schwimmende Objekte geschaffen, unsere Oberfläche mit einer beweglichen Textur versehen, um Kaustiken zu simulieren, und wir haben gesehen, wie wir den Tiefenpuffer verwenden können, um dynamische Schaumlinien zu erzeugen.
Um dies abzuschließen, werden im nächsten und letzten Teil Nachbearbeitungseffekte und deren Verwendung zum Erzeugen von Unterwasserverzerrungseffekten eingeführt.
Quellcode
Das fertig gehostete PlayCanvas-Projekt finden Sie hier. In diesem Repository ist auch ein Three.js-Port verfügbar.