Willkommen zu unserem vierzehnten Teil unserer Serie von WebGL Grundlagen. Darin stellen wir ihnen den letzten relevanten Inhalt des Phong Reflection Modells vor, welches wir bereits in unserem siebten Teil vorgestellt haben. Es geht dabei um Glanzlichter auf einer Oberfläche, die eine Szene etwas realistischer erscheinen lassen.
Dieses Ergebnis werden Sie nach der Umsetzung erhalten:
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 fangen oben an und arbeiten uns nach unten vor, was den großen Vorteil hat, dass wir den Fragment-Shader so ziemlich schnell sehen können. Nachfolgend thematisieren wir die interessantesten Änderungen. Bevor wir darauf stoßen, gibt es noch einen kleinen Unterschied zwischen diesem Code und in Teil 13. Wir haben keine Shader für die Beleuchtung mit per-Vertex-Beleuchtung. Die per-Vertex-Beleuchtung kann nicht wirklich gut mit Glanzlichtern umgehen, so dass wir uns nicht mit ihnen beschäftigen.
Der erste Shader, den Sie in der Datei sehen, ist also der Fragment-Shader für die Beleuchtung einzelner Fragmente. Es beginnt mit den üblichen Präzisionsarbeiten und Deklarationen von variierenden und einheitlichen Variablen, von denen ein Paar neu ist und eine Uniform, die die Farbe des Punktlichts trug wurde umbenannt, da das Punktlicht nun spiegelnde und diffuse Komponenten aufweist:
Hier sollte soweit alles klar sein. Sie sind genau dort, wo die Werte, die Sie vom HTML-Code aus ändern können zur Verarbeitung in den Shader eingespeist werden. Kommen wir nun zum Körper des Shaders. Als erstes behandeln wir den Fall, dass der Benutzer die Beleuchtung ausgeschaltet hat. In diesem Fall sind keine Änderungen zu vorher ersichtlich:
Jetzt kümmern wir uns um die Beleuchtung, und hier wird es natürlich interessant:
Wir berechnen die Richtung des Lichtes genau so, wie wir es bei normaler Beleuchtung pro Fragment getan haben. Wir normalisieren dann den normalen Vektor des Fragments noch einmal genau so wie vorher – denken Sie daran, wenn die per-Vertex-Normals linear interpoliert werden, um per-Fragment-Normals zu erzeugen, sind die Ergebnisse nicht notwendigerweise von der Länge eins. Um dies zu beheben müssen wir also normalisieren. Um es diesmal mehr zu benutzen, speichern wir es in einer lokalen Variablen. Als nächstes definieren wir eine Gewichtung für die Menge an zusätzlicher Helligkeit, welche vom Glanzlicht kommen wird. Wenn das Glanzlicht ausgeschaltet ist, beträgt die Gewichtung gleich Null, aber wenn nicht, müssen wir es berechnen.
Was bestimmt also die Helligkeit eines Glanzlichts? Wie Sie sich vielleicht aus der Erläuterung des Phong Reflection Modells im siebten Teil erinnern, werden Glanzlichter durch den Teil des Lichts erzeugt, welcher von einer Lichtquelle stammt, die von einer Oberfläche wie einem Spiegel abprallt:
„Der Teil des Lichts, der auf diese Weise reflektiert wird, prallt im gleichen Winkel von der Oberfläche ab, in dem es auf Sie trifft. In diesem Fall hängt die Helligkeit des vom Material reflektierten Lichts davon ab, ob sich ihre Augen zufällig in der Linie befinden, auf der das Licht aufgeprallt wurde, d.h. es hängt nicht nur vom Winkel ab, in dem das Licht auf die Oberfläche trifft, sondern auch vom Winkel zwischen ihrer Sichtlinie und der Oberfläche. Diese Spiegelreflexion ist es, die auf Objekten „Schimmer“ oder „Glanzlichter“ hervorruft und die Spigelreflexion kann natürlich von Material zu Material variieren. Ungeschliffenes Holz hat wahrscheinlich nur sehr wenig Spiegelreflexion und hochglanzpoliertes Metall hat ziemlich viel.“
Die spezifische Gleichung für die Berechnung der Helligkeit einer Spiegelreflexion lautet folgendermaßen:
(Rm . V)α
… wobei Rm der (normierte) Vektor ist, von dem ein perfekt reflektierter Lichtstrahl von der Lichtquelle ausgehen würde, wenn er von dem Punkt auf der betrachteten Oberfläche abprallt. V ist der Vektor, welche in Richtung der Augen des Betrachters zeigt. α ist eine Konstante, welche die Glanzwirkung beschreibt, je höher der Glanz ist. Sie erinnern sich vielleicht daran, dass das Produkt zweier Vektoren der Kosinus des Winkels zwischen ihnen ist. Das bedeutet, dass dieser Teil der Gleichung einen Wert von 1 ergibt, wenn das Licht der Lichtquelle direkt am Betrachter reflektiert wurde und dann ziemlich langsam verblasst, da das Licht weniger direkt reflektiert wird. Wenn man diesen Wert auf die Wirkung von α bezieht, hat das den Effekt ihn zu „komprimieren“, d.h. während das Ergebnis noch 1 ist, also die Vektoren parallel sind, fällt es schneller zu jeder Seite ab.
Vor diesem Hintergrund sind die ersten Dinge, mit denen wir uns beschäftigen müssen, zum einen die Richtung der Augen des Betrachters (V) und die Richtung eines perfekt reflektierten Lichtstrahls. Schauen wir uns zuerst V an, denn das ist ganz einfach. Unsere Szene ist im Eye Space konstruiert, an den Sie sich vielleicht aus unserer zehnten Übung erinnern werden. Das bedeutet, dass wir die Szene so zeichnen, als ob es eine Kamera am Ursprung gäbe (0, 0, 0, 0), die auf die negative Z-Achse blickt, wobei X nach rechts und Y nach oben zunimmt. Die Richtung eines beliebigen Punktes vom Ursprung aus umfasst natürlich nur jene Koordinaten, die als Vektor ausgedrückt werden – ebenso sind die Augen des Betrachters am Ursprung von einem beliebigen Punkt aus lediglich die negativen Koordinaten. Die Koordinaten des Fragments haben wir in vPosition. Anschließend negieren und normalisieren wir es, um einen Längenwert von 1 zu erreichen.
Jetzt schauen wir uns Rm an. Dies wäre etwas aufwendiger, wenn es nicht eine sehr komfortable GLSL-Funktion mit der Bezeichnung reflect gäbe:
Reflect (I, N): Gibt für den einfallenden Vektor I und die Oberflächenorientierung N die Reflexionsrichtung zurück.
Der einfallende Vektor ist die Richtung aus der ein Lichtstrahl auf die Oberfläche des Fragments trifft. Die Oberflächenorientierung wird N genannt, weil es nur das Normal ist, was wir auch schon haben. Bei all dem ist es einfach, sich zurechtzufinden:
Der letzte Schritt gestaltet sich recht einfach:
Das ist alles, was wir tun müssen, um den Beitrag der Spiegelkomponente zur Beleuchtung des Fragments zu ermitteln. Der nächste Schritt besteht darin, den Beitrag des diffusen Lichts zu ermitteln und zwar in der gleichen Logik wie zuvor:
Schließlich verwenden wir beide Gewichtungen, die diffusen und spiegelnden Farben und die Umgebungsfarbe, um die Gesamtmenge der Beleuchtung dieses Fragments für jede Farbkomponente zu berechnen. Das ist eine einfache Erweiterung dessen, was wir vorher verwendet haben:
Sobald dies alles erledigt ist, haben wir einen Wert für die leichte Gewichtung, welchen wir einfach in identischem Code zum Teil 13 verwenden können, um die Farbe zu gewichten, wie Sie durch die aktuelle Textur vorgegeben ist:
Dann sind wir soweit mit dem Fragment-Shader durch.
Gehen wir nun ein wenig weiter nach unten. Wenn Sie nach Unterschieden zum Teil 13 suchen, werden Sie als nächstes feststellen, dass initShaders wieder zu seiner früheren einfachen Form zurückkehrt und nur ein Programm erstellt, obwohl es natürlich auch jetzt ein oder zwei weitere einheitliche Orte für die neuen Einstellungen der Spiegelbeleuchtung initialisiert. Etwas weiter unten lädt initTextures nun Texturen für die Erde und die verzinkten Stahleffekte anstelle von Mond und Kiste. Ein bisschen weiter unten ist setMatrixUniforms, wie initShaders, wieder einmal für ein einzelnes Programm konzipiert – und dann kommen wir zu etwas Interessanterem.
Anstelle von initBuffers, um die WebGL-Puffer zu erzeugen, die die verschiedenen per-Vertex-Attribute enthalten, welche das Aussehen der Teekanne definieren, haben wir zwei Funktionen: handleLoadedTeapot und loadTeapot. Das Muster wird aus Teil 10 bekannt sein, aber es lohnt sich, es noch einmal zu wiederholen. Werfen wir einen Blick auf loadTeapot:
Die Gesamtstruktur sollte uns bereits aus Teil 10 bekannt sein. Wir erstellen einen neuen XMLHttpRequest und verwenden diesen, um die Datei teapot.json zu laden. Dies erfolgt asynchron, also fügen wir eine Callback-Funktion hinzu, die ausgelöst wird, wenn der Prozess des Ladens der Datei verschiedene Stadien erreicht. Im Callback machen wir einige Dinge, wenn die Last einen readyState von 4 erreicht, was bedeutet, dass Sie vollständig geladen ist.
Das Interessante daran ist, was anschließend passiert. Die Datei, welche wir laden, ist im JSON-Format, was im Grunde genommen bedeutet, dass Sie bereits in JavaScript geschrieben ist. Die Datei beschreibt ein JavaScript-Objekt mit Listen, die die Vertex-Positionen, Normals, Texturkoordinaten und eine Reihe von Vertex-Indizes enthalten, welche die Teekanne vollständig beschreiben. Wir könnten diesen Code natürlich auch direkt in die index.html-Datei einbetten, aber wenn Sie ein komplexeres Modell mit vielen separat modellierten Objekten erstellen würden, sollten Sie sie alle in separaten Daten haben wollen.
Welche Formate Sie für vorgefertigte Objekte in ihren WebGL-Anwendungen verwenden sollten, ist eine interessante Frage. Sie können Sie in einer Vielzahl verschiedener Programme entwerfen und diese Programme können Modelle in vielen verschiedenen Formaten ausgeben, von .obj bis 3DS. In Zukunft sieht es so aus, als ob mindestens einer von ihnen in der Lage sein wird, Modelle in einem JavaScript-nativen Format auszugeben, welche vermutlich so ähnlich aussehen würden wie das JSON-Modell, das ich für die Teekanne verwendet habe. Im Moment sollten Sie dieses Tutorial am Beispiel dafür betrachten, wie Sie ein vordefiniertes Modell im JSON-Format laden können und nicht als Beispiel für Best Practice.
Wir haben also Code, der eine Datei im JSON-Format lädt und eine Aktion auslöst, wenn Sie geladen wird. Die Aktion wandelt den JSON-Text in Daten um, die wir verwenden können. Wir könnten einfach die JavaScipt-Eval-Funktion verwenden, um ihn in ein JavaScript-Objekt zu konvertieren, aber diese Vorgehensweise ist im Allgemeinen verpönt. Stattdessen verwenden wir die eingebaute Funktion JSON.parse, um das Objekt zu analysieren. Sobald dies erledigt ist, übergeben wir es an handleLoadedTeapot:
In dieser Funktion gibt es nichts hervorzuheben – es nimmt einfach die verschiedenen Listen aus dem geladenen JSON-Objekt und legt Sie in WebGL-Arrays ab, die dann in neu zugeordneten Puffern auf die Grafikkarte geschoben werden. Sobald das alles erledigt ist, löschen wir ein Div im HTML-Dokument, welches dem Benutzer vorher mitgeteilt hat, dass das Modell geladen wird, genau wie im Teil 10.
Nun ist das Modell geladen. Nun, es gibt drawScene, das nun die Teekanne in einem angemessenen Winkel zeichnen muss, aber es gibt nichts wirklich Neues. Werfen Sie einen Blick auf den Code und stellen Sie sicher vor, dass Sie wissen, was vor sich geht. Es dürfte schon recht überraschend sein, wenn Sie etwas finden, was Sie überraschen wird.
Danach hat animate ein paar triviale Änderungen vorgenommen, um die Teekanne drehen zu lassen, anstatt den Mond und die Kistenumlaufbahn zu erzeugen. Zudem muss webGLStart loadTeapot anstelle von initBuffers aufrufen und der HTML-Code verfügt über das Div zur Anzeige des Textes „Loading World…“ während des Ladevorgangs der Teekanne mit dem dazugehörigen CSS-Style und natürlich über neue Eingabefelder für die neuen Glanzlichtparameter.
Jetzt sind wir mit dem vierzehnten Teil der Serie durch. Nun wissen Sie, wie man Shader schreibt, um Glanzlichter zu zeigen und wie man vorgefertigte Modelle lädt, die im JSON-Format gespeichert sind. Im nächsten Artikel werden die Verwendung von Texturen auf eine etwas andere und interessantere Art und Weise mit Specular Maps kennenlernen.