Willkommen zu unserem sechsten Teil unserer WebGL-Grundlagen. In diesem Beitrag werden wir thematisieren, wie Sie eine WebGL-Seite dazu bringen können Tastatureingaben zu akzeptieren, um die Geschwindigkeit und die Drehrichtung des Spin eines texturierten Würfels zu ändern. Zudem soll die Art der Filterung geändert werden, die auf der Textur verwendet wird, um eine niedrige Qualität mit schnellem oder eine hohe Qualität mit langsamem Rendering zu erhalten.
So wird das Ergebnis nach Umsetzen der entsprechenden Inhalte aussehen:
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.
Die größte Änderung zwischen diesem und dem letzten Teil ist, dass wir auch die Tastatur mit in die Thematik einbeziehen. Um das Verständnis zu erleichtern, werden wir uns in der Folge auch den Code ein wenig genauer ansehen, welcher direkt betroffen ist. Wenn Sie mit dem Scrollen in der Mitte des Codes beginnen, werden Sie eine Reihe definierter Variablen sehen:
xRot und yRot sind uns bereits aus dem fünften Grundlagenteil bekannt – Sie repräsentieren die aktuelle Rotation des Würfels um die X- und Y-Achse. Xspeed und ySpeed sollten ziemlich offensichtlich sein. Der Nutzer kann nun via Tastatureingabe die Rotationsgeschwindigkeit des Würfels mit den Cursortasten variieren. Dies sind auch die Bereiche, in denen wir die Änderungsraten von xRot und yRot beibehalten. Z ist natürlich die Z-Koordinate des Würfels und wird über die Tasten Page Up und Page Down gesteuert. Schließlich ist Filter eine ganze Zahl im Bereich von 0 und 2, die angibt, welcher von drei Filtern für die Textur verwendet wird, die wir auf dem Würfel abbilden.
Werfen wir einen Blick auf den Code, welcher den Filter jetzt antreibt. Die ersten Änderungen im Code finden sich weiter oben und betreffen das Laden der Textur. Der Code hat sich dieses Mal derart verändert, dass nichts rot markiert wird. Allerdings sollte es in dieser Form noch recht vertraut aussehen:
Wenn man sich zunächst die Funktion initTexture und die globale Variable createTexture ansieht, sollte klar sein, dass der Code zwar geändert wurde, aber jedoch der einzige wirkliche Unterschied darin besteht, dass wir drei WebGL-Texturobjekte in einem Array statt jeweils in einem erstellen. Zudem übergeben wir das Array an handleLoadedTexture in der Callback-Funktion, sobald das Bild geladen wird. Diesmal wird natürlich auch ein anderes Bild geladen.
HandleLoadedTexture wurde durch die Veränderungen nicht komplexer. Vorher haben wir nur ein einzelnes WebGL-Texturobjekt mit den Bilddaten initialisiert und zwei Parameter darauf gesetzt: gl.TEXTURE_MAG_FILTER und gl.TEXTURE_MIN_FILTER, beide auf gl.NEAREST. Hier initialisieren wir alle drei Texturen in unserem Array mit dem gleichen Bild, aber wir setzen unteschiedliche Paramter und die letzte Textur bekommt ein zusätzliches Bit Code. Hier sehen Sie, wie sich die verschiedenen Texturen im Detail unterscheiden:
Nächste Filterung.
Die erste Textur hat gl.TEXTURE_MAG_FILTER und gl.TEXTURE_MIN_FILTER wird auf gl.NEAREST gesetzt. Hierbei handelt es sich um unsere ursprüngliche Einstellung. Das bedeutet, dass WebGL sowohl bei der Vergrößerung als auch bei der Verkleinerung der Textur einen Filter verwenden sollte, welcher die Farbe eines bestimmten Punktes definiert, indem er einfach nach dem nächstgelegenen Punkt im Originalbild sucht. Optisch ansprechend ist das Ergebnis, wenn die Textur überhaupt nicht skaliert ist. Bei einem verkleinerten Bild hingegen ist die Darstellung auch noch ok. Wenn es jedoch vergrößert wird, sieht es blockig aus, da dieser Algorithmus die Pixel im Originalbild effektiv vergrößert.
Lineare Filterung.
Für die zweite Textur sind gl.TEXTURE_MAG_FILTER und gl.TEXTURE_MIN_FILTER beide gl.LINEAR. Hier verwenden wir wieder einmal den gleichen Filter, sowohl für die Vergrößerung als auch für die Verkleinerung. Allerdings kann der lineare Algorithmus für skalierte Texturen besser funktionieren. Im Grunde genommen verwendet er nur eine lineare Interpolation zwischen den Pixeln des ursprünglichen Texturbildes, also ein Pixel, welches auf halbem Weg zwischen einem schwarzen und weißen Pixel liegt erscheint grau. Das ergibt einen viel weicheren Effekt, obwohl scharfe Kanten etwas unscharf werden.
Mipmaps.
Für die dritte Textur sind gl.TEXTURE_MAG_FILTER und gl.TEXTURE_MIN_FILTER gl_LINEAR_MIPMAO_NEAREST. Dabei handelt es sich um die komplexeste der drei Optionen.
Lineare Filterung liefert ordentliche Ergebnisse, wenn die Textur vergrößert wird, aber es ist nicht wirklich besser als die nächstgelegene Filterung beim Verkleinern. Tatsächlich können beide Filter hässliche Aliasing-Effekte verursachen. Um zu sehen, wie diese aussehen, laden Sie das Sample erneut, so dass es die nächstgelegene Filterung verwendet und halten Sie die Taste Page UP einige Sekunden lang gedrückt, um es zu verkleinern. Sobald Sie dieses sehen stoppen Sie und versuchen Sie nach innen und außen zu zoomen und überwachen Sie das Funkeln. Anschließend betätigen Sie einmal F um zum linearen Filtern zu schalten, verschieben Sie es zurück und vorwärts ein Bit mehr und Sie werden sehen, dass Sie ziemlich genau den gleichen Effekt erhalten. Drücken Sie jetzt noch einmal F um die Mipmap-Filterung zu verwenden, vergrößern und verkleinern Sie das Bild und Sie sollten diesen Effekt eliminiert oder stark reduziert sehen.
Damit sind erst einmal die Würfel ziemlich weit weg. Versuchen Sie nun, durch die Filter zu gehen ohne Sie zu bewegen. Bei der nächsten oder linearen Filterung werden Sie feststellen, dass an manchen Stellen die dunklen Linien, die die Maserung des Holzes in der Textur ausmachen sehr klar sind, während Sie an anderen Stellen verschwunden sind. Der Würfel sieht ein wenig fleckig aus. Dies ist wirklich schlecht bei der nächsten Filterung, aber nicht viel besser als bei der linearen Filterung. Nur die Mipmapped-Filterung funktioniert gut.
Bei der nächsten und der linearen Filterung passiert nun Folgendes. Wenn die Textur auf ein Zehntel verkleinert wird, so wird auch der Filter jedes zehnte Pixel im Originalbild verwenden, um die verkleinerte Version zu erstellen. Die Textur hat nun ein hölzernes Korn Muster. Das bedeutet, dass das meiste hellbraun ist, aber es gibt auch dünne vertikale Linien. Stellen wir uns nun vor, dass das Korn zehn Pixel breit ist oder dass es mit anderen Worten ausgedrückt ein dunkelbraunes Pixel alle zehn Pixel horizontal gibt. Wenn das Bild auf ein Zehntel verkleinert wird, besteht die Chance, dass ein Pixel dunkelbraun ist und neun von zehn Pixeln hell. Oder anders ausgedrückt: Jede zehnte dunkle Linie im Originalbild wird genauso deutlich dargstellt wie zu der Zeit, als das Bild noch in voller Größe war und die anderen komplett verdeckt waren. Dies verursacht den Fleckeneffekt und fügt auch das Funkeln hinzu, wenn sich die Skala ändert, da die ausgewählten dunklen Linien bei den Skalierungsfaktoren 9.9, 10.0 und 10.1 völlig unterschiedlich sein können.
Wir möchten nun gerne die Situation schaffen, dass wenn das Bild auf ein Zehntel seiner ursprünglichen Größe skaliert wird, jedes Pixel auf der Grundlage des Durchschnitts des Zehntel-Pixel-Quadrats gefärbt wird von dem es eine verkleinerte Version ist. Um diese Situation zu schaffen könnten Echtzeitgrafiken verwendet werden. Allerdings wäre dies zu teuer und deshalb ist in diesem Fall die Mipmap-Funktion die bessere Option.
Die Mipmap-Filterung löst das Problem, indem Sie für die Textur eine Reihe von Nebenbildern erzeugt, welche sich auf die Hälfte, ein Viertel, ein Achtel usw. der Originalgröße belaufen bis hinunter zu einer 1:1 Pixel Version. Die Menge aller dieser Mip-Level wird als Mipmap bezeichnet. Jedes Mip-Level ist eine glatt gemittelte Version des Nächstgrößten und so kann die passende Version für den aktuellen Skalierungsgrad gewählt werden. Der Algorithmus hierfür hängt von dem Wert ab, der für gl.TEXTURE_MIN_FILTER verwendet wird. Das gewählte Mip-Level bedeutet im Grunde genommen: „Finde das nächstgelegene Mip-Level und mache einen linearen Filter darauf, um den Pixel zu erhalten.
Wir müssen eine zusätzliche Linie für diese Textur hinzufügen:
…ist die Zelle, die benötigt wird, um WebGL anzuweisen, die Mipmap zu generieren.
Nun wieder zurück zum Rest des Codes. Bisher haben wir uns die globalen Variablen angesehen und es wurde erläutert, wie die Texturen geladen und eingerichtet werden. Nun wollen wir sehen, wie die Globals und die Texturen verwendet werden, wenn wir tatsächlich die Szene zeichnen möchten.
DrawScene ist etwa drei Viertel des Weges durch die Seite und hat nur drei Änderungen. Die erste Änderung betrifft die Posittionierung, um den Würfel zu zeichnen. Anstatt einen festen Punkt zu verwenden, greifen wir auf die globale Variable Z zurück.
Die nächste Zeile ist eigentlich eine Zeile, die wir aus dem Code für den fünften Grundlagenteil entfernt haben. Jetzt drehen wir uns überhaupt nicht um die Z-Achse, sondern es gibt nur noch Rotationen um X und Y:
Schließlich müssen wir noch , welcher der zur Auswahl stehen Texturen wir noch verwenden möchten:
Das ist alles für die Änderungen in drawScene. Es gibt aber auch einige kleinere Änderungen in animate. Anstatt xRot und yRot mit konstanten Raten zu ändern, verwenden wir jetzt unsere neuen xSpeed- und ySpeed-Variablen.
Und das sind alle Änderungen am Code, mit Ausnahme derjenigen, die tatsächlich die Tastendrücke des Benutzers handhaben und unsere verschiedenen Globals, die darauf basieren, aktualisieren.
Die erste relevante Änderung ist unten rechts in webGLStart. Dort haben wir zwei neue Zeilen hinzugefügt:
Alles was wir hier tun, ist der JavaScript-Laufzeit mitzuteilen, dass wenn eine Taste gedrückt wird, wir wollen, dass unsere Funktion mit der Bezeichnung handleKeyDown aufgerufen wird und wenn eine Taste losgelassen wird, sollte Sie handleKeyUp aufrufen.
Werfen wir als Nächstes ein Blick auf diese Funktionen. Sie befinden sich im Code ungefähr in der Mitte und direkt unter den globalen Variablen, die wir uns früher angesehen haben:
Was wir hier tun, ist die Pflege eines Wörterbuchs, welches uns mit einem Schlüsselcode sagen kann, ob diese Taste gerade vom Benutzer gedrückt wird oder nicht. Wenn Sie sich mit der Funktionsweise von JavaScript nicht auskennen, ist es vielleicht interessant zu bemerken, dass jedes Objekt als Wörterbuch verwendet werden kann. Während die Synthax, die wir zur Initialisierung von currentlyPressedKeys verwenden, wie ein Python-Wörterbuch aussieht, handelt es sich eigentlich nur um eine leere Instanz des Basisobjekttyps.
Zusätzlich zur Pflege dieses Wörterbuchs der aktuell gedrückten Tasten, haben wir noch einiges mehr im Key-Down-Event-Handler für den Fall, dass die betreffende Taste F ist. In diesem Code wird die globale Filtervariable bei jedem Drücken der Taste durch die Werte 0, 1 und 2 getaktet.
Es lohnt sich jetzt die Zeit zu nehmen, um zu erklären, warum wir mit verschiedenen Schlüsseln auf zwei verschiedene Arten umgehen. In einem Computerspiel oder fast jedem anderen ähnlichen 3D-System können Tastendrücke auf zwei verschiedenen Arten funktionieren:
Sie können sofort eingreifen: „fire the laser“. Tastendrücke wie diese können sich automatisch mit einer festen Rate wiederholen, z.B. zweimal pro Sekunde.
Sie können einen Effekt haben, welcher davon abhängt, wie lange Sie sie gedrückt halten. Wenn Sie zum Beispiel die Taste drücken, um vorwärts zu gehen, werden Sie so lange vorwärts gehen, wie Sie die Taste gedrückt halten.
Wichtig ist, dass Sie mit der zweiten Art des Tastendrucks in der Lage sein werden simultan andere Aktionen auszuführen, wenn Sie z.B. mit einem Tastendruck vorwärts laufen, möchten Sie sich drehen bzw. schießen ohne das vorwärtslaufen zu unterbrechen. Dies ist eine grundlegend andere Art die Tastatur zu lesen als die normale Textverarbeitung. Wenn Sie die A-Taste in einem Textverarbeitungsprogramm gedrückt halten, erhalten Sie einen Strom von A`s, aber wenn Sie B drücken, während Sie A gedrückt halten, dann erhalten Sie ein B, aber der Strom von A`s wird gestoppt. Das Äquivalent in einem Spiel wäre, dass Sie aufhören jedes Mal zu laufen, wenn Sie eine Kurve gedreht haben. Das macht sicher auf Dauer keinen Spaß:
Also in dem Code, den wir gerade angesehen haben, wird die Taste F als die erste Art von Tastendruck behandelt. Das Wörterbuch wird von dem Code verwendet, der die zweite Art handhabt. Es verfolgt alle Tasten, die gerade gedrückt werden und nicht nur die letzte, welche gedrückt wird.
Das Wörterbuch wird tatsächlich in einer anderen Funktion verwendet handle Keys, die als nächstes auf der Seite erscheint. Bevor wir das durchgehen, springen Sie kurz an den unteren Rand des Codes und Sie werden sehen, dass es von der Tick-Funktion aufgerufen wird, genau wie die drawScene und animate:
Es handelt sich um eine lange, aber dafür auch sehr einfache Funktion. Diese prüft lediglich, ob gerade verschiedene Tasten gedrückt und unsere globalen Variablen entsprechend aktualisiert werden. Vor allem, wenn die Up und die Right-Cursortasten gedrückt werden, so kommt es zu einer Aktualisierung unserer globalen Variablen.
Und das war`s für dieses Mal. Jetzt wissen Sie alles, was Sie aus diesem Beitrag lernen können. Sie sollten mit der Zeit ein ziemlich gutes Verständnis dafür entwickeln, wie verschiedene Filter die Art und Weise beeinflussen, wie Texturen verschiedene Skalierungsfaktoren betrachten. Zudem werden Sie die Benutzereingaben von der Tastatur auf eine Weise lesen können, die gut mit 3D-Animationen funktioniert.