kind: reference

GLSL

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.

1. Grundlagen

Was ein Shader tut

Die Grafikkarte zeichnet alles aus Dreiecken, und an zwei Stellen fragt sie ein Programm von dir:

  1. Der Vertex-Shader läuft für jede Ecke eines Dreiecks. Er rechnet aus, wo die Ecke auf dem Bildschirm landet, und schreibt das nach gl_Position.
  2. Die Grafikkarte füllt das Dreieck mit Pixeln. Was der Vertex-Shader in einer 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.
  3. Der Fragment-Shader läuft für jeden dieser Pixel und rechnet seine Farbe aus. Die schreibt er nach 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.

Version und Genauigkeit

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.

2. Die Sprache

Typen und Zahlen

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.

Komponenten

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.

Ein- und Ausgaben

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.

Bedingungen, Schleifen, Funktionen

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));
}

3. Rechnen

Eingebaute Funktionen

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.

4. Mit raylib

Was raylib mitschickt

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);
}

5. Fehler finden

Wenn nichts oder Schwarz zu sehen ist

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.
  • Ein varying heißt im Vertex- und im Fragment-Shader verschieden oder hat einen anderen Typ.
  • Ein 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).

6. Zum Nachschlagen