So implementieren Sie Polygonbrechung in Echtzeit mit Threejs

So implementieren Sie Polygonbrechung in Echtzeit mit Threejs

Vorwort

In diesem Tutorial erfahren Sie, wie Sie mit Three.js in drei Schritten einem Objekt das Aussehen von Glas verleihen.

Wenn Sie ein 3D-Objekt rendern, sei es mit einer 3D-Software oder mit WebGL zur Echtzeitanzeige, müssen Sie ihm immer Materialien zuweisen, um es sichtbar zu machen und ihm das gewünschte Erscheinungsbild zu verleihen.

Mithilfe vorgefertigter Prozeduren aus Bibliotheken wie Three.js können viele Arten von Materialien nachgeahmt werden. In diesem Tutorial zeige ich Ihnen jedoch, wie Sie mithilfe von drei Objekten (in drei Schritten) einem Objekt den Anschein verleihen, als würde es sich wie Glas verhalten.

Schritt 1: Aufbau und Frontbrechung

Für diese Demonstration verwende ich eine Rautengeometrie, Sie können jedoch auch eine einfache Box oder eine andere Geometrie verwenden.

Lassen Sie uns unser Projekt erstellen. Wir benötigen einen Renderer, eine Szene, eine perspektivische Kamera und unsere Geometrie. Um unsere Geometrie zu rendern, müssen wir ihr ein Material zuweisen. Die Erstellung dieses Materials wird der Hauptschwerpunkt dieses Tutorials sein. Erstellen Sie also ein neues ShaderMaterial mit grundlegenden Vertex- und Fragment-Shadern.

Anders als Sie vielleicht erwarten, ist unser Material nicht transparent. Tatsächlich werden wir alles, was sich hinter dem Diamanten befindet, beproben und verformen. Dazu müssen wir die Szene (ohne die Diamanten) in eine Textur rendern. Ich rendern nur eine Vollbildebene mit einer orthographischen Kamera, aber dies könnte auch eine Szene sein, die mit anderen Objekten gefüllt ist. Die einfachste Möglichkeit, in Three.js die Hintergrundgeometrie von einer Raute zu trennen, ist die Verwendung von „Ebenen“.

this.orthoCamera = neue THREE.OrthographicCamera(Breite / - 2, Breite / 2, Höhe / 2, Höhe / - 2, 1, 1000);
// Weisen Sie die Kamera der Ebene 1 zu (Ebene 0 ist Standard)
dies.orthoCamera.layers.set(1);

const tex = warte auf loadTexture('texture.jpg');
this.quad = neues THREE.Mesh(neues THREE.PlaneBufferGeometry(), neues THREE.MeshBasicMaterial({map: tex}));

this.quad.scale.set(Breite, Höhe, 1);
// verschiebt das Flugzeug auch auf Ebene 1
dieses.quad.layers.set(1);
diese.Szene.hinzufügen(dieses.quad);

Unsere Rendering-Schleife sieht folgendermaßen aus:

this.envFBO = neues THREE.WebGLRenderTarget(Breite, Höhe);

this.renderer.autoClear = falsch;

rendern() {
    requestAnimationFrame(dieses.rendern);

    dies.renderer.clear();

    // Hintergrund in fbo rendern
    dies.renderer.setRenderTarget(dieses.envFbo);
    dieser.Renderer.render( diese.Szene, diese.orthoKamera );

    // Hintergrund auf dem Bildschirm rendern
    this.renderer.setRenderTarget(null);
    dieser.Renderer.render( diese.Szene, diese.orthoKamera );
    this.renderer.clearDepth();

    // Geometrie auf dem Bildschirm rendern
    dieser.Renderer.render( diese.Szene, diese.Kamera );
};

Okay, jetzt ist es Zeit für ein wenig Theorie. Transparente Materialien, wie beispielsweise Glas, können gebogen und dadurch sichtbar gemacht werden. Das liegt daran, dass sich Licht in Glas langsamer bewegt als in Luft. Trifft eine Lichtwelle in einem Winkel auf ein Glasobjekt, führt diese Geschwindigkeitsänderung zu einer Richtungsänderung der Welle. Diese Änderung der Wellenrichtung beschreibt das Phänomen der Brechung.

