GLSL ist die Sprache, in der Shader geschrieben werden: kleine Programme, die auf der Grafikkarte laufen, eins für jede Ecke und eins für jeden Pixel. Hier stehen die Grundlagen und das Wichtigste zum Nachschlagen, in der Fassung #version 100, die Browser verstehen. Die vollständige Beschreibung steht in den offiziellen Unterlagen, die am Ende verlinkt sind.
Die Grafikkarte zeichnet alles aus Dreiecken, und an zwei Stellen fragt sie ein Programm von dir:
gl_Position.varying-Variable an die drei Ecken geschrieben hat, mischt sie für jeden Pixel dazwischen: In der Mitte eines Dreiecks mit einer roten und zwei blauen Ecken kommt ein Lila an.gl_FragColor.Beide laufen tausendfach gleichzeitig, jeder für sich: Ein Pixel weiß nichts von seinen Nachbarn und kann sich nichts für den nächsten merken. Was für alle gleich ist, etwa die Richtung der Sonne, schickt das Programm auf dem Computer als uniform hinein.
Jeder Shader beginnt mit der Version. Browser (WebGL) verstehen GLSL ES 1.00:
#version 100 precision mediump float;
Die zweite Zeile braucht nur der Fragment-Shader: Er muss sagen, wie genau er mit Kommazahlen rechnet. mediump reicht für Farben und Licht, highp ist genauer, aber nicht auf jedem Handy da.
Auf dem Computer ohne Browser verwendet raylib meist #version 330. Dort heißen einige Dinge anders: in und out statt attribute und varying, texture statt texture2D und eine eigene out vec4-Variable statt gl_FragColor. Die Beispiele hier sind alle #version 100.
GLSL sieht aus wie C, hat aber eigene Typen für Vektoren und Matrizen:
| Typ | Was |
|---|---|
float, int, bool
|
Kommazahl, ganze Zahl, wahr oder falsch |
vec2, vec3, vec4
|
Zwei, drei oder vier Kommazahlen: Punkte, Richtungen, Farben |
ivec2 … ivec4, bvec2 … bvec4
|
Dasselbe mit ganzen Zahlen oder Wahrheitswerten |
mat2, mat3, mat4
|
Matrizen; mat4 * vec4 dreht, verschiebt und projiziert einen Punkt |
sampler2D |
Eine Textur, gelesen mit texture2D
|
Ein Vektor entsteht aus Zahlen oder anderen Vektoren, solange es insgesamt genug Zahlen sind:
vec3 up = vec3(0.0, 1.0, 0.0); vec3 grey = vec3(0.5); // alle drei Zahlen 0.5 vec4 color = vec4(grey, 1.0); // drei aus grey, dazu 1.0
Rechnen geht Zahl für Zahl: a + b, a * b und a * 2.0 rechnen mit jeder Komponente einzeln.
Zwei Stolperfallen: Kommazahlen schreibst du immer mit Punkt und Nachkommastelle, 1.0 statt 1. Und GLSL wandelt int nicht von selbst in float um. float x = 1; ist ein Fehler, float(i) macht aus der ganzen Zahl i eine Kommazahl.
Die Zahlen eines Vektors heißen x, y, z, w. Für Farben gibt es dieselben noch einmal als r, g, b, a, für Texturkoordinaten als s, t. Mehrere Buchstaben hintereinander ergeben einen neuen Vektor:
vec4 color = vec4(1.0, 0.5, 0.2, 1.0); vec3 rgb = color.rgb; // (1.0, 0.5, 0.2) float red = color.r; // 1.0 vec2 ground = position.xz; // nur x und z, ohne die Höhe vec3 swapped = rgb.bgr; // umgedreht: (0.2, 0.5, 1.0)
Innerhalb eines Ausdrucks bleibt man bei einer Sorte: color.rgb und color.xyz gehen, color.rgz nicht.
Woher ein Wert kommt, sagt ein Wort vor dem Typ:
| Wort | Woher | Beispiel |
|---|---|---|
attribute |
Für jede Ecke anders, vom Programm. Nur im Vertex-Shader. | attribute vec3 vertexPosition; |
uniform |
Für alle Ecken und Pixel gleich, vom Programm gesetzt | uniform vec3 sun; |
varying |
Vom Vertex-Shader geschrieben, im Fragment-Shader für jeden Pixel gemischt gelesen; in beiden gleich deklariert | varying vec3 normal; |
const |
Steht fest | const float FOG_START = 16.0; |
Dazu kommen Variablen, die GLSL schon kennt:
| Variable | Wo | Bedeutung |
|---|---|---|
gl_Position |
Vertex-Shader, schreiben | Wo die Ecke auf dem Bildschirm landet (vec4) |
gl_FragColor |
Fragment-Shader, schreiben | Die Farbe des Pixels, Rot, Grün, Blau und Deckkraft von 0.0 bis 1.0 |
gl_FragCoord |
Fragment-Shader, lesen | Wo der Pixel im Fenster liegt, in Pixeln (.xy) |
gl_FrontFacing |
Fragment-Shader, lesen |
true, wenn die Vorderseite zu sehen ist (Ecken gegen den Uhrzeigersinn) |
discard; im Fragment-Shader lässt einen Pixel ganz weg, etwa wo eine Textur durchsichtig ist.
if, else, Vergleiche und &&, || schreibst du wie in C. Eine Schleife braucht in #version 100 eine feste Obergrenze. Wie viele Durchläufe es wirklich sind, regelt ein break:
#define MAX_TORCHES 16 uniform vec3 torches[MAX_TORCHES]; uniform int torchCount; for (int i = 0; i < MAX_TORCHES; i++) { if (i >= torchCount) break; // … torches[i] … }
while gibt es nicht überall. Eigene Funktionen gehen, aber ohne Rekursion, und sie müssen über der Stelle stehen, an der du sie aufrufst:
float brightness(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
Fast alle Funktionen nehmen auch Vektoren und rechnen dann mit jeder Komponente einzeln: max(v, 0.0) setzt jede negative Zahl in v auf 0.
| Funktion | Ergebnis |
|---|---|
dot(a, b) |
Skalarprodukt: für zwei Richtungen der Länge 1 der Kosinus des Winkels dazwischen, 1 bei gleicher Richtung, 0 im rechten Winkel |
cross(a, b) |
Kreuzprodukt zweier vec3: senkrecht auf beiden |
normalize(v), length(v)
|
Dieselbe Richtung mit Länge 1; die Länge |
distance(a, b) |
Abstand zweier Punkte |
reflect(d, n) |
Richtung d, gespiegelt an einer Fläche mit der Normale n
|
min(a, b), max(a, b), clamp(x, a, b)
|
Kleinere, größere; x zwischen a und b gehalten |
mix(a, b, t) |
Mischung: a bei t = 0.0, b bei t = 1.0
|
step(edge, x) |
0.0, solange x kleiner als edge ist, sonst 1.0 |
smoothstep(a, b, x) |
Weicher Übergang von 0.0 bei a zu 1.0 bei b
|
abs, sign, floor, ceil, fract
|
Betrag, Vorzeichen, ab- und aufrunden, Nachkommaanteil |
mod(x, y) |
Rest beim Teilen, auch für Kommazahlen (statt %) |
pow(x, y), sqrt(x), exp, log
|
Potenz, Wurzel, Exponential- und Logarithmusfunktion |
sin, cos, tan, atan(y, x)
|
Winkelfunktionen im Bogenmaß; radians(deg) rechnet Grad um |
texture2D(t, uv) |
Farbe der Textur t an der Stelle uv, von (0.0, 0.0) bis (1.0, 1.0) |
dFdx(v), dFdy(v), fwidth(v)
|
Wie sehr sich v von einem Pixel zum nächsten ändert: nach rechts, nach oben, und beides zusammen (abs(dFdx(v)) + abs(dFdy(v))). In #version 100 nur mit #extension GL_OES_standard_derivatives : enable in der ersten Zeile, in #version 300 es immer |
fwidth sagt, wie viel von einer Fläche ein Pixel gerade zeigt. Damit wird eine feine Linie, eine Fuge zwischen zwei Fliesen etwa, weit weg weich und blass, statt zu flimmern: Ist ein Pixel breiter als die Linie, zeichnet man sie nicht scharf, sondern nur so stark, wie sie den Pixel füllt.
float d = abs(fract(uv.x * 10.0) - 0.5); // Abstand zur Mitte einer Fliese, 0 bis 0.5 float w = fwidth(uv.x * 10.0); // so viel misst ein Pixel hier float grout = smoothstep(0.5 - w, 0.5, d); // eine Fuge am Rand, weich über einen Pixel
Mehr dazu, mit Beispielen zum Ausprobieren, steht in Prozedurale Texturen.
Wie du einen Shader in C lädst und Werte hineinschickst, steht bei raylib. Einige Werte setzt raylib selbst, wenn sie im Shader unter diesen Namen vorkommen:
| Name | Was |
|---|---|
attribute vec3 vertexPosition |
Die Ecke in der Welt |
attribute vec3 vertexNormal |
Die Normale aus rlNormal3f
|
attribute vec4 vertexColor |
Die Farbe aus rlColor4ub, von 0.0 bis 1.0 |
attribute vec2 vertexTexCoord |
Die Texturkoordinate aus rlTexCoord2f
|
uniform mat4 mvp |
Kamera und Projektion zusammen: mvp * vec4(vertexPosition, 1.0) ist der Punkt auf dem Bildschirm |
uniform sampler2D texture0 |
Die Textur, mit der gerade gezeichnet wird |
uniform vec4 colDiffuse |
Die Farbe, die DrawModel und Co. mitgeben |
Der kleinste Vertex-Shader reicht Farbe und Normale weiter und setzt die Ecke an ihren Platz:
#version 100 attribute vec3 vertexPosition; attribute vec3 vertexNormal; attribute vec4 vertexColor; uniform mat4 mvp; varying vec3 normal; varying vec4 color; void main() { normal = vertexNormal; color = vertexColor; gl_Position = mvp * vec4(vertexPosition, 1.0); }
Einen Fehler im Shader findet der Compiler erst, wenn das Programm läuft. raylib meldet ihn in der Konsole der Seite, mit Zeilennummer im Shader; die Welt bleibt dann schwarz oder wird ohne Shader gezeichnet. Die häufigsten Ursachen:
1 statt 1.0, oder int und float gemischt.precision mediump float; fehlt im Fragment-Shader.varying heißt im Vertex- und im Fragment-Shader verschieden oder hat einen anderen Typ.uniform, das der Shader nirgends verwendet, entfernt der Compiler. GetShaderLocation liefert dann −1, und der Wert kommt nie an, ohne Fehlermeldung.Einen Wert, den man nicht sieht, macht man zur Farbe. Eine Normale hat Zahlen von −1 bis 1, Farben gehen von 0 bis 1:
gl_FragColor = vec4(normalize(normal) * 0.5 + 0.5, 1.0);
Jetzt hat jede Richtung ihre Farbe: Oberseiten (0, 1, 0) werden hellgrün, Seiten nach +x rosa. Genauso lässt sich ein Abstand oder ein Lichtwert als Grauwert anzeigen, vec4(vec3(sunAmount), 1.0).
#version 100. Die Einschränkungen für Schleifen stehen im Anhang A.#version 330 für raylib ohne Browser.#version 100 zum Lesen und Abwandeln.