Was ist ätzend?
Caustics sind Lichtmuster, die auftreten, wenn Licht von einer Oberfläche, in unserem Fall an der Grenze zwischen Wasser und Luft, gebrochen und reflektiert wird.
Da Reflexion und Brechung auf Wasserwellen auftreten, wirkt das Wasser hier als dynamische Linse und erzeugt diese Lichtmuster.

In diesem Beitrag konzentrieren wir uns auf Ätzmittel, die durch Lichtbrechung verursacht werden, was normalerweise unter Wasser geschieht.
Um stabile 60 fps zu erreichen, müssen wir sie auf einer Grafikkarte (GPU) berechnen, daher berechnen wir nur die Kaustik mit in GLSL geschriebenen Shadern.
Um es zu berechnen, brauchen wir:
- Berechnen Sie die auf der Wasseroberfläche gebrochenen Strahlen (in GLSL ist dies einfach, da hierfür eine Funktion eingebaut ist ).
- Berechnen Sie mithilfe des Schnittalgorithmus die Punkte, an denen diese Strahlen mit der Umgebung kollidieren
- Berechnen Sie die ätzende Helligkeit, indem Sie die Konvergenzpunkte der Strahlen überprüfen

Bekannte Wasserdemo auf WebGL
Ich war immer wieder begeistert von dieser Evan Wallace-Demo, die visuell realistische Wasserätzungen auf WebGL zeigt: madebyevan.com/webgl-water

Ich empfehle, seinen Artikel Medium zu lesen , in dem erklärt wird, wie Kaustiken in Echtzeit mithilfe der Funktionen Light Front Mesh und GLSL PD berechnet werden . Die Implementierung ist extrem schnell und sieht sehr gut aus, hat aber einige Nachteile: Es funktioniert nur mit einem Würfelpool und einer kugelförmigen Poolkugel . Wenn Sie einen Hai unter Wasser legen, funktioniert die Demo nicht: In den Shadern ist fest codiert, dass sich unter Wasser eine Kugel befindet.
Er platzierte eine Kugel unter Wasser, weil die Berechnung des Schnittpunkts zwischen einem gebrochenen Lichtstrahl und einer Kugel eine einfache Aufgabe ist, die sehr einfache Mathematik verwendet.
Das ist alles gut für eine Demo, aber ich wollte eine allgemeinere Lösung erstellen. Ätzmittel so zu berechnen, dass sich unstrukturierte Maschen wie ein Hai im Pool befinden können.