Um dies im Code zu replizieren, müssen wir den Winkel zwischen unserem Augenvektor und dem Oberflächenvektor (Normalvektor) des Diamanten im Weltraum kennen. Aktualisieren wir unseren Vertex-Shader, um diese Vektoren zu berechnen.

variierender vec3-Augenvektor;
variierender vec3 worldNormal;

void main() {
    vec4 WeltPosition = ModellMatrix * vec4(Position, 1,0);
    Augenvektor = normalisieren (Weltposition.xyz – Kameraposition);
    WeltNormal = normalisieren( modelViewMatrix * vec4(normal, 0,0)).xyz;
    gl_Position = Projektionsmatrix * Modellansichtsmatrix * vec4(Position, 1,0);
}

Im Fragment-Shader können wir jetzt eyeVector und worldNormal als erste beiden Parameter der integrierten Brechungsfunktion von glsl verwenden. Der dritte Parameter ist das Verhältnis der Brechungsindizes, also der Brechungsindex (IOR) unseres schnellen Mediums (Luft) geteilt durch den IOR unseres langsamen Mediums (Glas). In diesem Fall beträgt der Wert 1,0/1,5, Sie können den Wert jedoch anpassen, um die gewünschten Ergebnisse zu erhalten. Beispielsweise hat Wasser einen IOR von 1,33 und Diamanten einen IOR von 2,42.

einheitliches Sampler2D-EnvMap;
einheitliche VEC2-Auflösung;

variierender vec3 worldNormal;
variierende vec3-Ansichtsrichtung;

void main() {
    // Bildschirmkoordinaten abrufen
    vec2 uv = gl_FragCoord.xy / Auflösung;

    vec3 normal = WeltNormal;
    // Brechung berechnen und zu den Bildschirmkoordinaten hinzufügen
    vec3 gebrochen = Brechung(Augenvektor, normal, 1,0/ior);
    uv += gebrochen.xy;
    
    // Probieren Sie die Hintergrundtextur
    vec4 tex = texture2D(umgebungsMap, uv);

    vec4-Ausgabe = tex;
    gl_FragColor = vec4(output.rgb, 1.0);
} 

sehr schön! Wir haben erfolgreich einen Brechungs-Shader geschrieben. Unsere Diamanten sind jedoch kaum sichtbar … teilweise, weil es uns nur um eine optische Eigenschaft des Glases geht. Nicht das gesamte Licht durchdringt das zu brechende Material; ein Teil wird sogar reflektiert. Mal sehen, wie das geht!

Schritt 2: Reflexion und die Fresnel-Gleichungen

Der Einfachheit halber berechnen wir in diesem Tutorial keine richtigen Reflexionen und verwenden nur Weiß als reflektiertes Licht. Woher wissen wir nun, wann wir reflektieren und wann wir brechen müssen? Theoretisch hängt dies vom Brechungsindex des Materials ab, und wenn der Winkel zwischen dem Einfallsvektor und der Oberflächennormalen größer als der kritische Winkel ist, wird die Lichtwelle reflektiert.

Im Fragment-Shader verwenden wir die Fresnel-Gleichungen, um das Verhältnis zwischen reflektierten und gebrochenen Strahlen zu berechnen. Leider ist diese Gleichung auch in glsl nicht integriert, Sie können sie aber hier kopieren:

float Fresnel(vec3 Augenvektor, vec3 Weltnormal) {
    return pow(1,0 + dot(Augenvektor, Weltnormal), 3,0);
}

Jetzt können wir die Brechungstexturfarbe einfach mit der weißen Reflexionsfarbe mischen, basierend auf dem Fresnel-Verhältnis, das wir gerade berechnet haben.

einheitliches Sampler2D-EnvMap;
einheitliche VEC2-Auflösung;

variierender vec3 worldNormal;
variierende vec3-Ansichtsrichtung;

