Dreiseitige Refraktion in drei Schritten

Wenn Sie ein 3D-Objekt rendern, müssen Sie ihm immer etwas Material hinzufügen, damit es sichtbar ist und so aussieht, wie Sie es möchten. Es spielt keine Rolle, ob Sie dies in speziellen Programmen oder in Echtzeit über WebGL tun.


Der größte Teil des Materials kann mit den integrierten Werkzeugen von Bibliotheken wie Three.js simuliert werden. In diesem Tutorial werde ich Ihnen jedoch zeigen, wie Sie Objekte in drei Schritten wie Glas aussehen lassen, indem Sie - wie Sie es erraten haben - Three.js verwenden.


Schritt 1: Setup und Frontreflexionen


In diesem Beispiel verwende ich die Geometrie von Diamant, aber Sie können einen einfachen Würfel oder eine andere Form verwenden.


Lassen Sie uns unser Projekt einrichten. Wir brauchen einen Visualizer, eine Szene, eine Kamera und Geometrie. Um unsere Oberfläche sichtbar zu machen, brauchen wir Material. Die Erstellung dieses Materials wird der Hauptzweck des Unterrichts sein. Erstellen wir also ein neues SharedMaterial-Objekt mit Vertex- und Fragment-Shadern.


Entgegen Ihren Erwartungen wird unser Material nicht transparent sein, sondern wir werden verzerren, was sich hinter dem Diamanten verbirgt. Dazu müssen wir die Szene (ohne Diamant) in der Textur visualisieren. Ich rendere mit einer orthogonalen Kamera nur eine Ebene in der Größe des gesamten Bereichs, aber Sie können auch eine Szene mit anderen Objekten rendern. Der einfachste Weg, um die Hintergrundfläche von der Raute in Three.js zu trennen, ist die Verwendung von Ebenen.


this.orthoCamera = new THREE.OrthographicCamera( width / - 2,width / 2, height / 2, height / - 2, 1, 1000 ); //    1  (0    ) this.orthoCamera.layers.set(1); const tex = await loadTexture('texture.jpg'); this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(), new THREE.MeshBasicMaterial({map: tex})); this.quad.scale.set(width, height, 1); //      1  this.quad.layers.set(1); this.scene.add(this.quad); 

