Alles was Sie zu Babylon.js und Shader wissen müssen.

Shader sind ein Schlüsselkonzept, wenn Sie die Leistungsfähigkeit ihres Grafikprozessors voll ausschöpfen möchten. Wir werden ihnen helfen zu verstehen, wie sie funktionieren und wie Sie mit ihnen experimentieren können. Bevor wir beginnen, müssen wir zunächst verstehen, wie die Dinge intern funktionieren. Bei hardwarebeschleunigtem 3D haben Sie es mit zwei CPUs zu tun: der Haupt-CPU und der GPU. Die GPU ist eine Art extrem spezialisierte CPU.

Babylon.js und Shader

Wie funktioniert es?

Bevor wir experimentieren können, müssen wir verstehen, wie die Dinge intern funktionieren.

Bei hardwarebeschleunigtem 3D haben Sie es mit zwei CPUs zu tun: der Haupt-CPU und der GPU. Die GPU ist eines Art extrem spezialisierte CPU.

Die GPU ist eine State Machine, welchen Sie mit der CPU einrichten. Zum Beispiel konfiguriert die CPU den Grafikprozessor so, dass er Linien anstelle von Dreiecken rendert. So definiert die GPU z.B., ob Transparenz eingeschaltet ist.

Sobald alle Zustände gesetzt sind, kann die CPU definieren, was dargestellt werden soll: die Geometrie.

Die Geometrie besteht aus:

  • eine Liste von Punkten, die Nodes genannt werden und in einem Array mit der Bezeichnung Node Buffer gespeichert sind,
  • eine Liste von Indizes, die die Flächen (oder Dreiecke) definieren, die in einem Array mit der Bezeichnung Index Buffer gespeichert sind.

Der letzte Schritt für die CPU ist die Definition der Darstellung der Geometrie. Für diese Aufgabe definiert der CPU Shader in der GPU. Shader sind Codeteile, die der Grafikprozessor für jeden der zu rendernden Nodes und Pixel ausführt (Ein Node ist ein Punkt in 3D).

Es gibt zwei Arten von Shader: Vertex- und Fragment-Shader.

Grafikpipeline.

Bevor wir Shader vertiefen, gehen wir einen Schritt zurück. Um Pixel zu rendern, nimmt der Grafikprozessor die von der CPU definierte Geometrie und geht wie folgt vor:

  • Mit dem Index Buffer werden drei Eckpunkte gesammelt, um ein Dreieck zu definieren.
  • Der Index Buffer enthält eine Liste von Vertex-Indizes. Das bedeutet, dass jeder Eintrag im Index Buffer die Nummer eines Nodes im Node Buffer ist.
  • Dies ist sehr nützlich, um doppelte Nodes zu vermeiden.

GLSL.

Wir haben gerade gesehen, dass der Grafikprozessor für die Darstellung von Dreiecken zwei Shader benötigt: den Vertex- und den Pixel-Shader. Diese Shader sind in einer Sprache mit der Bezeichnung Graphics Library Shader Language (GLSL) geschrieben. Es sieht aus wie C.

Hier ist ein Beispiel für einen gängigen Vertex-Shader:

Copy to Clipboard

Vertex Shader Struktur.

Ein Vertex-Shader enthält Folgendes:

  • Attribute. Ein Attribut definiert einen Teil eines Nodes. Standardmäßig sollte ein Node mindestens eine Position enthalten (ein Vektor3:x, y, z). Als Entwickler können Sie jedoch entscheiden, ob Sie weitere Informationen hinzufügen möchten. Im ehemaligen Shader gibt es beispielsweise einen Vektor2 mit der Bezeichnung uv (d.h. Texturkoordinaten, mit denen Sie eine 2D-Textur auf ein 3D-Objekt anwenden können).
  • Uniforms. Eine Uniform ist eine Variable, die vom Shader verwendet und von der CPU definiert wird. Die einzige Einheit, die wir hier haben, ist eine Matrix, mit der die Position des Scheitels (x, y, z) auf den Bildschirm (x, y) projiziert wird.
  • Varying. Unterschiedliche Variablen sind Werte, die vom Vertex-Shader erzeugt und an den Pixel-Shader übergeben werden. Hier sendet der Vertex-Shader einen vUV-Wert an den Pixel-Shader. Das bedeutet, dass hier ein Pixel mit einer Position und Texturkoordinaten definiert wird. Diese Werte werden vom Grafikprozessor interpoliert und vom Pixel-Shader verwendet.
  • Main. Die Funktion mit der Bezeichnung Main ist der Code, der von der GPU für jeden Node ausgeführt wird und muss mindestens einen Wert für gl_position (die Position des aktuellen Nodes auf dem Bildschirm) erzeugen.

