Lektion 2: Der erste Shader

In dieser Lektion wollen wir unsere ersten Gehversuche im Programmieren eigener Shader wagen und uns in kleinen Schritten der Shadersprache GLSL zuwenden, ihre Datentypen und Konstrukte kennenlernen sowie einen Minimalshader programmieren. Dieser kann dann auch sogleich implementiert und ausprobiert werden.


 1. GLSL - Die GL Shading Language

Mussten die ersten Shader noch von Hand in Assembler geschrieben werden, so gibt es mittlerweile eine Handvoll Hochsprachen die das Programmieren von Shadern übersichtlicher und einfacher gestalten. Neben CG von Nvidia und HLSL von Microsoft gibt es seit einiger Zeit nun auch die GLSL vom GL ARB (Architectures Review Board). Sie ist stark an C gelehnt und ein fester Bestandteil von OpenGL 2.0. Das hat den großen Vorteil, dass man keinen externen Compiler benötigt sondern der Shadercode direkt von OpenGL übersetzt wird.


 Ein typischer Shader

Folgender Code beschreibt einen typischen GLSL Vertexshader der in einer Datei abgelegt ist (Vertexshader haben im Allgemeinen die Endung .vert, Pixelshader die Endung .frag):


[flat_wave.vert]
	uniform float time;

	void main(void)
	{
		vec4 v = vec4(gl_Vertex);
		v.z = sin(5.0 * v.x + time * 0.01) * 0.25;
		gl_Position = gl_ModelViewProjectionMatrix * v;
	}

Listing 1: Ein animierender Vertexshader


Gut zu sehen ist hierbei die starke Ähnlichkeit mit C. Jeder Shader muss zunächst einmal eine Funktion void main(void) enthalten, die weder einen Rückgabewert besitzt noch einen Parameter übergeben bekommt.

Innerhalb des Shaders können nun auf verschiedenste Weise Berechnungen angestellt und Variablen gelesen oder geschrieben werden. Neben den bekannten Standardtypen bool, int und float existieren noch weitere Typen für Vektoren, Matrizen und Texturen:

Datentyp Beschreibung
 vec2, vec3, vec4  Floatvector mit 2, 3 oder 4 Elementen
 ivec2, ivec3, ivec4  Integervector mit 2, 3 oder 4 Elementen
 bvec2, bvec3, bvec4  Boolvector mit 2, 3 oder 4 Elementen
 mat2, mat3, mat4  Floatmatrix mit 2x2, 3x3 oder 4x4 Elementen
 sampler1D, sampler2D,
 sampler3D
 1D-, 2D-, 3D-Textur
 samplerCube  Cubemap-Textur
Tabelle 1: Neue Datentypen von GLSL

Das Arbeiten mit diesen neuen Datentypen ist ein wenig über die Einschränkungen von C hinaus gegangen und sehr intuitiv gestaltet. Zum Beispiel können Matrizen problemlos mit Vektoren oder anderen Matrizen multipliziert werden. Auch der Umgang mit Vektoren wird stark vereinfacht.


 Vektoren

Vektoren werden über Konstruktoren gefüllt, die sowohl einzelne Werte (Skalare), Vektoren oder eine Kombination aus beiden aktzeptieren. Gefüllt werden die Vektoren dann der Reihe nach mit den Werten der übergebenen Skalaren und/oder Vektoren. Wird ein Vektor mit nur einem Skalar initialisiert so erhalten alle Felder diesen Wert.

Angesprochen werden die Felder des Vektors entweder über den altbekannten Klammeroperator [] oder über den Punktoperator. Jeweils vom Anwendungsfall abhängig kann der Entwickler hierbei die Felder über die Aliasnamen .r, .g, .b oder .a für Farben (aufsteigend in dieser Reihenfolge), .x, .y, .z, .w für Raumkoordinaten oder .s, .t, .p, .q für Texturkoordinaten auswählen. Sogar das Ansprechen mehrerer Felder gleichzeitig ist über eine Folge von Buchstaben möglich. Der folgende Code fasst die eben besprochenen Fakten nocheinmal zusammen:


	vec3 position = vec3(1.0, 2.0, 3.0);
	vec4 color = vec4(position, 3.0);
	vec4 white = vec4(1.0);
	
	float xPos = position[0];
	float yPos = position.y;
	vec2 redalpha = color.ra;
	vec2 doublePos = position.xyxy;
	color.r = 0.75;
		
	struct dirlight {
		vec3 direction;
		vec3 color;
	};
	
	dirlight d1;
	dirlight d2 = dirlight(vec3(1.0,1.0,0.0), vec3(0.8,0.8,0.4));
	d2.direction = vec3(gl_LightSource[0].position);

Listing 2: Beispielvektoren in GLSL


 Kontrollstrukturen