Kommen wir nun zu meiner Technik. In diesem Artikel gehe ich davon aus, dass Sie die Grundlagen des 3D-Renderns mit Rasterisierung bereits kennen und wissen, wie ein Vertex-Shader und ein Fragment-Shader zusammenarbeiten, um Grundelemente (Dreiecke) auf dem Bildschirm zu rendern.
Arbeiten mit GLSL-Einschränkungen
In Shadern, die in GLSL (OpenGL Shading Language) geschrieben sind, können wir nur auf eine begrenzte Menge an Informationen über die Szene zugreifen, zum Beispiel:
- Attribute des aktuell gezeichneten Scheitelpunkts (Position: 3D-Vektor, Normal: 3D-Vektor usw.). Wir können unsere GPU-Attribute übergeben, sie müssen jedoch vom integrierten GLSL-Typ sein.
- Einheitliche Konstanten für das gesamte aktuell gerenderte Netz im aktuellen Frame. Dies können Texturen, Kameraprojektionsmatrix, Beleuchtungsrichtung usw. sein. Sie müssen einen integrierten Typ haben: int, float, sampler2D für Texturen, vec2, vec3, vec4, mat3, mat4.
Es gibt jedoch keine Möglichkeit, auf die in der Szene vorhandenen Netze zuzugreifen .
Aus diesem Grund kann die Webgl-Water-Demo nur mit einer einfachen 3D-Szene erstellt werden. Es ist einfacher, den Schnittpunkt eines gebrochenen Strahls und einer sehr einfachen Form zu berechnen, die mit Uniform dargestellt werden kann. Im Fall einer Kugel kann sie durch Position (3D-Vektor) und Radius (Float) angegeben werden, sodass diese Informationen mit Uniform an Shader weitergegeben werden können. Die Berechnung von Schnittpunkten erfordert eine sehr einfache Berechnung, die im Shader einfach und schnell durchgeführt werden kann.
Einige Raytracing-Techniken, die in Shadern ausgeführt werden, rendern Netze in Texturen. Im Jahr 2020 ist diese Lösung jedoch nicht für das Echtzeit-Rendering in WebGL anwendbar. Es muss daran erinnert werden, dass wir 60 Bilder pro Sekunde mit vielen Strahlen berechnen müssen, um ein anständiges Ergebnis zu erzielen. Wenn wir die Kaustik mit 256x256 = 65536 Strahlen berechnen, müssen wir jede Sekunde eine erhebliche Anzahl von Schnittberechnungen durchführen (was auch von der Anzahl der Maschen in der Szene abhängt).
Wir müssen einen Weg finden, die Unterwasserumgebung in einer Uniform darzustellen und die Kreuzung unter Beibehaltung einer ausreichenden Geschwindigkeit zu berechnen.
Erstellen einer Umgebungskarte
Wenn die Berechnung dynamischer Schatten erforderlich ist, ist die Schattenzuordnung eine bekannte Technik . Es wird häufig in Videospielen verwendet, sieht gut aus und ist schnell auszuführen.
Shadow Mapping ist eine Zwei-Pass-Technik:
- Zunächst wird die 3D-Szene in Bezug auf die Lichtquelle gerendert. Diese Textur enthält nicht die Farben der Fragmente, sondern die Tiefe der Fragmente (den Abstand zwischen der Lichtquelle und dem Fragment). Diese Textur wird als Schattenkarte bezeichnet.
- Die Schattenkarte wird dann beim Rendern der 3D-Szene verwendet. Wenn Sie ein Fragment auf dem Bildschirm zeichnen, wissen wir, ob sich zwischen der Lichtquelle und dem aktuellen Fragment ein anderes Fragment befindet. Wenn ja, dann wissen wir, dass sich das aktuelle Fragment im Schatten befindet, und wir müssen es etwas dunkler zeichnen.
Weitere Informationen zur Schattenzuordnung finden Sie in diesem hervorragenden OpenGL-Tutorial: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping .
Sie können sich auch ein interaktives Beispiel auf ThreeJS ansehen (drücken Sie T, um die Schattenkarte in der unteren linken Ecke anzuzeigen ): threejs.org/examples/?q=shadowm#webgl_shadowmap .
In den meisten Fällen funktioniert diese Technik gut. Es kann mit jedem unstrukturierten Netz in der Szene arbeiten.
Zuerst dachte ich, ich könnte einen ähnlichen Ansatz für die Wasserätzung verwenden, dh zuerst die Unterwasserumgebung in eine Textur umwandeln und dann diese Textur verwenden, um den Schnittpunkt zwischen den Strahlen und der Umgebung zu berechnen.... Anstatt nur die Tiefen der Fragmente zu rendern, rendere ich auch die Position der Fragmente in der Umgebungskarte.
Hier ist das Ergebnis der Erstellung einer Umgebungskarte:

Umgebungskarte: Die XYZ-Position wird in den RGB-Kanälen gespeichert, die Tiefe im Alphakanal
Wie berechnet man den Schnittpunkt eines Strahls und seiner Umgebung?
Nachdem ich eine Karte der Unterwasserumgebung habe, muss ich den Schnittpunkt zwischen den gebrochenen Strahlen und der Umgebung berechnen.
Der Algorithmus funktioniert wie folgt:
- Stufe 1: Beginnen Sie am Schnittpunkt zwischen dem Lichtstrahl und der Wasseroberfläche
- Stufe 2: Berechnung der Brechung mit der Brechungsfunktion
- Stufe 3: Gehen Sie von der aktuellen Position in Richtung des gebrochenen Strahls, ein Pixel in der Umgebungskartentextur.
- Stufe 4: Vergleichen Sie die registrierte Umgebungstiefe (gespeichert im aktuellen Pixel der Umgebungstextur) mit der aktuellen Tiefe. Wenn die Tiefe der Umgebung größer als die aktuelle Tiefe ist, müssen wir fortfahren und Schritt 3 erneut anwenden . Wenn die Tiefe der Umgebung geringer ist als die aktuelle Tiefe, bedeutet dies, dass der Strahl an der aus der Umgebungstextur abgelesenen Position mit der Umgebung kollidierte und wir einen Schnittpunkt mit der Umgebung gefunden haben.

Die aktuelle Tiefe ist geringer als die Tiefe der Umgebung: Sie müssen weitermachen

