WebGL Grundlagen 5 – Einführung in die Texturierung.
Willkommen zum fünften Teil unserer Grundlagenserie zu WebGL. Diesmal werden wir einem 3D-Objekt eine Textur hinzufügen, d.h. wir werden es mit einem Bild bedecken, das wir aus einer separaten Datei laden. Dies ist wirklich ein nützlicher Weg, um ihrer 3D-Szene Details hinzuzufügen ohne dass Sie die zu zeichnenden Objekte zu komplex gestalten müssen. Stellen Sie sich eine Steinmauer in einem Labyrinth-Spiel vor. Sie wollen wahrscheinlich nicht jeden Block in der Wand als separates Objekt modellieren, sondern erstellen stattdessen ein Bild von Mauerwerk und bedecken die Wand damit. Eine ganze Wand kann dann nur noch ein Objekt sein.
Nach dem Umsetzen der kommenden Ausführungen werden wir das folgende Ergebnis erhalten:
Mehr darüber, wie das Ganze funktioniert, werden Sie weiter unten erfahren.
Ein kurzer Hinweis: Dieser Teil richtet sich an Personen mit fortgeschrittenen Programmierkenntnissen, aber ohne wirkliche Erfahrungen in 3D. Sie werden den Code verstehen und es zum Laufen bringen, so dass nichts mehr der Erstellung ihrer Website in 3D im Wege steht. Wenn Sie die vorherigen Teile noch nicht gelesen haben, sollten Sie dies vorher tun.
Es existieren genaue zwei Möglichkeiten den Code für dieses Beispiel zu generieren. Zum Einen können Sie einfach „View Source“ verwenden, während Sie sich die Live-Version ansehen. Alternativ könnten Sie GitHub verwenden und den Code aus dem dortigen Repository kopieren. Unabhängig von der verwendeten Methode können Sie den Code mit ihrem bevorzugten Texteditor laden und sich genauer anschauen.
Um den Trick zu verstehen, wie Texturen funktionieren müssen Sie vorher verinnerlichen, dass Texturen eine besondere Art sind die Farbe eines Punktes auf einem 3D-Objekt einzustellen. Sicher erinnern Sie sich noch an unseren zweiten Grundlagenteil. Dort lernten wir, dass Farben durch Fragment-Shader spezifiziert werden, also müssen wir das Bild laden und an den Fragment-Shader senden. Der Fragment-Shader muss auch wissen, welches Bit des Bildes für das Fragment an dem gearbeitet wird, verwendet werden soll. Also müssen wir diese Information auch an ihn weiterleiten.
Beginnen wir mit dem Code, der die Textur lädt. Wir callen es gleich zu Beginn der Ausführung des JavaScripts unserer Seite, in webGLStart am Ende der Seite:
Schauen wir uns initTexture an – es ist ungefähr ein Drittel des Weges vom Anfang der Datei entfernt und es ist alles neuer Code:
Wir erstellen also eine globale Variable, um die Textur zu halten. In einem Beispiel aus der realen Welt hätten Sie natürlich mehrere Texturen und würden keine Globals verwenden, aber wir möchten zu Beginn die Komplexität gering halten. Wir verwenden gl.createTexture, um eine Texturreferenz zu erstellen, die wir in die globale Textur einfügen. Anschließend erstellen wir ein JavaScript Image-Objekt und setzen es in ein neues Attribut, welches wir an die Textur anhängen, wobei wir wiederum die Bereitschaft von JavaScript ausnutzen jedes Feld auf ein beliebiges Objekt zu setzen. Textur-Objekte haben standardmäßig kein Bildfeld, aber es ist für uns bequemer eines zu haben und deswegen erstellen wir eines.
Der nächste logische Schritt besteht darin, das Image-Objekt dazu zu bringen das eigentliche Bild zu laden welches es enthalten wird. Deshalb fügen wir ihm eine Callback-Funktion hinzu. Diese wird aufgerufen, wenn das Bild vollständig geladen wurde und es ist daher am sichersten es zuerst zu setzen. Sobald wir das eingerichtet haben, setzen wir die src-Eigenschaft des Bildes und wir sind fertig. Das Bild wird asynchron geladen, d.h. der Code welcher den src des Bildes setzt wird sofort zurückgegeben und ein Hintergrund-Thread lädt das Bild vom Webserver. Sobald das erledigt ist wird unser Callback aufgerufen. Das Callback wiederum ruft handleLoadedTexture auf.
Wir müssen WebGL zunächst mitteilen, dass unsere Textur unsere aktuelle Textur ist. WebGL-Textur-Funktionen arbeiten alle auf dieser aktuellen Textur anstatt eine Textur als Parameter zu nehmen. BindTexture hingegen ist ähnlich zu dem gl.bindBuffer-Muster, welches wir uns vorher angesehen haben.
Als nächstes teilen wir WebGL mit, dass alle Bilder, die wir in Texturen laden vertikal gespeichert werden müssen. Wir müssen diesen Schritt aufgrund eines Unterschiedes in den Koordinaten tun. Für unsere Texturkoordinaten verwenden wir Koordinaten, die normalerweise in der Mathematik verwendet werden, wenn Sie sich entlang der vertikalen Achse nach oben bewegen. Diese stimmen mit den X-,Y und Z-Koordinaten überein, die wir dazu verwenden um unsere Scheitelpositionen zu spezifizieren. Im Gegensatz dazu verwenden die meisten anderen Computergrafiksysteme z.B. das GIF-Format, das wir für das Texturbild verwenden. Es handelt sich dabei um Koordinaten, die sich erhöhen, wenn Sie sich auf der vertikalen Achse nach unten bewegen. Die horizontale Achse ist in beiden Koordinatensystemen gleich. Dieser Unterschied auf der vertikalen Achse bedeutet, dass aus der WebGL Perspektive das GIF-Bild, welches wir für unsere Textur verwenden bereits vertikal gespiegelt ist und wir müssen es aufklappen.
Der nächste Schritt ist es unser frisch geladenes Bild mit texImage2D in den Texturbereich der Grafikkarte hochzuladen. Die Reihenfolge der Parameter sind abhängig von der Art des Bildes welches wir verwenden. Dazu gehören der Detaillierungsgrad, das Format, indem wir es auf der Grafikkarte speichern möchten, die Größe jedes Kanalbildes und schließlich das Bild selbst.
Die beiden nächsten Zeilen spezifizieren spezielle Skalierungsparameter für die Textur. Der erste Teil erklärt WebGL, was zu tun ist, wenn die Textur den großen Teil des Bildschirms relativ zur Bildgröße ausfüllt. Mit anderen Worten: Er gibt ihnen Hinweise, wie er vergrößert werden kann. Der zweite ist der äquivalente Hinweis, wie man ihn verkleinern kann. Es gibt verschiedene Arten von Hinweisen zur Skalierung, die Sie angeben können. NEAREST ist die unattraktivste davon. Es besagt lediglich, dass das Originalbild wie es ist verwendet werden soll. Das bedeutet, dass es bei Nahaufnahmen sehr blockig aussieht. Es hat jedoch den Vorteil, dass es auch auf langsamen Maschinen sehr schnell ist. In dem nächsten Teil werden wir uns mit der Verwendung verschiedener Hinweise zur Skalierung befassen, so dass Sie die Leistung und das Aussehen der einzelnen Komponenten vergleichen können.
Das ist alles, was benötigt wird, um die Textur zu laden. Als nächstes befassen wir uns mit initBuffers. Zum vorherigen Code haben wir eine interessante Änderung. Der Vertex-Farben-Puffer wurde durch den Textur-Koordinaten-Puffer ersetzt. So sieht der Code jetzt aus:
Sobald dies geschehen ist, setzen wir die aktuelle Textur auf Null. Dies ist nicht unbedingt notwendig, aber eine gute Praxis.
Sie sollten sich mit dieser Art von Code jetzt ziemlich wohl fühlen und sehen, dass wir nur ein neues Per-Vertex-Attribut in einem Array-Puffer angeben und dass dieses Attribut zwei Werte pro Vertex hat. Diese Texturkoordinaten geben an, wo in kartesischen X-, Y-Koordinaten der Scheitelpunkt in der Textur liegt. Für diese Zwecke dieser Koordinaten behandeln wir die Textur als 1.0 breit und 1.0 hoch, also (0,0) unten links und (1,1) für oben rechts. Die Konvertierung von dieser in die reale Auflösung des Texturbildes übernimmt für uns WebGL.
Das ist die einzigste Änderung in initBuffers, also lasst uns mit DrawScene fortfahren. Die interessantesten Änderungen in dieser Funktion sind natürlich jene, die Sie dazu bringen die Textur zu verwenden. Bevor wir diese jedoch durchgehen, gibt es eine Reihe von Änderungen, welche sich auf wirklich einfache Dinge beziehen, wie das Entfernen der Pyramide und die Tatsache, dass sich der Würfel nun auf eine andere Art und Weise dreht. Ich werde diese nicht im Detail beschreiben, da Sie ziemlich einfach zu berechnen sein sollte. Sie sind in diesem Snippet von oben in der DrawScene Funktion rot hervorgehoben.
Es gibt auch übereinstimmende Änderungen in der Animationsfunktion, um xRot, yRot und zRot zu aktualisieren, die wir hier nicht weiter ausführen werden.
Wenn diese aus dem Weg geräumt sind, schauen wir uns den Texturcode an. In initBuffers haben wir einen Puffer eingerichtet, welcher die Texturkoordinaten enthält. Dieser muss an das entsprechende Attribut gebunden werden, damit die Shader ihn sehen können.
…da WebGL nun jetzt genau weiß, welches Bit der Textur jeder Vertex verwendet, müssen wir ihm mitteilen, dass er die Textur verwenden soll, die wir zuvor geladen haben und anschließend den Würfel zeichnen.
Was hier passiert ist etwas komplexer. WebGL kann bis zu 32 Texturen während eines Aufrufs von Funktionen wie gl.drawElements verarbeiten, die von TEXTURE0 bis TEXTURE31 durchnummeriert sind. Wir teilen den ersten beiden Zeilen mit, das TEXTUR0 jene ist, die wir zuvor geladen haben und dann in der dritten Zeile übergeben wir den Wert Null bis zu einer Shader-Uniform. Diese teilt dem Shader mit, dass wir die Textur 0 verwenden. Wir werden später sehen, wie das Ganze genau funktioniert.
Sobald diese drei Linien ausgeführt werden, kommen wir unserem Ziel ein Stück näher. Deshalb benutzen wir einfach den gleichen Code wie vorher, um die Dreiecke zu zeichnen aus denen der Würfel besteht.
Der einzig verbleibende neue Code, welcher erklärt werden muss sind die Änderungen an den Shadern. Schauen wir uns zuerst den Vertex-Shader an:
Das ist sehr ähnlich wie das farbverwandte Material, welches wir im zweiten Grundlagenteil in unseren Vertex-Shader eingebaut hatten. Alles was wir tun, ist die Texturkoordinaten als ein per-vertex-Attribut zu akzeptieren und Sie direkt in einer Variable weiterzugeben.
Sobald dies für jeden Vertex aufgerufen wurde, berechnet WebGL Werte für die Fragmente zwischen den Verticles durch lineare Interpolation zwischen den Verticles. Ein Fragment auf halbem Weg zwischen den Knoten mit den Texturkoordinaten (1,0) und (0,0) enthält also die Texturkoordinaten (0.5,0) und ein Fragment auf halbem Weg zwischen (0,0) und (1,1) enthält (0.5, 0.5). Als nächstes kommt der Fragment-Shader.
Also nehmen wir die interpolierten Texturkoordinaten auf und können mit einer Variable vom Typ Sampler die Art und Weise, wie der Shader die Textur darstellt, beeinflussen. In drawScene wurde unsere gl.TEXTURE0 gebunden und der einheitliche uSampler auf den Wert Null gesetzt, so dass dieser Sampler unsere Textur repräsentiert. Der Shader beschränkt sich auf die Nutzung der Funktion texture2D, um die passende Farbe aus der Textur mit Hilfe der Koordinaten zu erhalten. Texturen verwenden traditionell s und t für ihre Koordinaten anstelle von x sowie y und die Shader-Sprache unterstützt diese als Aliase. Wir könnten genauso einfach vTextureCoord.x und vTextureCoord.y verwenden.
Sobald wir die Farbe für das Fragment haben, sind wir fertig. Wir haben ein texturiertes Objekt auf dem Bildschirm.
So, jetzt sind wir durch. Nun wissen Sie wie Sie Texturen zu 3D-Objekten hinzufügen können, indem Sie ein Bild laden, WebGL anweisen es für eine Textur zu verwenden, ihre Objekt-Texturkoordinaten angeben und die Koordinaten und die Shader in den Shadern verwenden.
Über den folgenden Link gelangen Sie zum nächsten Grundlagenteil.