WebGL - 3D im Browser

siehe auch WebGPU!
 Home
 WebGL Api Spickzettel
 WebGL Sicherheit

Tutorial
 0 : WebGL Browser
 1 : Das erste Dreieck
 2 : 3D-Mathematik
 3 : Farbe
 4 : Animation
 5 : Interaktion I
 6 : Texturen
 7 : Beleuchtung I
 8 : Interaktion II

Links
 WebGL Beispiele
 WebGL Frameworks
 ext. WebGL Tutorials


 Kontakt / Impressum
 webgl ([ät)] peter-strohm Punkt de

1 Das erste Dreieck

>>>> Direkt zum Beispiel <<<<     >>>Quellcode zum Beispiel <<<

Ich gehe jetzt davon aus, dass du schon WebGL-Beispiele die andere Leute programmiert haben in deinem Browser gesehen hast und damit die Grundvoraussetzung zum Start in die eigene Entwicklung gegeben ist. Die Softwareanforderungen sind damit auch schon erfüllt. Zum Erstellen von WebGL-Seiten benötigst du außer einem Browser (get.webgl.org gibt Auskunft über geeignete Browser) nur noch einen Texteditor wie z.B. Notepad++.
Da WebGL im Internet-Browser dargestellt wird, muss es in HTML-Dateien eingebettet werden. Wenn du nicht weißt was HTML ist, bis du hier falsch. HTML ist zwar nicht schwierig zu erlernen und man muss auch kein Experte sein um in WebGL einzusteigen, aber ein paar Grundkenntnisse sollten schon da sein. Das gleiche gilt für JavaScript und allgemeine Grundlagen der Programmierung.


1.1 Was ist WebGL und was ist es nicht ?

WebGL ist die Integration von OpenGL (genaugenommen OpenGL ES 2.0) in JavaScript.
Somit bietet WebGL eine Möglichkeit, rechenintensive Grafiken (i.d.R. 3D-Szenen) innerhalb von Webseiten zu verwenden.
WebGL ist NICHT eine beschreibende Sprache, in der du so etwas wie
<3DWuerfel x=1.0 y=1.0 z=1.0>
schreiben kannst und dann einen 3dimensionalen Würfel siehst. Zumindest nicht ohne Hilfsmittel, aber dazu später mehr.
Ich möchte damit ausdrücken, dass WebGL ziemlich weit unten anfängt. Wer WebGL programmieren will, sollte wissen was eine Matrix ist (in der Mathematik, nicht im Kino!), wie man in der räumlichen Geometrie mit Vektoren rechnet und Trigonometrische Funktionen anwendet.

Hallo ? Ist da noch jemand ?

Gut, ich werde natürlich auf die mathematischen Aspekte eingehen, aber mit pMathe = 0x0000000 wird's sehr viel Copy und Paste für dich.

1.2 Auf ins kalte Wasser

Als allererstes Beispiel wird hier die kleinste "halbwegs sinnvolle" WebGL-Seite vorgestellt. Da auch die komplexesten 3D-Szenen letztlich aus einzelnen Dreiecken zusammengesetzt sind, stellt diese erste Anwendung ein einzelnes weißes Dreieck dar. Das erste Dreieck mit WebGL

weißes Dreieck

Abbildung 1.1 : Screenshot des ersten Dreiecks

Der Quelltext ist so angelegt, dass er komplett ohne zusätzliche JavaScript-Biblotheken oder sonstige externe Resourcen auskommt. Alles passiert nacheinander innerhalb einer Datei. Üblicherweise und in den nachfolgenden Kapiteln werden die Standardoperationen wie die Initialisierung in eigene .js-Dateien ausgelagert, was jedoch gerade für den Einstieg die übersicht erschwert.

Jetzt springen wir ans Ende der Quellcodedatei in Zeile 110. Dort sind wir mitten im HTML-Umfeld das Dir einigermaßen bekannt sein sollte.

110<canvas id="meineWebGLCanvas" width="500" height="500"></canvas>

Diese Zeile platziert eine Zeichenfläche ("Canvas") auf der HTML-Seite. Mit minimalen Englischkenntnissen ist es nicht schwer zu erahnen, wie hoch und breit diese Zeichnungsfläches sein wird... Innerhalb dieser Fläche wird die WebGL-Szene dargestellt. Die id="meineWebGLCanvas" benötigen wir später noch, um Javascript mitzuteilen, wo das WebGL-Geraffel dargestellt werden soll.

