Seit DirectX 10 (genauer gesagt Direct3D 10) und OpenGL 3.2 sind Entwickler, die diese APIs nutzen wollen, verpflichtet, einen Vertex- und einen Fragment-Shader (Pixel-Shader in Microsoft-Sprache) zu schreiben. Das macht den Einstieg in die Grafikprogrammierung nicht unbedingt einfacher, aber da ein Shader nichts Magisches ist, sollte man damit leben können.
Shader sind Programme, die direkt auf der GPU der Grafikkarte laufen, also nicht auf der CPU. Es gibt dann noch weitere Shader, die man programmieren kann, wie z. B. Geometry-Shader oder Tesselation-Shader, welche hier aber nicht angesprochen werden. Da Vertex- und Fragment-Shader sowieso programmiert werden müssen, reicht es auch erstmal, dass man weiß, was diese Programme machen.
Erinnern Sie sich noch an die Grafik-Pipeline? Diese einzelnen Stufen sind die Shader, obwohl längst nicht alle Stufen programmierbar sind. Allgemein heißt das also, dass Shader immer Eingabedaten haben, damit etwas anstellen und diese anschließend in Ausgabevariablen zu speichern, welche dann von der nächsten Stufe der Grafik-Pipeline verarbeitet werden.
Ein Vertex-Shader bekommt als Eingabedaten genau einen Vertex. Das Format kann selbstständig an die eigenen Bedürfnisse festgelegt werden, also ob es dreidimensional ist oder noch zusätzliche Informationen außer der Position enthält. Dieser Vertex kann dann nach Belieben manipuliert werden, anschließend muss man als Ausgabeformat einen Vektor mit vier Komponenten angeben. In WebGL und älteren OpenGL-Versionen gibt es die feste Ausgabevariable gl_Position, in neueren OpenGL-Versionen benutzt man dafür die out-Variablen, die eigenständig definiert werden müssen.
Im letzten Tutorial sah der Code unseres Vertex-Shaders so aus:
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
In WebGL nennt man die Eingangsvariable auch Attribut. Hier befindet sich der Vertex a_position, ein Vektor mit drei Komponenten. In der main-Funktion geben wir der Ausgabevariable gl_Position einen neu erstellten Vektor mit vier Komponenten.
Im nächsten Tutorial lernen wir, wie man den Vertex-Shader für Transformationen benutzt.
Der Fragment-Shader braucht keine Eingabedaten, kann aber welche vom Vertex-Shader erhalten, sofern diese auch dort in einer Ausgabevariable gespeichert werden. Das Ausgabeformat ist wieder ein Vektor mit vier Komponenten, welche den RGBA-Anteil der Farbe eines Vertex angeben.
So sah der Code im letzten Tutorial aus:
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
Nicht großartig spektakulär, hier wird in der ersten Zeile die Präzision von Fließkommazahlen definiert und in der vierten Zeile eine weiße Farbe eines Vertex übergeben.
Uniforms sind globale Variablen, die in jedem Shader aufgerufen werden können. Sie dienen als eine Art Schnittstelle zwischen CPU und GPU. Das Datenformat kann hier nahezu beliebig gewählt werden (was nicht heißen soll, dass Strings übergeben werden können). Beispielsweise könnte man im normalen Programmcode eine Variable für eine Farbe haben und diese dann später dem Fragment-Shader übergeben.
Zuerst ändern wir unseren Fragment-Shader, indem wir eine Uniform definieren und diese der Ausgabevariable übergeben:
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
Dann definieren wir am Anfang unseres WebGL-Codes eine Farbe:
var redColor = [1.0, 0.0, 0.0, 1.0]; // RGBA
Wir speichern uns im WebGL-Code in der Initialisierungsphase nun in einer Variable die ID der Uniform, direkt nachdem wir das Shader-Programm erstellt haben:
var colorUniformLocation = gl.getUniformLocation(program, "u_color");
Jetzt können wir, kurz bevor glDrawArrays aufgerufen wird, die rote Farbe in der Uniform speichern:
gl.uniform4fv(colorUniformLocation, new Float32Array(redColor));
Diese Funktion muss immer genau dann aufgerufen werden, sobald ein Shader-Programm aktiviert wurde.