Kapitel 6: Shaderprogrammierung

Programmierung der GPU mit GLSL: die Shaderpipeline (Vertex, Tesselation, Geometry, Fragment), die Pflichten von Vertex- und Fragment-Shader, die Qualifizierer in/out/uniform mit automatischer Interpolation sowie das namensbasierte Hochladen von Uniforms von der Anwendung auf die GPU.

Überblick

Shader (shader) sind kleine Programmfragmente, die direkt auf der GPU ausgeführt werden. Sie erlauben, die hochgeladenen Daten vor bzw. während des Renderns zu manipulieren; die gewünschten Berechnungen werden in Shadern implementiert. Geschrieben werden sie in GLSL (OpenGL Shading Language), der Shader-Sprache von OpenGL. Alternativen sind HLSL (reine Windows/DirectX-Entwicklung) und Cg (von NVidia, 2012 eingestellt).

Roter Faden:

  • Die Shaderpipeline ist eine feste Abfolge von Stufen: Vertex Shader, optional Tesselation und Geometry Shader, dann Fragment Shader.
  • Der Vertex Shader berechnet die finale Position jedes Vertex (gl_Position), der Fragment Shader die Farbe jedes Pixels.
  • Shader kommunizieren untereinander und mit der Anwendung uber die Qualifizierer in, out und uniform.
  • out-Werte des Vertex-Shaders werden auf dem Weg zum Fragment-Shader automatisch interpoliert.
  • Uniforms werden per Variablenname adressiert und sind pro Draw-Call konstant.

Die Shaderpipeline

Die Shaderstufen laufen in fester Reihenfolge, jede mit genau definierter Aufgabe:

Vertex ShaderTesselation ShaderoptionalGeometry ShaderoptionalFragment Shader\text{Vertex Shader} \rightarrow \underbrace{\text{Tesselation Shader}}_{\text{optional}} \rightarrow \underbrace{\text{Geometry Shader}}_{\text{optional}} \rightarrow \text{Fragment Shader}

Zwischen und nach den programmierbaren Shadern liegen fixe Stufen (fixed function): Shape Assembly (Zusammenbau der Primitive), Rasterization (Rasterisierung) und Tests and Blending.

StufeAufgabePflicht?
Vertex ShaderFinale Position jedes Vertex; läuft pro Vertexja
Tesselation ShaderZerlegung der Objekte in feinere Dreiecksnetzeoptional
Geometry ShaderVeränderung der Geometrie-Daten zur Laufzeit; kann Primitive transformieren und Vertices hinzufügenoptional
Fragment ShaderFarbe jedes Pixels; läuft pro Fragmentja

Hinweis (Tradeoff): Beim Tesselation Shader gilt "Speicherplatz versus Performance" - feinere Netze kosten Rechenaufwand, sparen aber vorab abgelegte Geometrie.

Die learnopengl-Pipeline (Hello Triangle) verortet die Stufen im Gesamtablauf:

VERTEX DATA[]Vertex ShaderShape AssemblyGeometry ShaderRasterizationFragment ShaderTests and Blending\texttt{VERTEX DATA[]} \rightarrow \textbf{Vertex Shader} \rightarrow \texttt{Shape Assembly} \rightarrow \textbf{Geometry Shader} \rightarrow \texttt{Rasterization} \rightarrow \textbf{Fragment Shader} \rightarrow \texttt{Tests and Blending}

Vertex Shader

Der Vertex Shader ist ein "Programm fur jeweils einen Vertex": Er wird pro Vertex einmal ausgefuhrt und bildet, vereinfacht gesagt, den Eingabe-Vertex auf dem Bildschirm ab.

Eingabe:

  • Vertex-Attribute (vertex attributes): ein einzelner Vertex mit all seinen im VAO definierten Attributen.
  • Uniforms: zusätzliche, Vertex-unabhängige Variablen, die hochgeladen und verwendet werden können.
  • optional Texture und Temporaries (Zwischenvariablen).

Ausgabe:

  • beliebig viele Output-Variablen, die als Eingabe fur die nächste Shaderstufe dienen.
  • die eingebaute Variable gl_Position, die zwingend mit der homogenen (vec4) Vertexposition gefüllt werden muss.

Pflicht: Jeder Vertex-Shader MUSS gl_Position setzen. Die Position des Vertex muss definiert werden.

Fragment Shader

Der Fragment Shader ist ein "Programm fur jeweils ein Fragment": Er definiert die Ausgabefarbe fur jedes Pixel. Ein Fragment (fragment) ist ein Kandidat fur ein Pixel im Ausgabebild - der Out-Wert durchläuft anschliessend Tiefentest und ggf. Blending, bevor das Pixel tatsächlich gerendert wird.

Eingabe:

  • die Ausgabe der vorherigen Shaderstufe (Input 0 ... n).
  • optionale Uniforms.
  • die eingebaute Variable gl_FragCoord (READ-ONLY) zur Abfrage des aktuellen Fragments, d.h. dessen Bildschirmkoordinaten.
  • optional Texture und Temporaries.

Ausgabe:

  • eine oder mehrere Output-Variablen (Output 0, Output 1, ...).
  • die eingebaute Variable gl_FragDepth (Ausgabe der Fragmenttiefe).

Pflicht: Jeder Fragment-Shader MUSS mind. eine Output-Variable definieren und diese mit der finalen Farbe des Fragments füllen.

Worked Example 1: Minimaler Shader (rotes Quadrat)

Das durchgehende prüfungsrelevante Beispiel besteht aus einem minimalen Vertex- und Fragment-Shader. Output ist ein rotes Quadrat auf schwarzem Grund.

Vertex Shader:

#version 330
layout(location = 0) in vec3 vertex;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(vertex, 1.0);
}

Fragment Shader:

#version 330
out vec4 color;

void main()
{
    color = vec4(1.0, 0.0, 0.0, 1.0);
}

Erläuterung Zeile fur Zeile:

  • #version 330 legt die verwendete OpenGL/GLSL-Version fest (OpenGL 3.3).
  • layout(location = 0) in vec3 vertex; bindet die in-Eingabevariable an das VAO-Attribut mit Index 0.
  • uniform mat4 model; ist die von der Anwendung gelieferte, während des Renderns konstante Transformationsmatrix.
  • vec4(vertex, 1.0) homogenisiert die 3D-Position (w = 1.0, also ein Punkt, keine Richtung).
  • gl_Position = model * vec4(vertex, 1.0) multipliziert die homogenisierte Vertexposition mit der akkumulierten Transformationsmatrix:
gl_Position=model(vertex,1.0)\texttt{gl\_Position} = \mathbf{model} \cdot (\texttt{vertex}, 1.0)
  • Der Fragment-Shader setzt jede Pixelfarbe auf reines Rot mit Alpha = 1.0.

layout(location = index) und der VBO/VAO-Bezug

Die allgemeine Form einer Vertex-Attribut-Eingabe lautet:

layout(location = index) in type name;

Der Index verweist auf das jeweilige VAO-Attribut aus glVertexAttribPointer(GLuint index, ...). Das VAO (Vertex Array Object) kennt die Struktur, in der die Daten im VBO vorliegen; ein VertexAttribPointer beschreibt die Ablagestruktur eines Attributs (z.B. "3 Koordinaten vom Typ GL_FLOAT pro Vertex"). Das VBO (Vertex Buffer Object) sammelt die Daten und lädt sie von der CPU auf die GPU.

Hinweis (kein Typcheck): GLSL prüft layout(location = ...) nicht gegen die tatsächlichen VBO-Daten - der Programmierer muss selbst wissen, was das Attribut enthält.

GLSL-Datentypen

GrundtypVektorenBedeutung
floatvec2, vec3, vec4Gleitkommazahlen
intivec2, ivec3, ivec4Ganzzahlen
boolbvec2, bvec3, bvec4Wahrheitswerte
-mat2, mat3, mat4Matrizen

Der Komponentenzugriff nutzt drei funktional gleiche Sets: .xyzw (Position), .rgba (Farbe), .stqp (Textur). Die Sets dürfen innerhalb eines Zugriffs nicht gemischt werden.

vec2 v2; vec3 v3; vec4 v4;
v2.x       // ergibt einen float
v2.z       // Fehler: undefined for type (v2 hat nur x, y)
v4.rgba    // ergibt einen vec4
v4.xy      // ergibt einen vec2
v4.xgp     // Fehler: Sets xyzw/rgba/stqp gemischt

Swizzling und Smearing

  • Swizzling (swizzling): Umordnen der Vektorkomponenten beim Zugriff, z.B. v4.wzyx.
  • Smearing (smearing): Vervielfachen einzelner Komponenten, z.B. v4.xxx verdreifacht x.

Bei R-Values (lesend) sind Wiederholungen erlaubt:

v4.wzyx    // korrekt, ergibt vec4(w, z, y, x)
v4.xxx     // korrekt (Smearing), ergibt vec3(x, x, x)
v4.yyxx    // korrekt, ergibt vec4(y, y, x, x)
v2.yyzz    // Fehler: zu viele Komponenten fuer Typ (v2 hat kein z)

Bei L-Values (schreibend) darf jede Komponente nur einmal genannt werden. Die folgenden Zeilen bauen aufeinander auf (der Zustand von v4 wird fortgeschrieben):

vec4 v4 = vec4(1.0, 2.0, 3.0, 4.0);
v4.xw = vec2(5.0, 6.0);   // (5.0, 2.0, 3.0, 6.0)
v4.wx = vec2(7.0, 8.0);   // (8.0, 2.0, 3.0, 7.0)  -> w=7, x=8
v4.xx = vec2(9.0, 10.0);  // Fehler: x doppelt genannt (L-Value)
v4.yz = 11.0;             // Fehler: Type mismatch (Skalar an vec2)
v4.yz = vec2(12.0);       // (8.0, 12.0, 12.0, 7.0)  -> vec2(12.0) = (12, 12)

Nach den ersten zwei gültigen Zuweisungen ist v4 = (8.0, 2.0, 3.0, 7.0); die letzte gültige Zeile ergibt (8.0, 12.0, 12.0, 7.0). vec2(12.0) erzeugt durch Smearing den Vektor (12, 12).

Vordefinierte Funktionen

GLSL stellt vordefinierte Funktionen bereit, u.a. trigonometrische, exponentielle, allgemeine und geometrische Funktionen.

Merksatz: "Verwenden Sie nach Möglichkeit eingebaute Funktionen anstelle Ihrer eigenen!" (Performance und Korrektheit).

Speichertypen: in, out, uniform

Die Qualifizierer (storage qualifiers) sind fur die Kommunikation zwischen Shadern und Anwendung erforderlich:

QualifiziererRolleRechte
inVerknüpfung mit dem Shader aus der vorherigen Stufe; Eingabe pro Vertex (von OpenGL/Anwendung) bzw. pro FragmentREAD-ONLY
outVerknüpfung zum Shader der nächsten Stufe; Weitergabe von Vertex an Fragment-Shader, interpoliertREAD/WRITE
uniformEingabe in ein beliebiges Shader-Programm von OpenGL oder Anwendung; konstant während des RendervorgangsREAD-ONLY

Worked Example 2: Datenweitergabe Vertex -> Fragment (passOn)

Um Werte von der Vertex- zur Fragment-Stufe zu reichen, deklariert man dieselbe Variable im einen Shader als out, im anderen als in. Bedingung: gleicher Variablenname UND gleicher Typ in beiden Shadern.

Vertex Shader (Ausschnitt): out vec4 passOn; und im main: passOn = vec4(1.0, 0.0, 0.0, 1.0);

Fragment Shader (Ausschnitt): in vec4 passOn; und color = passOn;

Output: weiterhin rotes Quadrat - die Farbe wird durchgereicht.

Worked Example 3: Interpolierte Vertexfarbe (Farbverlauf)

Ein zusätzliches Farb-Attribut (location 1) wird per out/in durchgereicht. Da out-Werte automatisch zwischen den Vertexdaten interpoliert werden (perspektivkorrekt uber das Primitiv), entsteht ein Farbverlauf-Quadrat statt einer einfarbigen Fläche.

Vertex Shader:

#version 330
layout(location = 0) in vec3 vertex;
layout(location = 1) in vec3 color;

out vec4 passOn;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(vertex, 1.0);
    passOn = vec4(color, 1.0);
}

Fragment Shader:

#version 330
out vec4 color;

in vec4 passOn;

void main()
{
    color = passOn;
}

Achtung (gleicher Name, verschiedene Variablen): Der Fragment-Shader-Output color (vec4) und das Vertex-Attribut color (vec3, location 1) sind verschiedene Variablen in verschiedenen Shadern, die nur zufällig denselben Namen tragen. Die Kopplung läuft ausschliesslich uber passOn.

Uniforms hochladen

Uniforms sind vom Benutzer bereitgestellte Variablen, die von der Anwendung zu den Shadern gelangen. Sie werden per Variablenname adressiert (nicht per Index wie Vertex-Attribute) und sind fur alle Vertices/Fragmente eines Draw-Calls konstant.

Uniform-Location abfragen:

GLint glGetUniformLocation(GLuint programID, const GLchar *name);

Skalare/Vektor-Uniforms hochladen:

void glUniform{1,2,3,4}{i,f}(GLint location, GLfloat v0 /*[, v1, v2, v3]*/);

Matrix-Uniforms hochladen:

void glUniformMatrix{234}fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);

count = Anzahl der zu ändernden Matrizen: 1, wenn die Zielvariable kein Matrizenfeld ist; 1 oder mehr bei einem Array von Matrizen.