Innerhalb des Shaders können mit Ausnahme von switch die gängigen Kontrollstrukturen wie if und else, for und while verwendet werden.
Bei while-Schleifen muss jedoch bei älteren Grafikkarten darauf geachtet werden, dass diese vollständig ausrollbar sein müssen. Vollständig ausrollbar ist eine Schleife, wenn die Anzahl ihrer Wiederholungen vor der Laufzeit bestimmt werden kann, sie also nicht von Variablen abhängt deren Werte sich während der Laufzeit beliebig ändern können.


 Vordefinierte Funktionen

GLSL bietet dem Entwickler eine ganze Reihe vordefinierter Funktionen. Neben den trigonometrischen Funktionen sin(float) und cos(float) gibt es auch Funktionen aus dem Bereich der linearen Algebra wie zum beispiel dot(float).
Eine spezielle Funktion die hier genannt werden sollte ist ftransform(). Sie gewährleistet in einem Vertexshader das selbe Ergebnis zu liefern wie die Vertextransformation der festen Funktionalität. Auch greift sie dabei auf die selben Optimierungen zurück wie die feste Funktionalität auf Stufe der Vertextransformation.


 Kommunikation zwischen Anwendung und Shader

Anwendungen haben mehrere Möglichkeiten mit einem Shader zu kommunizieren. Diese Kommunikation ist jedoch nur in eine Richtung möglich, da die Ausgabe eines Shaders sich auf Ziele wie den Color- oder Depthbuffer beschränkt.

Wie in Lektion 1 bereits erwähnt, können z.B. Shader die OpenGL States auslesen. Dies kann bei OpenGL Lichtquellen oder der Nebelfarbe durchaus Sinn machen doch ist vollständig kontraintuitiv, wenn man beginnt z.B. nicht benutzte Felder von Lichtquellen für die Werteübergabe zu benutzen. Auch können Shader auf Texturen zugreifen. Interpretiert man nun die Texturdaten nicht als Farbwerte sondern als allgemeine Datenfelder, so ist wieder eine Möglichkeit der Kommunikation geschaffen, die auf neueren Grafikkarten sogar zwischen den Shadern funktioniert.

Zum Glück erlaubt GLSL das Definieren von benutzerdefinierten Variablen zur Kommunikation zwischen Anwendung und Shader über die Qualifier uniform und attribute. Auf Shadervariablen die diese Qualifier benutzen kann der Shader nur lesend zugreifen. Sie können jedoch von der Anwendung frei Werte zugewiesen bekommen, wie zum Beispiel eine Laufvariable über die Zeit, die dem Shader animierte Effekte ermöglichet.

Uniform Variablen kann man wie globale Variablen sehen die konstant für eine Primitive oder eine ganze Szene sind. Sie können die vom Shader unterstützten Datentypen verwenden und jederzeit von der Anwendung verändert und sowohl von Vertex- als auch Pixelshadern gelesen werden. Im Gegensatz zu Attribute Variablen können sie nicht zwischen einem glBegin() und einem glEnd() gesetzt werden.

Attribute Variablen können im Gegesatz zu Uniform Variablen nur zwischen glBegin() und glEnd() gesetzt werden. Sie bieten die Möglichkeit Vertizes neue Attribute hinzuzufügen wie zum Beispiel Wärme oder Gewicht. Da sie sich eh nur auf Vertizes beschränken können sie von einem Pixelshader nicht ausgelesen werden.

Wir werden in den folgenden Lektionen nur mit Uniform Variablen arbeiten, der Vollständigkeit halber sollten Attribute Variablen jedoch erwähnt worden sein.


 Kommunikation zwischen OpenGL und Shader

Die Werteübergabe zwischen OpenGL und einem Shader erfolgt über einen Satz vorgegebner globaler Variablen, die Shader lesen und zum Teil auch schreiben können:

Variablenname  Datentyp  Beschreibung
Lesbar in Vertexshadern (Vertexattribute)
 gl_Color  vec4  Erste Zeichenfarbe
 gl_SecondaryColor  vec4  Zweite Zeichenfarbe
 gl_Normal  vec3  Normalenvektor einer Vertex
 gl_Vertex  vec4  Ortsvektor einer Vertex
 gl_MultiTexCoord[0-7]  vec4  Texturkoordinaten der acht Textureinheiten
Schreibbar in Vertexshadern
 gl_Position  vec4  Finale Position der Vertex auf dem Schirm
 gl_PointSize  float  Pixelgröße der Vertex
Lesbar in Fragmentshadern
 gl_FragCoord  vec4  Position des Fragments auf dem Schirm
 gl_FrontFacing  bool  Zeigt die Primitive des Fragments nach vorne?
Schreibbar in Fragmentshadern
 gl_FragColor  vec4  Endgültiger Farbwert des Fragments
 gl_FragDepth  float  Überschreibt den berechneten Z-Wert des Fragments 
