Programmierbare Grafikchips sind nichts Neues, doch inzwischen berechnen sie mit Shadern auch Lichteffekte. Mit der OpenGL Shading Language erledigt das nach Cg und HLSL nun auch eine freie Hochsprache.
Die Einführung der Geforce-3-Grafikkarten von Nvidia rief im Jahre 2001 vor allem bei vielen Programmierern von Computerspielen wahre Begeisterungsstürme hervor. Bis dato nahmen Grafikchips den Hauptprozessoren zwar bereits einige Arbeit ab, doch Entwicklern stand nur ein sehr begrenzter Befehlssatz zur Verfügung. Für darüber hinausgehende Effekte waren sie gezwungen, auf den frei programmierbaren Hauptprozessor auszuweichen.
Der Chip der Geforce 3 kannte erstmals Shader und somit programmierbare Lichteffekte. Entwickler schrieben in Assembler und steuerten die Hardware über herstellerspezifische Erweiterungen an. Doch mittlerweile etablierten sich Hochsprachen und es gibt auch außerhalb der Spielewelt Anwendungen in der Bild- und Videobearbeitung, zum Beispiel Core Image von Apple.
Nach Microsofts HLSL und C for Graphics (Cg) von Nvidia hat das OpenGL Architecture Board (ARB) mit der OpenGL Shading Language (GLSL, [1]) als Teil von OpenGL 2 ebenfalls eine C-artige Sprache ins Leben gerufen. Der neue Nvidia-Grafikkartentreiber für Linux unterstützt erstmals auch OpenGL 2 und ermöglicht es den Linux-Entwicklern damit, GLSL einzusetzen.
OpenGL-Pipeline
Auch programmierbaren Grafikchips übergibt man eine 3D-Szene aus Punkten im Raum, den so genannten Vertices (Singular: Vertex). Neben der Position enthalten Vertices Attribute wie die Farbe, Texturkoordinaten oder den Normalvektor. Der Weg von Vertexdaten aus der Anwendung in der Grafikkarte zu Pixeln auf dem Schirm heißt Pipeline (Abbildung 1).
Zuerst rechnet die GPU (Graphics Processing Unit) die Vertices eines Dreiecks von 3D-Weltkoordinaten auf 2D-Bildschirmkoordinaten um. Das geschieht über die Multiplikation des Ortsvektors eines Vertex mit einer Matrix. Die Bildschirmkoordinaten von drei Vertices spannen ein Dreieck auf dem Bildschirm auf. Beim Rastern stellt die GPU fest, welche Punkte auf dem Schirm zu diesem Dreieck gehören, und füllt sie gemäß der Vertexattribute. Gerade bei typischen Vektor- und Matrixoperationen ergeben sich durch programmierbare GPUs erhebliche Geschwindigkeitsvorteile gegenüber der CPU-Programmierung.
Die beim Rastern errechneten Punkte entsprechen aber noch nicht den Pixeln, die im fertigen Bild auf dem Schirm erscheinen: Dreiecke, die räumlich vor dem gezeichneten Dreieck liegen, können Pixel noch ganz oder teilweise übermalen, etwa bei Halbtransparenz oder an den Kanten bei eingeschalteter Kantenglättung. Das Zusammensetzen der endgültigen Pixelfarbe erfolgt erst während der Rasteroperationen. Ein Bildpunkt heißt deshalb vor den Rasteroperationen Fragment, weil er nicht unbedingt einem fertigen Pixel auf dem Bildschirm entspricht.
Herkömmliche Grafikchips gaben diese Pipeline fest vor, man spricht von einer Fixed Function Pipeline. Die neuen Grafikchips ersetzen in einer programmierbaren Pipeline erstmals einen Teil der Fixed Function Pipeline durch eigenen Programmcode. Der Entwickler kann an zwei Stellen eingreifen: Mit einem Vertexshader bei der Vertextransformation, die für jedes Vertex einzeln geschieht, oder mit einem Fragmentshader beim Einfärben und Texturieren, das pro Fragment ausgeführt wird. Fragmentshader heißen häufig auch Pixelshader; diese Bezeichnung ist zwar aus technischer Sicht ungenau, beschreibt die Funktion aber anschaulich.
|
GLSL und OpenGL 1.5 |
|---|
|
Die OpenGL Shading Language gehört erst ab Version 2 fest zum OpenGL-Standard. Als Extension gibt es sie aber bereits für OpenGL 1.5. Dort tragen die API-Funktionen noch leicht abweichende Namen und wie die meisten ARB-Extensions das Suffix ARB. Wer OpenGL 2 nicht zur Verfügung hat oder abwärtskompatibel entwickeln möchte, sollte zur Laufzeit abfragen, ob die GLSL-Extension vorhanden ist. Hier hilft die OpenGL Extension Wrangler Library [3]. Dieser Artikel verwendet die Namenskonvention von OpenGL 2. Die unterschiedlichen Bezeichnungen zwischen der GLSL-Implentierung in OpenGL 2 und der OpenGL-1.5-Extension nennt Hersteller ATI auf seiner Homepage [2]. Viele Tutorials benutzen jedoch die Konventionen von OpenGL 1.5, etwa das von Lighthouse 3D [4]. |

