Übungen: OpenGL & Shader
Übungen zu Kapitel 4: OpenGL und Kapitel 6: Shaderprogrammierung. Geübt wird der komplette Datenpfad von der CPU zur GPU: interleaved Array, VBO (Vertex Buffer Object), VAO (Vertex Array Object) mit stride/offset in Bytes, IBO (Index Buffer Object) und die Zeichenbefehle, dazu die GLSL-Shaderprogrammierung (Vertex/Fragment, die Qualifizierer in/out/uniform, layout(location), gl_Position und Swizzling). Rechne alle Zahlenaufgaben selbst durch, bevor du die Lösung aufklappst. Merkregel für fast alles: ein
float= 4 Byte.
Beginner
Aufgabe 1 (Beginner) - State-Machine und Explizitheit
OpenGL wird als "State-Machine" (state machine) und als "explizit" (explicit) beschrieben. Erkläre beide Eigenschaften in je einem Satz und nenne je ein Beispiel.
Lösung anzeigen
State-Machine (Zustandsautomat): OpenGL merkt sich einen internen Zustand. Funktionen ändern diesen Zustand oder nutzen ihn zur Darstellung. Einmal gesetzt, bleibt ein Zustand aktiv, bis er umgeschaltet wird.
- Beispiel: das aktuell gebundene VBO/VAO oder die aktuelle Farbe. Im Legacy-
glBegin/glEnd-Block gilt eine mitglColor4fgesetzte Farbe fuer alle folgenden Vertices, bis sie neu gesetzt wird.
Explizitheit: Was nicht explizit aktiviert wurde, bleibt aus.
- Beispiel: Eine Transparenz (Alpha) zu setzen bringt nichts, solange die Transparenzberechnung (Blending) nicht explizit eingeschaltet ist. Ebenso müssen Vertex-Attribute mit
glEnableVertexAttribArrayerst aktiviert werden.
Tipp: Beide Begriffe sind laut Prüfungsrelevanz ein klassischer Erklär-Klassiker. Merke dir je ein Beispiel dazu.
Siehe Kapitel 4: OpenGL.
Aufgabe 2 (Beginner) - Der 3-Schritte-VBO-Upload
Ein VBO wird in genau drei Schritten mit Daten befüllt. Nenne die drei Schritte in der richtigen Reihenfolge mit dem jeweiligen OpenGL-Befehl.
Lösung anzeigen
- ID erzeugen -
glGenBuffers - Buffer aktivsetzen (binden) -
glBindBuffer(GL_ARRAY_BUFFER, vboID) - Daten hochladen -
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, GL_STATIC_DRAW)
Der usage hint im dritten Schritt beschreibt die Nutzung:
GL_STATIC_DRAW- einmal initialisiert, oft gerendert.GL_DYNAMIC_DRAW- oft geändert.GL_STREAM_DRAW- selten genutzt.
Typischer Fehler:
GL_ARRAY_BUFFER(Geometriedaten) mitGL_ELEMENT_ARRAY_BUFFER(Indizes im IBO) verwechseln.
Siehe Kapitel 4: OpenGL.
Aufgabe 3 (Beginner) - Vertex-Shader vs. Fragment-Shader
Ordne zu: Welche Stufe berechnet die finale Position und welche die Farbe jedes Pixels? Welche eingebaute Variable muss jede Stufe befuellen?
Lösung anzeigen
| Stufe | Aufgabe | läuft pro | Pflicht-Ausgabe |
|---|---|---|---|
| Vertex Shader | finale Position jedes Vertex | Vertex | gl_Position (homogen, vec4) |
| Fragment Shader | Farbe jedes Pixels/Fragments | Fragment | mind. eine Output-Variable (z.B. out vec4 color;) |
Die zwei Pflichten:
- Jeder Vertex-Shader MUSS
gl_Positionsetzen. - Jeder Fragment-Shader MUSS mind. eine Output-Variable definieren und mit der finalen Farbe füllen.
Typischer Fehler: "pro Fragment/Pixel" und "pro Vertex" verwechseln. Merke: Vertex = Eckpunkt (Position), Fragment = Pixel-Kandidat (Farbe).
Aufgabe 4 (Beginner) - Wozu dient das VAO?
Es heisst "OpenGL ist dumm". Was bedeutet das im Zusammenhang mit VBO und VAO? Welche Aufgabe hat das VAO?
Lösung anzeigen
OpenGL sieht im VBO nur ein Bündel rohen Speicher (rohe Bytes) und "versteht" die Struktur nicht. Das VAO (Vertex Array Object) beschreibt, wie die rohen VBO-Daten zu interpretieren sind:
- Es speichert Attributzeiger (vertex attribute pointers), je einen pro Vertex-Attribut, mit einem Attribut-Index (z.B. Attribut 0 = Position, Attribut 1 = Farbe).
- Es wird über
glVertexAttribPointer(index, size, type, normalized, stride, offset)gefüllt. - Nur ein VAO kann gleichzeitig aktiv sein.
Typischer Fehler: Glauben, OpenGL "versteht" die Datenstruktur von selbst. Nein: erst das VAO liefert die Interpretation.
Siehe Kapitel 4: OpenGL.
Fortgeschritten
Aufgabe 5 (Fortgeschritten) - stride und offset berechnen (interleaved)
Gegeben ist ein interleaved Array mit pro Vertex: Position (3 floats) + Farbe (3 floats). Berechne für ein glVertexAttribPointer-Setup:
- den
stride, - den
offsetder Position, - den
offsetder Farbe.
Lösung anzeigen
Ein float = 4 Byte; pro Vertex liegen floats.
Als Code:
glVertexAttribPointer(0, 3, GL_FLOAT, false, 24, 0); // Position, ID 0
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 24, (void*)12); // Farbe, ID 1
glEnableVertexAttribArray(1);
Der offset 12 wird als (void*)12 gecastet (der "seltsame Datentyp").
Typischer Fehler: stride/offset in Elementen statt in Bytes angeben. Beide sind in Bytes: 3 Positions-floats ergeben offset 12, nicht 3. Und: Attribute mit
glEnableVertexAttribArrayaktivieren, sonst ist nichts sichtbar.
Siehe Kapitel 4: OpenGL.
Aufgabe 6 (Fortgeschritten) - count bei glDrawElements
Ein Objekt besteht aus 5 Dreiecken, gezeichnet mit glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, 0). Wie gross ist count? Begründe.
Lösung anzeigen
count ist die Anzahl der Indizes, nicht die Anzahl der Dreiecke. Bei GL_TRIANGLES hat jedes Dreieck 3 Indizes:
Das Index-Array besteht aus Dreier-Gruppen, z.B. V0 V1 V2 | V2 V1 V3 | V2 V3 V4 | .... Beim Aufruf müssen zusätzlich der Primitivtyp (GL_TRIANGLES) und der IBO-Datentyp (GL_UNSIGNED_INT; alternativ GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT) angegeben werden.
Typischer Fehler:
countals Anzahl Dreiecke annehmen (hier fälschlich 5). Richtig ist .
Siehe Kapitel 4: OpenGL.
Aufgabe 7 (Fortgeschritten) - in / out / uniform und ihre Rechte
Vervollständige die Tabelle: Rolle und Zugriffsrecht (READ-ONLY oder READ/WRITE) für die drei Qualifizierer. Was bedeutet "interpoliert" bei out?
Lösung anzeigen
| Qualifizierer | Rolle | Rechte |
|---|---|---|
in | Eingabe aus vorheriger Stufe; pro Vertex bzw. pro Fragment | READ-ONLY |
out | Weitergabe an die nächste Stufe (Vertex -> Fragment), interpoliert | READ/WRITE |
uniform | Eingabe von OpenGL/Anwendung in ein Shader-Programm; konstant während des Rendervorgangs | READ-ONLY |
Interpoliert: Die out-Werte des Vertex-Shaders werden auf dem Weg zum Fragment-Shader automatisch zwischen den Vertexdaten interpoliert (perspektivkorrekt über das Primitiv). So entsteht z.B. aus drei Eckfarben ein weicher Farbverlauf über die Fläche.
Kopplung out/in: funktioniert nur bei gleichem Variablennamen UND gleichem Typ in beiden Shadern.
Typischer Fehler:
uniformals beschreibbar oder als pro-Vertex-variabel annehmen. Es ist READ-ONLY und pro Draw-Call konstant. Und: Uniforms werden per Name adressiert (glGetUniformLocation), nicht per Index.
Aufgabe 8 (Fortgeschritten) - Vertex-Shader vervollständigen
Ergänze die fehlenden Zeilen (Lücken mit /* ? */), sodass ein minimaler Vertex-Shader entsteht, der ein Attribut aus VAO-Index 0 einliest und die homogene Position setzt:
#version 330
layout(location = 0) in vec3 vertex;
uniform mat4 model;
void main()
{
gl_Position = /* ? */;
}
Lösung anzeigen
#version 330
layout(location = 0) in vec3 vertex;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(vertex, 1.0);
}
Begründung:
vec4(vertex, 1.0)homogenisiert die 3D-Position: bedeutet ein Punkt (keine Richtung).- Die Multiplikation mit der akkumulierten Transformationsmatrix
modelliefert die finale Position:
layout(location = 0)bindet diein-Variable an das VAO-Attribut mit Index 0 (ausglVertexAttribPointer(0, ...)).
Typischer Fehler: Homogenisierung vergessen und versehentlich schreiben (das wäre eine Richtung, kein Punkt). Und: GLSL prüft
layout(location = ...)nicht gegen die tatsächlichen VBO-Daten - der Programmierer muss selbst wissen, was das Attribut enthält.
Anspruchsvoll
Aufgabe 9 (Anspruchsvoll) - Swizzling: L-Value-Auswertung
Gegeben:
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
v.zx = vec2(7.0, 8.0);
v.yw = vec2(9.0, 5.0);
- Was ist der Wert von
vnach diesen beiden gültigen Zuweisungen? - Warum sind
v.xx = vec2(1.0, 2.0);undv.yz = 3.0;jeweils Fehler?
Lösung anzeigen
Schritt für Schritt (Start: v = (1, 2, 3, 4)):
v.zx = vec2(7.0, 8.0)weist der Reihenfolge nach zu:z = 7,x = 8. Alsov = (8, 2, 7, 4).v.yw = vec2(9.0, 5.0)weist zu:y = 9,w = 5. Alsov = (8, 9, 7, 5).
Ergebnis: v = (8.0, 9.0, 7.0, 5.0).
Die Fehler:
v.xx = vec2(1.0, 2.0)ist ungültig, weil beim schreibenden Zugriff (L-Value) jede Komponente nur einmal genannt werden darf.xdoppelt zu nennen ist verboten (Smearing ist nur beim Lesen erlaubt).v.yz = 3.0ist ein Type mismatch: links steht ein Swizzle mit zwei Komponenten (vec2), rechts ein Skalar. Korrekt wäre z.B.v.yz = vec2(3.0), was durch Smearing(3.0, 3.0)erzeugt.
Tipp: Beim Lesen (R-Value) sind Wiederholungen erlaubt (
v.xxx,v.yyxx); beim Schreiben (L-Value) nicht. Die drei Sets.xyzw/.rgba/.stqpdürfen innerhalb eines Zugriffs nie gemischt werden (v.xgpist Fehler).
Aufgabe 10 (Anspruchsvoll) - Vertices und Indizes zählen
Betrachte ein Gitter (fan/strip-artige Anordnung) aus 6 Dreiecken, die sich gemeinsame Eckpunkte teilen.
- Wie viele Vertices müsstest du bei
glDrawArraysmitGL_TRIANGLES(ohne Wiederverwendung, sequenziell) hochladen und zeichnen? - Wie viele Indizes braucht dagegen
glDrawElementsmitGL_TRIANGLES(IBO)? - Was ist der Vorteil des IBO-Ansatzes?
Lösung anzeigen
1. glDrawArrays (brute-force, sequenziell): jeder Vertex jedes Dreiecks wird einzeln abgelegt, ohne Wiederverwendung:
Gezeichnet wird mit glDrawArrays(GL_TRIANGLES, first, count) und (Primitive von bis ).
2. glDrawElements (indiziert): die Anzahl der Indizes ist ebenfalls
Der Unterschied liegt nicht in der Index-Anzahl, sondern darin, dass im VBO gemeinsam genutzte Vertices nur einmal gespeichert werden. Der IBO verweist mehrfach auf denselben Vertex (z.B. V0 V1 V2 | V2 V1 V3 | ...).
3. Vorteil: Wiederverwendung von Vertices -> weniger Datentransfer (der VBO-Speicher schrumpft, da geteilte Ecken nicht dupliziert werden), höhere Flexibilität (Teilrendering durch Anpassung des IBO möglich).
Typischer Fehler:
countbeiglDrawElementsmit der Anzahl Dreiecke gleichsetzen.countist immer die Anzahl der Indizes ( Dreiecke beiGL_TRIANGLES). Der Spareffekt des IBO steckt im VBO (weniger Vertexdaten), nicht in einem kleinerencount.
Siehe Kapitel 4: OpenGL.
Aufgabe 11 (Anspruchsvoll) - Triangle Strip: Vertices vs. Dreiecke
Hinweis: Die eigentliche Primitiv-Art
GL_TRIANGLE_STRIPgehört zum Themenblock "Primitive" (Kapitel 3). In Kapitel 4: OpenGL taucht das strip-typische Wiederverwendungs-Muster als Index-Reihenfolge auf (V0 V1 V2 | V2 V1 V3 | V2 V3 V4 | ...). Die folgende Zählformel ist die Standardregel für Triangle Strips; die konkretenglDraw*-Byte- und count-Regeln stammen aus Kapitel 4.
Ein Triangle Strip (Dreiecksstreifen) besteht aus einer fortlaufenden Vertex-Folge, bei der jedes neue Dreieck die letzten zwei Vertices des vorherigen weiterverwendet.
- Wie viele Dreiecke entstehen aus einem Strip mit 8 Vertices?
- Wie viele Vertices bräuchte man für 10 Dreiecke als Strip?
- Wie viele Vertices bräuchte
glDrawArraysmitGL_TRIANGLES(ohne Strip, ohne Wiederverwendung) für dieselben 10 Dreiecke, und wie gross ist die Ersparnis?
Lösung anzeigen
Beim Triangle Strip gilt: mit Vertices entstehen Dreiecke (die ersten 3 Vertices bilden das erste Dreieck, jeder weitere Vertex fügt genau ein Dreieck hinzu). Umgekehrt: Dreiecke brauchen Vertices.
1. Vertices Dreiecke.
2. Dreiecke Vertices.
3. glDrawArrays mit GL_TRIANGLES bräuchte Vertices. Ersparnis des Strips:
Das ist genau das Prinzip "Wiederverwendung von Vertices = weniger Datentransfer", das auch der IBO verfolgt.
Tipp: Verwechsle die beiden Formeln nicht. Strip: Dreiecke . Reines
GL_TRIANGLES: Vertices/Indizes Dreiecke.
Siehe Kapitel 4: OpenGL.
Aufgabe 12 (Anspruchsvoll) - Datenweitergabe Vertex -> Fragment
Der folgende Shader-Ausschnitt soll ein Farb-Attribut von der Vertex- zur Fragment-Stufe durchreichen, damit ein interpolierter Farbverlauf entsteht. Fülle die drei Lücken und erkläre, warum der Fragment-Output color und das Vertex-Attribut color sich nicht in die Quere kommen.
// Vertex Shader
#version 330
layout(location = 0) in vec3 vertex;
layout(location = 1) in vec3 color;
/* Lücke A: Ausgabe deklarieren */
uniform mat4 model;
void main()
{
gl_Position = model * vec4(vertex, 1.0);
/* Lücke B: Farbe weitergeben */
}
// Fragment Shader
#version 330
out vec4 color;
/* Lücke C: Eingabe deklarieren */
void main()
{
color = passOn;
}
Lösung anzeigen
// Vertex Shader
out vec4 passOn; // Lücke A
// ...
passOn = vec4(color, 1.0); // Lücke B (color ist hier das vec3-Attribut aus location 1)
// Fragment Shader
in vec4 passOn; // Lücke C
Warum kein Konflikt? Die Kopplung zwischen den Shadern läuft ausschliesslich über passOn (gleicher Name, gleicher Typ vec4 in beiden Shadern). Der Fragment-Output color (out vec4) und das Vertex-Attribut color (in vec3, location 1) sind verschiedene Variablen in verschiedenen Shadern, die nur zufällig denselben Namen tragen. Sie sind nicht miteinander verbunden.
Da out-Werte automatisch interpoliert werden, ergibt sich statt einer einfarbigen Fläche ein Farbverlauf über das Primitiv.
Typischer Fehler:
out/inmit unterschiedlichem Namen oder Typ deklarieren, sodass keine Verbindung entsteht. Bedingung ist immer: gleicher Name UND gleicher Typ.
Klausur-Niveau
Aufgabe 13 (Klausur-Niveau) - Fill-the-gap: minimaler Shader + die zwei Pflichten
Vervollständige das durchgehende Klausurbeispiel (rotes Quadrat). Fülle alle mit ______ markierten Lücken und benenne anschliessend die "zwei Pflichten" von Vertex- und Fragment-Shader.
// Vertex Shader
#version 330
layout(location = ______) in vec3 vertex; // (1)
uniform ______ model; // (2)
void main()
{
______ = model * vec4(vertex, ______); // (3) und (4)
}
// Fragment Shader
#version 330
______ vec4 color; // (5)
void main()
{
color = vec4(1.0, 0.0, 0.0, ______); // (6)
}
Lösung anzeigen
Lösungen der Lücken:
| # | Lücke | Lösung | Grund |
|---|---|---|---|
| (1) | layout(location = __) | 0 | verweist auf VAO-Attribut mit Index 0 (aus glVertexAttribPointer(0, ...)) |
| (2) | uniform __ model | mat4 | die Transformationsmatrix ist eine 4x4-Matrix |
| (3) | __ = model * ... | gl_Position | eingebaute Pflicht-Ausgabe des Vertex-Shaders |
| (4) | vec4(vertex, __) | 1.0 | Homogenisierung: (Punkt, keine Richtung) |
| (5) | __ vec4 color | out | Fragment-Shader braucht mind. eine Output-Variable |
| (6) | vec4(1.0, 0.0, 0.0, __) | 1.0 | Alpha = 1.0 (voll deckend) -> reines Rot |
Vollständig:
// 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); }
Die zwei Pflichten:
- Jeder Vertex-Shader MUSS
gl_Positionmit der homogenen Vertexposition (vec4) füllen. Hier:model * vec4(vertex, 1.0). - Jeder Fragment-Shader MUSS mind. eine Output-Variable definieren (
out vec4 color;) und mit der finalen Farbe füllen.
Tipp:
#version 330steht für OpenGL/GLSL 3.3. Der Fragment-Output ist noch nicht automatisch das Pixel: er durchläuft danach Tests and Blending (Tiefentest, Blending).
Aufgabe 14 (Klausur-Niveau) - stride/offset bei drei Attributen + Zeichenbefehl-Wahl
Ein interleaved VBO speichert pro Vertex: Position (3 floats) + Normale (3 floats) + Texturkoordinate (2 floats).
- Berechne
stridesowie denoffsetjedes der drei Attribute (in Bytes). - Schreibe die drei
glVertexAttribPointer-Aufrufe (Indizes 0, 1, 2) inklusive Aktivierung. - Ein Modell aus 12 Dreiecken soll mit maximaler Vertex-Wiederverwendung gezeichnet werden. Welchen Zeichenbefehl wählst du, welche Buffer brauchst du, und wie gross ist der zugehörige
count?
Lösung anzeigen
1. stride/offset (ein float = 4 Byte; floats pro Vertex):
2. Aufrufe:
glVertexAttribPointer(0, 3, GL_FLOAT, false, 32, 0); // Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 32, (void*)12); // Normale
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, false, 32, (void*)24); // TexCoord
glEnableVertexAttribArray(2);
3. Zeichenbefehl: Für maximale Vertex-Wiederverwendung nimmt man glDrawElements (indiziert). Nötige Buffer: VBO + VAO + IBO. Der count ist die Anzahl der Indizes:
glBindVertexArray(vaoID);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0); // count = 36 Indizes
glBindVertexArray(0);
Zum Vergleich glDrawArrays (brute-force, sequenziell): braucht VBO + VAO, aber Vertices im VBO ohne Wiederverwendung, also mehr Datentransfer und keine geteilten Ecken.
Typischer Fehler: stride/offset in Elementen statt Bytes; Attribute nicht mit
glEnableVertexAttribArrayaktivieren; undcountals 12 (Dreiecke) statt 36 (Indizes) angeben. Beachte auch: der IBO wird an das aktive VAO gebunden, deshalbglGenBuffersfür den IBO erst nachglGenVertexArrays/glBindVertexArray.
Siehe Kapitel 4: OpenGL.
Aufgabe 15 (Klausur-Niveau) - Multiple Choice: gemischt
Kreuze jeweils die richtige(n) Aussage(n) an und korrigiere die falschen.
A) Bei glDrawElements(GL_TRIANGLES, count, ...) ist count die Anzahl der Dreiecke.
B) uniform-Variablen sind pro Draw-Call konstant und READ-ONLY im Shader.
C) layout(location = 2) wird von GLSL gegen die tatsächlichen VBO-Daten geprüft.
D) Der Vertex-Shader ist optional, der Fragment-Shader ist Pflicht.
E) v4.xgp ist ein gültiger Swizzle-Zugriff.
F) Uniforms werden per Variablenname adressiert, Vertex-Attribute per Index.
Lösung anzeigen
- A) Falsch.
countist die Anzahl der Indizes ( Dreiecke beiGL_TRIANGLES). - B) Richtig.
uniformist während des Rendervorgangs konstant und im Shader READ-ONLY. - C) Falsch. GLSL prüft
layout(location = ...)nicht gegen die VBO-Daten. Der Programmierer muss selbst wissen, was das Attribut enthält. - D) Falsch. Beide sind Pflicht (Vertex und Fragment). Optional sind nur Tesselation- und Geometry-Shader.
- E) Falsch. Die Sets
.xyzw/.rgba/.stqpdürfen innerhalb eines Zugriffs nicht gemischt werden (xaus xyzw,gaus rgba,paus stqp). - F) Richtig. Uniforms per Name (
glGetUniformLocation), Vertex-Attribute per Index (layout(location = index)bzw.glVertexAttribPointer(index, ...)).
Richtig sind also B und F.
Tipp: Die häufigsten Klausur-Fallen bündeln sich hier: count-Definition, uniform-Rechte, kein layout-Typcheck, beide Pflicht-Shader, keine gemischten Swizzle-Sets, Name-vs-Index-Adressierung.
Siehe Kapitel 4: OpenGL und Kapitel 6: Shaderprogrammierung.