Willkommen zum zehnten Teil unserer Serie von WebGL-Grundlagen. Darin werden wir eine 3D-Szene aus einer Datei laden und einen einfachen Code schreiben, damit wir uns darin bewegen können.
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 Sie den Code genau ansehen.
Der einfachste Weg um diese Übung zu erklären ist es, sich im Code von unten nach oben zu bewegen. Beginnen wir mit dem HTML-Code in den Body-Tags am unteren Rand, welcher einen interessanten Aspekt enthält:
Wir haben also ein HTML-
, welches einen Placeholder-Text enthält welcher angezeigt wird, während die zu visualisierende Welt geladen wird. Wenn die Verbindung zwischen ihrem Server und ihrer Engine langsam ist, sehen Sie höchstwahrscheinlich eine Nachricht oben auf dem Canvas und nicht darunter, wie man es von HTML erwarten würde. Diese Bewegung wurde durch ein wenig CSS-Code weiter oben, direkt am Ende des HTML Headers gesteuert:
Das ist also das HTML. Werfen wir nun einen Blick auf das JavaScript.
Das erste was Nutzer zu sehen bekommen, ist eine einfache Änderung unserer Standard-WebGLStart-Funktion. Neben dem üblichen Setup-Zeug ruft es eine neue Funktion auf, um die Welt vom Server zu laden:
Gehen wir jetzt zu diesem Code. Die loadWorld-Funktion befindet sich knapp über drawScene, etwa drei Viertel des Weges durch die Datei. Es sieht so aus:
Die Art von Code mag vertraut aussehen. Er ist sehr ähnlich zu den Dingen, die wir zum Laden der Texturen verwendet haben. Wir erstellen ein XMLHttpRequest-Objekt, welches das gesamte Laden übernimmt und weisen es an eine HTTP-GET-Anfrage zu verwenden, um die Datei world.txt aus dem gleichen Verzeichnis auf dem gleichen Server wie die aktuelle Seite zu erhalten. Wir spezifizieren eine Callback-Funktion, die in verschiedenen Stadien des Downloads aktualisiert werden soll und diese ruft wiederum ein handleLoadedWorld auf, wenn der XMLHttpRequest meldet, dass sein readyState 4 ist, was passiert, wenn die Datei vollständig geladen wurde. Sobald all dies eingerichtet ist, weisen wir den XMLHttpRequest an, den Prozess des Abrufs der Datei zu starten, indem wir eine Sendemethode aufrufen.
Machen wir nun weiter mit handleLoadedWorld, welches sich direkt über loadWorld befindet.
Die Aufgabe der Funktion besteht darin, den Inhalt der geladenen Datei zu analysieren und daraus zwei Puffer zu erstellen, wie wir Sie in den vorangegangenen Übungen so oft gesehen haben. Der Inhalt der geladenen Datei wird als String-Parameter data übergeben und das erste Bit des Codes analysiert ihn einfach. Das Format der Datei, welches wir für dieses Beispiel verwenden ist sehr einfach. Es enthält eine Liste von Dreiecken, die jeweils durch drei Eckpunkte spezifiziert sind. Jeder Scheitelpunkt befindet sich auf einer Linie zu sich selbst und enthält 5 Werte: seine X-, Y- und Z-Koordinaten sowie seine S- und T-Texturkoordinaten. Die Datei enthält auch Kommentare und Leerzeichen, welche beide ignoriert werden und es gibt eine Zeile am oberen Rand, die die Gesamtzahl der Dreiecke angibt.
Ist das nun ein furchtbar gutes Dateiformat? Nun, eigentlich nicht – es ist ziemlich schrecklich. Es fehlen viele Informationen, die wir gerne in eine reale Szene einbauen würden, z.B. Normals oder unterschiedliche Texturen für verschiedene Objekte. In einem realen Beispiel würden Sie ein anderes Format oder sogar JSON verwenden. Ich habe mich jedoch an dieses Format gehalten, weil es dasjenige ist, welches von der ursprünglichen OpenGL-Übung verwendet wird und es zudem schön und einfach zu analysieren ist. Nach den Ausführungen hier, werde ich den Parsing-Code nicht im Detail erklären. Hier ist es:
Am Ende des Tages ist alles, was der Code wirklich tut, alle Zeilen mit fünf durch Leerzeichen getrennten Werten zu nehmen und davon auszugehen, dass Sie Eckpunkte enthalten und daraus VertexPositionen und VertexTextureCoords-Arrays aufzubauen. Außerdem werden in vertexCount die Eckpunkte gezählt.
Das nächste Stück Code sollte mittlerweile sehr vertraut aussehen:
Wir erstellen also zwei Puffer, welche die geladenen Vertex-Details enthalten. Nach dem alles erledigt ist, löschen wir schließlich das DIV in dem HTML, welches die Worte „Loading World“ zeigt:
Das ist der ganze Code, welcher benötigt wird, um die Welt aus einer Datei zu laden. Bevor wir uns den Code anschauen, der tatsächlich verwendet wird, lassen Sie uns kurz anhalten und uns etwas Interessantes in der world.txt ansehen. Die ersten drei Eckpunkte, die das Dreieck in der Szene beschreiben, sehen folgendermaßen aus:
X, Y und Z beschreiben die Achsen, S und T die Texturkoordinaten. Wie Sie sehen, liegen die Texturkoordinaten zwischen 0 und 6. Es wurde aber schon vorher angesprochen, dass die Texturkoordinaten von 0 bis 1 reichen. Was geht hier vor? Die Antwort ist, dass wenn Sie nach einem Punkt in einer Textur fragen die S- und T-Koordinaten automatisch modulo eins genommen werden, so dass 5.5 vom gleichen Punkt im Texturbild wie 0.5 genommen wird. Das bedeutet, dass die Textur automatisch gekachelt wird, so dass Sie so oft wiederholt wird, wie es nötig ist, um das Dreieck zu füllen. Das ist natürlich sehr nützlich, wenn Sie eine kleine Textur haben, die Sie auf einem großen Objekt verwenden möchten z.B. eine Textur für ein Mauerwerk, um eine Wand abzudecken.
Kommen wir nun zum nächsten interessanten Code: drawScene. Wir müssen zunächst prüfen, ob die erstellten Puffer nach dem Laden der Welt auch wirklich geladen wurden. Sollte dies nicht der Fall sein, werden Sie aus dem Weg geräumt:
Wenn wir die Puffer eingerichtet haben, ist der nächste Schritt die übliche Einstellung für die Projektion und die Model-View-Matrizen:
Der nächste Schritt ist der Umgang mit der Kamera. Wir wollen also, dass sich unser Viewport durch die Szene bewegt. Wir müssen aber beachten, dass WebGL wie viele andere Dinge auch Kameras nicht direkt unterstützt, aber die Simulation ist einfach genug. Wenn wir eine Kamera hätten, würden wir für dieses einfache Beispiel sagen können, dass Sie auf einer bestimmten X-, Y- oder Z-Koordinate positioniert war und eine gewisse Neigung um die X-Achse von oben nach unten (Pitch) und einem bestimmten Winkel um die Y-Achse von links ider rechts (Yaw) hatte.
Da wir die Position der Kamera nicht ändern können – welche immer effektiv bei (0, 0, 0, 0) ist und direkt auf die Z-Achse blickt, müssen wir WebGL irgendwie mitteilen, dass es die Szene, die wir zeichnen müssen mit Hilfe von X-, Y- und Z-Koordinaten in einem neuen Bezugsrahmen basierend auf der Position und Rotation der Kamera, welche wir Eye Space nennen, anpassen soll.
Ein einfaches Beispiel könnte hier helfen. Stellen wir uns vor, wir haben eine wirklich einfache Szene, in der es einen Kubus mit seinem Zentrum bei (1, 2, 3) im Weltraum gibt und sonst nichts. Wir wollen eine Kamera simulieren, die mit Blick auf die Z-Achse auf (0, 0, 7) positioniert ist und über keine Pitches oder Yaws verfügt. Dazu transformieren wir die Weltraumkoordinaten und wir enden mit Eye Space Koordinaten für das Zentrum des Würfels von (1, 2, -4). Rotationen verkomplizieren die Dinge ein wenig, aber nicht viel.
Es ist wahrscheinlich ziemlich klar, dass dies ein weiterer Fall für die Verwendung von Matrizen ist und wir könnten in der Tat eine Kamera-Matrix behalten, welche die Position und Rotation der Kamera darstellt. Aber für dieses Beispiel können wir es noch einfacher halten. Wir können einfach unsere bestehende Model-View-Matrix verwenden.
Es stellt sich heraus, dass wir eine Kamera simulieren können, indem wir die Szene in die entgegengesetzte Richtung ausfahren, wie wir uns bewegen würden, wenn wir zur Position und Rotation der Kamera gehen und anschließend die Szene mit unserem üblichen relativen Koordinatensystem zeichnen würden. Wenn wir uns als Kamera vorstellen, würden wir uns selbst positionieren, indem wir uns in die entsprechende Position bewegen und uns dann entsprechend drehen. Um also wieder auszusteigen, machen wir zunächst die Drehung und anschließend die Bewegung rückgängig.
Mathematischer ausgedrückt, können wir eine Kamera simulieren, die sich an der Position (x, y, z) befindet und um ein Yaw von ψ und einen Pitch von θ gedreht wird, indem wir zuerst mit θ um die X-Achse drehen, anschließend mit -ψ um die Y-Achse und dann mit (-x, -y, -z). Sobald dies erledigt ist, haben wir die Model-View-Matrix in einen Zustand gebracht, in dem alles, was von da an gezeichnet wird, Weltkoordinaten verwenden kann und Sie wird durch die Magie unserer Matrix-Multiplikation im Vertex-Shader automatisch in Eye Space Koordinaten transformiert.
Es finden sich aber noch andere Wege Kameras zu positionieren und wir werden Sie in späteren Übungen besprechen. Hier der Code dafür:
Sobald wir dies erledigt haben, müssen wir nur noch die Szene zeichnen, wie in den zuvor geladenen Puffern beschrieben. Hier der Code, dieser sollte aus früheren Übungen bekannt sein:
Nun haben wir einen Großteil des neuen Codes in dieser Übung behandelt. Das letzte Bit des Codes verwenden wir dazu, um unsere Bewegungen zu kontrollieren, einschließlich der Jogging-Bewegung, während Sie laufen. Wie in den vorangegangenen Tutorials bereits beschrieben sind die Tastaturaktionen auf dieser Seite so konzipiert, dass Sie jedem die gleiche Bewegungs-geschwindigkeit geben, unabhängig davon wie schnell oder langsam seine Maschine ist. Besitzer von schnelleren Maschinen profitieren von einer besseren Bildrate und nicht von einer schnelleren Bewegung.
Dies funktioniert dadurch, dass wir in unserer Funktion handleKeys die Tastenkombinationen verwenden, die der Benutzer gerade drückt, um eine Geschwindigkeit auszuarbeiten sowie eine Änderung der Rate der Pitches und Yaws. Diese sind alle gleich Null, wenn keine Tasten gedrückt werden. Sofern entsprechende Tasten gedrückt werden Sie durch Einheiten in Millisekunden auf bestimmte Festwerte gesetzt. Hier der Code dazu:
Ein Beispiel: Wird die linke Cursortaste gedrückt, dann ist unsere yawRate auf 0,1°/ms eingestellt, also 100°/s – oder anders ausgedrückt, wie beginnen uns alle 3,6 Sekunden mit einer Umdrehungsgeschwindigkeit nach links zu drehen.
Diese rate-of-changes Werte werden in der Animationsfunktion verwendet, um die Werte von xPos und zPos sowie die Yaws und Pitches einzustellen. Ypos wird ebenfalls in animate gesetzt, allerdings mit etwas anderer Logik. Werfen wir nun einen Blick auf das Besprochene. Sie können den Code in der Datei direkt unter drawScene sehen. Hier sind die ersten Zeilen:
Das meiste davon ist unser normaler Code, um die Anzahl der Millisekunden zu berechnen, die seit dem letzten Aufruf von animate vergangen sind. JoggingAngle ist interessanter. Die Art und Weise, wie wir unseren Jogging-Effekt bekommen, indem wir unsere Y-Position einer Sinuswelle um einen zentralen Wert auf „Head-Höhe“ folgen lassen, wenn wir gerade nicht ruhen. JoggingAngle ist der Winkel, den wir in die Sinusfunktion drücken, um unsere aktuelle Position zu erhalten.
Betrachten wir nun den Code und passen auch x und z an, um Bewegungen zu ermöglichen:
Offensichtlich sollten Positionsänderungen und der Jogging-Effekt nur dann stattfinden, wenn wir uns tatsächlich bewegen – wenn also die Geschwindigkeit nicht gleich Null ist, werden xPos und zPos mit einem einfachen Trigonomie-Effekt um die aktuelle Geschwindigkeit angepasst. Als nächstes wird joggingAngle weiterbewegt und verwendet, um unsere aktuellen yPos zu berechnen. Alle Zahlen, die wir verwenden, werden mit der Anzahl der Millisekunden seit dem letzten Aufruf multipiliziert, was bei der Geschwindigkeit durchaus sinnvoll ist, da Sie bereits in Einheiten pro Millisekunde angegeben ist. Im Anwendungsbeispiel wurde der Wert 0.6 für die verstrichene Anzahl von Millisekunden für den JoggingAngle eingestellt. Es ist im Grunde nur etwas, was durch Trial und Error bestimmt wird, um einen schönen, realistischen Effekt zu erzielen.
Sobald das erledigt ist, müssen wir Yaw und Pitch entsprechend ihrer jeweiligen Veränderungsrate anpassen, was auch dann möglich ist, wenn wir uns nicht bewegen:
Nun benötigen wir nur noch die aktuelle Zeit aufzuzeichnen, damit wir die verstrichene Anzahl von Millisekunden beim nächsten Aufruf von animate berechnen können.
Nun sind wir fertig. Sie wissen jetzt, wie Sie ein Szenendiagramm aus einer Textdatei laden und eine Kamera implementieren können.
Im nächsten Beitrag wird gezeigt, wie man eine Kugel anzeigt und mit Hilfe von Maus-Ereignissen dreht und es wird auch erklärt, wie man Rotationsmatrizen verwenden kann, um ein irritierendes Problem zu vermeiden, das als Gimbal-Lock bezeichnet wird.