WebGL 2 Guide - #6 Shader

Im letzten Schritt haben wir die ersten WebGL Befehle kennen gelernt.
In diesem Schritt wollen wir uns näher mit den Shadern beschäftigen. Da wir ohne diese nichts auf dem Bildschirm darstellen können.


Schauen wir uns das vereinfachte Diagramm an, können wir auch erkennen, warum wir ohne Shader nichts auf den Bildschirm gezaubert bekommen:




Der Vertex Shader verarbeitet die VBOs, teilt es dem Fragment Shader mit, dieser füllt daraufhin den Framebuffer, welcher an den Bildschirm geht. Ohne Shader gelangt also nichts auf den Bildschirm.

Wollen wir unser erstes Dreieck auf den Bildschirm bekommen, kommen wir also nicht an der GPU vorbei.

In der Tat würde es auch ohne GPU funktionieren, dann aber spricht man vom sogenannten "Software rendering". Die ganzen Berechnungen welche von der GPU übernommen würde, wird von der CPU übernommen. Dies ist allerdings extrem langsam und nicht mehr Zeitgemäß. Übernimmt die GPU die Berechnungen, spricht man auch von "Hardware accelerated graphics". Die erste 3D-Engine wurde 1992 von id Software für das Spiel "Wolfenstein 3D" entwickelt. Dort hatte die i386 CPU alle Berechnungen übernommen (Sofware renderer), da es damals noch keine dedizierten GPUs gab.

Wir müssen uns somit zwei Shader schreiben:
  • einen Vertex Shader
  • und einen Fragment Shader
Die Shader sagen der GPU also, was sie mit den Daten machen soll die wir ihr geben. Ohne diese, würden unsere Daten auf der GPU herum liegen und nichts bewirken.

Fangen wir also mit dem Vertex Shader an, da es das nächste Element in der Pipeline ist.

Dazu öffnen wir die main.js und fügen folgende Zeilen hinzu:

const vertexShaderSource =
`
#version 300 es
precision mediump float;

// Vertex position attribute
in vec3 aVertexPosition;

void main(void) {
    // Set the position coordinates
    gl_Position = vec4(aVertexPosition, 1.0);
}
`

ESSL(GLSL ES) ist eine C-ähnliche Programmiersprache und sehr strickt.
Gehen wir diese Zeilen daher Stück für Stück durch:

const vertexShaderSource =

Wir machen uns die in ES6 eingeführten multiline strings zu nutze und speichern den kompletten Sourcecode des Vertex Shaders in der Variable vertexShaderSource. Später werden wir noch lernen, wie wir den Shader Sourcecode aus einer separaten Datei laden. Für jetzt ist es aber erst einmal einfacher den Sourcecode in die main.js zu schreiben.

Danach kommt das Zeichen, welches den Start eines multiline string einleitet: ` (ein zurückgestelltes Apostroph (Shift + Taste links neben der Backspacetaste)



Nun startet der eigentliche Shader Code:

#version 300 es

Hier definieren wir, welche ESSL Version genutz wird (ESSL 3.0).
Wichtig ist hier, dass diese Zeile unbedingt die erste Zeile sein muss. Weder Leerzeichen noch Kommentare sind vor dieser Zeile erlaubt.

precision mediump float;

Diese Zeile definiert, wie präzise die GPU Berechnungen mit Floating Point Numbers (Zahlen mit Nachkommastellen) durchführt. Es gibt 3 Einstellungen:

  • highp (hohe Präzision und längere Berechnungszeit)
  • mediump (mittlere Präzision und mittlere Berechnungszeit)
  • lowp (niedrige Präzision und schnelle Berechnungszeit)

Was man beachten sollte ist, dass nicht alle Systeme highp unterstützen, vor allem ältere haben manchmal ihre Probleme, was dazu führt, dass der Shader auf diesen Systemen nicht funktioniert. Sowas sollte natürlich vermieden werden! Wer möchte ein Computerspiel kaufen, um dann festzustellen, dass es auf seinem PC nicht läuft?

Als grundsätzliche Regel kann man sich merken: Nutze lowp immer wenn es möglich ist und Sinn macht. Als Beispiel:
  • mediump/highp für Vertexpositionen
  • mediump für Texturkoordinaten
  • lowp für Farbwerte

// Vertex position attribute
in vec3 aVertexPosition;

Hier definieren wir unser erstes Attribut.
Tatsächlich kann die Zeile auch folgendermaßen geschrieben werden:

// Vertex position attribute
attribute vec3 aVertexPosition;

Dies ist allerdings noch aus WebGL 1 Zeiten, zwar immer noch gültig, gewöhnen wir uns jedoch besser die moderne Schreibweise an.

Das Keyword in legt also fest, dass es ein Attribut ist und hier Daten in den Shader hineinkommen.

void main(void) {

Dies ist die Funktion main und definiert den Einstiegspunkt in den Shader.
Hier ist wichtig zu wissen, dass ähnlich wie in C/C++ es eine (und nur eine) main-Funktion geben muss/darf, die genau so aussieht und den Einstiegspunkt definiert. void und (void) definieren, dass die Funktion main weder einen Wert zurückliefert, noch einen entgegennimmt.

// Set the position coordinates
gl_Position = vec4(aVertexPosition, 1.0);

gl_Position ist eine Variable von WebGL, welche wir mit dem Wert initialisieren, auf welche das Attribut aVertexPosition zeigt. Da aVertexPosition ein Array mit 3 Werten ist (x, y, z), gl_Position jedoch einen 4-Komponenten-Vektor (vec4) erwartet (x, y, z, w), konvertieren wir unseren 3-Komponenten-Vektor in einen vec4 und setzen das letzte Element w auf 1.0.
w besagt, dass es sich um eine Position handelt (1.0) oder um eine Richtung (0.0), in diesem Fall um eine Position.
Mehr dazu werden wir zu gegebener Zeit erfahren.

}
`

hier Beenden wir unsere main-Funktion mit der geschlossenen geschweiften Klammer und Beenden auch den multiline string wieder mit dem zurückgestellten Apostroph.

Weiter also mit dem Fragment Shader:

const fragmentShaderSource =
`
#version 300 es
precision mediump float;

// Color that is the result of this shader
out vec4 fragColor;

void main(void) {
    // Set the result as green
    fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
`

Hier ändert sich noch nicht viel im Gegensatz zum Vertex Shader.

Gehen wir daher nur auf die Änderungen ein:

// Color that is the result of this shader
out vec4 fragColor;

Hier definieren wir ein Varying vom Typ vec4 mit dem Namen fragColor.
Auch hier könnte die Zeile auch folgendermaßen aussehen:


varying vec4 fragColor;

Dies kommt allerdings ebenfalls noch aus WebGL 1 Zeiten.

out definiert also ein Varying, welches Daten von dem Shader nach außen gibt.

// Set the result as green
fragColor = vec4(0.0, 1.0, 0.0, 1.0);

Hier deklarieren wir, was nach außen gegeben werden soll; wir weisen dem Varying einen vec4 zu, welcher Farbwerte enthält. In diesem Fall einen reinen Grünton. (Rotanteil, Grünanteil, Blauanteil, Opazität).

Somit wäre auch der Fragment Shader geschrieben.

Unsere main.js sieht nun so aus:

"use-strict"

// Holds the reference to the WebGL context
let gl;

const vertexShaderSource =
`
#version 300 es
precision mediump float;

// Vertex position attribute
in vec3 aVertexPosition;

void main(void) {
    // Set the position coordinates
    gl_Position = vec4(aVertexPosition, 1.0);
}
`

const fragmentShaderSource =
`
#version 300 es
precision mediump float;

// Color that is the result of this shader
out vec4 fragColor;

void main(void) {
    // Set the result as green
    fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
`

// Initialize WebGL
// Is called on onload
function init()
{
    // Get the canvas reference
    const canvas = document.getElementById("webgl-canvas");

    // Ensure a canvas was found
    if (!canvas)
    {
        console.error("Error: no canvas found!");
        return;
    }

    // Get the WebGL context
    gl = canvas.getContext("webgl2")

    // Ensure context was found
    if (!gl)
    {
        console.error("WebGL is not available!");
        return;
    }

    updateClearColor(0.2, 0.2, 0.8, 1.0);
}

// Update the canvas clear color
function updateClearColor(...color)
{
    gl.clearColor(...color);
    gl.clear(gl.COLOR_BUFFER_BIT);
}


Dies scheint vielleicht ein bisschen vorgegriffen, jedoch ist es notwendig die Shader jetzt zu schreiben, da wir sonst keine Ausgabe auf dem Bildschirm hätten.



Im nächsten Schritt können wir nun endlich unser erstes Dreieck erstellen und auf dem Bildschirm anzeigen:

WebGL 2 Guide - #7 Das erste Dreieck


Comments

Popular posts from this blog

[Python] Machine Learning Intro #1 - Hello World

[Python] Passwort cracker for Zip-archives

WebGL 2 Guide - #1 Einführung