Wir können in unserem Beispiel sehen, dass der Vertex-Shader ziemlich einfach ist. Es erzeugt eine Systemvariable (beginnend mit gl_) mit der Bezeichnung gl_position, um die Position des zugehörigen Pixels zu definieren und es setzt eine Variable mit der Bezeichnung vUV.

Der Voodoo hinter den Matrizen.

Die Sache an unserem Shader ist, dass wir eine Matrix mit der Bezeichnung worldViewProjection haben und wir verwenden diese Matrix, um die Node-Position auf die Variable gl_position zu projizieren. So weit so gut, aber wie bekommen wir den Wert dieser Matrix?

Dies ist einer der komplexen Bestandteile von 3D. Sie müssen komplexe Mathematik verstehen (oder Sie müssen eine 3D-Engine wie Babylon.js verwenden, auf die wir später noch ein wenig näher eingehen werden).

Die worldViewProjection-Matrix ist die Kombination aus drei verschiedenen Matrizen:

  • World Matrix
  • View Matrix
  • Projection Matrix

Die Verwendung der resultierenden Matrix ermöglicht es uns, 3D-Eckpunkte in 2D-Pixel umzuwandeln, wobei der Blickwinkel und alles, was mit der Position, Skalierung und Drehung des aktuellen Objekts zusammenhängt, berücksichtigt wird.

Hier liegt ihre Verantwortung als 3D-Entwickler: diese Matrix zu erstellen und auf dem neuesten Stand zu halten.

Zurück zu den Shadern.

Sobald der Vertex-Shader an jedem node ausgeführt wird, haben wir drei Pixel mit der richtigen gl_position und einen vUV-Wert. Der Grafikprozessor wird diese Werte auf jedes Pixel interpolieren, das in dem mit diesen Pixeln erzeugten Dreieck enthalten ist.

Dann wird für jedes Pixel der Pixel-Shader ausgeführt:

Copy to Clipboard

Pixel- oder Fragment-Shader Struktur.

Die Struktur eines Pixel-Shaders ist ähnlich wie bei einem Vertex-Shader:

  • Varying. Unterschiedliche Variablen sind Werte, die vom Vertex-Shader erzeugt und an den Pixel-Shader übertragen werden. Hier erhält der Pixel-Shader einen vUV-Wert vom Vertex-Shader.
  • Uniforms. 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, welches ein Werkzeug zum Lesen von Texturfarben ist.
  • Main. Die Funktion Main ist der Code, der von der GPU für jedes Pixel ausgeführt wird und der mindestens einen Wert für gl_FragColor (d.h. die Farbe des aktuellen Pixels) liefern muss.

Dieser Pixel-Shader ist ziemlich einfach: Es liest die Farbe aus der Textur unter Verwendung von Texturkoordinaten aus dem Vertex-Shader (der sie wiederum aus dem Vertex bezieht).

Das Problem ist, dass man bei der Entwicklung von Shadern nur halbwegs da ist, weil man sich dann mit viel WebGL-Code auseinandersetzen muss. Tatsächlich ist WebGL wirklich mächtig, aber man muss alles selbst machen, von der Erstellung der Puffer bis zur Definition von Node-Strukturen. Sie müssen sich auch mit der gesamten Mathematik auseinandersetzen, alle Zustände setzen, Texture-Loading handhaben und so weiter.

Babylon.ShaderMaterial als Lösung.

Ich weiß, was Sie denken: „Shader sind wirklich cool, aber ich möchte mich nicht mit WebGL oder gar mit der Mathematik beschäftigen.“

Und Sie haben völlig Recht. Das ist eine völlig legitime Frage und genau deshalb wurde Babylon.js entwickelt.

Um Babylon.js nutzen zu können, benötigen Sie zunächst eine einfache Webseite:

Copy to Clipboard

Sie werden feststellen, dass Shader durch

Copy to Clipboard

Sie sehen, dass wir Babylon.ShaderMaterial verwendet haben, um die Last des Kompilierens, Verlinkens und Handhabens von Shadern zu beseitigen.

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 Dateien verwenden möchten, müssen Sie für jeden Shader eine Datei erstellen und folgendes Muster verwenden: basename.vertex.fx und basename.fragment.fx. Dann müssen Sie das Material wie folgt anlegen:

Copy to Clipboard

Außerdem müssen Sie die Namen der von ihnen verwendeten Attribute und Uniforms angeben.

Dann können Sie die Werte ihrer Uniforms und Sampler direkt mit den Funktionen setTexture, setFloat, setFloats, setColor3, setColor4, setVector2, setVector3, setVector4, setMatrix functions einstellen.

Und erinnern Sie sich an die vorherige worldViewProjection Matrix, die Babylon.js und Babylon.ShaderMaterial verwendet. Sie müssen sich keine Sorgen machen. Babylon.ShaderMaterial berechnet es automatisch für Sie, da Sie es in der Liste der Uniforms angeben.

Babylon.ShaderMaterial kann für Sie auch die folgenden Matrizen verarbeiten:

  • world,

  • view,

  • projection,

  • worldView,

  • worldViewProjection.

Kein Grund mehr für Mathe. So wird beispielsweise bei jeder Ausführung von sphere.rotation.y + = 0,05 die World Matrix der Kugel für Sie generiert und an die GPU übertragen.

Erstellen Sie ihren eigenen Shader (CYOS).

Gehen wir einen Schritt weiter und erstellen eine Seite, auf der Sie dynamisch ihre eigenen Shader erstellen und das Ergebnis sofort sehen können. Diese Seite wird den gleichen Code verwenden, den wir zuvor besprochen haben und wird das Objekt Babylon.ShaderMaterial verwenden, um Shader zu kompilieren und auszuführen, die Sie erstellen werden.

Wir haben den ACE-Code Editor für Create Your Own Shader (CYOS) verwendet. Es ist ein unglaublicher Code-Editor mit Snytax-Highlighting.

Über die erste Combobox können Sie vordefinierte Shader auswählen. Wir werden jeden von ihnen gleich danach sehen.

Sie können auch das Mesh (d.h. das 3D-Objekt) ändern, das für die Vorschau ihrer Shader verwendet wird, indem Sie die zweite Combobox verwenden.

Mit dem Kompilieren-Button können Sie ein neues Babylon.ShaderMaterial aus ihren Shadern erstellen. Der von dieser Taste verwendete Code lautet wie folgt:

Copy to Clipboard

Das Material ist nun bereit, ihnen drei vorbereitete Matrizen (world, worldView und worldViewProjection) zu schicken. Nodes werden mit Positions-, Normal- und Texturkoordinaten geliefert.

Schließlich aktualisieren wir im renderLoop zwei praktische Uniforms:

  • Man nennt es Time und kann damit einige lustige Animationen generieren.

  • Die andere heißt CameraPosition, die die Position der Kamera in ihre Shader bringt (nützlich für Beleuchtungsgleichungen).

Copy to Clipboard

Basis-Shader.

Beginnen wir mit dem allerersten in CYOS definierten Shader: dem Basis-Shader.

Wir kennen diesen Shader bereits. Es berechnet die gl_position und verwendet Texturkoordinaten, um eine Farbe für jedes Pixel zu erhalten.

Um die Pixelposition zu berechnen, benötigen wir nur die worldViewProjection-Matrix und die Position des Vertex:

Copy to Clipboard

Texturkoordinaten (uv) werden unverändert an den Pixel-Shader übertragen.

Bitte beachten Sie, dass wir in der ersten Zeile sowohl für den Vertex als auch für den Pixel-Shader einen präzisen Mediump-Float hinzufügen müssen, da Chrome diesen benötigt. Es legt fest, dass wir zur Verbesserung der Leistung keine vollständig präzisen Gleitkommazahlen verwenden.

Der Pixel-Shader ist noch einfacher, da wir nur Texturkoordinaten verwenden und eine Texturfarbe holen müssen:

Copy to Clipboard

Schwarze und weiße Shader.

Lassen Sie uns mit einem neuen Shader fortfahren: dem Schwarz-Weiß Shader. Das Ziel dieses Shaders ist es, den vorherigen zu verwenden, jedoch mit einem reinen Schwarzweiß-Rendering-Modus.

Um dies zu tun, können wir den gleichen Vertex-Shader beibehalten. Der Pixel-Shader wird leicht modifiziert.

Die erste Option, die wir haben, ist, nur eine Komponente zu nehmen, wie die Grüne:

Copy to Clipboard

Wie Sie sehen können, haben wir anstelle von .rgb (diese Operation wird als swizzle bezeichnet) .ggg verwendet.

Aber wenn wir einen wirklich genauen Schwarz-Weiß-Effekt wollen, dann wäre die Berechnung der Leuchtdichte (sie alle Komponenten berücksichtigt) besser:

Copy to Clipboard

Die Punktoperation (oder das Punktprodukt) wird wie folgt berechnet: Ergebnis = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z.

In unserem Fall ergibt sich die folgende Leuchtdichte = r * 0,3 + g * 0,59 + b * 0,11.

Cell-Shading Shader.

Kommen wir zu einem komplexeren Shader: dem Cell-Shading Shader.

Dies erfordert, dass wir die normale Position des Nodes und die Position des Nodes in den Pixel-Shader bringen. Der Vertex-Shader wird wie folgt aussehen:

Copy to Clipboard

Bitte beachten Sie, dass wir auch die World Matrix verwenden, da Position und Normal ohne Transformation gespeichert werden und wir müssen die World Matrix anwenden, um die Rotation des Objekts zu berücksichtigen.

Der Pixel-Shader sieht wie folgt aus:

Copy to Clipboard

Das Ziel dieses Shaders ist es, Licht zu simulieren und anstatt eine sanfte Abschattung zu berechnen, werden wir das Licht nach bestimmten Helligkeitsschwellenwerten anwenden. Wenn die Lichtintensität beispielsweise zwischen 1 (maximal) und 0,95 liegt, wird die Farbe des Objekts 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 in diesem Shader hauptsächlich vier Schritte.

Zuerst deklarieren wir Schwellenwerte und Stufenkonstanten.

Dann berechnen wir die Beleuchtung mit der Phong-Gleichung:

Copy to Clipboard

Die Intensität des Lichts pro Pixel hängt vom Winkel zwischen Normal- und Lichtrichtung ab.

Dann erhalten wir die Texturfarbe für den Pixel.

Schließlich überprüfen wir den Schwellenwert und wenden den Pegel auf die Farbe an.

Phong Shader.

Wir haben einen Teil der Phong-Gleichung im vorherigen Shader verwendet. Lasst es uns jetzt komplett nutzen.

Der Vertex-Shader ist hier eindeutig einfach, denn alles wird im Pixel-Shader gemacht:

Copy to Clipboard

Gemäß der Gleichung müssen wir die „diffusen“ und „spiegelnden“ Anteile unter Verwendung der Lichtrichtung und der Normalität des Scheitelpunktes berechnen.

Copy to Clipboard

Wir haben den diffusen Teil bereits im vorherigen Shader verwendet, so dass wir hier nur den spiegelnden Teil hinzufügen müssen.

Discard Shader.

Für den Discard Shader möchten wir ein neues Konzept vorstellen: das Discard Keyword.

Dieser Shader wirft jeden nicht roten Pixel ab und erzeugt die Illusion eines gegrabenen Objekts.

Der Vertex-Shader ist derselbe, der auch vom Basic-Shader verwendet wird:

Copy to Clipboard

Der Pixel-Shader auf seiner Seite muss die Farbe testen und verwerfen, wenn z.B. die Grünkomponente zu hoch ist:

Copy to Clipboard

Wave Shader.

Wir haben viel mit Pixel-Shader gespielt, aber ich möchte Sie auch wissen lassen, dass auch viel mit Vertex-Shadern möglich ist.

Für den Wave Shader werden wir den Phong Shader wiederverwenden.

Der Vertex-Shader verwendet die einheitlich benannte Zeit, um einige animierte Werte zu erhalten. Mit dieser Uniform erzeugt der Shader eine Welle mit den Positionen der Nodes:

Copy to Clipboard

Fresnel Shader.

Wir möchten diesen Artikel mit unserem Favoriten abschließen: dem Fresnel-Shader.

Dieser Shader wird verwendet, um eine unterschiedliche Intensität entsprechend dem Winkel zwischen der Blickrichtung und der Normalität des Nodes anzuwenden.

Der Vertex-Shader ist derselbe, der auch vom Cell-Shading-Shader verwendet wird und wir können den Fresnel-Term in unserem Pixel-Shader leicht berechnen (weil die Normal und die Kameraposition haben, mit denen wir die Blickrichtung auswerten können):

Copy to Clipboard

Ihr Shader.

Sie sind jetzt bestens vorbereitet, ihren eigenen Shader zu erstellen. Zögern Sie nicht, mit ihrer Arbeit zu beginnen.

Vielen Dank für ihren Besuch.