Erstellen von Shadern mit Babylon.js
Veröffentlicht: 2022-03-10Shader sind ein Schlüsselkonzept, wenn Sie die rohe Kraft Ihrer GPU entfesseln möchten. Ich werde Ihnen helfen zu verstehen, wie sie funktionieren, und dank Babylon.js sogar auf einfache Weise mit ihrer inneren Kraft experimentieren.
Wie funktioniert es?
Bevor wir experimentieren, müssen wir sehen, wie die Dinge intern funktionieren.
Bei hardwarebeschleunigtem 3D müssen Sie sich mit zwei CPUs auseinandersetzen: der Haupt-CPU und der GPU. Die GPU ist eine Art extrem spezialisierte CPU.
Weiterführende Literatur zu SmashingMag:
- Erstellen eines plattformübergreifenden WebGL-Spiels mit Babylon.js
- Verwenden der Gamepad-API in Webspielen
- Einführung in die polygonale Modellierung und Three.js
- So erstellen Sie eine reaktionsschnelle 8-Bit-Drum-Maschine
Die GPU ist eine Zustandsmaschine, die Sie mithilfe der CPU einrichten. Beispielsweise konfiguriert die CPU die GPU so, dass Linien anstelle von Dreiecken gerendert werden; es definiert, ob Transparenz eingeschaltet ist; und so weiter.
Sobald alle Zustände gesetzt sind, kann die CPU definieren, was zu rendern ist: die Geometrie.
Die Geometrie setzt sich zusammen aus:
- eine Liste von Punkten, die Vertices genannt und in einem Array namens Vertex Buffer gespeichert werden,
- eine Liste von Indizes, die die Flächen (oder Dreiecke) definieren, die in einem Array namens index buffer gespeichert sind.
Der letzte Schritt für die CPU besteht darin, zu definieren, wie die Geometrie gerendert werden soll; Für diese Aufgabe definiert die CPU Shader in der GPU. Shader sind Codeteile, die die GPU für jeden der Scheitelpunkte und Pixel ausführt, die sie rendern muss. (Ein Scheitelpunkt – oder Scheitelpunkte, wenn es mehrere davon gibt – ist ein „Punkt“ in 3D).
Es gibt zwei Arten von Shadern: Vertex-Shader und Pixel- (oder Fragment-) Shader.
Grafikpipeline
Bevor wir uns mit Shadern befassen, gehen wir einen Schritt zurück. Zum Rendern von Pixeln nimmt die GPU die von der CPU definierte Geometrie und führt Folgendes aus:
- Unter Verwendung des Indexpuffers werden drei Eckpunkte gesammelt, um ein Dreieck zu definieren.
- Der Indexpuffer enthält eine Liste von Scheitelpunktindizes. Das bedeutet, dass jeder Eintrag im Indexpuffer die Nummer eines Scheitelpunkts im Scheitelpuffer ist.
- Dies ist sehr nützlich, um das Duplizieren von Scheitelpunkten zu vermeiden.
Der folgende Indexpuffer ist beispielsweise eine Liste mit zwei Gesichtern: [1 2 3 1 3 4]. Die erste Fläche enthält Scheitelpunkt 1, Scheitelpunkt 2 und Scheitelpunkt 3. Die zweite Fläche enthält Scheitelpunkt 1, Scheitelpunkt 3 und Scheitelpunkt 4. Es gibt also vier Scheitelpunkte in dieser Geometrie:
Der Vertex-Shader wird auf jeden Scheitelpunkt des Dreiecks angewendet. Das Hauptziel des Vertex-Shaders besteht darin, für jeden Vertex (die Projektion des 3D-Vertex auf dem 2D-Bildschirm) ein Pixel zu erzeugen:
Unter Verwendung dieser drei Pixel (die ein 2D-Dreieck auf dem Bildschirm definieren) interpoliert die GPU alle an das Pixel angehängten Werte (zumindest ihre Positionen), und der Pixel-Shader wird auf jedes Pixel angewendet, das im 2D-Dreieck enthalten ist Erzeuge eine Farbe für jedes Pixel:
Dieser Vorgang wird für jede durch den Indexpuffer definierte Fläche durchgeführt.
Offensichtlich ist die GPU aufgrund ihrer Parallelität in der Lage, diesen Schritt für viele Gesichter gleichzeitig zu verarbeiten und eine wirklich gute Leistung zu erzielen.
GLSL
Wir haben gerade gesehen, dass die GPU zum Rendern von Dreiecken zwei Shader benötigt: den Vertex-Shader und den Pixel-Shader. Diese Shader sind in einer Sprache namens Graphics Library Shader Language (GLSL) geschrieben. Es sieht aus wie C.
Hier ist ein Beispiel für einen gängigen Vertex-Shader:
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Vertex-Shader-Struktur
Ein Vertex-Shader enthält Folgendes:
- Attribute . Ein Attribut definiert einen Teil eines Scheitelpunkts. Standardmäßig sollte ein Scheitelpunkt mindestens eine Position enthalten (ein
vector3:x, y, z
). Als Entwickler können Sie jedoch entscheiden, weitere Informationen hinzuzufügen. Zum Beispiel gibt es im vorherigen Shader einenvector2
namensuv
(dh Texturkoordinaten, mit denen Sie eine 2D-Textur auf ein 3D-Objekt anwenden können). - Uniformen . Eine Uniform ist eine Variable, die vom Shader verwendet und von der CPU definiert wird. Die einzige Einheitlichkeit, die wir hier haben, ist eine Matrix, die verwendet wird, um die Position des Scheitelpunkts (x, y, z) auf den Bildschirm (x, y) zu projizieren.
- Variierend . Variierende Variablen sind Werte, die vom Vertex-Shader erzeugt und an den Pixel-Shader übertragen werden. Hier überträgt der Vertex-Shader einen
vUV
-Wert (eine einfache Kopie vonuv
) an den Pixel-Shader. Das bedeutet, dass hier ein Pixel mit einer Position und Texturkoordinaten definiert wird. Diese Werte werden von der GPU interpoliert und vom Pixel-Shader verwendet. - Haupt . Die Funktion namens
main
ist der Code, der von der GPU für jeden Scheitelpunkt ausgeführt wird und mindestens einen Wert fürgl_position
(die Position des aktuellen Scheitelpunkts auf dem Bildschirm) erzeugen muss.
Wir können in unserem Beispiel sehen, dass der Vertex-Shader ziemlich einfach ist. Es generiert eine Systemvariable (beginnend mit gl_
) namens gl_position
, um die Position des zugehörigen Pixels zu definieren, und es setzt eine variable Variable namens vUV
.
Der Voodoo hinter Matrizen
Die Sache mit unserem Shader ist, dass wir eine Matrix namens worldViewProjection
haben und diese Matrix verwenden, um die Scheitelpunktposition auf die Variable gl_position
zu projizieren. Das ist cool, aber wie bekommen wir den Wert dieser Matrix? Es ist eine Uniform, also müssen wir es auf der CPU-Seite definieren (unter Verwendung von JavaScript).
Dies ist einer der komplexen Teile von 3D. Sie müssen komplexe Mathematik verstehen (oder Sie müssen eine 3D-Engine wie Babylon.js verwenden, die wir später sehen werden).
Die worldViewProjection
-Matrix ist die Kombination aus drei verschiedenen Matrizen:
Die Verwendung der resultierenden Matrix ermöglicht es uns, 3D-Scheitelpunkte in 2D-Pixel umzuwandeln, während der Blickwinkel und alles, was mit Position, Skalierung und Drehung des aktuellen Objekts zusammenhängt, berücksichtigt werden.
Das liegt in Ihrer Verantwortung als 3D-Entwickler: diese Matrix zu erstellen und aktuell zu halten.
Zurück zu den Shadern
Sobald der Vertex-Shader auf jedem Vertex ausgeführt wird (also dreimal), haben wir drei Pixel mit der richtigen gl_position
und einem vUV
Wert. Die GPU interpoliert diese Werte für jedes Pixel, das in dem mit diesen Pixeln erzeugten Dreieck enthalten ist.
Dann wird für jedes Pixel der Pixel-Shader ausgeführt:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Pixel- (oder Fragment-) Shader-Struktur
Der Aufbau eines Pixel-Shaders ähnelt dem eines Vertex-Shaders:
- Variierend . Variierende Variablen sind Werte, die von dem Vertex-Shader erzeugt und an den Pixel-Shader übertragen werden. Hier erhält der Pixel-Shader einen
vUV
Wert vom Vertex-Shader. - Uniformen . Eine Uniform ist eine Variable, die vom Shader verwendet und von der CPU definiert wird. Die einzige Uniform, die wir hier haben, ist ein Sampler, ein Werkzeug zum Lesen von Texturfarben.
- Haupt . Die Funktion namens
main
ist der Code, der von der GPU für jedes Pixel ausgeführt wird und der mindestens einen Wert fürgl_FragColor
(dh die Farbe des aktuellen Pixels) erzeugen muss.
Dieser Pixel-Shader ist ziemlich einfach: Er liest die Farbe aus der Textur unter Verwendung von Texturkoordinaten aus dem Vertex-Shader (der sie wiederum vom Vertex erhält).
Das Problem ist, dass man bei der Entwicklung von Shadern erst auf halbem Weg ist, weil man sich dann mit viel WebGL-Code auseinandersetzen muss. In der Tat ist WebGL wirklich leistungsfähig, aber auch sehr Low-Level, und Sie müssen alles selbst tun, von der Erstellung der Puffer bis zur Definition von Vertex-Strukturen. Sie müssen auch alle Berechnungen durchführen, alle Zustände festlegen, das Laden von Texturen handhaben und so weiter.
Zu schwer? BABYLON.ShaderMaterial zur Rettung
Ich weiß, was Sie denken: „Shader sind wirklich cool, aber ich möchte mich nicht mit den internen Installationen von WebGL oder sogar mit der Mathematik beschäftigen.“
Und du hast Recht! Das ist eine vollkommen berechtigte Frage, und genau deshalb habe ich Babylon.js erstellt!
Um Babylon.js zu verwenden, benötigen Sie zunächst eine einfache Webseite:
<!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>
Sie werden feststellen, dass Shader durch <script>
-Tags definiert werden. Mit Babylon.js können Sie diese auch in separaten Dateien ( .fx
-Dateien) definieren.
- Babylon.js-Quelle
- GitHub-Repository
Schließlich ist der Haupt-JavaScript-Code dieser:
"use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };
Sie können sehen, dass ich BABYLON.ShaderMaterial
verwende, um die Last des Kompilierens, Verknüpfens und Handhabens von Shadern loszuwerden.
Wenn Sie BABYLON.ShaderMaterial
erstellen, müssen Sie das DOM-Element angeben, das zum Speichern der Shader verwendet wird, oder den Basisnamen der Dateien, in denen sich die Shader befinden. Wenn Sie sich für die Verwendung von Dateien entscheiden, müssen Sie für jeden Shader eine Datei erstellen und das folgende Muster verwenden: basename.vertex.fx
und basename.fragment.fx
. Dann müssen Sie das Material wie folgt erstellen:
var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });
Sie müssen auch die Namen der von Ihnen verwendeten Attribute und Uniformen angeben.
Dann können Sie die Werte Ihrer Uniformen und Musterstücke direkt mit den setTexture
, setFloat
, setFloats
, setColor3
, setColor4
, setVector2
, setVector3
, setVector4
, setMatrix
.
Ziemlich einfach, oder?
Und erinnern Sie sich an die vorherige worldViewProjection
-Matrix mit Babylon.js und BABYLON.ShaderMaterial
. Sie müssen sich einfach keine Sorgen machen! BABYLON.ShaderMaterial
berechnet es automatisch für Sie, weil Sie es in der Liste der Uniformen deklarieren.
BABYLON.ShaderMaterial
kann auch die folgenden Matrizen für Sie verarbeiten:
-
world
, -
view
, -
projection
, -
worldView
, -
worldViewProjection
.
Keine Notwendigkeit mehr für Mathe. Beispielsweise wird jedes Mal, wenn Sie sphere.rotation.y += 0.05
ausführen, die world
der Kugel für Sie generiert und an die GPU übertragen.
Überzeugen Sie sich selbst vom Live-Ergebnis.
Erstellen Sie Ihren eigenen Shader (CYOS)
Lassen Sie uns jetzt größer werden und eine Seite erstellen, auf der Sie Ihre eigenen Shader dynamisch erstellen und das Ergebnis sofort sehen können. Diese Seite wird denselben Code verwenden, den wir zuvor besprochen haben, und wird das BABYLON.ShaderMaterial
Objekt verwenden, um Shader zu kompilieren und auszuführen, die Sie erstellen werden.
Ich habe den ACE-Code-Editor für Create Your Own Shader (CYOS) verwendet. Es ist ein unglaublicher Code-Editor mit Syntaxhervorhebung. Schauen Sie es sich gerne an.
Über das erste Kombinationsfeld können Sie vordefinierte Shader auswählen. Wir werden jeden von ihnen gleich danach sehen.
Sie können auch das Mesh (dh das 3D-Objekt) ändern, das zur Vorschau Ihrer Shader verwendet wird, indem Sie das zweite Kombinationsfeld verwenden.
Die Compile-Schaltfläche wird verwendet, um aus Ihren Shadern ein neues BABYLON.ShaderMaterial
zu erstellen. Der von dieser Schaltfläche verwendete Code lautet wie folgt:
// Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;
Unglaublich einfach, oder? Das Material ist bereit, Ihnen drei vorberechnete Matrizen ( world
, Weltbild und worldViewProjection
worldView
Scheitelpunkte werden mit Positions-, Normalen- und Texturkoordinaten geliefert. Zwei Texturen sind auch bereits für Sie geladen:
Schließlich aktualisiere ich im renderLoop
zwei praktische Uniformen:
- Eine heißt
time
und bekommt ein paar lustige Animationen. - Die andere heißt
cameraPosition
, die die Position der Kamera in Ihre Shader bringt (nützlich für Beleuchtungsgleichungen).
engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });
Einfacher Shader
Beginnen wir mit dem allerersten in CYOS definierten Shader: dem Basis-Shader.
Diesen Shader kennen wir bereits. Es berechnet die gl_position
und verwendet Texturkoordinaten, um eine Farbe für jedes Pixel abzurufen.
Um die Pixelposition zu berechnen, brauchen wir nur die worldViewProjection
-Matrix und die Position des Scheitelpunkts:
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Texturkoordinaten ( uv
) werden unverändert an den Pixelshader übertragen.
Bitte beachten Sie, dass wir in der ersten Zeile sowohl für den Vertex- als auch für den Pixel-Shader ein precision mediump float
hinzufügen müssen, da Chrome dies erfordert. Es gibt an, dass wir für eine bessere Leistung keine Floating-Werte mit voller Genauigkeit verwenden.
Der Pixel-Shader ist noch einfacher, da wir nur Texturkoordinaten verwenden und eine Texturfarbe abrufen müssen:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Wir haben zuvor gesehen, dass die textureSampler
Uniform mit der amiga
-Textur gefüllt ist. Das Ergebnis ist also folgendes:
Schwarz-Weiß-Shader
Fahren wir mit einem neuen Shader fort: dem Schwarz-Weiß-Shader. Das Ziel dieses Shaders ist es, den vorherigen zu verwenden, aber mit einem reinen Schwarz-Weiß-Rendermodus.
Dazu können wir denselben Vertex-Shader beibehalten. Der Pixel-Shader wird leicht modifiziert.
Die erste Option, die wir haben, ist, nur eine Komponente zu nehmen, wie zum Beispiel die grüne:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }
Wie Sie sehen können, haben wir anstelle von .rgb
(dieser Vorgang wird Swizzle genannt) .ggg
verwendet.
Aber wenn wir einen wirklich genauen Schwarz-Weiß-Effekt wollen, wäre die Berechnung der Luminanz (die alle Komponenten berücksichtigt) besser:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }
Die dot
(oder das dot
) wird wie folgt berechnet: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z
.
In unserem Fall ist also luminance = r * 0.3 + g * 0.59 + b * 0.11
. (Diese Werte basieren auf der Tatsache, dass das menschliche Auge grün empfindlicher ist.)
Klingt cool, oder?
Zellschattierungs-Shader
Kommen wir zu einem komplexeren Shader: dem Zellschattierungs-Shader.
Dieser erfordert, dass wir die Normale des Scheitelpunkts und die Position des Scheitelpunkts in den Pixel-Shader bekommen. Der Vertex-Shader sieht also so aus:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }
Bitte beachten Sie, dass wir auch die Weltmatrix verwenden, da Position und Normale ohne Transformation gespeichert werden und wir die Weltmatrix anwenden müssen, um die Drehung des Objekts zu berücksichtigen.
Der Pixel-Shader ist wie folgt:
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }
Das Ziel dieses Shaders ist es, Licht zu simulieren, und anstatt eine glatte Schattierung zu berechnen, wenden wir das Licht gemäß bestimmten Helligkeitsschwellenwerten an. Wenn beispielsweise die Lichtintensität zwischen 1 (Maximum) und 0,95 liegt, wird die Farbe des Objekts (aus der Textur abgerufen) direkt angewendet. Liegt die Intensität zwischen 0,95 und 0,5, würde die Farbe um den Faktor 0,8 abgeschwächt. Und so weiter.
Es gibt hauptsächlich vier Schritte in diesem Shader.
Zuerst deklarieren wir Schwellen- und Pegelkonstanten.
Dann berechnen wir die Beleuchtung mit der Phong-Gleichung (wir gehen davon aus, dass sich das Licht nicht bewegt):
vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));
Die Lichtintensität pro Pixel hängt vom Winkel zwischen der Normalen und der Lichtrichtung ab.
Dann erhalten wir die Texturfarbe für das Pixel.
Schließlich überprüfen wir den Schwellenwert und wenden den Pegel auf die Farbe an.
Das Ergebnis sieht aus wie ein Cartoon-Objekt:
Phong-Shader
Wir haben einen Teil der Phong-Gleichung im vorherigen Shader verwendet. Lassen Sie es uns jetzt vollständig verwenden.
Der Vertex-Shader ist hier eindeutig einfach, da alles im Pixel-Shader erledigt wird:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }
Gemäß der Gleichung müssen wir die „diffusen“ und „spiegelnden“ Teile berechnen, indem wir die Lichtrichtung und die Normale des Scheitelpunkts verwenden:
precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }
Wir haben den diffusen Teil bereits im vorherigen Shader verwendet, also müssen wir hier nur den spiegelnden Teil hinzufügen. Weitere Informationen zur Phong-Schattierung finden Sie auf Wikipedia.
Das Ergebnis unserer Sphäre:
Shader verwerfen
Für den Discard-Shader möchte ich ein neues Konzept einführen: das discard
Schlüsselwort.
Dieser Shader verwirft alle nicht roten Pixel und erzeugt die Illusion eines ausgegrabenen Objekts.
Der Vertex-Shader ist derselbe, der vom Basis-Shader verwendet wird:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Der Pixel-Shader auf seiner Seite muss die Farbe testen und Verwerfen verwenden, wenn beispielsweise der Grünanteil zu hoch ist:
precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }
Das Ergebnis ist etwas komisch:
Wellen-Shader
Wir haben viel mit Pixel-Shader gespielt, aber ich möchte Sie auch wissen lassen, dass wir mit Vertex-Shadern eine Menge machen können.
Für den Wave-Shader werden wir den Phong-Pixel-Shader wiederverwenden.
Der Vertex-Shader verwendet die einheitlich benannte time
, um einige animierte Werte zu erhalten. Unter Verwendung dieser Uniform erzeugt der Shader eine Welle mit den Positionen der Scheitelpunkte:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }
Ein Sinus wird auf position.y
angewendet, und das Ergebnis ist wie folgt:
Kartierung der sphärischen Umgebung
Dieser wurde weitgehend von dem Artikel „Creating a Spherical Reflection/Environment Mapping Shader“ inspiriert. Ich lasse Sie diesen ausgezeichneten Artikel lesen und mit dem zugehörigen Shader spielen.
Fresnel-Shader
Ich möchte diesen Artikel mit meinem Favoriten abschließen: dem Fresnel-Shader.
Dieser Shader wird verwendet, um eine unterschiedliche Intensität entsprechend dem Winkel zwischen der Blickrichtung und der Normalen des Scheitelpunkts anzuwenden.
Der Vertex-Shader ist der gleiche, der vom Cell-Shader-Shader verwendet wird, und wir können den Fresnel-Term in unserem Pixel-Shader leicht berechnen (weil wir die Normale und die Kameraposition haben, die zur Bewertung der Blickrichtung verwendet werden können):
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); }
Dein Shader?
Sie sind jetzt besser darauf vorbereitet, Ihren eigenen Shader zu erstellen. Sie können gerne im Babylon.js-Forum posten, um Ihre Experimente zu teilen!
Wenn Sie weiter gehen möchten, finden Sie hier einige nützliche Links:
- Babylon.js, offizielle Website
- Babylon.js, GitHub-Repository
- Babylon.js-Forum, HTML5-Spieleentwickler
- Erstellen Sie Ihren eigenen Shader (CYOS), Babylon.js
- OpenGL Shading Language“, Wikipedia
- OpenGL Shading Language, Dokumentation