Lektion 3: Gouraud-Shading

Diese Lektion befasst sich mit einer recht einfachen und doch effektiven Beleuchtungstechnik aus der 3D-Welt: dem Gouraud-Shading. Sie erläutert die Mathematik hinter dem Verfahren (keine Angst, es wird nicht viel :) und macht einen kurzen Anriss von Objektmaterialien in OpenGL. Letztendlich soll dann versucht werden, selbst das Gouraud-Shading in Form eines Vertexshaders zu implementieren.


 1. Gouraud-Shading

Gourad-Shading ist ein spezielles Rendering-Verfahren. Beim Gouraud Shading wird lediglich die Farbe der Vertizes einer Primitive berechnet, diese kann z.B. vom Material und auch von der Beleuchtung abhängen. Nach der Projektion der Vertizes werden die einzelnen Fragmente eingefärbt, indem zwischen den Farben der Eckpunkte des Polygons interpoliert wird. OpenGL setzt dieses Verfahren bei Benutzung der festen Funktionalität ein wenn glShadeModel auf GL_SMOOTH gesetzt wurde. Wir werden dieses Verfahren im Laufe dieser Lektion jedoch mit einem Shader umsetzen.


 Wichtige GLSL Variablen

In welcher Farbe ein Objekt einem Betrachter erscheint hängt in der Realität von sehr vielen Faktoren ab. Um die ganze Angelegenheit nicht allzu kompliziert zu machen, wollen wir uns auf einige wenige beschränken:

Faktor GLSL Entsprechung
 Die Farbe des Objektes  gl_FrontMaterial.diffuse
 Die Farbe des Lichtes  gl_LightSource[0].diffuse
 Die Position des Lichtes  gl_LightSource[0].position
Tabelle 1: Faktoren der Farbberechnung und deren GLSL Entsprechungen


Wie man der Tabelle entnehmen kann, kann in einem GLSL-Shader auf das aktuelle Material über die vordefinierte Uniform Variable gl_FrontMaterial zugreifen. Genauer gesagt ist dieses das Material der Vorderseite der Faces, analog dazu existiert auch die vordefinierte Variable gl_BackMaterial über die auf das Material der Rückseite der Faces zugegriffen werden kann. Da wir aber ohnehin nur die Faces von der Vorderseite rendern interessiert uns dies nicht weiter.

Über die Material Variablen kann man grundsätzlich auf alle Materialeigenschaften zugreifen, die ein OpenGL-Material hat:

Name  Datentyp  Beschreibung
 gl_FrontMaterial.diffuse  vec4  Der diffuse Lichtanteil, der vom Material reflektiert wird
 gl_FrontMaterial.ambient  vec4  Der ambiente Lichtanteil, der vom Material reflektiert wird 
 gl_FrontMaterial.specular  vec4  Der Glanzlichtanteil, der vom Material reflektiert wird
 gl_FrontMaterial.emission  vec4  Der Lichtanteil, der vom Material ausgeht
 gl_FrontMaterial.shininess  float  Der Glanzlichtexponent des Materials
Tabelle 2: Die Elemente von gl_FrontMaterial und ihre Bedeutung

Auf sämtliche Lichtquellen kann über das vordefinierte Array gl_LightSource[] zugegriffen werden. Auch hier sind neben diffuse sämtliche anderen Eigenschaften einer Lichtquelle über GLSL zugreifbar.

Diese Angaben dienen nur zur Information, in dieser Lektion werden wir uns auf die in Tabelle 1 angegebenen Faktoren beschränken.


 Die Mathematik der Lichtberechnung

Bei der Bestimmung der Färbung einer von einer Lichtquelle beleuchteten Oberfläche, geht man davon aus, dass die Intensität proportional zur diffusen Komponente des Materials und zur diffusen Komponente des Lichtes ist. Außerdem ist die Intensität proportional zum Winkel zwischen dem Lichtvektor und der Normalen der Oberfläche. Demnach ergibt sich folgende Formel:


Formel 1: Berechnung des diffusen Terms

Diese Berechnung muss also in einem Gouraud-Shader für jede Vertex durchgeführt werden. Die Bestimmung der ersten beiden Komponenten der Formel ist dabei trivial: Ld entspricht gl_LightSource[0].diffuse und Md entspricht gl_FrontMaterial.diffuse.

Um die dritte Komponente zu bestimmen, muss man den Cosinus des Winkels zwischen Lichtvektor und Normale berechnen. Zum Glück kommt uns OpenGL da sehr entgegen, da es dem Shader für jede Vertex auch ihre Normale (gl_Normal im Vertexshader) übergibt. Der Cosinus des Winkels zwischen Normale und Lichtvektor lässt sich dann berechnen, indem man das Skalarprodukt des Normalenvektors und des Lichtvektors bildet. Hierfür müssen beide Vektoren die Länge 1 besitzen, also normalisiert sein. In GLSL heißt die Funktion zum Berechnen des Skalarproduktes dot(vec1, vec2) (vom englischen dot-product).

Die Folgende Graphik veranschaulicht den oben beschriebenen Vorgang:


Abbildung 1: Veranschaulichung des Skalarproduktes

 Die gl_NormalMatrix

Bei der Berechnung des Skalarproduktes mittels der Normalen muss einiges beachtet werden. Das Skalarprodukt kann nur funktionieren, wenn sich sowohl Normale als auch Lichtvektor im selben Korrdinatensystem befinden. Der Lichtvektor befindet sich im "Eye Space", das ist das Korrdinatensystem in dem die Szene von der Kamera (oder vom Auge) aus betrachtet wird. Die Normale hingegen befindet sich im lokalen Koordinatensystem ihres Modells. Wir müssen also einen der beiden Vektoren transformieren. Da es sich als umständlich erweisen wird den Lichtvektor in alle möglichen lokalen Modellkoordinatensysteme zu transformieren, entscheiden wir uns für die Normale. Da wir in einem Shader alles selbst in die Hand nehmen müssen, müssen wir auch diese Aufgabe selbst erledigen.

Die erste Idee, die man haben könnte ist die Normale mit der gl_ModelViewMatrix zu transformieren. Schließlich ist dies die Matrix die eine Vertex in den "Eye Space" transformiert. Dieses muss aber fehlschlagen, da die Normale nur drei Komponenten hat und die gl_ModelViewMatrix eine 4x4 Matrix ist. Die Reduzierung der gl_ModelVieMatrix auf ihre oberen 3x3 Komponenten ist eine Lösung die häufig funktioniert in einigen Fällen aber fehlschlägt (aus Gründen auf die wir hier nicht näher eingehen können). Um diesem Dilemma ein Ende zu bereiten bietet uns GLSL die gl_NormalMatrix, eine 3x3 Matrix, die die Normale in den "Eye Space" transformiert. Da diese Transformation die Länge der Normalen verändern kann muss sie danach wieder normalisiert werden. Die GLSL Funktion hierfür ist normalize(). Der vollständige Code zum transformieren der Normalen heißt also:


	vec3 Normal = normalize(gl_NormalMatrix * gl_Normal);


 2. Der Shader

Wie Eingangs bereits erwähnt, soll nun ein Gouraud-Shader programmiert werden.


 Aufgabe

Es soll das oben beschriebene Gouraud-Shading umgesetzt werden. Dabei müssen nur die diffusen Komponenten von Licht und Material beachtet werden. Der Vertexshader muss also neben der Transformation, die bereits aus Lektion 2 bekannt ist, einen Farbwert berechnen.

Der Pixelshader ist bereits vorgegeben und muss nicht weiter verändert werden, ein Blick in den Code schadet natürlich dennoch nicht. Er gibt nur die vom Vertexshader berechnete Farbe aus. Die für das Gouraudshading notwendige Interpolation wird automatisch von der Hardware erledigt.


Abbildung 2: Mögliches Ergebnis des Gouraud-Shaders


 Tipps