Meine Werkzeuge
Namensräume
Varianten

WebGL Jumpstart/Das erste Dreieck

Aus indiedev
Wechseln zu: Navigation, Suche
WebGL Jumpstart
Das erste Dreieck
Thumbnail WebGL Jumpstart 02.png
Autor Astrorenales
Programmier­sprache JavaScript
Kategorie WebGL
Diskussion Thread im Forum
Lizenz indiedev article license


Inhaltsverzeichnis

Das Inhaltsverzeichnis

Einleitung

In diesem kleinen Tutorial werde ich erklären, wie man in WebGL ein simples Triangle mit Vertex-Farben zeichnen kann. Es wäre zwar sehr einfach und auch mit WebGL möglich, dieses über die Fixed-Function-Pipeline zu rendern, allerdings ist es für spätere Tutorials sinnvoller, das Rendering direkt mit Shadern zu realisieren.

Shader

Die Shader in WebGL werden wie bei OpenGL üblich in GLSL geschrieben. Da es sich bei WebGL allerdings um eine OpenGL ES Implementierung handelt, sollte man auf die kleinen Unterschiede achten, wie z.B. die Angabe der "precision" im Fragment-Shader. Um Shader in einer Internetseite laden zu können, gibt es zwei Wege. Zum einen kann der GLSL-Code direkt in den Source der Seite, als spezielle script-Tags, geschrieben und über "getElementById" geladen werden. Die andere Möglichkeit ist es, den Source klassisch in extra vs- und fs-Dateien zu schreiben und über Ajax nachzuladen. Die einfachere Variante ist natürlich die direkte Integration in die Internetseite, weshalb wir diese auch in diesem Tutorial verwenden werden. Für größere Projekte sollte allerdings auf die zweite Möglichkeit zurückgegriffen werden.

Die Shader werden wie folgt in unserer Seite integriert. Hierbei handelt es sich um einen simplen VertexPositionColor-Shader, um unser Dreieck rendern zu können. Auf den GLSL-Code soll in diesem Tutorial nicht weiter eingegangen werden, da er unabhängig von WebGL ist und in entsprechenden Tutorials behandelt wird.


<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 Position;
  attribute vec4 Color;
  uniform mat4 WorldViewProj;
  varying vec4 vertexColor;
  void main(void) {
    gl_Position = WorldViewProj * vec4(Position, 1.0);
    vertexColor = Color;
  }
</script>
<script id="shader-fs" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 vertexColor;
  void main(void) {
    gl_FragColor = vertexColor;
  }
</script>

Das Kompilieren des Shader-Programms verläuft nun wie bei OpenGL gewohnt. Nur die Methode wie wir unseren eben definierten Shader-Code erreichen, ist anders. Hierfür schreiben wir nun zwei kleine Hilfsmethoden, welche es erleichtern werden, diesen wie auch weitere Shader in anderen Tutorials zu laden:


function getShader(gl, id) {
  var shaderScript = document.getElementById(id);
  if (!shaderScript)
    return null;
  var str = "";
  var k = shaderScript.firstChild;
  while (k) {
    if (k.nodeType == 3) {
      str += k.textContent;
    }
    k = k.nextSibling;
  }
  var shader;
  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
    return null;
  }
  gl.shaderSource(shader, str);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert("Failed to compile shader " + id + ", " + gl.getShaderInfoLog(shader));
    return null;
  }
  return shader;
}

function getShaderProg(gl, vertexId, fragmentId) {
  var vertexShader = getShader(gl, vertexId);
  var fragmentShader = getShader(gl, fragmentId);
  var shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert("Could not initialize shader program " + vertexId + ", " + fragmentId);
  }
  return shaderProgram;
}

Zuerst einmal gehen wir näher auf die "getShaderProg"-Methode ein, da diese dem typischen OpenGL-Code sehr nahe ist. Zuerst erstellen wir unseren Vertex- und Fragment-Shader mit dem entsprechenden GLSL-Code. Anschließend wir ein "Program"-Objekt erstellt, in welchem unsere beiden Shader-Teile vereint werden und über welches die spätere Kommunikation verlaufen wird. Die Anbindung des Vertex- und Fragment-Shaders erfolgt über die Methode "attachShader". Folgend muss der Shader noch mit Hilfe von "linkProgram" finalisiert werden. Sollten Fehler im GLSL-Code oder bei der Veknüpfung mit dem "Program"-Objekt auftreten, können wir dies nun mit ProgramParameter "LINK_STATUS" überprüfen und ggf. eine Fehlermeldung ausgeben. Zum Schluss wird noch das fertige Programm zurückgegeben.

