
Es gibt einige Einführungen in die Grundlagen der Arbeit mit Three.js im Web, aber Sie werden möglicherweise einen Mangel an Materialien zu fortgeschritteneren Themen feststellen. Eines dieser Themen ist die Kombination von Shadern und Szenen mit dreidimensionalen Modellen. In den Augen vieler unerfahrener Entwickler sind dies scheinbar inkompatible Dinge aus verschiedenen Welten. Anhand eines einfachen Beispiels für eine „Plasmakugel“ werden wir heute sehen, was ShaderMaterial ist und womit es gegessen wird, was der Effekt ist und wie schnell es möglich ist, eine gerenderte Szene nachzubearbeiten.
Es wird davon ausgegangen, dass der Leser mit den Grundlagen der Arbeit mit Three.js vertraut ist und versteht, wie Shader funktionieren. Wenn Sie dies noch nicht erlebt haben, empfehle ich dringend, dies zuerst zu lesen:
Aber fangen wir an ...
ShaderMaterial - was ist das?
Wir haben bereits gesehen, wie eine flache Textur verwendet wird und wie sie über ein dreidimensionales Objekt gespannt wird. Da diese Textur ein gewöhnliches Bild war. Als wir das Schreiben von Fragment-Shadern untersuchten, war auch dort alles flach. Also: Wenn wir mit einem Shader ein flaches Bild erzeugen können, warum nicht als Textur?
Diese Idee bildet die Grundlage für Shader-Material. Beim Erstellen von Material für ein dreidimensionales Objekt geben wir Shader anstelle einer Textur dafür an. In seiner Grundform sieht es ungefähr so aus:
const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
Der Fragment-Shader wird verwendet, um die Textur des Materials zu erstellen, und Sie fragen sich natürlich, was der Vertex-Shader tun wird. Wird er wieder eine banale Nachzählung der Koordinaten durchführen? Ja, wir beginnen mit dieser einfachen Option, können aber auch einen Versatz festlegen oder andere Manipulationen für jeden Scheitelpunkt eines dreidimensionalen Objekts durchführen - jetzt gibt es keine Einschränkungen für die Ebene. Aber es ist besser, all dies anhand eines Beispiels zu betrachten. In Worten wird wenig verstanden. Erstellen Sie eine Szene und machen Sie eine Kugel in der Mitte.

Als Material für die Kugel verwenden wir ShaderMaterial:
const geometry = new THREE.SphereBufferGeometry(30, 64, 64); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
Der Vertex-Shader ist neutral:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Beachten Sie, dass Three.js seine einheitlichen Variablen übergibt. Wir müssen nichts tun, sie sind impliziert. An sich enthalten sie alle Arten von Matrizen, auf die wir bereits von JS aus zugreifen können, sowie die Position der Kamera. Stellen Sie sich vor, dass am Anfang der Shader selbst etwas eingefügt wird:
Darüber hinaus werden mehrere Attributvariablen an den Vertex-Shader übergeben:
attribute vec3 position; attribute vec3 normal; attribute vec2 uv;
An den Namen ist klar, was es ist - die Position des aktuellen Scheitelpunkts, die Normale zur Oberfläche an diesem Punkt und die Koordinaten auf der Textur, der der Scheitelpunkt entspricht.
Traditionell werden Koordinaten im Raum als (x, y, z) und Koordinaten auf der Texturebene als (u, v) bezeichnet. Daher der Name der Variablen. Sie werden ihn oft in verschiedenen Beispielen treffen. Theoretisch müssen wir diese Koordinaten auf den Fragment-Shader übertragen, um dort mit ihnen arbeiten zu können. Wir werden es tun.
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Zunächst sollte der Fragment-Shader ungefähr so aussehen:
#define EPSILON 0.02 varying vec2 vUv; void main() { if ((fract(vUv.x * 10.0) < EPSILON) || (fract(vUv.y * 10.0) < EPSILON)) { gl_FragColor = vec4(vec3(0.0), 1.0); } else { gl_FragColor = vec4(1.0); } }
Erstellen Sie einfach ein Netz. Wenn Sie ein wenig nachdenken, dann ist es in der Ebene nur ein Quadratgitter, aber da wir es einer Kugel überlagern, ist es verzerrt und verwandelt sich in einen Globus. Auf Wikipedia gibt es ein gutes Bild, das zeigt, was passiert:

Das heißt, im Fragment-Shader erstellen wir eine flache Textur, wie in der Mitte dieser Abbildung, und Three.js zieht sie dann auf die Kugel. Sehr bequem.
Bei komplexeren Modellen ist der Sweep natürlich komplizierter. In der Regel arbeiten wir beim Erstellen verschiedener Design-Sites mit einfachen geometrischen Formen, und es ist leicht vorstellbar, dass sich Ihr Kopf bewegt.
Ok, was kannst du dagegen tun?
Das Hauptmerkmal ist, dass sich das Shader-Material im Laufe der Zeit ändern kann. Dies ist keine statische Sache, die wir einmal gezeichnet und vergessen haben, wir können sie animieren. Darüber hinaus sowohl in Farbe (im Fragment-Shader) als auch in Form (im Scheitelpunkt). Dies ist ein sehr mächtiges Werkzeug.
In unserem Beispiel machen wir ein Feuer, das eine Kugel umhüllt. Es gibt zwei Kugeln - eine gewöhnliche (innen) und eine zweite aus dem Shader-Material (außen, mit großem Radius). Das Hinzufügen einer weiteren Kugel wird nicht kommentiert.

Fügen Sie zunächst die Zeit als einheitliche Variable für die Shader in unserem Material hinzu. Nirgendwo ohne Zeit. Wir haben dies bereits in reinem JS gemacht, aber in Three.js ist es genauso einfach. Lassen Sie die Zeit in den Shadern uTime heißen und in der Variablen TIME gespeichert werden:
function updateUniforms() { SCENE.traverse((child) => { if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') { child.material.uniforms.uTime.value = TIME; child.material.needsUpdate = true; } }); }
Wir aktualisieren alles bei jedem Aufruf der Animationsfunktion:
function animate() { requestAnimationFrame(animate); TIME += 0.005; updateUniforms(); render(); }
Feuer
Das Erzeugen eines Feuers ist im Wesentlichen dem Erzeugen einer Landschaft sehr ähnlich, aber anstelle von Höhen Farbe. Oder Transparenz wie in unserem Fall.
Funktionen für Zufälligkeit und Rauschen, die wir bereits gesehen haben, werden wir nicht im Detail analysieren. Alles, was wir tun müssen, ist, Geräusche mit unterschiedlichen Frequenzen zu erzeugen, um Abwechslung zu schaffen, und jedes dieser Geräusche mit unterschiedlichen Geschwindigkeiten zu bewegen. Du bekommst so etwas wie Flammen, große bewegen sich langsam, kleine bewegen sich schneller:
uniform float uTime; varying vec2 vUv; float rand(vec2); float noise(vec2); void main() { vec2 position1 = vec2(vUv.x * 4.0, vUv.y - uTime); vec2 position2 = vec2(vUv.x * 4.0, vUv.y - uTime * 2.0); vec2 position3 = vec2(vUv.x * 4.0, vUv.y - uTime * 3.0); float color = ( noise(position1 * 5.0) + noise(position2 * 10.0) + noise(position3 * 15.0)) / 3.0; gl_FragColor = vec4(0.0, 0.0, 0.0, color - smoothstep(0.1, 1.3, vUv.y)); } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 position) { vec2 blockPosition = floor(position); float topLeftValue = rand(blockPosition); float topRightValue = rand(blockPosition + vec2(1.0, 0.0)); float bottomLeftValue = rand(blockPosition + vec2(0.0, 1.0)); float bottomRightValue = rand(blockPosition + vec2(1.0, 1.0)); vec2 computedValue = smoothstep(0.0, 1.0, fract(position)); return mix(topLeftValue, topRightValue, computedValue.x) + (bottomLeftValue - topLeftValue) * computedValue.y * (1.0 - computedValue.x) + (bottomRightValue - topRightValue) * computedValue.x * computedValue.y; }
Damit die Flamme nicht die gesamte Kugel bedeckt, spielen wir mit dem vierten Farbparameter - Transparenz - und binden ihn an die y-Koordinate. In unserem Fall ist diese Option sehr praktisch. Allgemeiner ausgedrückt wenden wir einen Gradienten mit Transparenz auf Rauschen an.
In Zeiten wie diesen ist es nützlich, sich an die Smoothstep-Funktion zu erinnern.
Im Allgemeinen ist ein solcher Ansatz zur Erzeugung von Feuer mit Shadern ein Klassiker. Sie werden ihn oft an verschiedenen Orten treffen. Es wird nützlich sein, mit magischen Zahlen zu spielen - sie werden im Beispiel zufällig festgelegt, und wie das Plasma aussehen wird, hängt von ihnen ab.
Um das Feuer interessanter zu machen, gehen wir weiter zum Vertex-Shader und einem kleinen Schamanen ...
Wie kann man die Flamme im Weltraum ein wenig "gießen" lassen? Für Anfänger kann diese Frage trotz ihrer Einfachheit große Schwierigkeiten verursachen. Ich habe sehr komplexe Ansätze zur Lösung dieses Problems gesehen, aber im Wesentlichen müssen wir die Eckpunkte auf der Kugel entlang der Linien „von ihrem Zentrum“ aus reibungslos verschieben. Hin und her, hin und her. Three.js hat uns bereits die aktuelle Position des Scheitelpunkts übergeben und normal - wir werden sie verwenden. Für "hin und her" passt eine eingeschränkte Funktion, zum Beispiel ein Sinus. Sie können natürlich experimentieren, aber der Sinus ist die Standardoption.
Ich weiß nicht, was ich nehmen soll - nimm den Sinus. Besser noch die Summe der Sinusse mit unterschiedlichen Frequenzen.
Wir verschieben die Koordinaten normal auf den erhaltenen Wert und berechnen nach der zuvor bekannten Formel neu.
uniform float uTime; varying vec2 vUv; void main() { vUv = uv; vec3 delta = normal * sin(position.x * position.y * uTime / 10.0); vec3 newPosition = position + delta; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
Was wir bekommen, ist keine Kugel mehr. Dies ... Ich weiß nicht einmal, ob dieser einen Namen hat. Aber vergessen Sie auch hier nicht, mit den Gewinnchancen herumzuspielen - sie wirken sich stark aus. Bei der Erstellung solcher Effekte wird häufig etwas durch Ausprobieren ausgewählt, und es ist sehr nützlich, die „mathematische Intuition“ in sich selbst zu entwickeln - die Fähigkeit, sich mehr oder weniger vorzustellen, wie sich eine Funktion verhält und wie sie von welchen Variablen abhängt.
Zu diesem Zeitpunkt haben wir ein interessantes, aber etwas ungeschicktes Bild. Schauen wir uns also zuerst die Nachbearbeitung an und gehen dann zu einem lebenden Beispiel über.
Nachbearbeitung
Die Fähigkeit, etwas mit dem gerenderten Three.js-Bild zu tun, ist eine sehr nützliche Sache, während sie in zahlreichen Unterrichtsreihen zu Unrecht vergessen wird. Technisch wird dies wie folgt implementiert: Das Bild, das der Renderer uns gegeben hat, wird an EffectComposer gesendet (solange es sich um eine Black Box handelt), das etwas an sich schamanisiert und das endgültige Bild auf Leinwand anzeigt. Das heißt, nach dem Renderer wird ein weiteres Modul hinzugefügt. Wir übertragen Parameter an diesen Komponisten - was tun mit dem empfangenen Bild? Ein solcher Parameter heißt pass. In gewisser Weise arbeitet der Komponist wie ein Gulp - er macht nichts, wir geben ihm Plugins, die die Arbeit bereits erledigen. Vielleicht ist es nicht ganz richtig, das zu sagen, aber die Idee sollte klar sein.
Alles, was wir weiter verwenden werden, ist nicht in der Grundstruktur von Three.js enthalten, daher verbinden wir einige Abhängigkeiten und Abhängigkeiten der Abhängigkeiten selbst:
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script>
Denken Sie daran, dass diese Skripte in den drei Paketen enthalten sind und Sie all dies mithilfe eines Webpacks oder von Analoga in einem einzigen Bundle zusammenfassen können.
In seiner Grundform wird der Komponist wie folgt erstellt:
COMPOSER = new THREE.EffectComposer(RENDERER); COMPOSER.setSize(window.innerWidth, window.innerHeight); const renderPass = new THREE.RenderPass(SCENE, CAMERA); renderPass.renderToScreen = true; COMPOSER.addPass(renderPass);
RenderPass macht eigentlich nichts Neues. Es wird nur das gerendert, was wir von einem normalen Renderer erhalten haben. Wenn Sie sich den Quellcode von RenderPass ansehen, finden Sie dort den Standard-Renderer. Da jetzt das Rendern dort stattfindet, müssen wir den Renderer in unserem Skript durch den Komponisten ersetzen:
function render() {
Dieser Ansatz mit RenderPass als erstem Durchgang ist bei der Arbeit mit EffectComposer Standard. Normalerweise müssen wir zuerst ein gerendertes Bild der Szene erstellen und dann etwas damit anfangen.
In den Beispielen von Three.js finden Sie im Abschnitt Nachbearbeitung etwas namens UnrealBloomPass. Dies ist ein portiertes Skript aus der Unreal-Engine. Es fügt ein wenig Glanz hinzu, der verwendet werden kann, um eine schönere Beleuchtung zu erzeugen. Oft ist dies der erste Schritt zur Bildverbesserung.
const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 1, 0.1); bloomPass.renderToScreen = true; COMPOSER.addPass(bloomPass);
Bitte beachten Sie: Die Option renderToScreen ist nur auf den letzten Durchgang festgelegt, den wir an den Komponisten übergeben haben.
Aber lassen Sie uns schon sehen, welche Art von Glanz dieser BloomPass uns verlieh und wie er zur Kugel passt:
Stimmen Sie zu, dies ist viel interessanter als nur eine Kugel und eine gewöhnliche Lichtquelle, wie sie normalerweise in den ersten Lektionen auf Three.js gezeigt werden.
Aber wir werden noch weiter gehen ...
Noch mehr Shader für den Shadergott!

Es ist sehr nützlich, console.log zu verwenden und sich die Struktur des Komponisten anzusehen. Darin finden Sie einige Elemente mit den Namen renderTarget1, renderTarget2 usw., wobei die Zahlen den Indizes der übergebenen Durchgänge entsprechen. Und dann wird klar, warum EffectComposer so genannt wird. Es funktioniert nach dem Prinzip der Filter in SVG. Denken Sie daran, dass Sie dort das Ergebnis der Durchführung einiger Filter in anderen verwenden können? Hier das gleiche - Sie können Effekte kombinieren.
Die Verwendung von console.log zum Verständnis der internen Struktur von Three.js-Objekten und vielen anderen Bibliotheken ist sehr nützlich. Verwenden Sie diesen Ansatz häufiger, um besser zu verstehen, was was ist.
Fügen Sie einen weiteren Pass hinzu. Diesmal wird es ShaderPass sein.
const shader = { uniforms: { uRender: { value: COMPOSER.renderTarget2 }, uTime: { value: TIME } }, vertexShader: document.getElementById('postprocessing-vertex-shader').textContent, fragmentShader: document.getElementById('postprocessing-fragment-shader').textContent }; const shaderPass = new THREE.ShaderPass(shader); shaderPass.renderToScreen = true; COMPOSER.addPass(shaderPass);
RenderTarget2 enthält das Ergebnis des vorherigen Durchlaufs - bloomPass (es war der zweite in Folge). Wir verwenden es als Textur (dies ist im Wesentlichen ein flach gerendertes Bild) und übergeben es als einheitliche Variable an den neuen Shader.
Wahrscheinlich lohnt es sich, hier zu bremsen und die ganze Magie zu verwirklichen ...
Erstellen Sie als Nächstes einen einfachen Vertex-Shader. In den meisten Fällen müssen wir zu diesem Zeitpunkt nichts mit den Eckpunkten tun, sondern übergeben nur die Koordinaten (u, v) an den Fragment-Shader:
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Und fragmentarisch können wir Spaß an unserem Geschmack und unserer Farbe haben. Zum Beispiel können wir einen leichten Glitch-Effekt hinzufügen, alles schwarz und weiß machen und mit Helligkeit / Kontrast spielen:
uniform sampler2D uRender; uniform float uTime; varying vec2 vUv; float rand(vec2); void main() { float randomValue = rand(vec2(floor(vUv.y * 7.0), uTime / 1.0)); vec4 color; if (randomValue < 0.02) { color = texture2D(uRender, vec2(vUv.x + randomValue - 0.01, vUv.y)); } else { color = texture2D(uRender, vUv); } float lightness = (color.r + color.g + color.b) / 3.0; color.rgb = vec3(smoothstep(0.02, 0.7, lightness)); gl_FragColor = color; } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
Schauen wir uns das Ergebnis an:
Wie Sie sehen können, wurden Filter auf die Kugel gelegt. Es ist immer noch dreidimensional, nichts ist kaputt gegangen, aber auf der Leinwand haben wir ein verarbeitetes Bild.
Fazit
Die Shader-Materialien und die Nachbearbeitung in Three.js sind zwei kleine, aber sehr leistungsstarke Tools, die es definitiv wert sind, verwendet zu werden. Es gibt viele Möglichkeiten für ihre Verwendung - alles ist durch Ihre Vorstellungskraft begrenzt. Selbst die einfachsten Szenen können mit ihrer Hilfe bis zur Unkenntlichkeit verändert werden.