CGExercises

CG Exercise #08

Computergraphik Ãœbungsblatt #08


OpenGL Tutorial: Shader



Aufgabe 8.1a: Shader Editor

Starten Sie mit Ihrem Code aus der vorhergehenden Ãœbung #07.

Ergänzen Sie den Code um einen Shader bzw. bauen Sie in Ihren Code einen Shader-Editor ein. Dazu benötigen Sie die folgenden zusätzliche Zeilen:

Im Globalen Namensraum:

GLuint prog_id;
const char shader[] = "";

In der Methode initializeOpenGL():

prog_id = lglCompileGLSLProgram(shader);
create_lgl_Qt_ShaderEditor("shader", &prog_id);

In der Methode renderOpenGL():

lglUseProgram(prog_id, false);

Sie sollten nun eine unbeleuchtete (und untexturierte) Szene sehen. Der Vertex Shader multipliziert nur die Vertices mit der MVP (die mit lglModelView gesetzt wurde). Und der Fragment Shader verwendet nur die aktuell spezifizierte Farbe (die zuletzt mit lglColor gesetzt wurde).

D.h. es wird der einfachste denkbare Shader - der sog. Plain-Shader - verwendet (siehe Cheat Sheet → Shaders).

Im Folgenden werden wir nun Schritt für Schritt diesen Shader weiterentwickeln, so dass die Szene wieder beleuchtet dargestellt wird. Editieren des Shaders im Editor Fenster erfolgt wie folgt:

  • Der aktuell editierte Shader wird benutzt
  • Save & Quit merkt sich den aktuellen Shader
  • Commit → Abspeichern des Shaders in den C++ Code

Zur Erinnerung vorher noch ein paar Verständnisfragen:

  • Welche Daten werden im Vertex- bzw. Fragment-Shader verarbeitet?
  • In welchem Koordinatensystem befindet man sich im Vertex- bzw. Fragment-Shader?
  • Wo findet man den Vertex- bzw. Fragment-Shader im GLSL Shader-Editor?

Materialien:

Aufgabe 8.1b: GLSL Shader Laden (alternativ zu 8.1a)

Anstelle den Shader Editor zu benutzen kann man auch einfach einen GLSL Shader aus einer Datei laden:

  • Datei “shader.glsl” erstellen mit Inhalt:
#version 120
attribute vec4 vertex_position;
uniform mat4 mvp;
void main()
{
   gl_Position = mvp * vertex_position;
}
---
#version 120
uniform vec4 color;
void main()
{
   gl_FragColor = color;
}
  • Shader Id als Instanzvariable deklarieren:
GLuint prog_id = 0;
  • GLSL Shader in initializeOpenGL() laden und übersetzen:
prog_id = lglLoadGLSLProgram("shader.glsl");
  • Und schließlich den Shader in renderOpenGL() aktivieren:
lglUseProgram(prog_id, false);

Aufgabe 8.2: GLSL Fog


Non-Linear Fog

Wir steigen mit einem weiteren einfachen GLSL Shader ein: Fogging! D.h. verwenden Sie den folgenden Fragment Shader:

#version 120
uniform vec4 color; // kommt aus dem Hauptprogramm (via lglColor)
void main()
{
   const float density = 0.01f;
   float z = 1.0f / gl_FragCoord.w;
   float f = 1.0f - exp(-3 * density * z*z*z);
   gl_FragColor = (1.0f-f)*color + f*vec4(1);
}

Ändern Sie die Hintergrundfarbe, damit diese zur Darstellung durch den Shader passt. Ändern Sie die Dichte density so dass sie einen passenden Farbverlauf erhalten. Der passende Wert für die Dichte liegt je nach Szenenaufbau zwischen 0.1 und 1E-10.

Aufgabe 8.3: GLSL Uniform


glVertex Cheat Sheet

Ersetzen Sie die Konstante density im GLSL Shader durch einen uniformen Parameter, den Sie im C++ Hauptprogramm bestimmen können (via lglUniformf(“density”, value)). Verwenden Sie wie in Ãœbung #06 passende Tasten, um den Parameter zu verändern.

Aufgabe 8.4: GLSL Varying

Momentan wird die Szene noch einfarbig pro Objekt gezeichnet, d.h. es gibt keinerlei Farbvariation. Diese bringen wir jetzt ins Spiel:

