CGExercises

CG Exercise #08

Computergraphik ├ťbungsblatt #08


OpenGL Tutorial: Shader



Aufgabe 8.1a: Shader Editor

Achtung: Wenn Sie Ubuntu in der VM verwenden, so ist Ubuntu 18.04 LTS empfohlen. ├ťberpr├╝fen Sie die aktuelle Ubuntu Version via “lsb_release -ds”. Ansonsten fahren Sie bitte mit Aufgabe 8.1b fort.

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 (Plan B)

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 neunten 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: