Shader-Programmierung ist ein zentraler Aspekt der WebGL-Entwicklung. Shader sind spezielle Programme, die direkt auf der GPU ausgeführt werden und für die Verarbeitung von Vertices und Pixeln zuständig sind.

Diagramm: WebGL Rendering Pipeline mit Hervorhebung der Shader-Stufen

Grundlagen der Shader

Bearbeiten

In WebGL gibt es zwei Haupttypen von Shadern:

  1. Vertex-Shader
    • Verarbeitet jeden einzelnen Vertex (Eckpunkt) der Geometrie.
    • Hauptaufgaben:
      • Transformation der Vertex-Positionen
      • Berechnung von Vertex-Attributen (z.B. Farben, Texturkoordinaten)
  2. Fragment-Shader
    • Wird für jeden Pixel des gerenderten Objekts ausgeführt.
    • Hauptaufgaben:
      • Bestimmung der finalen Farbe jedes Pixels.
      • Anwendung von Texturen, Beleuchtung und anderen visuellen Effekten.

Shader-Sprache

Bearbeiten

Die Shader-Sprache in WebGL ist GLSL ES (OpenGL ES Shading Language), eine spezielle Variante von GLSL (OpenGL Shading Language) für eingebettete Systeme und Webbrowser.

Hauptmerkmale von GLSL ES:

  • C-ähnliche Syntax
  • Starke Typisierung
  • Eingebaute Vektor- und Matrixtypen
  • Spezielle Funktionen für grafische Berechnungen


Vertex-Shader Beispiel

Bearbeiten

Ein einfacher Vertex-Shader könnte so aussehen:

// Definiert ein 2D-Vertex-Attribut für die Position
attribute vec2 position;

// Hauptfunktion des Vertex-Shaders
void main() {
    // Konvertiert den 2D-Vektor in einen 4D-Vektor für WebGL
    // (x, y, z, w), wobei z = 0.0 und w = 1.0 für 2D-Rendering
    gl_Position = vec4(position, 0.0, 1.0);
}

Dieser Shader transformiert die Position jedes Vertex mit Modell-, Ansichts- und Projektionsmatrizen.

Fragment-Shader Beispiel

Bearbeiten
 
Ausgabe eines einfachen Fragment-Shaders

Ein einfacher Fragment-Shader könnte so aussehen:

precision mediump float;    // Setzt die Fließkomma-Präzision auf medium
uniform vec4 u_color;       // Einheitliche Farbe für alle Fragmente

// Hauptfunktion des Fragment-Shaders
void main() {
    gl_FragColor = u_color; // Setzt die Ausgabefarbe für das Fragment
}

Dieser Shader setzt die Farbe jedes Pixels auf einen einheitlichen Wert.

Shader kompilieren und verknüpfen

Bearbeiten

Um Shader in WebGL zu verwenden, müssen sie kompiliert und zu einem Programm verknüpft werden:

function createShader(gl, type, source) {
    const shader = gl.createShader(type);     // Erstellt einen neuen Shader
    gl.shaderSource(shader, source);          // Übergibt den Quellcode des Shaders
    gl.compileShader(shader);                 // Kompiliert den Shader
    return shader;
}

function createProgram(gl, vertexShader, fragmentShader) {
    const program = gl.createProgram();       // Erstellt ein neues Programm
    gl.attachShader(program, vertexShader);   // Fügt den Vertex-Shader hinzu
    gl.attachShader(program, fragmentShader); // Fügt den Fragment-Shader hinzu
    gl.linkProgram(program);                  // Verknüpft die Shader zum Programm
    return program;
}

// Verwendung
const vertexShader   = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program        = createProgram(gl, vertexShader, fragmentShader);

Uniforms und Attributes

Bearbeiten

Uniforms und Attributes ermöglichen es, Daten von der JavaScript-Anwendung an die Shader zu übertragen. Während Uniforms globale Variablen sind, die für alle Ausführungen eines Shaders konstant bleiben, sind Attributes Variablen, die für jeden Vertex unterschiedliche Werte haben können.

Uniforms können von beiden Shader-Typen (Vertex und Fragment) genutzt werden und bleiben während eines Renderdurchgangs unverändert. Sie eignen sich für Daten, die für alle Vertices oder Fragmente gleich sind. Typische Verwendungen sind Transformationsmatrizen, Lichtparameter, Zeitvariablen für Animationen oder globale Farbwerte.

Attributes sind nur im Vertex-Shader verfügbar. Die Werte werden für jeden Vertex aus Vertex-Buffer-Objekten (VBOs) gelesen. Typische Verwendungen sind Vertex-Positionen, Normalen, Texturkoordinaten oder Vertex-Farben.

Beispiel für das Setzen von Uniforms und Attributes:

// Holt die Speicheradresse der Uniform-Variable 'u_color'
const colorUniformLocation = gl.getUniformLocation(program, "u_color");

// Setzt den Wert der Uniform-Variable (rot)
gl.uniform4f(colorUniformLocation, 1.0, 0.0, 0.0, 1.0);


// Holt die Speicheradresse des Attributs 'a_position'
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");

// Aktiviert das Vertex-Attribut-Array
gl.enableVertexAttribArray(positionAttributeLocation);

// Definiert das Format und Layout der Vertex-Attributdaten:
// 2 Komponenten pro Vertex vom Typ FLOAT
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

Fortgeschrittene Shader-Techniken

Bearbeiten

Einige fortgeschrittene Shader-Techniken sind:

  • Texturierung
  • Beleuchtungsberechnungen
  • Normalen-Mapping
  • Umgebungsverdeckung (Ambient Occlusion)
  • Schatten

Übungen

Bearbeiten
Übung 1: Grundlegende Vertex-Transformation
Bearbeiten

Aufgabe: Schreibe einen Vertex-Shader, der eine einfache 2D-Translation durchführt.

attribute vec2 a_position;
uniform vec2 u_translation;

void main() {
    // Füge hier den Code für die Translation hinzu
    // Tipp: Addiere u_translation zu a_position
    gl_Position = vec4(/* Dein Code hier */, 0.0, 1.0);
}
Übung 2: Farbmanipulation im Fragment-Shader
Bearbeiten

Aufgabe: Erstelle einen Fragment-Shader, der die Farbe eines Objekts basierend auf seiner Position auf dem Bildschirm ändert.

precision mediump float;

void main() {
    // Verwende gl_FragCoord, um die Farbe zu berechnen
    // Tipp: Normalisiere die x- und y-Koordinaten
    vec2 normalizedPosition = /* Dein Code hier */;
    gl_FragColor = vec4(normalizedPosition, 0.0, 1.0);
}
Übung 3: Interaktive Uniform-Variablen
Bearbeiten

Aufgabe: Erstelle einen Fragment-Shader, der die Farbe eines Objekts basierend auf der Mausposition auf dem Bildschirm ändert. Die Farbe sollte sich dynamisch ändern, wenn die Maus über das Canvas bewegt wird.

function drawMouseMove(gl, canvas, event) {
    const rect = canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;
    
    // Füge hier den Code für die Berechnung und Übergabe des Farbwertes hinzu
    // Tipp: die Ausmaße des Canvas erhält man über canvas.width und canvas.height.

    gl.clear(gl.COLOR_BUFFER_BIT);
	gl.drawArrays(gl.TRIANGLES, 0, 3);	// Erstellung von Dreiecken wird im nächsten Kapitel behandelt
}

// Event-Listener für die Mausbewegung
canvas.addEventListener("mousemove", (event) => drawMouseMove(gl, canvas, event));