|
|
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.
|
|
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.
|
|
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;
}
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 |
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 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);
|
|
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.
|
|
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.
|
|
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.
|
|
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 |
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 |
|
|
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 |
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.
|
|
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 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.

|
|
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.