Nun zum etwas unübersichtlicheren Teil: Um den Shader-Code zu erhalten, müssen wir unsere zuvor definierten Script-Tags über die Html-DOM entsprechend auslesen. Dies erledigt unsere "getShader"-Methode. Zuerst suchen wir das Html-Element mit Hilfe der an die Methode übergebenen Id, welche wir unserem Shader-Element zugewiesen haben. In diesem Fall ist das "shader-vs" für den Vertex- und "shader-fs" für den Fragment-Shader. Diese Suche geschieht über die Methode "getElementById", welche über das "document"-Objekt aufgerufen werden kann. Da Html auf Xml aufbaut und der Text-Inhalt von Knoten in Text-Knoten verpackt wird, iterieren wir nun durch alle Kindknoten unseres Script-Elements. Wenn einer dieser Knoten vom Typ 3 (Text-Knoten) ist, fügen wir den Inhalt an unseren Shader-Code an (Näheres zu den Element-Typen ist hier beschrieben [1]).

Da wir nun unseren Shader-Code gelesen haben, können wir das Shader-Objekt erstellen und den Code kompilieren. Ob es sich um einen Vertex- oder Fragment-Shader handelt, können wir über das von uns beim Script-Tag angegebene Attribut "type" auslesen. Entsprechend des Typs rufen wir also nun die Methode "createShader" auf. Anschließend übergeben wir den Shader-Code an unser Objekt und starten den Kompilierungsprozess über "compileShader". Wenn es zu einem Fehler kommen sollte, fangen wir dieses mit dem ShaderParameter "COMPILE_STATUS" ab und zeigen eine Fehlermeldung. Damit auch entsprechend auf den Fehler verwiesen wird, zeigen wir zusätzlich den Log-Text des Kompilierungsprozesses mit an. Dies geschieht mit Hilfe der "getShaderInfoLog"-Methode.

Was jetzt noch zu tun bleibt, ist der Aufruf unserer Helfermethode:


 var shaderProgram = getShaderProg(gl, "shader-vs", "shader-fs");
 gl.useProgram(shaderProgram);
 shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "Position");
 gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
 shaderProgram.colorAttribute = gl.getAttribLocation(shaderProgram, "Color");
 gl.enableVertexAttribArray(shaderProgram.colorAttribute);
 shaderProgram.worldViewProjUniform = gl.getUniformLocation(shaderProgram, "WorldViewProj");

Damit wir im Folgenden WebGL mit unseren Vertex-Daten versorgen können, müssen noch die Vertex-Attribute und Uniforms gemappt und aktiviert werden. Dies geschieht über die Methode "getAttribLocation", bzw. "getUniformLocation" und nimmt als Argumente das Shader-Programm und den entsprechenden Namen der Variable. In diesem Fall ist es das "Position"- und "Color"-Attribut für die Vertices und die "WorldViewProj"-Matrix die wir später global setzen werden. Der Vorteil von JavaScript an dieser Stelle ist, dass wir unserem Shader-Programm beliebige Eigenschaften hinzufügen können, indem wir diesen einfach einen Wert zuweisen. So benötigen wir kein Wrapper-Objekt, welches die Positionen der Attribute usw. speichert.

Vertex- und Index-Buffer

Der Shader ist geladen und einsatzbereit, doch ohne Vertices kann man auch nichts zeichnen. Dafür werden wir jetzt einen Vertex- und Index-Buffer für das Triangle erstellen. In OpenGL und auch WebGL setzen wir dies über die sog. Vertex-Buffer-Objects (kurz VBOs) um. Hierbei wird für einen Vertex-Buffer der Typ "ARRAY_BUFFER" und für einen Index-Buffer der Typ "ELEMENT_ARRAY_BUFFER" verwendet. Zuerst wird der Buffer über die Methode "createBuffer" erstellt und mit "bindBuffer" aktiviert. Hierbei wird auch bereits festgelegt, um welche Art Buffer es sich handeln soll. Mit der "bufferData"-Methode befüllen wir nun den Vertex- und Index-Buffer mit unseren Daten. Hier kommen die sog. TypedArrays ins Spiel, welche im Zuge von WebGL eingeführt wurden. Diese ermöglichen es in JavaScript den Typ und entsprechend auch die Größe von Daten festzulegen, da OpenGL ein festes Layout für die Daten benötigt. Für die Vertices wird das Float32Array verwendet, da die Vertex-Position und -Farbe in Floats gut darstellbar sind. In diesem Beispiel sind beide Vertex-Attribute in einem Buffer-Objekt kombiniert (Float 0-2 für die Position und float 3-6 für die Farbe). Da sich das Triangle nicht verändern wird, definieren wir den Buffer als STATIC_DRAW. Für den Index-Buffer verwenden wir ein Uint16Array, welches die Indices als ushort repräsentiert. Hier sei angemerkt, dass aktuelle keine Uint32Arrays für Indices verwendet werden können. Ob und wann diese Beschränkung für WebGL aufgehoben wird ist unklar. Allerdings reichen Uint16Array auch vollkommen aus. Abschließend speichern wir noch in die Objekte jeweils den Vertex- bzw. Index-Stride und die Anzahl der Vertices.


var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     0.0, -75.0, 0.0, 1.0, 0.0, 0.0, 1.0,
   100.0,  75.0, 0.0, 0.0, 1.0, 0.0, 1.0,
  -100.0,  75.0, 0.0, 0.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW);
vertexBuffer.itemSize = 7;
vertexBuffer.numItems = 3;
    
var vertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2]), gl.STATIC_DRAW);
vertexIndexBuffer.itemSize = 1;
vertexIndexBuffer.numItems = 3;

Die Kamera

Mit dem Vertex- und Index-Buffer haben wir nun die Daten für die Vertex-Attribute unseres Shaders. Die letzte Komponente ist die Kamera für den WorldViewProj-Uniform. Für diesen Artikel wird das glMatrix-Skript [2] verwendet, damit nicht alle Berechnungen selber geschrieben werden müssen. Eine weiter Alternative steht direkt im GitHub-Repository der KhronosGroup zur Verfügung [3]. Da das Triangle nur 2D dargestellt wird, reicht eine orthographische Matrix vollkommen aus. Zuerst wird eine neue 4x4-Matrix über "mat4.create()" erstellt. Anschließend wird diese mit den Werten für eine orthographische Matrix befüllt. Hierfür verwenden wir die "otho"-Methode, welche als Parameter die zu befüllende Matrix, das Fenster-Rechteck und die Near- und Far-Plane annimmt. Folgend setzten wir noch den viewport für unser Canvas, sodass der volle Bildbereich zum zeichnen verwendet wird. Damit das Triangle immer zentriert gerendert wird, egal wie groß das Canvas ist, liegen die Vertex-Koordinaten um den Nullpunkt in der Ecke links oben. Zur Zentrierung wird eine weitere 4x4-Matrix erstellt und um die Hälfte der Canvas-Größe über die Methode "translate" verschoben. Für die genauen Parameter und Methoden-Aufrufe verweise ich an dieser Stelle auf die Dokumentation von glMatrix [4]. Abschließend multiplizieren wir noch die world- mit der proj-Matrix und erhalten die fertige Kamera-Matrix.


 proj = mat4.create();
 mat4.ortho(proj, 0.0, surface.width, surface.height, 0.0, 0.0, 1.0);
 gl.viewport(0, 0, surface.width, surface.height);
 
 var world = mat4.create();
 world = mat4.translate(world, world, vec3.fromValues(surface.width / 2, surface.height / 2, 0));
 var wvp = mat4.create();
 wvp = mat4.multiply(wvp, proj, world);

Zusammenbau und Rendering

Kommen wir zum finalen Teil dieses Artikels, wo alles zusammenkommt. Zuerst wird der Shader mit "useProgram" aktiviert. Dann wird der Vertex-Buffer mit "vertexAttribPointer" auf die entsprechenden Shader-Attribute gemappt. Da unser Vertex-Buffer zwei Komponenten pro Vertex definiert, wird "vertexAttribPointer" entsprechend zwei Mal aufgerufen, mit jeweils der Position des Attributs, der Größe des Elements und der Position im Vertex-Buffer-Stride. Folgend wird der Index-Buffer aktiviert und unsere eben erstelle wvp-Matrix an das Shader-Uniform "WorldViewProj" übergeben. Dies geschieht über die "uniformMatrix4fv"-Methode. Abschließend wird noch der Draw-Call aufgerufen. Da ein Index-Buffer verwendet wird, rufen wir die Methode "drawElements" auf. Als Parameter geben wir an, dass wir ein Triangle zeichnen wollen, wieviele Indices genutzt werden sollen und von welchem Typ diese sind.


gl.useProgram(shaderProgram);

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, vertexBuffer.itemSize * 4, 0);
gl.vertexAttribPointer(shaderProgram.colorAttribute, 4, gl.FLOAT, false, vertexBuffer.itemSize * 4, 3 * 4);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

gl.uniformMatrix4fv(shaderProgram.worldViewProjUniform, false, wvp);

gl.drawElements(gl.TRIANGLES, vertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Das Endergebnis

WebGL Jumpstart

Und hier noch einmal der ganze Code:


<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>WebGL Jumpstart</title>
  <script type="text/javascript" src="gl-matrix-min.js"></script>
  <script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 Position;
    attribute vec4 Color;
    uniform mat4 WorldViewProj;
    varying vec4 vertexColor;
    void main(void) {
      gl_Position = WorldViewProj * vec4(Position, 1.0);
      vertexColor = Color;
    }
  </script>
  <script id="shader-fs" type="x-shader/x-fragment">
    precision mediump float;
    varying vec4 vertexColor;
    void main(void) {
      gl_FragColor = vertexColor;
    }
  </script>
</head>
<body>
  <canvas id="glcanvas" width="400" height="275" style="border:1px solid black"></canvas>
  <script>
  function getShader(gl, id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript)
      return null;

    var str = "";
    var k = shaderScript.firstChild;
    while (k) {
      if (k.nodeType == 3) {
        str += k.textContent;
      }
      k = k.nextSibling;
    }
    var shader;
    if (shaderScript.type == "x-shader/x-fragment") {
      shader = gl.createShader(gl.FRAGMENT_SHADER);
    } else if (shaderScript.type == "x-shader/x-vertex") {
      shader = gl.createShader(gl.VERTEX_SHADER);
    } else {
      return null;
    }
    gl.shaderSource(shader, str);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      alert("Failed to compile shader " + id + ", " + gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  function getShaderProg(gl, vertexId, fragmentId) {
    var vertexShader = getShader(gl, vertexId);
    var fragmentShader = getShader(gl, fragmentId);
    var shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      alert("Could not initialize shader program " + vertexId + ", " + fragmentId);
    }
    return shaderProgram;
  }

  window.onload = function () {
    var surface = window.document.getElementById("glcanvas");
    var gl = surface.getContext("experimental-webgl") || surface.getContext("webgl");
    
    gl.clearColor(0.392, 0.584, 0.929, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    var vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
         0.0, -75.0, 0.0, 1.0, 0.0, 0.0, 1.0,
       100.0,  75.0, 0.0, 0.0, 1.0, 0.0, 1.0,
      -100.0,  75.0, 0.0, 0.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW);
    vertexBuffer.itemSize = 7;
    vertexBuffer.numItems = 3;
    
    var vertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2]), gl.STATIC_DRAW);
    vertexIndexBuffer.itemSize = 1;
    vertexIndexBuffer.numItems = 3;
    
    proj = mat4.create();
    mat4.ortho(proj, 0.0, surface.width, surface.height, 0.0, 0.0, 1.0);
    gl.viewport(0, 0, surface.width, surface.height);
    
    var world = mat4.create();
    world = mat4.translate(world, world, vec3.fromValues(surface.width / 2, surface.height / 2, 0));
    var wvp = mat4.create();
    wvp = mat4.multiply(wvp, proj, world);
    
    shaderProgram = getShaderProg(gl, "shader-vs", "shader-fs");
    gl.useProgram(shaderProgram);
    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "Position");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
    shaderProgram.colorAttribute = gl.getAttribLocation(shaderProgram, "Color");
    gl.enableVertexAttribArray(shaderProgram.colorAttribute);
    shaderProgram.worldViewProjUniform = gl.getUniformLocation(shaderProgram, "WorldViewProj");
    
    gl.useProgram(shaderProgram);

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, vertexBuffer.itemSize * 4, 0);
    gl.vertexAttribPointer(shaderProgram.colorAttribute, 4, gl.FLOAT, false, vertexBuffer.itemSize * 4, 3 * 4);
    
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

    gl.uniformMatrix4fv(shaderProgram.worldViewProjUniform, false, wvp);
    
    gl.drawElements(gl.TRIANGLES, vertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
  }
  </script>
</body>
</html>

Navigation
Tutorials und Artikel
Community Project
Werkzeuge