Transportieren Sie die Vertices aus dem Vertex- in den Fragment-Shader, indem Sie die Koordinaten im Vertex-Shader zusätzlich in ein sog. Varying schreiben. Wir verwenden dazu die unveränderten Objekt-Koordinaten, d.h. es wird in diesem Fall nicht mit der MVP multipliziert!


Objekt-Koordinaten als Fragment-Farbe

Diese Objekt-Koordinaten lesen wir nun im Fragment-Shader aus dem Varying aus und verwenden sie als Fragment-Farbe.

Eventuell müssen die Farbwerte etwas angepasst werden, um eine leicht psychedelische Darstellung (wie rechts beispielhaft abgebildet) zu erreichen. Dazu multipliziert man die einzelnen Komponenten mit einer Konstante und verwendet nur den Nachkommaanteil (GLSL: fract).

Hinweis: Für diese und die folgenden Aufgaben sind keine weiteren Änderungen am C++ Programm erforderlich, lediglich der GLSL Shader ist anzupassen.

Aufgabe 8.5: GLSL Attribute (Normalen)

Transportieren Sie nun nicht nur die Vertices sondern auch die Normalen aus dem Vertex- in den Fragment-Shader. D.h. schreiben Sie das Attribut vertex_normal in ein zusätzliches Varying.

Aufgabe 8.6: GLSL “Debugging”

Jetzt wird es noch psychedelischer: Verwenden Sie die Normalen für die Fragment-Farbe. Aber nicht die tatsächlichen Normalen, sondern wir verwenden die folgende Abbildung:

$f(\vec{n}) = 0.5\cdot\vec{n} + (0.5, 0.5, 0.5)^T$

Auf diese Weise können wir die Normalen “lesen” und Fehler erkennen → Graphisches Debugging.

Wir erkennen dadurch, dass die Normalen nicht konsistent zwischen den einzelnen Robotersegmenten sind: Welche Normale bzw. welche Farbe müsste eine Fläche tatsächlich haben, auf die der Betrachter frontal schaut? Bestimmen Sie den konkreten RGB-Farbwert! Tipp: himmelblau.

Aufgabe 8.7: GLSL Normalentransformation

Die Normalen müssen im Vertex-Shader mit einer speziellen Matrix transformiert (bzw. multipliziert) werden! Verwenden Sie diese Matrix nun (siehe Cheat Sheet → GLSL)! Dadurch werden die Normalen in Augenkoordinaten transformiert, so dass sie anschließend im Fragment-Shader auch in Augenkoordinaten vorliegen. Renormalisieren Sie die transformierten Normalen außerdem im Fragment-Shader (GLSL: normalize)!

Achtung: Für die Transformation der Normalen casten wir zuerst die Matrix auf 3×3 und multiplizieren dann mit dem Normalenvektor!

Aufgabe 8.8: GLSL Shading


Ambiente Beleuchtung

Diffuse Beleuchtung

Zum Abschluß können wir die Normalen nun für die Beleuchtung der Objekte verwenden. Die am einfachsten zu berechnende Beleuchtung ist die diffuse lokale Beleuchtung:

Die diffuse Beleuchtung verwendet den Cosinus des Zwischenwinkels von Lichtvektor $\vec{l}$ und Normale $\vec{n}$ als Beleuchtungsintensität bzw Helligkeit. Dazu muss das Skalarprodukt der Normalen mit dem Lichtvektor berechnet werden. Als Lichtvektor $\vec{l}$ wählen wir einen beliebigen konstanten 3D Vektor als Beleuchtungsrichtung für eine rein direktionale Lichtquelle. Diesen Vektor definieren wir als normalisierten Lichtrichtungsvektor im Fragment-Shader. Da die Normalen $\vec{n}$ im Fragment-Shader in Augenkoordinaten vorliegen, wird auch der Lichtrichtungsvektor $\vec{l}$ in Augenkoordinaten angegeben.

Das Ergebnis der obigen Beleuchtungsberechung ist die Reflektanz $r_d = \vec{n} \cdot \vec{l}$ des diffusen Anteils des lokalen Blinn-Phong Beleuchtungsmodells mit der Beleuchtungsintensität $I_d = r_dI_L$ ($I_L$ wird zu 1 angenommen). Mit dieser Reflektanz modulieren (d.h. multiplizieren) wir nun die bisherige Vertex-Farbe (color). Achtung: $r_d$ sollte nicht negativ werden (GLSL: max)!


Ambiente & Diffuse Beleuchtung