Bevor wir mit der Theorie weitermachen (bzw. richtig anfangen) verrate ich dir die Stellen, an denen du schon jetzt direkt Einfluss auf das dargestellte Dreieck nehmen kannst:
In Zeilen 88-90 stehen sechs Zahlen. Diese geben die Eckpunkte des Dreiecks in karthesischen Koordinaten an (siehe Kommentare im Quellcode).
Da in diesem rudimentär-Beispiel noch keine echte 3D-Projektion verwendet wird, können die Z-Koordinaten alle auf 0.0 gesetzt sein. Später wird die Z-Koordinate die Tiefe im Raum angeben, wobei die negative z-Achse "in den Bildschirm hinein" zeigt. (Du brauchst nicht zu versuchen, in diesem Beispiel die Z-Werte zu ändern; da passiert noch nichts!)
Jetzt kannst du es wagen: Falls noch nicht geschehen, lade die Datei "kapitel1.html" in ein lokales Verzeichnis auf deinem Rechner und experimentiere mit den Koordinaten. Setze das Dreieck nach links oder rechts, mache es spitzer oder stumpfer.

Fertig? Ich hoffe du hast nun ein Gefühl für den Zusammenhang zwischen der Canvas-Zeichenfläche und den drei Dreieckspunkten.
Die zweite Stelle im Quellcode, an der du jetzt schon Einfluss nehmen kannst ist in Zeile 65 der Datei:

65gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n\


Hier kannst du die Farbe des Dreiecks ändern. Die vier 1.0-Werte stehen für RGBA (Rot,Grün,Blau,Alpha). Es würde zu weit (zurück) führen hier dieses Farbschema zu erklären. Alpha beschreibt Opazität (Gegenteil von Transparenz); Alpha=0 --> Dreieck ist unsichtbar.

1.3 Initialisierung und Struktur

In diesem Beispiel beziehe ich mich nur auf die Browser FireFox ab Version 4.0 und Google Chrome ab Version 9.0. Zukünftig sollte es auch für weitere Browser funktionieren.
Wir wollen uns nun anschauen, was im Quellcode passiert bevor das Dreieck auf dem Bildschirm erscheint.
Die Zeichenfläche ("Canvas") habe ich ja im letzten Absatz bereits erwähnt. Sie ist im Body-Teil des HTML Dokuments eingebettet.
Nachdem die Datei vom Browser geladen wurde, wird die Javascript-Funktion meinWebGLStart() aufgerufen:

103window.onload = function () {
104    meinWebGLStart();
105};


meinWebGLStart() ist ein paar Zeilen darüber implementiert. Die Funktion ist in diesem Beispiel das Ein und Alles. Alle Schritte von der Initialisierung von WebGL bis zum Zeichnen des Dreiecks werden nacheinander ausgeführt:

Ich fange vorne an:
26canvas = window.document.getElementById("meineWebGLCanvas");
27
28try {
29    // Falls der Browser es unterstuetzt, wird hier WebGL
30    // erstmalig angesprochen und der "WebGL-Context" in
31    // dem Objekt gl gespeichert.
32    gl = canvas.getContext("experimental-webgl");
33} catch (e) {}
34if (!gl) {
35    window.alert("Fehler: WebGL-Context nicht gefunden");
36}

In der lokalen Variable canvas wird zunächst über das globale document-Objekt von Javascript das Element mit dem Namen "meineWebGLCanvas" gesucht und damit lokal (in dieser Funktion verfügbar).
Danach wird die Objektmethode getContext("experimental-webgl") aufgerufen und der Rückgabewert dem ebenfalls lokalen Objekt gl zugewiesen.
Mit dieser Zeile wird geprüft, ob der verwendete Browser den "experimental-webgl" -Context unterstützt und falls dies so ist, steht das Objekt gl ab sofort für alle WebGL-Befehle bereit. Die Verknüpfung zwischen der oben beschriebenen Canvas und WebGL ist damit ebenfalls hergestellt. Die Initialisierung ist in einen try-catch-Block eingeschlossen um alle Arten von Fehler abzufangen, die auftreten falls der verwendete Browser WebGL (noch) nicht unterstützt.
Direkt danach finden noch eine überprüfung statt, ob die Initialisierung geklappt hat. Wenn kein WebGL-context gefunden wurde, wird mit dem alert-Befehl eine Fehlermeldung im Browser angezeigt.
In den darauffolgenden Codezeilen wird ein WebGL-Program-Objekt mit zwei Shadern erzeugt:

40webGLProgramObject = gl.createProgram();
41
42// Der folgende String enthaelt den kompletten Quellcode
43// fuer einen minimalistischen Vertex-Shader:
44vShaderQuellcode =
45    'attribute vec4 vPosition; \n\
46    void main() \n\
47    { \n\
48        gl_Position = vPosition; \n\
49    } \n';
50// Das Vertex-Shader-Objekt wird angelegt:
51vShader = gl.createShader(gl.VERTEX_SHADER);
52// - mit seinem Quelltext verknuepft:
53gl.shaderSource(vShader, vShaderQuellcode);
54// - kompiliert:
55gl.compileShader(vShader);
56// - dem Shader-Program-Objekt hinzugefuegt:
57gl.attachShader(webGLProgramObject, vShader);
58
59// Nochmal das gleiche Vorgehen wie fuer den Vertex-
60// Shader; analog fuer den Fragment-Shader:
61fShaderQuellcode =
62    'precision mediump float;\n\
63    void main() \n\
64    { \n\
65        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n\
66    } \n';
67fShader = gl.createShader(gl.FRAGMENT_SHADER);
68gl.shaderSource(fShader, fShaderQuellcode);
69gl.compileShader(fShader);
70gl.attachShader(webGLProgramObject, fShader);
71// Das Shader-Program-Objekt ist vollstaendig und muss
72// gelinkt werden.
73gl.linkProgram(webGLProgramObject);
74// Da theoretisch mehrere Shader-Program-Objekte moeglich
75// sind, muss angegeben werden, welches benutzt werden soll.
76gl.useProgram(webGLProgramObject);
Shaders sind ein Kapitel für sich. In der OpenGL Literatur tauchen sie üblicherweise erst in den hinteren Kapiteln auf. In WebGL (bzw. OpenGL ES 2.0) kommt man jedoch von Anfang an mit Shaders in Berührung.

Für diese Kapitel 1 würde es zu weit führen, auf die Shader detailiert einzugehen und es ist auch erstmal nicht notwendig. Das Grundkonzept will ich trotzdem versuchen mit einfachen Worten zu erklären.

Was WebGL von herkömmlichen Javascript (2D) Grafikfunktionen unterscheidet ist, dass ein Großteil der erforderlichen Berechnungen auf der Grafikarte statt im Hauptprozessor ausgeführt werden (GPU statt CPU). Die GPU (Graphics Processing Unit) ist für grafische Berechnungen ausgelegt (wer hätte das gedacht) und hat v.A. gegenüber der CPU den Vorteil dass sie viele Rechenoperationen GLEICHZEITIG berechnen kann. Wenn wir z.B. ein 2D-Bild (Bildschirm) aus einer 3D-Szene (WebGL) berechnen wollen, muss für jeden Pixel ein eingener Farbwert berechnet werden. D.h. jede Menge "kleine" Berechnungen die voneinander unabhängig sind und damit ideal auf der GPU berechnet werden können.

SHADER sind Mini-Programme, die auf der GPU vielfach parallel laufen. (diese Definition sollte zumindest für Kapitel 1 ausreichen)

Der Quellcode der beiden Shader (es gibt immer diese zwei Shader in jeder WebGL-Anwendung) ist als gewöhnlicher String in den JavaScript Quellcode eingebettet. Den Vertex-Shader findest du in Zeile 45-49, den Fragment-Shader in Zeile 62-66. Möglicherweise hast du wie ich weiter oben vorgeschlagen habe, den Fragment-Shader bereits bearbeitet, wenn du die Farbe des Dreiecks geändert hast.

Die beiden Shader-Quellcode-Strings werden jeweils mit gl.compileShader(..) übersetzt (Kompilieren kennst du hoffentlich von anderen Programmiererfahrungen...) und mit attachShader(..) dem WebGL-Program-Objekt hinzugefügt. Sobald dem Program-Objekt beide Shader übergeben wurden, kann es gelinkt werden (Compiler + Linker ähnlich wie in C, Pascal,...etc.). Das Abschließende useProgram(..) "aktiviert" das gerade erstellte Program-Objekt. Theoretisch können mehrere Programm-Objekte verwendet werden, die mit useProgram jeweils ausgetauscht werden.

Der Datenfluss geht üblicherweise folgendermaßen:
1. im JavaScriptcode wird ein Rohdatenpuffer erzeugt (Raumkoordinaten, Farben, etc.)
2. im Vertex-Shader werden die punktbezogenen Werte berechnet (Transformationen, Normalenvektoren, Texturkoordinaten,...) und die vordefinierte Variable $gl_Position$ belegt.
3. der Fragment-Shader berechnet die pixelbezogenen Werte zwischen den Vertices (interpoliert z.B. Farben zwischen Vertices) und belegt die vordefinierte Variable $gl_FragColor$.

Die folgenden zwei Zeilen sind leicht zu verstehen:
78gl.clearColor(0.0, 0.0, 0.0, 1.0);
79// Hintergrund loeschen
80gl.clear(gl.COLOR_BUFFER_BIT);

Die beiden Funktion legen die Hintergrundfarbe der 3D-Szene als RGB-Wert fest und löschen die komplette Szene in dieser Farbe.
Jetzt kommt die Antwort auf die spannende Frage, wie Daten zwischen dem "normalen" JavaScript-Code und den WebGL-Shadern ausgetauscht werden.
84vertexAttribLoc = gl.getAttribLocation(webGLProgramObject, "vPosition");

getAttribLocation(..) ist eine WebGL-Api-Funktion, die hier anhand des Variablennamens vPosition eine Zugriffsmöglichkeit auf eine Eingangsvariable des Vertex-Shaders liefert.

Dass in Zeile 88-90 die Dreieckskoordinaten angelegt werden, ist leicht zu erahnen. Datentyp ist das WebGL-spezifische Float32Array. Um die Berechnungen auf die GPU zu verlagern, müssen diese Daten in den Grafikspeicher übertragen werden. Hierzu wird mit createBuffer ein neuer Speicherbereich angelegt, der dann mit bindBuffer aktiviert wird und zuletzt mit bufferData befüllt wird. Der Inhalt von vVertices wird hier in den Grafikspeicher kopiert.
Nun müssen wir WebGL noch mitteilen, wofür der gerade aktivierte und befüllte Puffer verwendet werden soll:
97gl.vertexAttribPointer(vertexAttribLoc, 3, gl.FLOAT, false, 0, 0);
98gl.enableVertexAttribArray(vertexAttribLoc);

Der erste Parameter vertexAttribLoc verweist auf die Eingangsvariable des Vertex-Shaders, "3" ist die Anzahl der Werte pro Element (3 Koordinatenwerte pro Punkt) und "gl.Float" ist der Datentyp.
enableVertexAttribArray(..) aktiviert die Verwendung des Puffers für folgende Zeichenfunktionen.

Der letzte Befehl bringt nun alles "auf den Schirm" um mit Cpt. Picard zu sprechen:
100gl.drawArrays(gl.TRIANGLES, 0, 3);

Hiermit werden aus den zuletzt aktivierten Pufferdaten Dreiecke generiert (in diesem Fall nur eines). Den ersten Parameter gl.TRIANGLES kannst du z.B. auch in gl.POINTS oder gl.LINE_STRIP ändern und dich überraschen lassen, wie sich die Darstellung verändert.
Der zweite und dritte Parameter geben Start- und Anzahl der Array-Elemente an, die gezeichnet werden sollen.
Das soll es jetzt erstmal gewesen sein für Kapitel 1 meines WebGL-Tutorials. Ich hoffe es ist einigermaßen verständlich. Kommentare nehme ich gerne unter der u.A. Emailadresse entgegen.

Im Kapitel zwei werden wir die räumliche Projektion einführen und ausführlich auf die Modelview- und Perspektivmatrizzen eingehen.


<< Kapitel 0 <<    >> Startseite <<   ^ Seitenanfang ^     >> Kapitel 2 >>
Fehler? Kommentare? webgl ([ät)] peter-strohm Punkt de