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,outunduniform. 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:
Zwischen und nach den programmierbaren Shadern liegen fixe Stufen (fixed function): Shape Assembly (Zusammenbau der Primitive), Rasterization (Rasterisierung) und Tests and Blending.
| Stufe | Aufgabe | Pflicht? |
|---|---|---|
| Vertex Shader | Finale Position jedes Vertex; läuft pro Vertex | ja |
| Tesselation Shader | Zerlegung der Objekte in feinere Dreiecksnetze | optional |
| Geometry Shader | Veränderung der Geometrie-Daten zur Laufzeit; kann Primitive transformieren und Vertices hinzufügen | optional |
| Fragment Shader | Farbe jedes Pixels; läuft pro Fragment | ja |
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 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
TextureundTemporaries(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_Positionsetzen. 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
TextureundTemporaries.
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 330legt die verwendete OpenGL/GLSL-Version fest (OpenGL 3.3).layout(location = 0) in vec3 vertex;bindet diein-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:
- 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
| Grundtyp | Vektoren | Bedeutung |
|---|---|---|
float | vec2, vec3, vec4 | Gleitkommazahlen |
int | ivec2, ivec3, ivec4 | Ganzzahlen |
bool | bvec2, bvec3, bvec4 | Wahrheitswerte |
| - | mat2, mat3, mat4 | Matrizen |
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.xxxverdreifacht 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:
| Qualifizierer | Rolle | Rechte |
|---|---|---|
in | Verknüpfung mit dem Shader aus der vorherigen Stufe; Eingabe pro Vertex (von OpenGL/Anwendung) bzw. pro Fragment | READ-ONLY |
out | Verknüpfung zum Shader der nächsten Stufe; Weitergabe von Vertex an Fragment-Shader, interpoliert | READ/WRITE |
uniform | Eingabe in ein beliebiges Shader-Programm von OpenGL oder Anwendung; konstant während des Rendervorgangs | READ-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-Attributcolor(vec3, location 1) sind verschiedene Variablen in verschiedenen Shadern, die nur zufällig denselben Namen tragen. Die Kopplung läuft ausschliesslich uberpassOn.
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:
- Jeder Vertex-Shader MUSS die eingebaute Variable
gl_Positionmit der homogenen Vertexposition (vec4) füllen. Im Beispiel geschieht das durchmodel * vec4(vertex, 1.0)(Multiplikation der Vertexposition mit der akkumulierten Transformationsmatrix). - 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 ausglVertexAttribPointer(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").
uniformals 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
| Deutsch | English |
|---|---|
| Shader | shader |
| OpenGL Shading Language | GLSL |
| Shaderpipeline | shader pipeline |
| Vertex Shader | vertex shader |
| Tesselation Shader (optional) | tessellation shader |
| Geometrieshader (optional) | geometry shader |
| Fragment Shader | fragment shader |
| Fragment (Pixel-Kandidat) | fragment |
| Vertex Array Object | VAO |
| Vertex Buffer Object | VBO |
| VertexAttribPointer | vertex attribute pointer |
| Speichertyp / Qualifizierer | storage qualifier |
| Uniform | uniform |
| Swizzling | swizzling |
| Smearing | smearing |
| lesender / schreibender Zugriff | R-value / L-value |
| automatische Interpolation | interpolation |
| Homogenisierung | homogenization |
| Tiefentest | depth test |
| Mischen (Transparenz) | blending |
| akkumulierte Transformationsmatrix | model matrix |