Unser Visualisierungszyklus sieht folgendermaßen aus:


 this.envFBO = new THREE.WebGLRenderTarget(width, height); this.renderer.autoClear = false; render() { requestAnimationFrame( this.render ); this.renderer.clear(); //    fbo this.renderer.setRenderTarget(this.envFbo); this.renderer.render( this.scene, this.orthoCamera ); //      this.renderer.setRenderTarget(null); this.renderer.render( this.scene, this.orthoCamera ); this.renderer.clearDepth(); //      this.renderer.render( this.scene, this.camera ); }; 

Toll, Zeit für einen kleinen Ausflug in die Theorie. Transparente Materialien wie Glas sind sichtbar, weil sie Licht brechen. Dies liegt daran, dass das Licht langsamer durch das Glas als durch die Luft geht und wenn der Lichtstrahl in einem Winkel auf ein solches Objekt trifft, bewirkt der Unterschied in der Geschwindigkeit, dass das Licht die Richtung ändert. Diese Richtungsänderung ist mit Brechung gemeint.



Um dies im Code zu wiederholen, müssen wir den Winkel zwischen dem Blickrichtungsvektor und der Oberflächennormalen kennen. Lassen Sie uns den Vertex-Shader ändern, um diese Vektoren zu zählen.


 varying vec3 eyeVector; varying vec3 worldNormal; void main() { vec4 worldPosition = modelMatrix * vec4( position, 1.0); eyeVector = normalize(worldPos.xyz - cameraPosition); worldNormal = normalize( modelViewMatrix * vec4(normal, 0.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } 

Im Fragment-Shader können wir jetzt eyeVector und worldNormal als die ersten beiden Parameter in der in glsl integrierten refract-Funktion refract . Der dritte Parameter ist das Verhältnis der Brechungsindizes, dh der Brechungsindex (IOR) unseres dichten Mittelglases. In unserem Fall ist es 1.0 / 1.5, aber Sie können diesen Wert ändern, um das gewünschte Ergebnis zu erzielen. Beispielsweise beträgt der Brechungsindex von Wasser 1,33 und der von Diamant 2,42.


 uniform sampler2D envMap; uniform vec2 resolution; varying vec3 worldNormal; varying vec3 viewDirection; void main() { // get screen coordinates vec2 uv = gl_FragCoord.xy / resolution; vec3 normal = worldNormal; // calculate refraction and add to the screen coordinates vec3 refracted = refract(eyeVector, normal, 1.0/ior); uv += refracted.xy; // sample the background texture vec4 tex = texture2D(envMap, uv); vec4 output = tex; gl_FragColor = vec4(output.rgb, 1.0); } 

https://codesandbox.io/embed/multi-side-refraction-step-13-pzxf9?fontsize=14&hidenavigation=1&theme=dark


Großartig! Wir haben erfolgreich einen Shader geschrieben. Aber der Diamant ist kaum sichtbar ... Zum Teil, weil wir nur eine Eigenschaft des Glases bearbeitet haben. Nicht alles Licht wird durchgelassen und gebrochen, sondern ein Teil wird reflektiert. Mal sehen, wie das geht!


Schritt 2: Reflexionen und die Fresnel-Gleichung


Der Einfachheit halber werden wir in dieser Lektion die Lichtbrechungen nicht für echt berechnen, sondern einfach die weiße Farbe für das gebrochene Licht verwenden. Wir gehen weiter: Woher wissen Sie, wann Sie reflektieren und wann Sie brechen müssen? Theoretisch hängt dies vom Brechungsindex des Materials ab: Wenn der Winkel zwischen dem einfallenden Vektor und der Oberflächennormalen größer als der Schwellenwert ist, wird das Licht reflektiert.



Im Fragment-Shader werden wir die Fresnel-Gleichung verwenden, um die Proportionen zwischen reflektierten und gebrochenen Strahlen zu berechnen. Leider hat glsl diese Gleichung nicht, Sie können sie hier kopieren:


 float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); } 

Wir können einfach die Texturfarbe des gebrochenen Strahls mit der reflektierten weißen Farbe unter Verwendung des soeben berechneten Anteils mischen.


 uniform sampler2D envMap; uniform vec2 resolution; varying vec3 worldNormal; varying vec3 viewDirection; float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); } void main() { // get screen coordinates vec2 uv = gl_FragCoord.xy / resolution; vec3 normal = worldNormal; // calculate refraction and add to the screen coordinates vec3 refracted = refract(eyeVector, normal, 1.0/ior); uv += refracted.xy; // sample the background texture vec4 tex = texture2D(envMap, uv); vec4 output = tex; // calculate the Fresnel ratio float f = Fresnel(eyeVector, normal); // mix the refraction color and reflection color output.rgb = mix(output.rgb, vec3(1.0), f); gl_FragColor = vec4(output.rgb, 1.0); } 

https://codesandbox.io/embed/multi-side-refraction-step-23-3vdty?fontsize=14&hidenavigation=1&theme=dark


Es sieht schon viel besser aus, aber es fehlt noch etwas ... Genau genommen sehen wir die Rückseite des transparenten Objekts nicht. Lass es uns reparieren!


Schritt 3: Multilaterale Refraktion


Unter Berücksichtigung dessen, was wir bereits über Lichtbrechungen und -reflexionen gelernt haben, können wir verstehen, dass Licht innerhalb eines Objekts viele Male hin und her laufen kann, bevor es verlassen wird.
Um das richtige Ergebnis zu erzielen, müssen wir aus physikalischer Sicht jeden Strahl verfolgen. Leider sind solche Berechnungen für eine Echtzeitvisualisierung zu umfangreich. Stattdessen zeige ich Ihnen, wie Sie die Annäherung verwenden, um zumindest die verborgenen Kanten des Diamanten anzuzeigen.
Wir benötigen eine normale Karte und Vorder- und Rückseite in einem Fragment-Shader. Da wir nicht beide Seiten gleichzeitig visualisieren können, müssen wir zuerst die Hinterkanten als Textur erhalten.



Erstellen ShaderMaterial wie im ersten Schritt ein neues ShaderMaterial , aber jetzt rendern wir die normale Map in gl_FragColor .


 varying vec3 worldNormal; void main() { gl_FragColor = vec4(worldNormal, 1.0); } 

Als nächstes aktualisieren wir den Visualisierungszyklus und fügen die Verarbeitung der Rückseiten hinzu.


 this.backfaceFbo = new THREE.WebGLRenderTarget(width, height); ... render() { requestAnimationFrame( this.render ); this.renderer.clear(); // render background to fbo this.renderer.setRenderTarget(this.envFbo); this.renderer.render( this.scene, this.orthoCamera ); // render diamond back faces to fbo this.mesh.material = this.backfaceMaterial; this.renderer.setRenderTarget(this.backfaceFbo); this.renderer.clearDepth(); this.renderer.render( this.scene, this.camera ); // render background to screen this.renderer.setRenderTarget(null); this.renderer.render( this.scene, this.orthoCamera ); this.renderer.clearDepth(); // render diamond with refraction material to screen this.mesh.material = this.refractionMaterial; this.renderer.render( this.scene, this.camera ); }; 

Jetzt verwenden wir eine Textur mit Normalen im Material.


 vec3 backfaceNormal = texture2D(backfaceMap, uv).rgb; 

Und schließlich sind die Normalen der Vorder- und Rückseite kompatibel.


 float a = 0.33; vec3 normal = worldNormal * (1.0 - a) - backfaceNormal * a; 

In dieser Gleichung ist a nur eine skalare Größe, die angibt, wie viele Normalen der Hinterkanten verwendet werden sollen.


https://codesandbox.io/embed/multi-side-refraction-step-33-ljnqj?fontsize=14&hidenavigation=1&theme=dark


Es stellte sich heraus! Alle Seiten des Diamanten sind nur mit Hilfe von Reflexionen und Brechungen sichtbar, die wir dem Material hinzugefügt haben.


Einschränkungen


Wie ich bereits erklärt habe, ist es mit dieser Methode nicht sehr einfach, in Echtzeit transparente Materialien zu erhalten, die physikalisch korrekt sind. Ein weiteres Problem ist die Visualisierung mehrerer Glasobjekte, die sich gegenüberstehen. Da wir den Hintergrund nur einmal visualisieren, funktioniert es nicht, ihn durch eine Reihe von Objekten zu sehen. Und schließlich funktionieren die hier gezeigten Reflexionen im Sichtfeld an den Rändern des Bildschirms nicht normal, da die Strahlen mit Werten gebrochen werden können, die über die Ränder der Ebene hinausgehen, und wir können diese Fälle nicht erfassen, wenn wir die Szene in eine Textur rendern.


Natürlich gibt es Möglichkeiten, diese Einschränkungen zu umgehen, aber nicht alle davon sind großartige WebGL-Lösungen.


Ich hoffe, Ihnen hat dieses Tutorial gefallen und Sie haben etwas Neues gelernt. Ich frage mich, was Sie jetzt damit machen werden! Lass es mich auf Twitter wissen. Und zögern Sie nicht, mich nach allem zu fragen!

Source: https://habr.com/ru/post/de477418/


All Articles