Tabelle 2: Globale Variablen und Attribute in Shadern

Die Variablen gl_Position und gl_FragColor spielen hierbei eine der zentralsten Rollen. Sie stellen praktisch die Ergebnisse der einzelnen Shader dar. gl_Position muss (!) von jedem Vertexshader gesetzt werden. Ob der gesetzte Wert sinnvoll ist oder nicht spielt dabei keine Rolle und liegt in der Hand des Entwicklers. Ähnlich verhält es sich mit gl_FragColor, wobei dies das Pendant des Pixelshaders ist und die endgültige Farbe des Fragments angibt.

Zudem existiert auch noch eine Reihe eingebauter Uniform Variablen, die vor allem Zugriff auf die verschiedenen Matrizen von OpenGL ermöglichen:

Uniformname  Datentyp  Beschreibung
 gl_ModelViewMatrix  mat4  Verschiebung, Drehung und Skallierung eines Models
 gl_ProjectionMatrix  mat4  Transformation von Welt- zu Bildschirmkoordinaten
 gl_ModelViewProjectionMatrix  mat4  Das Ergebnis der Multiplikation der beiden Matrizen
 gl_NormalMatrix  mat3  Gesonderte Transformationsmatrix für Normalvektoren 
Tabelle 3: Eingebaute Uniform Variablen in Shadern


 Kommunikation zwischen Shader und Shader

Für die Kommunikation der Shader untereinander gibt es den Qualifier varying. Variablen die damit gekennzeichnet werden müssen mit gleichen Namen in beiden Shadern global deklariert werden. Die im Vertexshader zugewiesenen Werte werden von OpenGL automatisch zwischen den Vertices der die Primitive bildenen Vertices interpoliert und an den Fragmentshader übergeben. Dass diese Interpolation nötig ist wird deutlich, wenn man sich vorstellt, dass z.B. eine Dreiecksprimitive aus nur drei Vertices, jedoch aus tausenden von Fragmenten bestehen könnte. Drei Aufrufen des Vertexshaders folgen also u.U. tausende von Aufrufen des Fragmentshaders.
Neben den selbst definierbaren varying Variablen bietet GLSL auch eine Reihe vordefinierter varying Variablen für die Kommunikation zwischen den Shadern:

Varyingname  Datentyp  Beschreibung
Nur im Vertexshader (schreibend)
 gl_FrontColor  vec4  Farbe der Vorderseite der Vertex
 gl_BackColor  vec4  Farbe der Rückseite der Vertex
 gl_FrontSecondaryColor  vec4  Zweite Farbe der Rückseite der Vertex
 gl_BackSecondaryColor  vec4  Zweite Farbe der Rückseite der Vertex
Nur im Fragmentshader (lesend)
 gl_Color  vec4  Erste Zeichenfarbe
 gl_SecondaryColor  vec4  Zweite Zeichenfarbe
In beiden Shadern
 gl_TexCoord[]  vec4  Texturkoordinaten der jeweiligen Textureinheit 
Tabelle 4: Vordefinierte varying Variablen


Mit Ausnahme von gl_TexCoord[] können all diese Variablen ohne vorangehende Deklaration genutzt werden. gl_TexCoord benötigt in beiden Shadern eine Deklaration mit derselben Anzahl von Elementen.

Die Varyings gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor und gl_BackSecondaryColor können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertexshaders im Fragmentshader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.


 2. Der erste Shader

Nachdem wir nun einen Blick auf Shader allgemein und auf die Shadersprache GLSL geworfen haben wird es Zeit für die ersten praktischen Gehversuche.


 Aufgabe

Aufgabe ist es, einen Minimalshader zu schreiben der nichts anderes tut als ein Model mit einer festen Farbe einzufärben. Beleuchtung oder Texturierung soll vernachlässigt werden. Hierfür ist sowohl ein Vertex- als auch ein Pixelshader nötig. Während der Vertexshader nichts anderes tun sollte, als die eingehenden Vertexdaten zu transformieren, färbt der Fragmentshader die eingehenden Fragmente mit einer konstanten Farbe ein.


Abbildung 1: Mögliches Ergebnis des Minimalshaders


 Tipps

Wie bereits erwähnt, ist der Vertexshader für das Transformieren der Vertices verantwortlich. Dies tut er durch das Schreiben der Variable gl_Position.
In der festen Pipeline wird die transformierte Position einer Vertex durch das Produkt von ProjectionMatrix, ModelviewMatrix und der Position der Vertex errechnet. Diese Rechnung sollte innerhalb des Vertexshaders nachgebildet werden. Die dafür notwendigen Variablen werden von GLSL bereitgestellt und finden sich im ersten Teil dieser Lektion.

Der Pixelshader sollte für alle Fragmente für die er aufgerufen wird hingegen einfach nur eine feste RGBA-Farbe vom Typ vec4 ausgeben.