Willkommen zu unserem elften Teil unserer Serie von WebGL Grundlagen. Darin zeigen wir eine texturierte Kugel mit gerichteter Beleuchtung, die der Betrachter mit der Maus drehen kann.
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.
Der leichteste Weg den Code für diese Seite zu verstehen ist es, indem man ganz unten anfängt und sich nach oben arbeitet und alle neuen Sachen ansieht. Der HTML-Code in den body-Tags unterscheidet sich nicht von den Inhalten im Teil 7, also lasst uns mit dem neuen Code in webGLStart beginnen:
Diese drei neuen Linien erlauben es uns Mouse Events zu erkennen und so den Mond zu drehen, wenn der Benutzer das entsprechend wünscht. Offensichtlich wollen wir nur Mouse-Down-Ereignisse auf 3D-Canvases aufnehmen. Etwas weniger offensichtlich ist es, dass wir auf Mouse-Up hören und Ereignisse auf dem Dokument und nicht auf dem Canvas verschieben wollen. Auf diese Weise sind wir in der Lage Drag-Ereignisse zu erfassen, auch wenn die Maus außerhalb des 3D-Canvas bewegt oder freigegeben wird, solange der Drag im Canvas begonnen hat – das hält uns davon ab, eine jener irritierenden Seiten zu sein auf denen Sie die Maustaste innerhalb der Szene drücken, welche Sie drehen möchten und Sie dann nach außen loslassen nur um festzustellen, dass wenn die Maus wieder über die Szene bewegt wird, diese nicht aktiviert wurde und Sie immer noch denkt, dass Sie ziehen.
Wenn wir uns ein wenig nach oben durch den Code bewegen, kommen wir zu unserer Trick-Funktion, welche für diese Seite nur das nächste Frame einplant und drawScene aufruft, da Sie keine Notwendigkeit hat, Tasten zu bedienen oder die Szene zu animieren.
Die nächsten relevanten Änderungen sind in drawScene. Wir fangen mit unserem Textbaustein Canvas-Clearing und perspective Code an und machen anschließend dasselbe wie im siebten Grundlagenteil, um die Beleuchtung einzurichten:
Als nächstes bewegen wir uns in die richtige Position, um den Mond zu zeichnen.
…und hier kommt der erste Teil, der vielleicht merkwürig aussieht, Aus Gründen, die später erläutert werden, speichern wir den aktuellen Rotationszustand des Mondes in einer Matrix. Diese Matrix beginnt als Identitätsmatrix und anschließend, wenn der Benutzer Sie mit der Maus manipuliert, ändert Sie sich, um diese Manipulationen zu reflektieren.
Bevor wir also den Mond zeichnen, müssen wir die Rotationsmatrix auf die aktuell Model-View-Matrix anwenden, was wir mit der mat4.multiply-Funktion tun können:
Wenn das erledigt ist, muss nur noch der Mond gezeichnet werden. Dieser Code ist ziemlich standardisiert. Wir setzen einfach die Textur und verwenden dann den gleichen Code, welchen wir schon oft verwendet haben, um WebGL anzuweisen, einige vorbereitete Puffer zu verwenden, um eine Reihe von Dreiecken zu zeichnen:
Also: Wie erzeugen wir Vertex-Position-, Normal-, Texturkoordinaten und Index-Puffer mit den richtigen Werten, um eine Kugel zu zeichnen? Praktischer ist das mit einer uns schon bekannten Funktion: initBuffers.
Es beginnt damit globale Variablen für die Puffer zu definieren und zu entscheiden, wie viele Breiten- und Längengrade verwendet werden sollen und wie groß der Radius der Kugel sein soll. Wenn Sie diesen Code in einer eigenen WebGL-Seite verwenden, würden Sie die Breiten- und Längengrade sowie den Radius parametrisieren und die Puffer irgendwo anders als in globalen Variablen speichern. Ich habe es auf diese einfache und zwingende Art und Weise getan, um ihnen kein bestimmtes funktionales Design aufzuzeigen.
Also, was sind die Breiten- und Längengrade? Um eine Reihe von Dreiecken zu zeichnen, welche eine Annäherung an eine Kugel darstellen, müssen wir Sie aufteilen. Es gibt viele clevere Methoden für die Umsetzung und eine einfache, die auf der Geometrie des Abiturs basiert, die vollkommen annehmbare Resultate erhält und recht leicht zu verstehen ist.
Es basiert auf einem der Demos auf der Khronos-Website, wurde ursprünglich vom WebKit-Team entwickelt und funktioniert folgendermaßen:
Beginnen wir mit der Terminologie. Die Breitengrade definieren z.B. auf einem Globus, wie weit nördlich oder südlich Sie sich befinden. Der Abstand zwischen ihnen, gemessen entlang der Kugeloberfläche, ist konstant. Wenn man eine Kugel entlang ihrer Breitengrade von oben nach unten aufschneiden würde, resultieren daraus dünne linsenförmige Bits für oben und unten sowie immer dickere scheibenförmige Scheiben für die Mitte.
Die Längengrade sind unterschiedlich und teilen die Kugel in Segmente auf. Wenn man eine Kugel entlang ihrer Längengrade aufschneiden würde, löst Sie sich eher wie eine Orange auf.
Um nun eine Kugel zu zeichnen, stellen Sie sich vor, dass wir die Breitengrade von oben nach unten und die Längengrade um Sie herum gezeichnet haben. Wir möchten alle Punkte, an denen sich diese Linien schneiden, herausarbeiten und diese als Scheitelpunkt verwenden. Wir können dann jedes Quadrat, welches aus zwei benachbarten Längengraden und Breitengraden besteht in zwei Dreiecke aufteilen und zeichnen.
Wie berechnen wir nun die Punkte, an denen sich die Breiten- und Längengrade schneiden? Nehmen wir an, dass die Kugel einen Radius von einer Einheit hat und beginnen wir damit, eine Scheibe senkrecht durch ihr Zentrum in der Ebene der X- und Y-Achse zu zeichnen.
Offensichtlich ist die Form der Scheibe ein Kreis und die Breitengrade sind Linien über diesen Kreis. Der Winkel zwischen der Y-Achse und dem Punkt, an dem das Breitenband den Rand des Kreises erreicht ist θ. Mit ein wenig einfacher Trigonometrie können wir sehen, dass der Punkt eine Y-Koordinate von cos(θ) und eine X-Koordinate von sin(θ) hat.
Lassen Sie un das verallgemeinern, um die äquivalenten Punkte für alle Breitengrade zu berechnen. Wir wollen, dass jede Linie um den gleichen Abstand um die Oberfläche der Kugel von ihrem Nachbarn getrennt wird. Dazu definieren wir Sie einfach durch Werte von θ , die gleichmäßig verteilt sind. Es gibt π Bogemaße in einem Halbkreis, also können wir unserem zehnzeiligen Beispiel Werte von θ von 0, π/10, 2π/10, 3π/10, 3π/10 und so weiter bis 10π/10 geben und wir können sicher sein, dass wir die Kugel in gerade Breitengrade aufgeteilt haben.
Nun haben alle Punkte auf einem bestimmten Breitengrad, unabhängig von ihrer Länge, die gleiche Y-Koordinate. Wenn man also die Formel für die obige Y-Koordinate betrachtet, kann man sagen, dass alle Punkte um den n-ten Breitengrad der Kugel mit Radius 1 und zehn Breitengraden die Y-Koordinate von cos(nπ / 10) haben.
Damit ist die Y-Koordinate geklärt. Was ist aber mit X und Z? Da wir den Wert für die Y-Koordinate mit cos(nπ / 10) haben, können wir die X-Koordinate durch die Z-Koordinate bestimmen. Die X-Koordinate hat den Wert, an dem Z Null also sin(nπ / 10) ist. Nehmen wir einen anderen Schnitt durch die Kugel wie im Bild links oben: einen horizontalen Schnitt durch die Ebene des n-ten Breitengrades. Wir können sehen, dass sich alle Punkte in einem Kreis mit einem Radius von sin befinden (nπ / 10). Wir nennen diesen Wert k. Wenn wir den Kreis durch die Längengrade teilen, von denen wir annehmen, dass es 10 ist und bedenken, dass es 2π Bogenmaße im Kreis und damit Werte für φ geben wird. Den Winkel, den wir um den Kreis herum annehmen hat Werte von 0, 2π/10, 4π/10, 4π/1 und so weiter. Durch einfache Trigonometrie können wir sehen, dass unsere X-Koordinate kcosφ und unsere Z-Koordinate ksinφ ist.
Verallgemeinernd können wir Folgendes annehmen: Für eine Kugel mit dem Radius r und mit m Breiten- und n Längengraden, können wir Werte für x, y und z generieren. Dazu nehmen wir einen Wertebereich für θ an, indem wir den Bereich 0 bis π in m Teile aufteilen. Natürlich nehmen wir auch einen Wertebereich für φ an, indem wir den Bereich 0 bis 2π in n Teile aufteilen und dann einfach nur rechnen:
x = r sinθ cosφ
y = r cosθ
z = r sinθ sinφ
So arbeiten wir die Eckpunkte aus. Was ist mit den anderen Werten, die wir für jeden Punkt brauchen: z.B. für die Normals oder Texturkoordinaten? Die Normals sind wirklich einfach: Ein Normal ist ein Vektor mit einer Länge von eins, welcher direkt aus einer Fläche herausragt. Für eine Kugel mit einem Radius von einer Einheit entspricht das dem Vektor, der von der Mitte der Kugel zur Oberfläche führt – diese Tatsache haben wir bereits im Rahmen der Berechnung der Position des Scheitelpunktes ausgearbeitet. Tatsächlich ist es der einfachste Weg die Vertexposition und das Normal zu berechnen, die obigen Berechnungen durchzuführen, aber dabei nicht mit dem Radius zu multiplizieren. Die Ergebnisse müssen als Normal gespeichert und anschließend die Normalwerte mit dem Radius multipliziert werden, um die Vertexpositionen zu erhalten.
Die Texturkoordinaten sind vergleichweise einfacher. Wenn eine Textur zur Verfügung steht, um Sie auf eine Kugel zu legen, erwarten wir, dass Sie uns ein rechteckiges Bild geben wird. Wir können davon ausgehen, dass diese Textur nach Mercator-Projektion oben und unten gedehnt wird. Das bedeutet, dass wir die Texturkoordinaten von links nach rechts in die Längengrade und von oben nach unten in die Breitengrade aufteilen können.
Der JavaScript-Code sollte jetzt unglaublich leicht zu verstehen sein. Wir durchlaufen einfach alle Breitenschnitte, dann durchlaufen wir innerhalb dieser Schleife die Längensegmente und generieren die Normals, Texturkoordinaten und Scheitelpunkte für jedes Einzelne. Die einzige Besonderheit ist, dass unsere Schleifen enden, wenn der Index größer als die Anzahl der Breitenschnitte ist, d.h. wir verwenden <= anstatt < in den Schleifenbedingungen. Das bedeutet, dass wir für z.B. 30 Längengrade 31 Scheitelpunkte pro Breitengrad erzeugen. Da die trigonometrischen Funktionen zyklisch ablaufen, befindet sich die letzte in der gleichen Positionen wie die erste und so ergibt sich eine Überlappung, so dass alles zusammenläuft.
Da wir nun die Eckpunkte haben, müssen wir Sie zusammenfügen, indem wir eine Liste von Vertex-Indizes erzeugen, welche Sequenzen von sechs Werten enthält, die jeweils ein Quadrat repräsentieren, die als Paar von Dreiecken ausgedrückt werden. Hier ist der Code:
Das ist eigentlich ziemlich einfach zu verstehen. Wir durchlaufen unsere Verticles und für jeden einzelnen speichern wir seinen Index zuerst. Anschließend zählen wir LongitudeBands +1 vorwärts, um als Gegenstück ein Spielraum nach unten zu finden. Zudem fügen wir einen zusätzlichen Verticle hinzu, die wir hinzufügen mussten, um die Überlappung zu ermöglichen. Dieser wird in einem zweiten Verticle gespeichert. Wir erzeugen dann zwei Dreiecke, wie im Diagramm dargestellt.
Das war nun der schwierige Teil, welcher gemacht werden musste.
Unmittelbar über der Funktion initBuffers befinden sich drei Funktionen, die sich mit der Maus befassen. Diese verdienen eine sorgfältige Prüfung. Wir sollten zunächst einmal sorgfältig darüber nachdenken, was wir eigentlich vorhaben. Wir wollen, dass der Betrachter unserer Szene in der Lage ist den Mond zu drehen, indem er ihn zieht. Eine naive Umsetzung könnte beispielsweise darin bestehen, drei Variablen zu haben, die Rotationen um die X-, Y- und Z-Achse darstellen. Anschließend können wir jedes Einzelne entsprechend anpassen, wenn die Nutzer die Maus bewegen. Wenn die Maus z.B. nach unten oder nach oben gezogen wird, kann die Rotation um die X-Achse angepasst werden und bei Bewegungen von links nach rechts oder rechts nach links, könnten wir es um die Y-Achse herum verstellen. Das Problem dabei ist, dass wenn Sie ein Objekt um verschiedene Achsen drehen und Sie eine Reihe von verschiedenen Drehungen durchführen, es darauf ankommt, in welcher Reihenfolge Sie es anwenden. Angenommen der Betrachter dreht den Mond um 90° um die Y-Achse und zieht dann die Maus nach unten. Wenn wir uns wie geplant um die X-Achse drehen, werden Sie sehen, dass der Mond um die aktuelle Z-Achse rotiert. Die erste Drehung hat auch die Achsen gedreht. Das wird für Sie komisch aussehen. Das Problem verschlimmert sich, wenn der Betrachter um z.B. 10° um die X-Achse gedreht hat und dann 23° um die Y-Achse dreht. Aus der darausfolgenden Logik können wir die folgende Schlußfolgerung ziehen:
„Angesichts des aktuellen Rotationszustandes, wenn der Benutzer die Maus nach unten zieht, müssen wir alle drei Rotationswerte entsprechend anpassen.“
Eine einfachere Weise damit umzugehen, wäre es, eine Art Aufzeichnung jeder Drehung, die der Betrachter auf den Mond angewendet hat, aufzubewahren und Sie dann jedes Mal wiederzugeben, wenn wir Sie zeichnen. Auf dem ersten Blick mag das alles sehr aufwendig klingen. Es sei denn Sie erinnern sich, dass wir bereits eine sehr gute Möglichkeit haben, eine Abfolge verschiedener geometrischer Transformationen an einem Ort zu verfolgen und Sie in einer Operation anzuwenden: einer Matrix.
Wir haben eine Matrix, um den aktuellen Rotationszustand des Mondes zu speichern, logischerweise als moonRotationMatrix bezeichnet. Wenn der Benutzer an der Maus zieht, erhalten wir eine Abfolge von Mausbewegungen und jedes mal, wenn wir eines sehen, berechnen wir, wie viele Grad der Drehung um die aktuelle X- und Y-Achse aus der Sicht des Benutzers, der die Maus bewegt, insgesamt beträgt. Ansschließend berechnen wir eine Matrix, welche die beiden Rotationen repräsentiert und multiplizieren die moonRotationMatrix damit vor und zwar aus dem selben Grund, aus dem wir Transformationen in umgekehrter Reihenfolge bei der Positionierung der Kamera anwenden. Die Rotation erfolgt in Bezug auf den Eye Space nicht auf den Modellraum.
Mit den bisherigen Erklärungen sollte der Code unten somit schon ziemlich klar sein:
Das ist der letzte wesentliche Teil des neues Codes in dieser Übung. Von dort aus werden alle Änderungen, die Sie sehen können von unserem Texturcode benötigt, um die neue Textur in die geänderten Variablenamen zu laden.
Das wars. Jetzt wissen Sie, wie man eine Kugel mit einem einfachen und effektiven Algorithmus zeichnet, wie man Mouse Events miteinander verknüpft, so dass der Betrachter die 3D-Objekte manipulieren kann und wie man Matrizen verwendet, um den aktuellen Rotationszustand eines Objekts in einer Szene darzustellen.
In nächsten Teil werden wir ihnen mit dem Point Lighting eine spezielle Art der Beleuchtung präsentieren.
Vielen Dank fürs Lesen.