Abbildung 1: Ohne eine programmierbare GPU übergibt eine Anwendung Vertexdaten an die Fixed Function Pipeline, die daraus die einzelnen Pixel errechnet. Shader ersetzen die Vertextransformation oder das Einfärben und Texturieren durch Programmcode.
Elemente der Sprache
GLSL lehnt sich an C an. Sie kennt die Variablentypen »float«, »int« und »bool«, die sich auch zu Structures zusammenfassen lassen. Zur Verzweigung bietet GLSL nur »if« und »if-else« an, »switch« sieht sie nicht vor. Es gibt außerdem »for«- und »while«-Schleifen. Allerdings führt nur neuere Hardware, etwa die Geforce 6 und besser, Schleifen aus, die nicht vollständig ausrollbar sind. Also solche, deren Anzahl von Durchläufen nicht schon beim Kompilieren feststeht, sondern die eine Bedingung enthalten, die jeder Durchlauf überprüft. Auf älterer Hardware schlägt das Kompilieren einer nicht ausrollbaren Schleife fehl. Zum bedingten Kompilieren steht ein Präprozessor zur Verfügung, es gibt aber keine »#include«-Direktive.
Jeder Shader muss eine »main«-Methode enthalten, die keinen Rückgabewert ausgibt (»void«) und keine Argumente erwartet. Über die aus C übernommenen Datentypen hinaus bietet GLSL einige Erweiterungen für Grafikaufgaben. So gibt es eigene Datentypen für Texturen, Vektoren und Matrizen (Tabelle 1).
Die GLSL-Vektoren sind Datenstrukturen aus zwei, drei oder vier Elementen gleichen Typs. Sie eignen sich dazu, Koordinaten im Raum, Normalvektoren, Farben oder auch Texturkoordinaten zu speichern. Der Entwickler füllt Vektoren über Konstruktoren mit Daten. Diese erwarten Skalare, also einzelne Zahlenwerte, andere Vektoren oder eine Kombination aus beiden.
|
Tabelle 1: Neue |
|
|---|---|
|
Datentyp |
Beschreibung |
|
vec2, vec3, vec4 |
Floatvector mit 2, 3 und 4 Elementen |
|
ivec2, ivec3, ivec4 |
Integervector mit 2, 3 und 4 Elementen |
|
bvec2, bvec3, bvec4 |
Boolvector mit 2, 3 und 4 Elementen |
|
mat2, mat3, mat4 |
Floatmatrix mit 2×2, 3×3 und 4×4 Elementen |
|
sampler1D, sampler2D, sampler3D |
1D-, 2D- und 3D-Textur |
|
samplerCube |
Cubemap-Textur |
Vektoren
GLSL füllt die Elemente des Vektors der Reihe nach mit den übergebenen Skalaren und den Elementen der übergebenen Vektoren auf. Die Anzahl der übergebenen Skalare muss mit der Größe des zu füllenden Vektors übereinstimmen. Ein Sonderfall ist die Initialisierung eines Vektors mit nur einem Skalar: Erhält der Konstruktor nur einen Wert, initialisiert er damit alle Elemente des Vektors.
Zu schreiben und zu lesen sind Vektorelemente entweder als Elemente eines Array oder wie bei objektorientierten Programmiersprachen über den Punktoperator als Mitglieder einer Klasse. Die einzelnen Elemente bekommen dazu einen vom Anwendungsfall abhängigen Buchstaben als Aliasnamen: bei Farbwerten »r«, »g«, »b« für die einzelnen Farben und »a« für die Transparenz. Bei Ortsvektoren sind die homogenen Koordinaten »x«, »y«, »z« und »w« üblich, für Vektoren mit Texturkoordinaten die Aliasnamen »s«, »t«, »p« und »q«.
Der Entwickler fordert nicht nur einzelne Elemente an, sondern auch Untervektoren mit Hilfe mehrerer Buchstaben nach dem Punkt. Dabei darf er die Reihenfolge beliebig wählen und Elemente auch mehrfach lesen (Listing 1).
Für Vektoren und Matrizen definiert GLSL alle Grundrechenarten: Sie erfolgen mit Ausnahme der Matrix-Matrix- und der Matrix-Vektor-Multiplikation komponentenweise. Außerdem kennt GLSL Punktprodukt, Kreuzprodukt und weitere mathematische Funktionen, die auch Vektoren als Argumente verarbeiten: Will der Entwickler beispielsweise den Sinus von vier Winkeln berechnen, übergibt er der Sinusfunktion einen Vektor mit den vier Winkeln und erhält als Rückgabewert einen Vektor mit den vier Sinuswerten.
Werteübergabe zwischen OpenGL und Shadern
Die Werteübergabe zwischen OpenGL und einem Shader erfolgt über einen Satz globaler Variablen, die Shader lesen und zum Teil auch schreiben (Tabelle 2). Alle Variablen, die OpenGL vorgibt, tragen das Präfix »gl_«. Eigene Variablennamen dürfen dieses Präfix nicht tragen, den Versuch bestraft der Compiler mit einer Fehlermeldung. Auf schreibbare Variablen können Programme stets auch lesend zugreifen. Das Resultat bleibt jedoch undefiniert, wenn die Variable noch leer ist.
Neben den erwähnten Variablen legt der GLSL-Standard [1] eine Reihe von Konstanten fest. »gl_ProjectionMatrix« enthält die Transformation von Weltkoordinaten in Bildschirmkoordinaten, »gl_ModelViewMatrix« die Verschiebung, Skalierung oder Drehung eines Modells im Raum und »gl_NormalMatrix« eine gesonderte Transformationsmatrix für Normalvektoren. Benutzt der Entwickler die Model-View-Matrix, um Normalvektoren zu transformieren, stehen die Normalvektoren aber unter Umständen nicht mehr senkrecht auf der Fläche (siehe Abbildung 2).
|
Listing 1: Beispielvektoren in |
|---|
01 vec3 position = vec3(1.0,2.0,3.0); 02 vec4 color = vec4(vec3(1.0,0.0,0.5),1.0); 03 vec4 white = vec4(1.0); 04 05 float xcoord = position[0]; 06 float ycoord = position.y; 07 vec2 redalpha = color.ra; 08 vec4 positionTwoTimes = position.xyxy; 09 color.r = 0.75; |
Minimale Shader
Ein Shader ersetzt immer entweder das gesamte Vertexprocessing oder das Fragmentprocessing. Ein Vertexshader muss deshalb das von der Fixed Function Pipeline benötigte Vertexprocessing vollständig nachbilden. Beschränkt sich ein Vertexshader auf das Berechnen der Bildschirmposition, finden keine Einfärbung pro Vertex und kein Evaluieren von Texturkoordinaten statt. Diese Beschränkung empfiehlt sich, wenn etwa ein Fragmentshader ohnehin die Farbberechnung pro Fragment übernimmt.
Ein Vertexshader muss aber mindestens »gl_Position« schreiben, ein Fragmentshader »gl_FragColor«. Beispiele für zwei Minimalshader, die gerade genug Funktion haben, um sich kompilieren und ausführen zu lassen, zeigt Listing 2. Der Vertexshader führt die Vertextransformation der Fixed Function Pipeline durch, der Fragmentshader färbt alle Pixel der Oberfläche rot ein.
|
Listing 2: Beispiele für |
|---|
01 // Vertexshader
02 void main() {
03 gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
04 }
05 // Fragmentshader
06 void main() {
07 gl_FragColor = vec4(1.0,0.0,0.0,1.0);
08 }
|
|
Listing 3: Shader in eine |
|---|
01 GLuint v = glCreateShader(GL_VERTEX_SHADER); 02 GLuint f = glCreateShader(GL_FRAGMENT_SHADER); 03 glShaderSource(v, 1, Quelltext,NULL); 04 glCompileShader(v); 05 glShaderSource(f, 1, Quelltext,NULL); 06 glCompileShader(f); 07 GLuint p = glCreateProgram(); 08 glAttachShader(p,v); 09 glAttachShader(p,f); 10 glLinkProgram(p); 11 glUseProgram(p); |
Shader in OpenGL
Eine Anwendung, die Shader benutzt, liest einzelne Quelltexte ein, kompiliert sie und linkt anschließend mehrere, üblicherweise zwei Quellcodes zu einem Programm zusammen (Listing 2). Anders als bei einem C-Programm geschieht dies erst zur Laufzeit. Shaderquelltexte bleiben in der Regel übersichtlich, sodass sich die Kompilierzeiten in Grenzen halten. Dafür unterscheiden sich die Grafikprozessoren in der Regel so sehr, dass sich eine optimierte Neukompilierung für die Zielhardware lohnt.
»glShaderSource« erwartet den Shaderquelltext als »char*«-Array, sodass der Programmierer frei entscheidet, ob er den Shader aus einer veränderbaren Datei liest oder fest in die Anwendung einkompiliert. Eine Anwendung kann auch mehrere Shaderprogramme nutzen. Mittels »glUseProgram« schaltet der Entwickler zwischen ihnen um. Übergibt er statt eines gültigen Shaderprogramms »0«, verwendet OpenGL die Fixed Function Pipeline.
Anwendungen erfordern oft das Zusammenspiel zwischen einem Vertex- und einem Fragmentshader. Zuerst leistet der Vertexshader die Vorarbeit pro Vertex, denn nur er hat Zugriff auf die Vertexattribute und damit auf die Daten der 3D-Szene. Zur Interaktion zwischen den Shadern schreibt ein Vertexshader in Variablen, die der Fragmentshader später liest. Solche gemeinsamen Variablen müssen beide Shader global mit dem gleichen Namen deklarieren und als »varying« kennzeichnen.
Da die Vertices nur den Eckpunkten, die Fragmente aber allen Punkten eines Dreiecks entsprechen, kümmert sich bei einer programmierbaren Pipeline OpenGL um die perspektivisch korrekte Interpolation der »varying«-Variablen zwischen den Vertices, ebenso wie bei den Vertexattributen einer Fixed Function Pipeline. Vertexshader übergeben in der Praxis häufig Texturkoordinaten, Farben oder auch Normalvektoren, mit denen dann der Fragmentshader die Beleuchtung pro Fragment berechnet.
OpenGL definiert einige »varying«-Variablen von Haus aus (Tabelle 3). Sie halten sich jedoch nicht immer an die Regel, dass derartige Variablen in beiden Shadern denselben Namen tragen müssen. Der Programmierer kann sie ohne Deklaration benutzen, abgesehen von einer Ausnahme: Möchte er das Array »gl_TexCoord« verwenden, muss er es in beiden Shadern mit derselben Anzahl von Elementen deklarieren, da OpenGL nur den Namen des Array vorgibt, nicht aber die Anzahl.
Toonshading heißt ein Beleuchtungsmodell, bei dem die GPU Fragmente nicht kontinuierlich heller zeichnet, je mehr sie dem Licht zugewandt sind, sondern in nur wenigen Stufen schattiert. Auf diese Weise entsteht ein nicht fotorealistisches, flächiges und comicartiges Erscheinungsbild. Als Grundlage dient ein einfaches Beleuchtungsmodell namens Gouraudshading. Es benutzt als Maß für die Helligkeit eines Fragments den Cosinus des Winkels zwischen dem Vektor zur Lichtquelle und dem Normalvektor einer Fläche.
|
Tabelle 2: Globale |
||
|---|---|---|
|
Variablenname |
Datentyp |
Beschreibung |
|
Lesbar in Vertexshadern (Vertexattribute) |
|
|
|
gl_Color |
vec4 |
Erste Zeichenfarbe |
|
gl_SecondaryColor |
vec4 |
Zweite Zeichenfarbe |
|
gl_Normal |
vec3 |
Normalenvektor eines Vertex |
|
gl_Vertex |
vec4 |
Ortsvektor eines Vertex |
|
gl_MultiTexCoord0 – gl_MultiTexCoord7 |
vec4 |
Texturkoordinaten der acht Textureinheiten |
|
Schreibbar in Vertexshadern |
|
|
|
gl_Position |
vec4 |
Position des Vertex auf dem Schirm |
|
Lesbar in Fragmentshadern |
|
|
|
gl_FragCoord |
vec4 |
Position des Fragments auf dem Schirm |
|
gl_FrontFacing |
bool |
Vorder- oder Rückseite eines Polygons füllen |
|
Schreibbar in Fragmentshadern |
|
|
|
gl_FragColor |
vec4 |
Endgültige Farbe eines Fragments |
Beispiel: Toonshading
Strahlt eine Lichtquelle senkrecht auf eine Fläche, beträgt der Winkel 0 Grad, das entspricht einem Cosinus von 1, die Fläche hat die maximale Helligkeit. Stehen Normalvektor und Vektor zur Lichtquelle im rechten Winkel zueinander, beträgt der Cosinus 0, die Fläche bleibt schwarz. Winkel zwischen 90 und 270 Grad ergeben einen negativen Cosinus. Das entspräche allerdings einer wenig sinnvollen negativen Beleuchtung; dagegen hilft eine Beschränkung auf Cosinus-Werte zwischen 0 und 1.
Für reguläres Goraudshading reicht die Berechnung pro Vertex mit anschließender Interpolation zwischen den Vertices durch die Fixed Function Pipeline. Da beim Toonshading aber Stufen in der Einfärbung zwischen zwei Vertices vorgesehen sind, muss ein Fragmentshader pro Pixel berechnen. Er benötigt vom Vertexshader den Normalenvektor und einen Vektor, der zur Lichtquelle zeigt. Die Übergabe erfolgt über zwei »varying«-Variablen.
Neben dem Lichtvektor stellt der Vertexshader den Normalvektor nach der Transformation bereit. Dann bildet er einen Teil der Fixed Function Pipeline nach, nämlich die Transformationen des Vertex- und des Normalvektors mit Hilfe der entsprechenden Matrizen (Listing 4).
|
Tabelle 3: Durch |
||
|---|---|---|
|
Variablenname |
Datentyp |
Beschreibung |
|
Nur schreibend in Vertexshadern |
|
|
|
gl_FrontColor |
vec4 |
Erste Zeichenfarbe Vorderseite |
|
gl_BackColor |
vec4 |
Erste Zeichenfarbe Rückseite |
|
gl_FrontSecondaryColor |
vec4 |
Zweite Zeichenfarbe Vorderseite |
|
gl_BackSecondaryColor |
vec4 |
Zweite Zeichenfarbe Rückseite |
|
Nur lesend in Fragmentshadern |
|
|
|
gl_Color |
vec4 |
Erste Zeichenfarbe |
|
gl_SecondaryColor |
vec4 |
Zweite Zeichenfarbe |
|
In beiden Shadern |
|
|
|
gl_TexCoord[] |
vec4 |
Texturkoordinaten |
Der Fragmentshader berechnet zuerst die Intensität, also den Cosinus zwischen Licht- und Normalvektor. Das geschieht über das Punktprodukt aus der analytischen Geometrie, das GLSL als »dot(vec a, vec b)« bereitstellt:
Haben beide Vektoren den Betrag 1, liefert das Punktprodukt direkt den Cosinus. Über die Bibliotheksfunktion »normalize()« bringt der Fragmentshader im Beispiel in Listing 5 beide Vektoren auf diese Länge. Die ebenfalls von der Bibliothek bereitgestellte Funktion »max(a, b)« schränkt den Cosinus auf den Bereich zwischen 0 und 1 ein. Das Beispiel beherrscht vier verschiedene Helligkeitsstufen. Über mehrere »if«-Abfragen weist der Shader jedem Fragment eine von vier Farben zu, je nachdem, in welchem Helligkeitsbereich die Intensität liegt.
Eine Anwendung und Shader tauschen Daten mit Hilfe von Variablen aus.
|
Listing 4: Vertexshader für |
|---|
01 varying vec3 normal;
02 varying vec3 lightDirection;
03
04 void main() {
05 lightDirection = vec3(gl_LightSource[0].position);
06 normal = gl_NormalMatrix * gl_Normal;
07 gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
08 }
|
Datenaustausch zwischen Anwendung und Shader
Eine Variante dieser Variablen ist im Vertex- oder Fragmentshader als »uniform« gekennzeichnet. Die Funktion »glGetUniformLocation(GLuint Program, const GLchar* Name)« liefert die Position einer »uniform«-Variablen innerhalb eines Programms als »GLuint«. Mit Hilfe der ermittelten Position und über Funktionen der Familie »glUniform« setzt die Anwendung beim weiteren Ablauf den Wert der Variablen.
Wie bei anderen Funktionen in OpenGL liegt »glUniform« in einer Reihe von Ausführungen für verschiedene Datentypen vor, da man in C – anders als in C++ – Funktionen nicht überladen darf. Will der Entwickler beispielsweise eine Variable vom Typ »float« setzen, verwendet er dafür etwa die Funktion »glUniform1f(GLint location, GLfloat v0)«. Sie erwartet als erstes Argument die Position einer Variablen und als zweites Argument den zu schreibenden Wert. Die Mutteranwendung darf »uniform«-Variablen nicht zwischen »glBegin« und »glEnd« schreiben.
Vergleichbar mit den Vertexattributen wie der Farbe und den Texturkoordinaten, die OpenGL bereits anbietet, kann der Entwickler in einem Vertexshader eine Variable pro Vertex verändern, sofern sie als »attribute« gekennzeichnet ist.
Analog zu den »uniform«-Variablen gibt es die Funktionen »glGetAttribLocation()« und die Familie »glVertexAttrib()« für den Zugriff auf »attribute«-Variablen. Die Anwendung darf die »attribute«-Variablen zwischen »glBegin()« und »glEnd()« schreiben, sie sind aber nur in Vertexshadern zulässig.
Findet OpenGL die Position einer Variablen innerhalb eines Programms nicht, gibt sie statt einer gültigen Adresse »-1« zurück. Das passiert auch, wenn der Shaderquelltext eine Variable zwar deklariert, aber nicht benutzt. Dann optimiert der Compiler den Code und nimmt die Variable raus. Leider gibt er dann keine Warnung aus, was häufig zu aufwändiger Fehlersuche führt.
Neben einfachen Datentypen wie Skalaren und Vektoren kann ein Shader auch Variablen der einzelnen Texturtypen als »uniform« deklarieren. Mit Hilfe von Funktionen der Familie »vec4 textureDimension(samplerDimensionD Textur, vecDimension Position)« liest er einen Farbwert der Textur aus. Für die gängigen 2D-Texturen lautet der Funktionsname zum Beispiel »texture2D(sampler2D Textur, vec2 Position)«. Aus Sicht der Anwendung sind Texturen in Shadern Zeiger auf eine der acht Textureinheiten der OpenGL-Multitextur-Extension. Mit Hilfe von »glUniform1i()« und der Position einer Texturvariablen übergibt der Entwickler dem Shader eine dieser acht Textureinheiten als »GLuint«.

