WebGL ist eine Grafikbibliothek für Webbrowser, die auf einer Version von OpenGL für eingebettete Geräte, OpenGL ES, basiert. Die Bibliothek ermöglicht die individuelle 3D-Grafikwiedergabe in Echtzeit in modernen Browsern, einschließlich die Verwendung von Shadern. Es gibt eine Vielzahl von Szenarien, in denen Sie eine solche Bibliothek nutzen können wie z.B. Browsergames, 3D Maps oder Produktansichten. Die einfache WebGL-Oberfläche ist über JavaScript zugänglich und sogar ganze Frameworks sind verfügbar wie z.B. three.js.
In diesem Tutorial werde ich die Grundlagen zum Herumfrickeln und Experimentieren mit WebGL und Fragment-Shadern liefern, da dadurch das Grundkonzept relativ leicht zu verstehen ist.
Wie bereits erwähnt, erfolgt der Zugriff auf WebGL über JavaScript. Für meine Experimente verwende ich die neueste Version von Chrome und einen einfachen Texteditor. Der Rahmen unseres Spielzeugs wird minimalistisches HTML5 sein, das ein Canvas-Element enthält, da WebGL Canvaselemente zum Zeichnen verwendet:
Das Ergebnis ist ein leeres Bild. WebGL wird dann initialisiert, indem ein WebGL-Objekt auf der Basis des Canvas abgerufen wird, das dann für weitere WebGL-Operationen verwendet werden kann. Hier ist eine Erweiterung unseres JavaScript, wie WebGL initialisiert und das WebGL-Canvas mit einer roten Farbe löscht:
Der Code ist recht simpel. Wir deklarieren zwei globale Variablen, das GL- und Canvas-Objekt, die wir zur Durchführung der Grundinitialisierung und in der Renderfunktion zur kontinuierlichen Durchführung der Zeichnung benötigen. Als nächstes setzen wir das Window Onload Event auf init.
Die Initialisierungsfunktion ruft das HTML-Canvas-Objekt ab und initialisiert ein WebGL-Objekt auf seiner Basis. Wir legen die Höhe und Breite des Canvas fest und initialisieren dann das sogenannte Viewport des WebGL-Objekts. Danach wird ein erster Aufruf zum Rendern gemacht, um tatsächlich etwas zu zeichnen.
Die Renderfunktion fordert zunächst einen nächsten Animationsframe an, um ein kontinuierliches Rendering zu gewährleisten. Danach wird eine klare Farbe mit voller Deckkraft auf Rot gesetzt und gl.clear wird aufgerufen. Der Aufruf von gl.clear enthält eine Konstante, die angibt, dass die Farbe des Bildschirmpuffers beeinflusst werden soll.
Bislang ist das nicht allzu aufregend. Um etwas Nützlicheres zu bekommen und etwas, mit dem man herumspielen kann, benötigen wir das Konzept eines Shaders. Shader sind kleine Programme, die normalerweise auf dem Grafikprozessor parallel ausgeführt werden. Die beiden hier relevanten Shadertypen sind der sogenannte Fragment- und der Vertex-Shader. Im Falle von WebGL sind sie in einer Ableitung der GLSL geschrieben.
Die 3D-Grafikverarbeitung funktioniert folgendermaßen: Es wird ein 3D-Modell geladen, das im Grunde genommen eine Menge von Punkten im dreidimensionalen Raum ist, sowie eine Beschreibung, wie diese Punkte Polygone bilden. Dieses Modell und insbesondere die Menge der Eckpunkte wird dann von einer lokalen, natürlichen Beschreibung in eine Beschreibung transformiert, die dann direkt zum Zeichnen der Polygone verwendet werden kann. Die verschiedenen Transformationen spielen hier keine große Rolle, aber es ist erwähnenswert, dass Skalierungen, Rotationen und perspektivische Korrekturen alle während der Phase der Transformation eines Meshes vom Objektraum in den Bildschirmraum durchgeführt werden. Die meisten dieser Berechnungen können Vertex für Vertex durchgeführt werden – mit einem Vertex-Shader. Allerdings werden wir hier nur einen trivialen Vertex-Shader verwenden, der keine Transformationen vornimmt, so dass Sie die meisten Informationen in diesem Abschnitt ignorieren können.
Mit dieser normalisierten Information kann die Render-Engine dann mit relativ einfachen Algorithmen Polygone Pixel für Pixel zeichnen. Hier kommt der Fragment-Shader ins Spiel. Stellen Sie sich Fragmente als Pixel vor, die von einem Polygon bedeckt sind. Für jedes Fragment wird ein Fragment-Shader aufgerufen, der mindestens die Koordinaten des Fragments als Eingabe hat und die Farbe liefert, die als Ausgabe gerendert werden soll.
Normalerweise werden Fragment-Shader für die Beleuchtung und Nachbearbeitung verwendet. Die Idee für ein einfaches Experimentieren ist jedoch wie folgt: Wir zeichnen zwei Polygone (Dreiecke), die lediglich den gesamten sichtbaren Bildschirm bedecken. Mit Hilfe eines Fragment-Shaders können wir dann die Farbe jedes einzelnen Pixels auf dem Bildschirm bestimmen. So können wir uns den Fragment-Shader als Funktion von Bildschirmkoordinaten bis zu Farben vorstellen und mit solchen Funktionen experimentieren. Und es stellt sich heraus, dass dies ein sehr lustiges Spielzeug ist. Tatsächlich ist das, was Sie auf diese Weise zeichnen können, nur durch ihre Neugier begrenzt. Als Beispiel sei angeführt, dass ganze Raytracing-Engines mit Fragment-Shadern ausgestattet sind. Wir werden jedoch mit etwas Einfacherem beginnen. Starten wir mit etwas Code, der den Rahmen für unsere Experimente definiert.
Zuerst müssen wir ein Primitiv auf den aufgeräumten Bildschirm zeichnen, ein Quad bestehend aus zwei Dreiecken, das den ganzen Bildschirm ausfüllt und als Canvas für unseren Fragment-Shader dient, wie oben beschrieben. Aus diesem Grund führen wir eine neue globale Variable ein, die ihre Beschreibung enthält:
Wir initialisieren den Puffer in unserer init-Funktion:
Die Werte sind das, was wir wollen, denn standardmäßig werden -1 und 1 auf die Grenzen unseres Bildschirms gemappt. Das Primitiv wird dann in der Renderfunktion gezeichnet, so wie hier:
Wenn Sie diesen Code ausführen, werden Sie nichts mehr sehen. Das liegt daran, dass WebGL immer noch nicht weiß, wie man das Quad zeichnet. Hier kommen unsere beiden Shader ins Spiel. Wie ich bereits erwähnt habe, benötigen wir sowohl einen Vertex- als auch einen Fragment-Shader. Wir stellen sie als neue Skriptelemente über unserem JavaScript vor. Unser sehr einfacher Standard-Vertex-Shader sieht so aus:
Es teilt WebGL mit, dass für das finale Rendering genau die gleiche Position verwendet werden soll, wie in der Vertex-Beschreibung angegeben ist. Mit anderen Worten, wie verwenden direkt die Dateneingabe des Scheitelpunktes als Ausgabe. Der Pixel-Shader ist etwas interessanter. Als erstes das Skript:
Was wir hier haben, ist eine Funktion, die die endgültige Farbe eines Fragments oder Pixels auf unserem gezeichneten Canvas liefert. Dies geschieht durch Setzen der Variablen gl_FragColor. gl_FragCoord.x und gl_FragCoord.y sind die x- und y-Positionen auf unserem Canvas im Bildschirmbereich, d.h. sie reichen von 0 bis 640 und von 0 bis 480, da dies die Größe des Canvas auf dem Bildschirm ist. Für jede Kombination dieser Werte wird der Pixel-Shader aufgerufen und eine Farbe berechnet. In diesem Fall setzen wir die rote Komponente in Abhängigkeit von der x-Position und die grüne Komponente in Abhängigkeit von der y-Position. Die Farbkomponenten von gl_FragCoord.y reichen von 0.0 bis 1.0, also müssen wir die Position durch die Größe des Bildschirms teilen, um alle Werte zwischen 0 und 1 zu erhalten.
Anschließend müssen wir WebGL mitteilen, dass diese Shader benutzt werden sollten, um unser Primitiv zu rendern. Dies geschieht wie folgt. Zuerst müssen wir die Shader in unsere init-Funktion laden. Wir deklarieren zunächst vier lokale Variablen:
Und fügen Sie anschließend den folgenden Code direkt vor dem Aufruf von render() ein:
Dieser Code ist eigentlich ziemlich einfach. Wir laden den Inhalt der Skriptelemente, erstellen einen Shader des richtigen Typs mit einem Aufruf von gl.createShader() und kompilieren den Shader-Code. Dies ist genau das gleiche Verfahren sowohl für den Vertex- als auch für den Fragment-Shader. Der dritte Block erzeugt ein sogenanntes Programm, das im Grunde genommen eine Art und Weise ist, wie Primitive gerendert werden. Ein Programm kann aus mehreren Shadern bestehen, aber wir benötigen mindestens einen Vertex- und einen Fragment-Shader. Das Programm wird mit einem Aufruf von gl.createProgram() erstellt und die beiden Shader werden angehängt. Das Programm wird dann verlinkt und wir weisen WebGL an, es im nachfolgenden Polygon-Rendering mit einem Aufruf von gl.useProgram() zu verwenden.
Wenn wir diesen Code aufrufen, sehen wir immer noch nichts. Das liegt daran, dass die Shader eine entsprechende Einstellung der Eingangsvariablen erfordern. In diesem Fall müssen wir WebGL anweisen, dem Vertex-Shader die Positionsdaten aus dem Puffer, den wir oben erstellt haben, zur Verfügung zu stellen. Dies geschieht wie folgt in unserer Render-Funktion, direkt vor dem Aufruf von gl.drawArrays:
Der resultierende Code ist funktionsfähig und erfüllt unsere Erwartungen. Unser Canvas ist nicht mehr rot, sondern sieht nun wie folgt aus:
Dies ist der Beweis dafür, dass der Wertebereich von gl_FragCoord wie oben beschrieben ist. Du könntest ein wenig innehalten und darüber nachdenken. Eine Sache, die Sie unbedingt beachten sollten ist, dass der höchste y-Wert (480) tatsächlich auf dem Canvas liegt. Es ist wichtig zu wissen, wie genau die Koordinaten hier funktionieren:
Hier können Sie anfangen, herumzuspielen. Als zweites Beispiel sei hier der folgende Fragment-Shader genannt:
Das Bit am Anfang, das die Genauigkeit der Float-Werte festlegt, scheint notwendig zu sein, um Float-Variablen in ihren Shadern zu verwenden. Wie Sie sehen, können Sie einfache Kontrollstrukturen in Shadern verwenden. Sie werden wahrscheinlich sofort bemerkt haben, was der Code bewirkt. Es gilt die einfache Formel für einen Punkt, der in einer zweidimensionalen Kugel mit dem Radius 80 liegt. Das Ergebnis entspricht den Erwartungen:
Nur zu, spielen Sie ein wenig damit herum. Welche Arten von Formen können Sie erstellen? Als letztes Beispiel wollen wir die Mandelbrot Menge rendern. Betrachten wir dazu den folgenden Fragment-Shader:
Mit einem Trick können wir die zweite Variable verwenden und uns die Anzahl der erforderlichen Iterationen merken. Das Problem ist, dass WebGL die for-Schleife benötigt, um eine lokale Zählvariable zu verwenden, die im Kopf der for-Schleife deklariert ist.
Mit diesen Dingen herumzuspielen kann sehr schnell süchtig machen. Tatsächlich existieren Websites, die einfache Frameworks für das Herumspielen mit Fragment-Shadern bereitstellen und ganze Communities, die sich um die drehen. Empfehlenswert ist ShaderToy. Hier können Sie ihre Shader freigeben, die Ergebnisse sofort sehen und bewerten. Anbei ein Beispiel:
Vielen Dank fürs Lesen.