WebGL Grundlagen 13 – fragmentierte Beleuchtung und mehrere Programme.

Willkommen zu unserem elften Teil unserer Serie von WebGL Grundlagen. Darin behandeln wir die Beleuchtung pro Fragment, welche wesentlich realistischere Ergebnisse liefert. Wir werden auch untersuchen, wie Sie die im Code verwendeten Shader umschalten können, indem Sie das verwendete WebGL-Programmobjekt ändern.

Dieses Ergebnis werden Sie nach der Umsetzung erhalten:

Aus datenschutzrechtlichen Gründen benötigt YouTube Ihre Einwilligung um geladen zu werden. Mehr Informationen finden Sie unter Datenschutzerklärung.

Ein wichtiger Hinweis: Dieser Teil richtet sich an Personen mit fortgeschrittenen Programmierkenntnissen, aber ohne wirkliche Erfahrung mit 3D-Grafiken. Sie werden ein gutes Verständnis für den Code mitbringen und es zum Laufen bringen, so dass Sie so schnell wie möglich mit der Erstellung einer eigenen Website in 3D beginnen können.

Es existieren zwei verschiedene Möglichkeiten, den Code für dieses Beispiel zu erhalten. In den Browsereinstellungen mit „View Source“, während Sie sich die Live-Version ansehen oder wenn Sie GitHub verwenden, können Sie es aus dem Repository kopieren. Unabhängig davon für welchen Weg Sie sich entscheiden, sollten Sie ihren bevorzugten Texteditor herunterladen und sich den Code genau ansehen.

Wir werden zunächst genau beschreiben, weshalb es sich lohnt, mehr Grafikprozessorleistung zu verbrauchen, indem wir die Beleuchtung pro Fragment programmieren.

Sie erinnern sich vielleicht an das untere Diagramm von Teil 7.

fragmentierte Beleuchtung

Wie Sie bereits wissen, wird die Helligkeit einer Oberfläche durch den Winkel zwischen ihrem normalen und den einfallenden Lichtstrahlen der Lichtquelle bestimmt. Bisher wurde unsere Beleuchtung im Vertex-Shader berechnet, indem die für jeden Vertex spezifizierten Normals mit der Beleuchtungsrichtung aus ihm kombiniert wurden. Dies hat einen Light-Weighting-Faktor geliefert, den wir in einer Variable vom Vertex-Shader zum Fragment-Shader weitergereicht haben und mit dem wir die Helligkeit der Farbe des Fragments entsprechend variiert haben. Dieser Leightweighting-Faktor wird, wie alle veränderlichen Variablen, vom WebGL-System linear interpoliert worden sein, um Werte für die Fragmente zu liefern, die zwischen den Eckpunkten liegen. So wird z.B. B im Diagramm ziemlich hell sein, weil das Licht parallel zum normalen dort ist. A hingegen wird dimmbar sein, weil das Licht es in einem größeren Winkel erreicht und die Punkte dazwischen werden sanft zwischen hell und dunkel schattieren. Das wird genau richtig aussehen:

Aber nun stellen Sie sich vor, dass das Licht höher ist wie im Diagramm unten:

Beleuchtung fragmentieren

A und C werden dunkel, wenn das Licht schräg auf Sie trifft. Wir berechnen die Beleuchtung nur an den Scheitelpunkten und so wird Punkt B die durchschnittliche Helligkeit von A und C haben, also wird es auch dunkel sein. Das ist natürlich falsch – das Licht ist parallel zur Normalität der Oberfläche bei B, also sollte es eigentlich heller sein als beide. Um also die Beleuchtung von Fragmenten zwischen den Ecken zu berechnen, müssen wir Sie natürlich für jedes Fragment separat berechnen.

Die Berechnung der Beleuchtung für jedes Fragment bedeutet, dass wir für jedes Fragment die Position und Normalität benötigen. Wir können diese erhalten, indem wir Sie vom Vertex-Shader zum Fragment-Shader weiterleiten. Sie werden beide linear interpoliert, so dass die Positionen entlang einer geraden Linie zwischen den Eckpunkten liegen und die Normals gleichmäßig variieren. Diese gerade Linie ist genau das, was wir wollen und weilen die Normals bei A und C gleich sind, werden die Normals für alle Fragmente gleich sein, was auch perfekt ist.

Das alles erklärt, warum der Würfel auf unserer Webseite mit fragmentarischer Beleuchtung besser und realistischer aussieht. Ein weiterer Vorteil besteht darin, dass es eine große Wirkung auf Formen aus flachen Flächen hat, die dazu bestimmt sind, gekrümmte Flächen wie unsere Kugel anzunähern. Wenn die Normals an zwei Scheitelpunkten unterschiedlich sind, dann bewirken die sich sanft verändernden Normals an den dazwischen liegenden Fragmenten den Effekt einer gekrümmten Fläche. Auf diese Weise betrachtet, ist die Beleuchtung pro Fragment eine Form des sogenannten Phong-Shading. Sie können dies im Demo-Video sehen. Wenn Sie per-vertex Beleuchtung verwenden, können Sie sehen, dass der Rand des Schattens ein wenig zerlumpt aussieht. Das liegt daran, dass die Kugel aus vielen Dreiecken besteht und man kann ihre Ränder sehen. Wenn Sie die fragmentierte Beleuchtung einschalten, können Sie sehen, dass der Rand dieses Übergangs glatter ist, was zu einem besseren Rundheitseffekt führt.