float Fresnel(vec3 Augenvektor, vec3 Weltnormal) {
    return pow(1,0 + dot(Augenvektor, Weltnormal), 3,0);
}

void main() {
    // Bildschirmkoordinaten abrufen
    vec2 uv = gl_FragCoord.xy / Auflösung;

    vec3 normal = WeltNormal;
    // Brechung berechnen und zu den Bildschirmkoordinaten hinzufügen
    vec3 gebrochen = Brechung(Augenvektor, normal, 1,0/ior);
    uv += gebrochen.xy;

    // Probieren Sie die Hintergrundtextur
    vec4 tex = texture2D(umgebungsMap, uv);

    vec4-Ausgabe = tex;

    // Berechnen Sie das Fresnel-Verhältnis
    float f = Fresnel(Augenvektor, normal);

    // Mischen Sie die Brechungsfarbe und die Reflexionsfarbe
    Ausgabe.rgb = Mischung(Ausgabe.rgb, vec3(1.0), f);

    gl_FragColor = vec4(output.rgb, 1.0);
} 

Es sieht viel besser aus, aber es gibt immer noch einige Mängel ... Nun, wir können die andere Seite des transparenten Objekts nicht sehen. Lassen Sie uns das beheben!

Schritt 3: Mehrseitige Brechung

Aus unserem bisherigen Wissen über Reflexion und Brechung können wir verstehen, dass Licht innerhalb eines Objekts mehrmals hin und her reflektiert werden kann, bevor es es verlässt.

Um physikalisch korrekte Ergebnisse zu erhalten, müssten wir jeden Strahl verfolgen, aber das ist leider zu rechenintensiv, um es in Echtzeit zu rendern. Ich zeige Ihnen also eine einfache Annäherung, um zumindest optisch die Rückseite unseres Diamanten zu sehen.

In einem Fragment-Shader benötigen wir die Weltnormalen für die Vorder- und Rückseite der Geometrie. Da wir nicht beide Seiten gleichzeitig rendern können, müssen wir zuerst die nach hinten gerichteten Normalen in eine Textur rendern.

Lassen Sie uns wie in Schritt 1 ein neues ShaderMaterial erstellen, dieses Mal rendern wir die Welt jedoch normal als gl_FragColor.

variierender vec3 worldNormal;

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

Als Nächstes aktualisieren wir unsere Rendering-Schleife, um den Backface-Pass einzuschließen.

this.backfaceFbo = neues THREE.WebGLRenderTarget(Breite, Höhe);

...

rendern() {
    requestAnimationFrame(dieses.rendern);

    dies.renderer.clear();

    // Hintergrund in fbo rendern
    dies.renderer.setRenderTarget(dieses.envFbo);
    dieser.Renderer.render( diese.Szene, diese.orthoKamera );

    // Rendern Sie die Rückseiten der Diamanten in FBO
    dieses.Netz.Material = dieses.Rückseitenmaterial;
    dies.renderer.setRenderTarget(dieses.backfaceFbo);
    this.renderer.clearDepth();
    dieser.Renderer.render( diese.Szene, diese.Kamera );

    // Hintergrund auf dem Bildschirm rendern
    this.renderer.setRenderTarget(null);
    dieser.Renderer.render( diese.Szene, diese.orthoKamera );
    this.renderer.clearDepth();

    // Diamant mit Brechungsmaterial auf dem Bildschirm rendern
    dieses.Netz.Material = dieses.RefraktionsMaterial;
    dieser.Renderer.render( diese.Szene, diese.Kamera );
};

Jetzt probieren wir die normale Rückseitentextur in unserem brechenden Material aus.

vec3 RückseiteNormal = Texture2D(Rückseitenkarte, uv).rgb;

Schließlich kombinieren wir die vorderen und hinteren Normalen.

Gleitkomma a = 0,33;
vec3 normal = WeltNormal * (1,0 - a) - RückseiteNormal * a;

In dieser Gleichung ist a lediglich ein Skalarwert, der angibt, wie viel von der rückseitigen Normalen angewendet werden soll.

