GLSL Vertex Shader und Fragment Shader Grundlagen

Im heutigen Artikel werden wir uns mit den Grundlagen der GLSL Shader Programmierung vertraut machen. Ausgangspunkt für unsere Betrachtungen ist das OpenGL Tutorial Nr. 2. Ziel dieses Tutorials ist die Darstellung eines texturierten Vertexquads. Zugegeben, dass hört sich nicht nach besonders viel an, nichtsdestotrotz beinhaltet dieses Programmbeispiel alles, was zum Einstieg in die Shader Programmierung erforderlich ist.


Im Artikel GLSL Shader Framework Entwurf sind wir bereits auf die notwendigen Schritte eingegangen, um besagtes texturiertes Vertexquad mithilfe unseres Shader Frameworks zu rendern. Hier eine kurze Wiederholung:

// Den zu verwendenden Shader auswählen:
TestShader->Use_Shader();

// Matrix für die Transformation der Vertices an die korrekte
// Bildschirmposition (siehe TestShader.vert) übergeben:
TestShader->Set_ShaderMatrix4X4(&WorldViewProjectionMatrix,
                                "matWorldViewProjection");

// Hinweis:
// Die Namen der in den Shadern verwendeten Variablen sind in roter
// Schrift dargestellt!!

// Textur übergeben (siehe TestShader.frag):
TestShader->Set_Texture(GL_TEXTURE0, 0, pTexture, "TestTexture");

// Zusätzliche Farbe übergeben, die mit der Texturfarbe kombiniert
// werden soll (siehe TestShader.frag):
TestShader->Set_ShaderFloatVector4(&D3DXVECTOR4(1.5f, 1.5f, 1.5f, 1.0f),
                                   "AmbientColor");

// Vertexbuffer und Indexbuffer des zu rendernden Vertexquads binden:
glBindBuffer(GL_ARRAY_BUFFER, VertexBufferId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IndexBufferId);

// Vertex Attribute setzen, damit die Vertexdaten vom
// Vertex Shader korrekt verarbeitet werden können:
TestShader->Set_VertexAttributes();

// Draw Call – mit dem Rendern des Vertexquads beginnen:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);

// Arbeit mit dem Shader beenden:
TestShader->Stop_Using_Shader();


Auf den Source Code der zum Einsatz kommenden Shader sind wir bislang noch nicht zu sprechen gekommen. Dies werden wir an dieser Stelle nachholen. Betrachten wir zunächst den Vertex Shader (siehe Datei TestShader.vert im Verzeichnis Bin/Shader/):

// Shader Version festlegen:
#version 330

// Genauigkeit für Fließkomma-Berechnungen festlegen:
precision highp float;

// Shader Semantiken, dienen zur Idenifizierung der Vertex Attribute
// der im Vertexbuffer gespeicherten Daten:
#define ATTR_POSITION  0
#define ATTR_NORMAL    1
#define ATTR_TEXCOORD0 2
[...]

// Hinweis:
// Die Anzahl der benötigten Semantiken orientiert sich am verwendeten
// Vertexformat!


// Variablen zum Zugriff auf die im Vertexbuffer gespeicherten Daten:
layout(location = ATTR_POSITION)  in vec4 gs_Vertex;
layout(location = ATTR_NORMAL)    in vec3 gs_Normal;
layout(location = ATTR_TEXCOORD0) in vec4 gs_MultiTexCoord0;
[...]

// Texturkoordinaten für die Weiterleitung an den Fragment Shader
// (Rückgabewert der Vertex Shader Funktion):

out vec4 gs_TexCoord[1];

// Transformationsmatrix, ermöglicht die Transformation der
// Vertices in den Bildraum (Bildschirmposition):
uniform mat4 matWorldViewProjection;

void main()
{
    // Vertexposition im Bildraum berechnen:
    gl_Position    = matWorldViewProjection*gs_Vertex;
   
    // Hinweis:
    // gl_Position muss von uns nicht deklariert werden; es handelt sich
    // um eine sogenannte Build-In-Variable!

    // Texturkoordinaten für die spätere Verwendung im Fragment Shader:
    gs_TexCoord[0] = gs_MultiTexCoord0;
}

Kommen wir nun zum verwendeten Fragment Shader (siehe Datei TestShader.frag im Verzeichnis Bin/Shader/):

// Shader Version festlegen:
#version 330

// Genauigkeit für Fließkomma-Berechnungen festlegen:
precision highp float;

// Übernahme der vom Vertex Shader gelieferten Texturkoordinaten:
in vec4 gs_TexCoord[1];

// Hinweis:
// Der Vertex Shader liefert die Texturkoordinaten auf Vertexbasis.
// Mithilfe der vom Vertex Shader bereitgestellten Daten werden dann im
// Fragment Shader für jedes Pixel interpolierte Texturkoordinaten
// berechnet!

// die vom Fragment Shader berechnete Pixelfarbe
// (Rückgabewert der Fragment Shader Funktion):

out vec4 gs_FragColor;

// die zu verwendende Textur:
uniform sampler2D TestTexture;

// zusätzliche Farbe, die mit der Texturfarbe kombiniert werden soll:
uniform vec4 AmbientColor;

void main()
{
    // Texturfarbe ermitteln:
    vec4 TextureColor = texture(TestTexture, gs_TexCoord[0].st);

    // durchsichtige Texturbereiche nicht rendern:
    if(TextureColor.a < 0.01)
        discard; // Shader vorzeitig verlassen!!!

    // Hinweis:
    // Beim Laden einer Texturen wird für die transparenten Bereiche
    // ein Alpha-Wert von 0 festgelegt!

    // Rückgabewert des Fragment Shaders berechnen,
    // Texturfarbe und Zusatzfarbe multiplikativ verknüpfen:
    gs_FragColor = AmbientColor*TextureColor;

     // die Pixelfarbe berechnet sich hierbei wie folgt:
     // gs_FragColor = vec4(AmbientColor.r*TextureColor.r,
     //                     AmbientColor.g*TextureColor.g,
     //                     AmbientColor.b*TextureColor.b,
     //                     AmbientColor.a*TextureColor.a);

}


Dank der in den Source Code eingefügten Kommentare sollte sich die Arbeitsweise der hier vorgestellten Shader ohne weitere Schwierigkeiten nachvollziehen lassen. Für den Anfang ist es nicht zwingend erforderlich, gleich die komplette GLSL Shader Dokumentation durchzuarbeiten – auch wenn es natürlich nicht schaden kann (die aktuellen OpenGL und GLSL Spezifikationen finden sie auf opengl.org/registry/). Wichtige GLSL Funktionen (sogenannte Build-In-Funktionen) werden Sie in den folgenden Kapiteln erst dann kennenlernen, wenn wir sie auch wirklich benötigen. Dennoch ist es erforderlich, dass wir uns an dieser Stelle zumindest mit einigen der grundlegenden GLSL Konzepte vertraut machen.


wichtige Qualifizierer:

layout-Variablen ermöglichen den Zugriff auf die im Vertexbuffer gespeicherten Daten (Vertexposition, Texturkoordinaten, usw.).

uniform-Variablen dienen zum Speichern von Variablen, die von einer OpenGL Anwendung an ein Shader Programm übergeben werden.

Beispiele:
Transformationsmatrizen, Texturen, Farbwerte, Beleuchtungsvektoren, usw.

out-Variablen dienen zum Speichern von Rückgabewerten eines Shaders.

Beispiele:
Im Vertex Shader berechnete Texturkoordinaten, die im Fragment Shader berechnete Pixelfarbe usw.

in-Variablen dienen zur Parameterübergabe an eine Funktion oder einen Shader (von einem anderen Shader Bsp. Vertex Shader -> Fragment Shader).

Beispiel:
Der Fragment Shader benötigt als Parameter die vom Vertex Shader bereitgestellten Texturkoordinaten. Mithilfe dieser Daten werden im Fragment Shader für jedes Pixel interpolierte Texturkoordinaten berechnet, um den korrekten Farbwert aus der Textur auslesen zu können.

Hinweis:
out-Variablen werden im Vertex Shader für jeden Vertex zurückgegeben. in-Variablen des Fragment Shaders werden für jedes Pixel mittels Interpolation aus den out-Variablen des Vertex Shaders berechnet, sofern beide Variablen den gleichen Namen tragen.

inout-Variablen dienen gleichsam als Parameter und Rückgabewerte einer Funktion.

Sampler Objekte:

Sampler Objekte dienen zur Verarbeitung von Texturen. „normale“ Texturen (2D-Texturen) sind vom Datentyp sampler2D, Cube Maps vom Typ samplerCube.


wichtige Datentypen:

int, uint (vorzeichenlose Ganzzahl), float, bool: für einzelne Variablen oder Arrays

Beispiele:
float tempDot;
float ValueArray[2];

Vektoren:

vec2, vec3, vec4: zwei-, drei- sowie vierdimensionale Vektoren vom Typ float
ivec2, ivec3, ivec4: zwei-, drei- sowie vierdimensionale Vektoren vom Typ int
uvec2, uvec3, uvec4: zwei-, drei- sowie vierdimensionale Vektoren vom Typ uint
bvec2, bvec3, bvec4: zwei-, drei- sowie vierdimensionale Vektoren vom Typ bool

Für den Zugriff auf die einzelnen Vektorkomponenten stehen mehrere Möglichkeiten zur Auswahl:

Vector.x, Vector.y, Vector.z, Vector.w
Vector.r, Vector.g, Vector.b, Vector.a
Vector.s, Vector.t, Vector.p, Vector.q
Vector[0], Vector[1], Vector[2], Vector[3]

Auch für die Wertübergabe gibt es mehrere Möglichkeiten. Hier einige Beispiele:

vec4 Vector;

// allen Vektorkomponenten wird ein Wert zugewiesen:
Vector = vec4(1.0, 1.0, 1.0, 1.0);

// einer einzelnen Komponente wird ein Wert zugewiesen:
Vector.x = 5.0;

// zwei der Komponenten wird ein neuer Wert zugewiesen:
Vector.xz = vec2(1.0, 5.0);

Zugriff auf einzelne bzw. mehrere Vektorkomponenten ist wie folgt möglich:

// Texturfarbe ermitteln, es werden hierfür lediglich die ersten
// zwei Komponenten des gs_TexCoord-Vektors benötigt:
vec4 TextureColor = texture(TestTexture, gs_TexCoord[0].st);

Beim Zugriff auf die einzelnen Vektorkomponenten ist die Zugriffsreihenfolge frei wählbar. Betrachten wir als Beispiel die Berechnung der Pixelfarbe. Die folgende Codezeile:

gs_FragColor = AmbientColor*TextureColor;

ist gleichbedeutetend mit:

gs_FragColor = AmbientColor.rgba * TextureColor.rgba;

Ändert man nun die Zugriffsreihenfolge bei beiden Farbvektoren wie folgt:

gs_FragColor = AmbientColor.grba * TextureColor.grba;

dann sind bei der resultierenden Pixelfarbe rot und grün vertauscht!


Matrizen:

Syntax für quadratische Matrizen:
mat2, mat3, mat4: 2x2-, 3x3- sowie 4x4-Matrizen

Matrizen lassen sich zudem mit der genauen Anzahl ihrer Spalten- und Zeilenelemente deklarieren. Dies ermöglicht auch die Verwendung von nicht quadratischen Matrizen:

Beispiele:
mat4x4: (äquivalent zu mat4) 4 Spalten und 4 Zeilen
mat3x2: 3 Spalten und 2 Zeilen

Zugriff auf einzelne Matrixelemente erhält man wie folgt:

Matrix[SpaltenNr][ZeilenNr]

Die Wertzuweisung kann elementweise erfolgen:

mat3 matrix;
matrix[0][0] = 1.0;

usw.

mat3x2 matrix;
matrix = mat3x2(0.0, 0.0,  /*Spalte 1*/
                0.0, 0.5,  /*Spalte 2*/
                0.0, 1.0); /*Spalte 3*/

oder spaltenweise mithilfe von entsprechend dimensionierten Vektoren:

vec2 Spalte1 = vec2(0.0, 0.0);
vec2 Spalte2 = vec2(0.0, 0.5);
vec2 Spalte3 = vec2(0.0, 1.0);

mat3x2 matrix;
matrix = mat3x2(Spalte1,
                Spalte2,
                Spalte3);

bzw.

mat3x2 matrix;

matrix[0] = Spalte1;
matrix[1] = Spalte2;
matrix[2] = Spalte3;


Strukturen

Falls notwendig lassen sich auch eigene Strukturen deklarieren:

struct LightProperties
{
    vec4 Color;
    vec3 Direction;
    vec3 Position;
};

LightProperties Light[2];
Light[0].Color = vec4(2.0, 0.5, 1.0, 1.0);


Funktionen (benutzerdefiniert)

Müssen umfangreichere Berechnungen im einem Shader durchgeführt werden, dann bietet sich die Verwendung von benutzerdefinierten Funktionen an.

Beispiel:

vec4 GetTextureColor(sampler2D TextureName, vec4 TextureCoord)
{
    return texture2D(TextureName, TextureCoord.st);
}


Schleifen

GLSL unterstützt die aus C/C++ Programmen bekannten Schleifenkonstrukte for, while sowie do while.


Kontrollstrukturen

GLSL unterstützt die aus C/C++ Programmen bekannten Kontrollstrukturen if, else if, else sowie switch.