Nun sind wir mit der Theorie soweit durch – schauen wir uns also den Code an. Die Shader befinden sich also ganz oben in der Datei, also schauen wir Sie uns erst einmal an. Da in diesem Beispiel je nach Einstellung des Kontrollkästchens entweder per-vertex oder per-fragment Lighting verwendet wird, gibt es für jede Art Vertex- und Fragment-Shader. Die Art und Weise, wie wir zwischen ihnen hin- und herwechseln, werden wir später erfahren, aber für den Moment sollten Sie nur beachten, dass wir zwischen ihnen unterscheiden, indem wir verschiedene ID-Tags verwenden, wenn wir Sie als Skripte in der Webseite definieren. Als erstes erscheinen die Shader für die per-Vertex-Beleuchtung und das sind genau die gleichen, wie wir Sie bereits im 7 Grundlagenteil gesehen haben. Also zeigen wir ihnen hier lediglich die Skript-Tags:

Copy to Clipboard

Als nächstes kommt der Fragment-Shader für die Beleuchtung der einzelnen Fragmente:

Copy to Clipboard

Sie können sehen, dass es starke Ähnlichkeiten zu Vertex-Shadern gibt, die wir bisher benutzt haben. Es führt genau dieselben Berechnungen durch, um die Richtung des Lichts zu berechnen und es anschließend mit dem Normal zu kombinieren, um eine leichte Gewichtung zu berechnen. Der Unterschied besteht darin, dass die Eingaben für diese Berechnung nun aus Variablen und nicht mehr aus Attributen pro Vertex stammen und die resultierende Gewichtung sofort mit der Texturfarbe aus der Probe kombiniert wird, anstatt Sie später für die Weiterverarbeitung zu verwenden. Es ist auch erwähnungswert, dass wir die Variable, welche das interpolierte Normal enthält, normalisieren müssen. Die Normalisierung passt einen Vektor so an, dass seine Länge eine Einheit ist. Das liegt daran, dass die Interpolation zwischen zwei Lenght-One-Vektoren nicht notwendigerweise einen Lenght One Vektor ergibt, sondern nur einen Vektor, welcher in die korrekte Richtung zeigt. Die Normalisierung behebt das.

Da der Fragment-Shader das ganze schwere Heben übernimmt, ist der Vertex-Shader für die Beleuchtung der einzelnen Fragmente sehr einfach:

Copy to Clipboard

Wir müssen noch die Position des Scheitelpunktes nach Anwendung der Model-View-Matrix herausfinden und das Normal mit der normalen Matrix multiplizieren, aber jetzt verstecken wir Sie einfach in verschiedenen Variablen für die spätere Verwendung im Fragment-Shader.

Das war`s für die Shader. Der Rest des Codes wird ihnen schon aus vorangegangenen Beiträgen vertraut sein, mit einer einzigen Ausnahme. Bisher haben wir nur einen Vertex-Shader und einen Fragment-Shader pro WebGL-Seite verwendet. Dieses verwendet zwei Paare. Eines für die Beleuchtung pro Vertex und eines für die Beleuchtung pro Fragment. Sie erinnern sich mit Sicherheit an unsere erste Übung, dass das WebGL-Programmobjekt mit dem wir unseren Shader-Code an die Grafikkarte übergeben, lediglich einen Fragment- und einen Vertex-Shader haben kann. Das bedeutet, dass wir zwei Programme benötigen, die wir je nach Einstellung des Kontrollkästchens „pro Fragment“ umschalten müssen.

Die Art und Weise, wie wir das realisieren, ist recht unkompliziert. Unsere initShaders-Funktion wurde so geändert, dass Sie folgendermaßen aussieht:

Copy to Clipboard

Also, wir haben zwei Programme in getrennten globalen Variablen, eines für die Beleuchtung pro Vertex, eines für die Beleuchtung pro Fragment und eine separate currentProgram Variable, um die jeweils aktuell genutzte Variable zu speichern. Das createProgram, welches wir benutzen, um Sie zu erstellen, ist einfach eine parametrisierte Version des Codes, den wir in initShaders hatten, also werden wir es hier nicht duplizieren.

Wir schalten dann gleich zu Beginn der drawScene-Funktion das entsprechende Programm ein:

Copy to Clipboard

Wir müssen dies vor allem tun, weil wir beim Zeichnen von Code das aktuelle Programm entsprechend einrichten müssen, da wir sonst das falsche Programm verwenden könnten:

Copy to Clipboard

Das bedeutet, dass wir für jeden Aufruf von drawScene ein und nur ein Program verwenden. Es unterscheidet sich nur zwischen den Aufrufen. Wenn Sie sich fragen, ob Sie verschiedene Shader-Programme zu verschiedenen Zeiten innerhalb von drawScene verwenden können, so dass verschiedene Teile der Szene mit verschiedenen Programmen gezeichnet werden können, dann lautet die Antwort ganz klar Ja. Sie wurde für dieses Beispiel nicht benötigt, ist aber durchaus gültig und kann nützlich sein.

Und auch diese Lektion ist hiermit beendet. Sie wissen jetzt, wie Sie mehrere Programme zum Wechseln von Shadern verwenden und wie Sie die Beleuchtung pro Pixel programmieren können. In unserer nächsten Übung schauen wir uns die Glanzlichter ein wenig genauer an.