Abbildung 2: Ein Normalvektor steht nicht mehr senkrecht auf einer Fläche, wenn er mit der gleichen Matrix wie das Model transformiert wird.
Da OpenGL alle Variablen mit »0« initialisiert und die erste Textureinheit die Nummer »0« hat, landet eine Textur aus der Anwendung automatisch in allen Texturvariablen im Shader. Soll ein Shader zwei Texturen benutzen, ist eine andere Textureinheit auszuwählen (»glActiveTexture(GL_TEXTURE1)«), eine Textur zu laden (»glBindTexture()«) und die »uniform«-Variable im Shader auf die Nummer der zweiten zu setzen.
Laut Standard sollen sowohl Vertex- als auch Pixelshader auf Texturen lesend zugreifen, leider setzen die Chips von Nvidia den Standard diesbezüglich erst ab der Geforce 6 um. Ältere Chips lesen Texturen nur in Fragmentshadern.
|
Listing 5: Fragmentshader |
|---|
01 varying vec3 normal;
02 varying vec3 lightDirection;
03
04 void main() {
05 float intensity = max(dot(normalize(normal), normalize(lightDirection)), 0.0);
06 if (intensity > 0.95) {
07 gl_FragColor = vec4(1.0,1.0,1.0,1.0);
08 } else if (intensity > 0.65) {
09 gl_FragColor = vec4(0.65,0.65,0.65,1.0);
10 } else if (intensity > 0.45) {
11 gl_FragColor = vec4(0.45,0.45,0.45,1.0);
12 } else {
13 gl_FragColor = vec4(0.30,0.30,0.30,1.0);
14 }
15 }
|
|
Listing 6: Fragmentshader: |
|---|
01 uniform sampler2D texture;
02 uniform float begin;
03 uniform float end;
04
05 varying vec4 gl_TexCoord[1];
06
07 void main() {
08 vec4 color = texture2D(texture, gl_TexCoord[0].xy);
09 float brightness = (color.r*0.30+color.g*0.59+color.b*0.11);
10 if (brightness > begin && brightness < end) {
11 gl_FragColor = color;
12 } else {
13 gl_FragColor = vec4(0.0,0.0,0.0,1.0);
14 }
15 }
|
Beispiel: Helligkeit auswählen
Beispiel für den Einsatz von »uniform«-Variablen ist ein Programm, das nur jene Pixel einer Textur darstellt, die in einem bestimmten Helligkeitsbereich liegen. Die Pixel-Helligkeit berechnet sich mit der Formel:
h = r*0.30+g*0.59+b*0.11
Diese Kalkulation gewichtet die Farbkanäle unterschiedlich, da eine grüne Fläche auf dem Bildschirm für das menschliche Auge am hellsten wirkt, eine blaue dagegen am dunkelsten.
In dem folgenden Beispiel transformiert der Vertexshader lediglich die Vertexkoordinaten und stellt die Texturkoordinaten für den Fragmentshader über eine »varying«-Variable bereit:
varying vec4 gl_TexCoord[1];
void main() {
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = ftransform();
}
Der Fragmentshader erhält von der Anwendung sowohl die zu benutzende Textur als auch beide Grenzen als »uniform«-Variablen. Zuerst holt er mit »texture2D()« den Farbwert an den Texturkoordinaten, anschließend bestimmt er mit Hilfe der oben genannten Formel die Helligkeit. Wenn der Farbwert in dem vorgegebenen Wertebereich liegt, übernimmt der Shader ihn für das Fragment, liegt er außerhalb, färbt er es schwarz ein (siehe Listing 6).
Das Beispielprogramm benutzt die X-Koordinate des Mauszeigers im Anwendungsfenster für die Definition der Grenzwerte. Der linke Fensterrand entspricht der Helligkeit 0, der rechte Fensterrand der Helligkeit 1. Der Benutzer legt die erste Schranke beim Drücken der Maustaste, die zweite beim Loslassen fest. Mit einem Drag&Drop von der linken zur rechten Fensterkante wählt er beispielsweise den gesamten Farbbereich, mit dem Ziehen der Maus von der Mitte zur rechten Fensterkante nur Werte zwischen 0,5 und 1.
Fazit
Die OpenGL Shading Language bietet Grafikentwicklern viele neue und wichtige Möglichkeiten. Mit wenigen Zeilen Quelltext lassen sich mit ihr beeindruckende Effekte effizient implementieren. Im Gegensatz zur ebenfalls für Linux erhältlichen Cg von Nvidia fügt sich GLSL als Teil von OpenGL harmonisch in das API ein. Es bleibt zu hoffen, dass Nvidia-Konkurrent ATI bald nachzieht und ebenfalls Treiber mit Unterstützung für OpenGL 2 anbietet, sodass auch Besitzer von Grafikkarten dieses Herstellers von der standardisierten Sprache für Grafikeffekte profitieren. (csc)
|
Infos |
|---|
|
[1] OpenGL Shading Language: [http://www.opengl.org/documentation/oglsl.html] [2] GLSL in OpenGL 1.5 und 2.0: [http://www.ati.com/developer/gdc/GDC2005_OpenGL_Shading_Language.pdf] [3] OpenGL Extension Wrangler Library: [http://glew.sourceforge.net] [4] OpenGL Shading Language Tutorial von Lighthouse: [http://www.lighthouse3d.com/opengl/glsl] [5] GPU programmieren: Stephan Siemen, “Glänzende Effekte”: Linux-Magazin 02/04, S. 98 |