Die aktuelle Tiefe ist größer als die Umgebungstiefe: Wir haben einen Schnittpunkt gefunden
Ätzende Textur
Nachdem wir den Schnittpunkt gefunden haben, können wir die ätzende Luminanz (und die ätzende Luminanztextur) mit der von Evan Wallace in seinem Artikel beschriebenen Technik berechnen . Die resultierende Textur sieht ungefähr so aus:

Ätzende Luminanztextur (Beachten Sie, dass der ätzende Effekt beim Hai weniger wichtig ist, da er näher an der Wasseroberfläche liegt, wodurch die Konvergenz der Lichtstrahlen verringert wird.)
Diese Textur enthält Informationen zur Lichtintensität für jeden Punkt im 3D-Raum. Beim Rendern der fertigen Szene können wir diese Lichtintensität aus der ätzenden Textur ablesen und das folgende Ergebnis erhalten:


Eine Implementierung dieser Technik finden Sie im Github-Repository: github.com/martinRenou/threejs-caustics . Gib ihr einen Stern, wenn es dir gefällt!
Wenn Sie die Ergebnisse der Kaustikberechnung sehen möchten, können Sie die Demo ausführen : martinrenou.github.io/threejs-caustics .
Über diesen Schnittalgorithmus
Diese Entscheidung hängt stark von der Auflösung der Umgebungstextur ab . Je größer die Textur, desto besser die Genauigkeit des Algorithmus, aber desto länger dauert es, eine Lösung zu finden (bevor Sie sie finden, müssen Sie mehr Pixel zählen und vergleichen).
Außerdem ist es akzeptabel, die Textur in Shadern zu lesen, solange Sie dies nicht zu oft tun. Hier erstellen wir eine Schleife, die weiterhin neue Pixel aus der Textur liest, was nicht empfohlen wird.
Außerdem sind while-Schleifen in WebGL nicht zulässig.(und das aus gutem Grund), daher müssen wir einen Algorithmus in einer for-Schleife implementieren, der vom Compiler erweitert werden kann. Dies bedeutet, dass wir eine zur Kompilierungszeit bekannte Schleifenbeendigungsbedingung benötigen, normalerweise den Wert "maximale Iteration", der uns zwingt, nicht mehr nach einer Lösung zu suchen, wenn wir sie nicht innerhalb der maximalen Anzahl von Versuchen gefunden haben. Diese Einschränkung führt zu falschen Ätzergebnissen, wenn die Brechung zu wichtig ist.
Unsere Technik ist nicht so schnell wie die von Evan Wallace vorgeschlagene vereinfachte Methode, aber viel flexibler als der vollständige Raytracing-Ansatz und kann auch für Echtzeit-Rendering verwendet werden. Die Geschwindigkeit hängt jedoch immer noch von einigen Bedingungen ab - der Richtung des Lichts, der Helligkeit der Brechungen und der Auflösung der Umgebungstextur.
Schließen der Demo-Überprüfung
In diesem Artikel haben wir uns mit der Berechnung des Ätzmittels von Wasser befasst, aber in der Demo wurden andere Techniken verwendet.
Beim Rendern der Wasseroberfläche haben wir eine Skybox-Textur und Würfelkarten verwendet, um Reflexionen zu erhalten. Wir haben auch eine Brechung auf die Wasseroberfläche angewendet, indem wir eine einfache Brechung im Bildschirmraum durchgeführt haben (siehe diesen Artikel über Reflexionen und Brechungen im Bildschirmraum). Diese Technik ist physikalisch falsch, aber visuell überzeugend und schnell. Wir haben auch chromatische Aberration hinzugefügt, um mehr Realismus zu erzielen.
Wir haben weitere Ideen zur weiteren Verbesserung der Methodik, darunter:
- Chromatische Aberration bei Ätzmitteln: Wir wenden jetzt chromatische Aberration auf die Wasseroberfläche an, aber dieser Effekt sollte auch bei Unterwasser-Ätzmitteln sichtbar sein.
- Lichtstreuung im Wasservolumen.
- Wie Martin Gerard und Alan Wolf auf Twitter empfohlen haben , können wir die Leistung mit hierarchischen Umgebungskarten verbessern (die als Quad-Bäume zum Auffinden von Schnittpunkten verwendet werden). Sie rieten auch, Umgebungskarten in Form von gebrochenen Strahlen zu rendern (vorausgesetzt, sie sind vollkommen flach), wodurch die Leistung unabhängig vom Einfallswinkel der Beleuchtung wird.
Danksagung
Diese Arbeit auf realistische Echtzeit - Visualisierung Wasser wurde durchgeführt QuantStack und finanziert von ERDC .