Ich hatte mich Anfang Dezember 2024 angemeldet und angefangen, damit etwas zu programmieren. Es erinnerte mich etwas an die Zeit vor so 15–20 Jahren, als ich mit dem scriptbasierten Raytracing-Programm POV-Ray auch durch Schreiben von mathematiklastigem Code Grafiken erzeugte. Da dauerte ein Bild gut und gerne Stunden bis Tage zum Berechnen. Hier jedoch geht es darum, dass 60 Bilder pro Sekunde fertig werden.
Im Benutzerprofil gibt es meine dort programmierten Shader:
https://www.shadertoy.com/user/Svenofnine/sort=newest
Was ist Shadertoy?
Die Startseite von Shadertoy enthält die jeweils besten und neusten von Community-Mitgliedern programmierten und geteilten Shader.
Shadertoy ist eine englischsprachige Programmierumgebung für sogenannte Shader. Rechts schreibt man ein Programm in der Sprache GLSL, das dann direkt auf der Grafikkarte von der Shader-Kernen der GPU ausgeführt wird und links sieht man gleich, was es tut.
Für Programmierer ist es wirklich wie ein Spiel: Schreibe ein Programm, was in einer Sandbox läuft. Von außen bekommst du Werte für Auflösung, abgelaufene Zeit und die Koordinate des zu berechnenden Fragments (Pixels) eines Frames. Die Funktion soll den Farbwert dieses Pixels berechnen. Und nicht nur den: Die Grafikkarte führt diese Funktion für jedes Pixel eines Frames gleichzeitig aus, bis alle Pixel berechnet wurden und das Frame fertig ist. Das passiert im Idealfall 60-mal in der Sekunde.
Also geht es um Bilderzeugung allein mit Mathematik und darum, seine Erfahrung und den Code zu teilen. Das macht wirklich Spaß, zumindest wenn man sich wie ich an Programmierung und Mathematik erfreuen kann.
Doch wie errechnet man anhand der Pixelkoordinate eine Farbe und warum und vor allem wie können alle Pixel gleichzeitig errechnet werden? Dazu habe ich unter https://www.shadertoy.com/view/XXKcR1 ein einfach gehaltenes Beispiel erstellt:
Dreh- und Angelpunkt eines Shaders ist die mainImage()-Funktion, welche von der Host-Anwendung (hier: Browser mit Javascript im HTML-Canvas) mit jeder möglichen Pixelkoordinate (=Fragment) aufruft und die Farbe, die sie zurückerhält, auf den Bildschirm bringt. Dabei bekommt jeder Kern der Grafikkarte eine andere Pixelkoordinate zugeteilt, so dass der Prozess parallel abläuft.
Als Programmierer sitzt man quasi in dieser Funktion mainImage() drin. Neben den Koordinaten XY bekommt man von außen noch Lesezugriff auf sogenannte Uniform-Variablen für Auflösung, Zeit, Framenummer, Mausposition von der Host-Anwendung, welche für alle Pixel eines Frames gleich sind.
Dieses Beispiel soll zeigen, wie man nun innerhalb dieser Black Box mit Mathematik Formen definiert und diese im einfachsten Fall darstellt, indem man nur entscheidet, ob eine Pixelkoordinate auf der Form liegt oder nicht. Die einfachste Form dafür ist meiner Meinung nach ein Kreis. Die Formel in Zeile 12 errechnet dafür den Abstand zwischen Pixelkoordinate und Kreismitte. Ist er kleiner als der Radius, wird 1 zurückgeliefert, ansonsten 0.
Der ermittelte Wert wird dann mit allen drei Komponenten der Kreisfarbe multipliziert und der Pixelfarbe zugewiesen (Zeile 18).
Das macht ein Shader: Pixelkoordinate rein, Pixelfarbe raus. Die Pixelfarbe zu ermitteln ist selten so eine einfache Wenn-Dann-Entscheidung, sondern wird bestimmt auf künstlerisch gestalteten Wegen vom Betrachter aus durch bewegliche Distanzfelder, Oberflächen mit Texturen und Reflexionen in verzerrten Räumen. Das kann alles sein, was sich berechnen lässt. Das Rechnen mit Vektoren, Matritzen und Fließkommazahlen sind in den Grafikprozessoren mit ihren vielen Shader-Kernen nativ verdrahtet. Die warten drauf!
Quellen für den Anfang
Das »Book Of Shaders« setzt hier an und erklärt in einfacher Sprache und Handzeichnungen, wie das Ganze im Grunde funktioniert.
Youtube-Video »Lean to Paint with Mathematics« von Nutzer iq ist eine gute Einführung in das Tool Shadertoy und Nutzer FabriceNeyret2 zeigt auf seinem Blog Shadertoy Unofficial, was damit alles möglich ist. Vorweg: Grafisch ist alles möglich!
Nachdem etwa eine Woche vergangen war, ließ mich das Thema nicht mehr los und es kribbelte in den Fingern. Das Erzeugen von Bildern und Animationen mit Mathematik und Programmcode war ja schon 2003 mit POV-Ray künstlerisches Werkzeug. Anders als mit dem Raytracer POV-Ray geht es bei Shadern um Echtzeitgrafik, also es müssen 60 Bilder pro Sekunde entstehen, nicht eins in Stunden oder Tagen. Beides behält aber trotzdem in ihrer Verschiedenheit seine Daseinsberechtigung.
Bei den Programmiersprachen, mit welchen ich mich bisher beschäftigt habe, gab es immer Referenzen der Funktionen, Variablen und Operatoren. Bei Shadertoy hatte es eine Weile gedauert, bis ich auf dieses kleine Fragezeichen rechts unten am Rand des Texteditors entdeckte. Klickt man da drauf, gibt es direkt von Shadertoy das Wichtigste auf einer Seite.
Oben am Editor gibt es die »Shader Inputs«. Das sind sogenannte uniforme Variablen, welche für alle Pixel eines Frames konstant sind und zur lesenden Verfügung stehen.
Es gibt neben den bekannten Datentypen float (Fließkomma-Zahl) und int (Ganzzahl) noch verschiedene Vektor‑, Matrix- und Buffer-Datentypen, die auch auf der Hardware der Grafikkarte unterstützt werden.
Dieser Blog-Post ist kein Tutorial, weil ich da ja selbst ganz am Anfang stehe und es viele Ressourcen dafür gibt. Ich versuche, soweit es geht, meinen Code zu kommentieren und Fragen zu beantworten.
Hier sind noch weitere Links zum Thema:
- inspirnathan.com — Shadertoy Tutorial Part 1 — Intro
- Into Shadertoy and Shaders useful links and tips
- OpenGL Shading Language (GLSL) Quick Reference (PDF)
- Ray Marching, and making 3D Worlds with Math
- SimonDev