Wir haben es geschafft! Wir können alle Seiten eines Diamanten nur sehen, weil wir ihn aufgrund des Materials, aus dem er besteht, brechen und reflektieren.

Einschränkung

Wie ich bereits erklärt habe, ist es mit diesem Ansatz unmöglich, physikalisch korrekte transparente Materialien in Echtzeit zu rendern. Ein weiteres Problem tritt auf, wenn mehrere Glasobjekte voreinander gerendert werden. Da wir die Umgebung nur einmal abtasten, können wir nicht durch eine Kette von Objekten hindurchsehen. Schließlich funktioniert die Bildschirmraumbrechung, die ich hier demonstriert habe, in der Nähe der Kanten der Leinwand nicht gut, da Licht möglicherweise auf Werte außerhalb seiner Grenzen gebrochen wird und wir diese Daten beim Rendern der Hintergrundszene für das Renderziel nicht erfasst haben.

Natürlich gibt es Möglichkeiten, diese Einschränkungen zu überwinden, aber möglicherweise sind nicht alle davon gute Lösungen für Ihr Echtzeit-Rendering in WebGL.

Oben finden Sie Einzelheiten zur Verwendung von Threejs zur Realisierung einer Polygonbrechung in Echtzeit. Weitere Informationen zur JS-Bibliothek finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • So erzielen Sie mit three.js einen dynamischen 3D-Texteffekt
  • Three.js-Beispielcode zur Implementierung des Tautropfen-Animationseffekts
  • Detaillierte Erläuterung der Verwendung und Leistungstests von Multithreading in three.js
  • Detaillierte Analyse von three.js zur Anzeige chinesischer Schriftarten und Tween-Anwendungen
  • Beispielcode der Three.js-Off-Screen-Leinwand im WeChat-Minispiel
  • three.js verwendet uv und ThreeBSP, um eine Kurierkabinettfunktion zu erstellen
  • Detaillierte Erläuterung des Beispiels für integrierte Variablen im Three.js-Shader-Material
  • Die Vue-Seite führt three.js ein, um den Betrieb von 3D-Animationsszenen zu realisieren
  • Three.js-Beispielcode zum Erstellen dynamischer QR-Codes
  • Three.js-Beispielcode zum Mosaikieren von Bildern

<<:  So lassen sich Python-Skripte direkt unter Ubuntu ausführen

>>:  MySQL 5.7.10 Installations- und Konfigurations-Tutorial unter Windows

Artikel empfehlen

Sollte ich beim Erstellen einer Website die Kodierung UTF-8 oder GB2312 verwenden?

Beim Öffnen ausländischer Websites werden häufig ...

Detaillierte Erklärung des Cocoscreater-Prefabs

Inhaltsverzeichnis Fertighaus So erstellen Sie ei...

Detaillierte Erklärung des Plattformbusses des Linux-Treibers

Inhaltsverzeichnis 1. Einführung in den Plattform...

Neunundvierzig JavaScript-Tipps und Tricks

Inhaltsverzeichnis 1. Betrieb von js Integer 2. S...

Detaillierte Erklärung einiger Einstellungen für Tabellenanpassung und Überlauf

1. Zwei Eigenschaften des Tabellen-Resets: ①borde...

Einige Dinge, die beim Erstellen einer Webseite zu beachten sind

--Backup der Homepage 1.txt-Text 2. Scannen Sie da...

Detaillierte Erläuterung der Verwendung von MySQL Explain (Analyseindex)

EXPLAIN zeigt, wie MySQL Indizes verwendet, um Au...

So implementieren Sie die Vue-Bindungsklasse und den Inline-Bindungsstil

Inhaltsverzeichnis Bindungsklasse Inline-Stile bi...

Telnet wird im Alpine-Image zu busybox-extras verschoben

Das Telnet im Alpine-Image wurde nach Version 3.7...

MySQL-Optimierung: Cache-Optimierung (Fortsetzung)

In MySQL gibt es überall Caches. Wenn ich den Que...

JS-Version des Bildlupeneffekts

In diesem Artikel wird der spezifische JS-Code zu...