WebGL: Shader-Programmierung
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.
Grundlagen der Shader
BearbeitenIn WebGL gibt es zwei Haupttypen von Shadern:
- Vertex-Shader
- Verarbeitet jeden einzelnen Vertex (Eckpunkt) der Geometrie.
- Hauptaufgaben:
- Transformation der Vertex-Positionen
- Berechnung von Vertex-Attributen (z.B. Farben, Texturkoordinaten)
- 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
BearbeitenDie 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
BearbeitenEin 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
BearbeitenEin 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
BearbeitenUm 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
BearbeitenUniforms 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
BearbeitenEinige fortgeschrittene Shader-Techniken sind:
- Texturierung
- Beleuchtungsberechnungen
- Normalen-Mapping
- Umgebungsverdeckung (Ambient Occlusion)
- Schatten
Übungen
BearbeitenÜbung 1: Grundlegende Vertex-Transformation
BearbeitenAufgabe: 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
BearbeitenAufgabe: 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
BearbeitenAufgabe: 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));