Einrichtung

Hinweis
Falls Sie mehr Komfort benötigen, können Sie auch eine bereits eingerichtete Ordnerstruktur von GitHub klonen. Zusätzlich gibt es die Möglichkeit, den Browser automatisch neu laden zu lassen, sobald Sie Änderungen am Quellcode vornehmen.

Um mit WebGL durchzustarten, sollten wir nun schrittweise versuchen, ein einfaches, weißes Dreieck zu zeichnen. Dazu brauchen wir eine HTML-Datei, deren Körper lediglich aus einem Canvas bestehen kann. Zusätzlich müssen sowohl ein Vertex- als auch ein Fragment-Shader eingebettet werden. Was diese Shader sind und machen, wird im nächsten Tutorial erklärt. Es reicht, lediglich zu wissen, dass Shader kleine Programme sind, die wie C- oder C++-Programme kompiliert und gelinkt werden müssen.

Nachdem dies erledigt ist, erweitern wir unsere kleine Demo, indem wir ein simples Haus darstellen.

HTML und CSS

Da dies ein Tutorial für WebGL ist, wird der Fokus auf JavaScript gelegt, deshalb hier beispielhaft ein vollständiges HTML-Dokument, inklusive Anpassungen per CSS:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="stylesheet" href="style.css">

    <title>WebGL Template</title>
  </head>

  <body>
    <canvas id="webgl-canvas"></canvas>

    <script id="vertex-shader" type="x-shader/x-vertex">
      attribute vec3 a_position;

      void main() {
        gl_Position = vec4(a_position, 1.0);
      }
    </script>

    <script id="fragment-shader" type="x-shader/x-fragment">
      precision mediump float;

      void main() {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
      }
    </script>

    <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/mathjs/3.12.0/math.min.js"></script>
    <script type="text/javascript" src="helpers.js"></script>
    <script type="text/javascript" src="script.js"></script>
  </body>
</html>
body {
  margin: 0;
}

#webgl-canvas {
  width: 100vw;
  height: 100vh;
  display: block;
}

Um die Dateien lokal zu öffnen, müssen die Shader zwingend im HTML-Dokument eingebettet sein. Falls man die Shader in separate Dateien auslagern will, muss man einen lokalen Webserver einrichten, welches den Rahmen dieses Tutorials aber sprengen würde.

In der HTML-Datei befinden sich auch Verweise auf mehrere JavaScripts. Das erste ist Math.js, eine Bibliothek mit vielen nützlichen mathematischen Funktionen. Das zweite Skript enthält zwei Funktionen, die dafür sorgen, dass die oben angegebenen Shader kompiliert und gelinkt werden. Dieses Skript sieht so aus:

function priv_compileShader(gl, type, source) {
    var shader = gl.createShader(type);

    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (success) return shader;

    console.log(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
}

/**
 * Creates a shader program from the given source code of the vertex and
 * fragment shader.
 *
 * @param gl WebGL context
 * @param vertexShader ID of the vertex shader
 * @param fragmentShader ID of the fragment shader
 * @return Program ID for WebGL
 */
function createShaderProgram(gl, vertexShader, fragmentShader) {
    var compiledVertexShader = priv_compileShader(gl, gl.VERTEX_SHADER, document.getElementById(vertexShader).text);
    var compiledFragmentShader = priv_compileShader(gl, gl.FRAGMENT_SHADER, document.getElementById(fragmentShader).text);
    var program = gl.createProgram();

    gl.attachShader(program, compiledVertexShader);
    gl.attachShader(program, compiledFragmentShader);
    gl.linkProgram(program);

    var success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) return program;

    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
}

Zu guter Letzt das wichtigste Skript, dort werden die WebGL-Funktionen aufgerufen.

Initialisierung

Um WebGL zu initialisieren, müssen wir ein Kontext erstellen. Dazu benötigen wir das Canvas-Element:

var canvas = document.getElementById("webgl-canvas");

Anschließend kann mit

var gl = canvas.getContext("webgl");

ein WebGL-Kontext erstellt werden. Alle WebGL-Funktionen werden nun über die gl-Variable aufgerufen.

Koordinaten übergeben

Nachdem der Kontext erstellt wurde, können wir anfangen, das Dreieck zu definieren. Dafür brauchen wir zuerst einen Puffer, der diese Koordinaten enthält:

var positionBuffer = gl.createBuffer();

Dieser Puffer muss nun an WebGL angebunden (aktiviert) werden, um diesen mit Daten zu füllen:

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

Der erste Parameter gibt an, um welche Art von Puffer es sich handelt, der zweite Parameter erwartet den Puffer selbst.

Jetzt müssen wir die Koordinaten übergeben. Dazu muss ein eindimensionales Array erstellt und an WebGL übergeben werden:

var vertices = [
    /** x, y, z */
    0.0, 0.0, 0.0,  /** Vertex bottom-left */
    0.0, 0.5, 0.0,  /** Vertex top-left */
    0.5, 0.0, 0.0,  /** Vertex bottom-right */
];

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

Der erste Parameter gibt wiederum an, um welche Art von Puffer es sich handelt, der zweite Parameter ein Float-Array (hier mit Float32Array erstellt, da WebGL zwingend ein Float-Array voraussetzt). Der letzte Parameter gibt an, wie oft sich diese Daten ändern werden. Da sich die Koordinaten nie ändern werden, wird hier ein gl.STATIC_DRAW übergeben. Damit wird WebGL mitgeteilt, wo die Daten im Arbeitsspeicher der GPU abgelegt werden, damit diese das Rendering optimieren kann.

Da WebGL mindestens einen Vertex- und einen Fragment-Shader erfordert, werden wir nun diese kompilieren und linken.

Shader kompilieren und linken

Da wir bereits eine Funktion haben, die sich darum kümmert, brauchen wir lediglich

var program = createShaderProgram(gl, "vertex-shader", "fragment-shader");

zu schreiben. Die letzten beiden Parameter stehen jeweils für die IDs, die wir im so HTML-Dokument angegeben haben.

Für die Koordinaten benötigen wir noch eine Anbindung an den Shader. Diese wird erstellt mit

var positionAttributeLocation = gl.getAttribLocation(program, "a_position");

a_position ist ein Attribut, der später eine Koordinate enthält.

Rendern

Die nachfolgenden Schritte werden stets pro Frame vorgenommen.

Kontext an Canvas-Größe anpassen

Damit WebGL jeweils mit dem Browser zusammen skaliert, muss der Viewport gesetzt werden:

gl.canvas.width = gl.canvas.clientWidth;
gl.canvas.height = gl.canvas.clientHeight;

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

Hintergrundfarbe setzen

Dazu wird zuerst die Farbe gesetzt und anschließend der Bildspeicher mit dieser Farbe gefüllt:

gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

Die erste Funktion setzt die Farbe und die zweite Funktion führt den Füllbefehl aus.

Shader aktivieren

Jetzt muss das zuvor erstellte Shader-Programm aktiviert werden und das Positionsattribut angebunden werden:

gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);

Koordinaten-Puffer aktivieren

Der zuvor erstellte Puffer für die Koordinaten muss jetzt wieder aktiviert werden:

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);

Der erste Parameter der zweiten Funktion enthält die Anbindung an das Attribut im Vertex-Shader, der zweite Parameter die Anzahl der Dimensionen (hier 3), der dritte Parameter den Variablentyp der Vertizen. Die anderen Parameter ignorieren wir vorerst.

Zeichenbefehl übergeben

Jetzt kommt der eigentlich wichtigste Moment, das Zeichnen des Dreiecks:

gl.drawArrays(gl.TRIANGLES, 0, 3);

Der erste Parameter gibt an, auf welche Art er die zuvor angebundenen Koordinaten zeichen soll, der zweite Parameter den Offset im Array und der letzte Parameter die Anzahl der Koordinaten.

Auf Größenveränderungen des Browserfensters reagieren

Dazu teilen wir unseren Code in zwei Teile auf: einen Initialisierungsteil und einen Rendering-Teil. Das sollte ungefähr so aussehen:

function init() {
    // Alles aus dem Abschnitt "Initialisierung" inkl. Unterabschnitte hier rein
}

function render() {
    // Alles aus dem Abschnitt "Rendern" inkl. Unterabschnitte hier rein
}

Dabei sollten Sie beachten, dass die Variablen nun global (außerhalb der Funktion) deklariert werden sollen.

Jetzt ergänzen Sie am Ende des gesamten Codes folgenden Funktionsaufruf:

init();
requestAnimationFrame(render);

requestAnimationFrame sorgt dafür, dass die übergebene Funktion dann aufgerufen wird, sobald die Grafikkarte bereit zum Rendern ist. Da wir aber kontinuierlich rendern wollen (um auf die Größenänderungen zu reagieren), fügen wir diesen Funktionsaufruf auch am Ende der render-Funktion ein.

Ein Haus

Jetzt wollen wir versuchen, ein Haus darzustellen. Normalerweise würde man jetzt hingehen, jedes Dreieck einzeln zu definieren und den entsprechenden Parameter von gl.drawArrays() anzupassen:

var vertices = [
    /** Linke Dachhälfte */
    0.0,  0.5,  0.0,
    0.25, 0.75, 0.0,
    0.25, 0.5,  0.0,

    /** Rechte Dachhälfte */
    0.25, 0.5,  0.0,
    0.25, 0.75, 0.0,
    0.5,  0.5,  0.0,

    /** ... */
];

/** ... */

gl.drawArrays(gl.TRIANGLES, 0, 6);

Da dies sehr unübersichtlich werden kann und obendrein auch noch zu viel Overhead verursacht, wollen wir nun ein weiteres WebGL-Feature einsetzen.

Vertizen und Indizen

Man kann das ganze sich nämlich vereinfachen, indem man nur die Punkte genau einmal setzt und dann mit Index-Werten darauf zugreift. So könnte es aussehen:

var vertices = [
    0.0,  0.0,  0.0,
    0.0,  0.5,  0.0,
    0.25, 0.75, 0.0,
    0.5,  0.5,  0.0,
    0.5,  0.0,  0.0,
];

var indices = [
    /** Dach */
    3, 2, 1,

    /** Linke Haushälfte */
    0, 3, 1,

    /** Rechte Haushälfte */
    0, 4, 3,
];

Erstellen wir nun ein neuen Puffer für die Indizen, direkt nach dem Übertragen der Vertizen in den Vertex-Puffer:

var positionIndexBuffer = gl.createBuffer();

Nun übertragen wir die Indizen in diesen Puffer:

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, positionIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

Zum Schluss ersetzen wir drawArrays mit folgenden Funktionen:

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, positionIndexBuffer);
gl.drawElements(gl.TRIANGLES, 9, gl.UNSIGNED_SHORT, 0);

Im nächsten Tutorial gehen wir näher auf die Shader ein, deren Verständnis wichtig ist für die darauffolgenden Tutorials.