Worked Example 4: model-Matrix hochladen

Rohes OpenGL:

GLint loc = glGetUniformLocation(programID, "model");
glUniformMatrix4fv(loc, 1, GL_FALSE, value);

Kurzform im Kurs-Framework (ShaderProgram::setUniform(string variable, TYPE value, [bool transpose]), der optionale transpose-Parameter gilt bei Matrizen):

ShaderProgram::setUniform("model", value, false);

Was nach dem Fragment-Shader passiert

Der Out-Wert des Fragment-Shaders ist nicht automatisch das finale Pixel. In der Stufe Tests and Blending wird geprüft:

  • Tiefentest (depth test): Wird das Pixel uberhaupt gerendert?
  • Blending: Wird es transparent dargestellt (Mischen)?

Worked Example 5: Die zwei Pflichten

Das vollständige Beispiel fasst die zwei zentralen Pflichten zusammen:

  1. Jeder Vertex-Shader MUSS die eingebaute Variable gl_Position mit der homogenen Vertexposition (vec4) füllen. Im Beispiel geschieht das durch model * vec4(vertex, 1.0) (Multiplikation der Vertexposition mit der akkumulierten Transformationsmatrix).
  2. Jeder Fragment-Shader MUSS mind. eine Output-Variable definieren (out vec4 color;) und diese mit der finalen Farbe des Fragments füllen.

Prüfungsrelevanz

  • Pipeline-Reihenfolge auswendig: Vertex -> Tesselation (optional) -> Geometry (optional) -> Fragment; danach Rasterization und Tests/Blending. Wissen, welche Stufen optional sind.
  • Aufgaben-Zuordnung: Vertex Shader = finale Position pro Vertex (gl_Position); Fragment Shader = Farbe pro Pixel/Fragment (Output-Variable).
  • Die zwei Pflichten: gl_Position (homogen, vec4) im Vertex-Shader; mind. eine gefüllte Output-Variable im Fragment-Shader.
  • Qualifizierer und Rechte: in = READ-ONLY, out = READ/WRITE (interpoliert), uniform = READ-ONLY und konstant während des Rendervorgangs.
  • out/in-Kopplung: gleicher Name und gleicher Typ in beiden Shadern; out-Werte werden automatisch interpoliert.
  • layout(location = index) verweist auf das VAO-Attribut aus glVertexAttribPointer(index, ...).
  • Homogenisierung vec4(vertex, 1.0): w = 1.0 nicht vergessen (Punkt, nicht Richtung).
  • Swizzling/Smearing-Regeln und die drei Komponenten-Sets .xyzw / .rgba / .stqp.
  • Uniform-Adressierung per Name (glGetUniformLocation), nicht per Index.

Typische Fehler

  • Fragment und Vertex verwechseln ("pro Fragment/Pixel" vs. "pro Vertex").
  • uniform als beschreibbar oder pro-Vertex-variabel annehmen (es ist READ-ONLY und pro Draw-Call konstant).
  • out/in-Variablen mit unterschiedlichem Namen oder Typ deklarieren, sodass keine Verbindung entsteht.
  • Annehmen, GLSL prüfe layout(location=...) gegen die VBO-Daten (tut es nicht).
  • Homogenisierung vergessen: vec4(vertex, 1.0) statt versehentlich w = 0.
  • Swizzling-Fehler: (a) Komponente ausserhalb des Vektors (v2.z); (b) Sets mischen (v4.xgp); (c) beim Schreiben eine Komponente doppelt (v4.xx = ...); (d) Skalar an Swizzle mit mehreren Komponenten (v4.yz = 11.0).
  • Vergessen, dass der Fragment-Output erst durch Tiefentest und Blending muss, bevor das Pixel gerendert wird.
  • Uniforms per Index statt per Name adressieren zu wollen.

Glossar

DeutschEnglish
Shadershader
OpenGL Shading LanguageGLSL
Shaderpipelineshader pipeline
Vertex Shadervertex shader
Tesselation Shader (optional)tessellation shader
Geometrieshader (optional)geometry shader
Fragment Shaderfragment shader
Fragment (Pixel-Kandidat)fragment
Vertex Array ObjectVAO
Vertex Buffer ObjectVBO
VertexAttribPointervertex attribute pointer
Speichertyp / Qualifiziererstorage qualifier
Uniformuniform
Swizzlingswizzling
Smearingsmearing
lesender / schreibender ZugriffR-value / L-value
automatische Interpolationinterpolation
Homogenisierunghomogenization
Tiefentestdepth test
Mischen (Transparenz)blending
akkumulierte Transformationsmatrixmodel matrix