Lektion 1: Einführung

Bevor wir mit dem tatsächlichen Programmieren eigener Shader beginnen, wollen wir ersteinmal über die Grundlagen sprechen. Was sind eigentlich Shader, wo und wie kommen sie zum Einsatz und wie reihen sie sich in die OpenGL Pipeline ein. Das alles sind Fragen die in der folgenden, ersten Lektion erläutert werden sollen.


 1. Die OpenGL Pipeline

Um zu erläutern was Vertex- und Pixelshader sind, schauen wir uns einen Teil der OpenGL Renderpipeline etwas genauer an. Der hier betrachtete Teil bezieht sich auf die fest eingebaute Funktionalität von OpenGL. Das heißt die hier durchgeführten Berechnungen laufen verborgen vom Entwickler ab und können von ihm auch nicht manipuliert werden.


Abbildung 1: Exemplarischer Teil der OpenGL Pipeline

 Vertextransformation

Eine Vertex besitzt in diesem Kontext eine Menge von Attributen wie die Position im Raum, seine Farbe, Normale und Texturkoordinaten. Diese dienen hier als Eingabewerte. Operationen die hier durchgeführt werden sind zum Beispiel:


 Primitivenzusammensetzung und Rasterisierung

Eingabewerte dieser Stufe sind die Transformierten Vertizes sowie ihre Konnektivitätsinformationen, also wie einzelne Vertizes miteinander verbunden sind. Hier werden die geometrischen Primitiven erstellt und zusammengesetzt.

Die Rasterisierung bestimmt die Fragmente und die Pixelpositionen einer Primitive. Fragmente sind hier den späteren Pixeln auf dem Bildschirm schon sehr nahe, enthalten jedoch nicht nur einen Farbwert als Attribut sondern u.a. auch Normalen und Texturkoordinaten. Die tatsächliche Farbe des Pixels auf dem Bildschirm bestimmt sich erst später, wenn halbdurchsichtige Pixel, die vielleicht über diesem Fragment liegen auch mit in die Farbwertbestimmung eingeflossen sind. Sehen wir Fragmente also als eine erweiterte Vorstufe des späteren Pixels. Mehrere, übereinanderliegende Fragmente können später in die Berechnung der eigentlichen Pixelfarbe einfließen.

Die Ergebnisse dieser Stufe sind zweierlei:

Die Werte aus der Stufe der Vertextransformation kombiniert mit den Konnektivitätsinformationen der anderen Vertizes der Primitive erlauben dieser Stufe genaue Werte für die Attribute des Fragments zu berechnen. Beispielweise hat jede Vertex eine transformierte Position. Anhand der transformierten Positionen der weiteren Vertizes die die Primitive ausmachen kann die genaue Position des Fragmentes anhand der Abstände der Vertizes zum Fragment interpoliert werden. Das selbe geschieht für den Farbwert eines Fragments bei z.B. Dreiecken deren Vertizes verschiedene Farben besitzen.


 Fragmentfärbung und -texturierung

Diese Stufe behandelt die interpolierten Fragmentinformationen. In der vorigen Stufe wurde durch Interpolation bereits eine Farbe berechnet und kann nun z.B. mit einem Texel kombiniert werden. Hinzu kommt dann auch noch Nebel sodass letztendlich ein definitiver Farbwert und ein Tiefenwert für dieses Fragment an die nächste Stufe weitergegeben werden. Diese Stufe kann unter Umständen mehrere Fragmente mit der selben Position ausgeben, die in der nächsten Stufe zu einem Pixel kombiniert werden.


 Rasteroperationen

Eingabewerte für diese Stufe sind:

Nun werden Testoperationen ausgeführt, wie zum Beispiel:

Sind alle Tests erfolgreich wird der Pixel unter Berücksichtigung des Blendingmodus in den Framebuffer geschrieben. Da die Färbungs- und Texturierungsstufe keinen Zugriff auf den Framebuffer hat ist dies der einzige Punkt innerhalb der Pipeline an dem Blending auftritt.


 Zusammenfassung

Das folgende Bild fasst noch einmal die oben genannten Stufen zusammen:


Abbildung 2: Zusammenfassung der festen Funktionalität



 2. Vertex- und Pixelprozessoren

Moderne Grafikkarten bieten nun dem Entwickler die Möglichkeit die Funktionalität von zwei der oben genannten Stufen selbst zu bestimmen:


 Der Vertexprozessor (Vertexshader)

Der Vertexprozessor ist für das Ausführen von Vertexshadern zuständig. Eingabewerte sind, wie in der Pipelinestufe der Vertextransformation, die Position im Raum, Farbe, Normalen etc. In Vertexshadern kann also Code für Aufgaben wie z.B. folgende geschrieben werden:

Die Ausgabe eines Vertexshaders ist die projeziert Vertexposition, die in die Variable gl_Position geschrieben wird. Bis auf das Schreiben dieser Variablen (gewöhnlich durch Transformieren mit der Modelview- und der Projection Matrix) besteht keine Pflicht die oben genannten Operationen durchzuführen. Es liegt zum Beispiel in der Hand des Entwicklers, auf Beleuchtung zu verzichten. Jedoch muss beachtet werden: Wenn ein Vertexshader geschrieben wird, wird die feste Funktionalität der Vertextransformation ersetzt! Folglich kann man nicht nur Beleuchtungsberechnungen auszuführen und die Generierung der Texturkoordinaten wieder von der festen Funktionalität übernehmen lassen. Wenn ein Shader genutzt wird, ist er verantwortlich die Funktionalität der Pipelinestufe die er ersetzt selbst bereit zu stellen.

Wie aus der vorigen Sektion hervorgeht, hat der Vertexprozessor keinerlei Informationen über Konnektivitäten. Flächenbasierte Operationen wie Backface Culling sind also nicht durch einen Vertexshader zu implementieren. Jeder Vertex wird einzeln bearbeitet ohne jegliche Information über andere Vertizes zu besitzen.
Jedoch kann auf OpenGL States zugegriffen werden. Ein Zugriff auf dem Framebuffer ist allerdings auf keiner Hardware möglich.


 Der Fragmentprozessor (Pixelshader)

Auf dem Fragmentprozessor laufen die Pixelshader. Mögliche Aufgaben sind zum Beispiel:

Die Eingabewerte dieser Einheit bestehen aus den interpolierten Werten der vorigen Stufe der Pipeline wie Vertexposition, Farbe, Normalen etc. Zudem hat der Programmierer die Möglichkeit zusätzliche Werte vom Vertexshader an den Pixelshader zu übergeben. Diese werden automatisch von der Hardware interpoliert. Lag beim Vertexshader das Augenmerk noch auf den Vertizes selbst, behandeln wir hier die Fragmente innerhalb der Primitiven, die die Vertizes zusammen bilden.

Wie auch ein Vertexshader, ersetzt ein Pixelshader alle feste Funktionalität seiner Pipelinestufe. Ein Fragment selbst zu texturieren, den Nebel aber über die feste Funktionalität zu berechnen ist also nicht möglich.

Ähnlich wie beim Vertexshader weiß der Pixelshader auch nur über ein einziges Fragment bescheid. Er kann auch auf OpenGL States zugreifen und somit z.B. die Nebelfarbe auslesen, doch wie es um die benachbarten Fragmente steht ist ihm unzugänglich.

Des weiteren ist zu beachten, dass ein Pixelshader die Position eines Fragmentes nicht beeinflussen kann. Die dafür zuständigen Matrizenmultiplikationen sind in der Pipeline zu diesem Zeitpunkt bereits ausgeführt worden. Der Pixelshader hat zwar Zugriff auf die Position des Fragments, er kann sie aber nicht ändern.

Im Gegensatz zum Vertexshader hat der Pixelshader zwei Möglichkeiten:

Zudem kann er noch den Tiefenwert, der von der vorherigen Stufe berechnet wurde, überschreiben.

Auch hier gilt, dass der Pixelshader keinen Zugriff auf den Framebuffer besitzt. Es besteht durchaus Zugriff auf den Texturspeicher, doch Operationen wie Blending finden statt nachdem der Pixelshader ausgeführt wurde.