Verwenden Sie außerdem einen ambienten bzw. konstanten Beleuchtungsanteil $I_a = const$. Für einen warmen Beleuchtungston, wie er z.B. durch die Sonne erzeugt wird, verwendet man $I_a$ = (0.2, 0.1, 0). Dieser ambiente Anteil wird zum diffusen Beleuchtungsanteil hinzuaddiert.

Aufgabe 8.9: Toon Shading

Anstelle einer physikalisch motivierten, diffusen Beleuchtung erzeugen wir jetzt einen Comic-Look.

Beim so genannten Toon Shading (Cell bzw. Tone Shading oder eben auch Comic Shading) werden nur wenige Farben einer festen Farbpalette benutzt, um die Beleuchtungsintensität darzustellen.


Toon Shading

Benutzen Sie jetzt das Skalarprodukt (die vorher berechnete Reflektanz $r_d$), um anhand einer Fallunterscheidung (if-else-Kaskade) folgende Farben zu verwenden:

  1. (1.0 1.0 1.0) im Bereich [0.98 .. 1.0]
  2. (0.7 0.3 0.3) im Bereich [0.7 .. 0.98]
  3. (0.5 0.15 0.15) im Bereich [0.4 .. 0.7]
  4. (0.3 0.1 0.1) im Bereich [0 .. 0.4]
  5. (0.1 0.1 0.1) im Bereich [−1 .. 0]

Am Roboter ist das eventuell nicht so schön zu erkennen. Stellen Sie daher neben den Roboter noch einen Teapot (lglTeapot).


Hausaufgaben bis zum nächsten Praktikum


Musterlösung: Bitte stellen Sie Ihre Musterlösung aus dem Praktikum kurz vor.

1. OpenGL / GLSL:

  1. Skalieren Sie die Vertex-Position (das Attribut vertex_position) im Vertex-Shader auf ein Viertel seiner ursprünglichen Größe. Wie sieht die entsprechende Zuweisung zu gl_Position aus? Achtung: homogene Koordinaten!
  2. Mischen Sie zwei Farben, d.h. interpolieren Sie zwischen zwei Farbwerten im Fragment-Shader. Verwenden Sie eine lineare Interpolation mit einem normalisierten Interpolationsfaktor $w\in[0,1]$. Wie sieht die allgemeine Formel der lineraren Interpolation aus? Im Ergebnis soll die interpolierte Farbe zu $w=34.5$% Weiss und der restliche Farbanteil soll Rot sein. Wie sieht die entsprechende Zuweisung zu gl_FragColor aus?
  3. Optional: Mischen Sie im Fragment-Shader mehrere Farbwerte: Im Ergebnis soll die Mischfarbe 25% Weiss, 55% Rot und 20% Gelb enthalten (Tipp: Linearkombination). Wie sieht die entsprechende Zuweisung zu gl_FragColor aus?
  4. Optional: Angenommen, die Tiefe $z$ stehe im Fragment-Shader zur Verfügung und sei auf den Bereich [0,1] normiert. Wie erzeugen Sie durch eine entsprechende lineare Farbinterpolation einen Nebeleffekt? D.h. mit welchen zwei Farben interpolieren Sie (Formel)?
  5. Welches Attribut enthält die per-vertex spezifizierten Normalen?
  6. Mit welcher speziellen Matrix werden diese Normalen transformiert?

2. GLSL / Parameter:
Für welche der folgenden Werte macht es Sinn, ein Attribut, einen uniformen Parameter oder einen varying Parameter zu verwenden?

  • Normale
  • Konstanter Skalierungsfaktor
  • Eine Konstante für ein Beleuchtungsmodell
  • Vertexfarbe
  • interpolierte Vertexposition

3. Optional: GLSL / Funktionen:
Wie würden Sie die folgende GLSL-Funktion in C++ schreiben?

void swap(inout float a, inout float b)
{
  float t;
  t = a; a = b; b = t;
}
Warum müssen die Parameter mit inout deklariert werden?

4. GLSL / Konstruktoren & Swizzling:
Beschreiben die folgenden Konvertierungen mit einem einzigen Swizzling; dabei sei v4 ein 4er-Vektor, v2 ein 2er-Vektor, f ein float. Benutzen Sie ausschließlich die Swizzling-Komponenten x, y, z und w! Die erste Zeile dient als Beispiel.

    float a = float(v4); --> float a = v4.x;
    vec3 b = vec3(v4);
    vec3 c = vec3(v4.b, v4.g, v4.r);
    vec4 d = vec4(f, f, f, f);
    vec4 e = vec4(v2, v2);

Options: