- Eine Rick-Animation entsteht mit nur 240 Zeilen GLSL-Code; ohne Bilder oder Bibliotheken berechnet ein GPU-Shader die Farbe jedes Pixels abhängig von der Zeit
- Kernstück ist
color_for_pixel(pixel, time), das für jedes Pixel ausgeführt wird; ein Signed Distance Field (SDF) beschreibt Innen- und Außenbereiche sowie Konturen von Formen über Distanzwerte
- Das Gesicht wird aus SDF-Formen wie
round_rect(), circle(), star(), bezier() und parabola() aufgebaut, kombiniert durch Verschieben, Skalieren, Spiegeln, Wiederholen und Vereinigungen
- Haarwellen, wiederholte Zähne, Lippenkontur, Blinzeln, Pupillenbewegung und Kopfwackeln werden schrittweise mit Domain Warping,
mod(), sin(time) und noise() ergänzt
- Die fertige Version enthält auch einen Portal-Hintergrund, doch da der Editor keinen Videoexport bietet, ist ein separater macOS-Workflow mit
glslviewer und ffmpeg nötig
Shader-Struktur zur Berechnung der Farbe pro Pixel
- Die Animation ist in OpenGL Shading Language (GLSL) geschrieben
- Beispiele lassen sich direkt im Live-Coding-Editor auf der Seite ausführen und bearbeiten
- Der grundlegende Einstiegspunkt ist die Funktion
color_for_pixel(vec2 pixel, float time)
- Die GPU führt diese Funktion für jedes Pixel der Vorschau aus
- Die Frage, die die Funktion beantwortet, lautet: „Welche Farbe soll dieses Pixel zu diesem Zeitpunkt haben?“
- Ein einfaches Beispiel schreibt die Werte
pixel.x, pixel.y und time in Farbkanäle, um Pixelposition und zeitliche Veränderung zu visualisieren
time ist die seit der letzten Bearbeitung verstrichene Zeit in Sekunden und steigt fortlaufend an
Formen mit SDF zeichnen
- Einen Kreis kann man zeichnen, indem man mit
length(pixel) die Entfernung vom Ursprung zum Pixel berechnet und sie mit dem Radius vergleicht
- Die Funktion
circle() gibt nicht einfach ein bool für innen/außen zurück, sondern die Distanz zum Umfang
- Innerhalb der Form ist der Wert negativ
- Außerhalb der Form ist der Wert positiv
- An der Grenze liegt der Wert nahe 0
- Eine solche Funktion ist eine Signed-Distance-Field-(SDF-)Funktion
- Mit Distanzwerten lassen sich Füllung und Kontur auf dieselbe Weise behandeln
- Bei
dist < -0.01 wird die Innenfarbe verwendet
- Bei
dist < 0.0 wird eine schwarze Kontur gezeichnet
- Mehrere SDF-Formen lassen sich mit
min() zusammenführen
- Wenn Ricks Kopf und Ohren getrennt konturiert werden, entsteht zwischen ihnen eine unerwünschte Linie
- Werden die beiden Distanzwerte mit
min() kombiniert, bleibt nur die Kontur der Vereinigung von Kopf und Ohren übrig
- Es gibt auch andere Kombinationsarten; als Referenzen sind Inigo Quilez’ 2D distance functions und Material zu Primitive Combination verlinkt
Formen, aus denen Ricks Gesicht besteht
- Die Kopfform entsteht mit
round_rect(), die Ohren werden als separate round_rect()-Formen ergänzt
- Das Rick-Bild vom Poster der ersten Staffel wird blinkend über die Vorschau gelegt, um die Werte abzugleichen
- Viele Zahlen sind durch diesen Trial-and-Error-Prozess entstanden
- Die Farbwerte wurden mit dem Pipettenwerkzeug eines Bildeditors ermittelt
- Die Augen bestehen aus einem kreisförmigen SDF und einem
star() mit 6 Spitzen
- Der Augapfel wird durch Koordinatentransformationen wie
pixel.y *= .93 vertikal leicht gestreckt
- Die Pupille ist ein 6-zackiger Stern-SDF, von dem ein kleiner Distanzwert abgezogen wird, um die Ecken abzurunden
- Beide Augen werden nicht durch duplizierten Code erzeugt, sondern per Links-rechts-Spiegelung mit
pixel.x = abs(pixel.x)
- Nase, Mund und Augenbrauen werden mit
bezier() gezeichnet
- Die Haare beginnen als vertikal gestreckter
star() mit 11 Spitzen
- Auch die Reihenfolge des Codes, der Formen zeichnet, beeinflusst das Ergebnis
- Die zuerst zurückgegebene Farbe wird zur vorne sichtbaren Form
- Zähne und Zunge werden nur innerhalb der Bedingungen für die Mundform gezeichnet und ragen daher nicht aus dem Mund heraus
Techniken zum Verfeinern von Haaren, Zähnen und Zierlinien
- Die starre Sternform der Haare wird durch Domain Warping wellenartig verändert
- Domain Warping verschiebt Koordinaten mit zufälligen Offsets auf Basis der Pixelposition
- Auf dieselbe Position wird derselbe Offset angewendet, wodurch über die Zeit eine konsistente Verzerrung entstehen kann
- Als verwandte Ressource ist Inigo Quilez’ domain warping verlinkt
- Die Zähne werden aus einem einzelnen
parabola()-SDF erstellt und dann mit mod() horizontal wiederholt
mod(pixel.x, width) lässt die x-Koordinate in festen Abständen wieder bei 0 beginnen, sodass dieselbe Form wiederholt wird
- Mit
pixel.y = abs(pixel.y)-.06 werden obere und untere Zähne gespiegelt
- Ein y-Offset auf Basis von
pow(pixel.x, 2.) passt die Zähne an die Kurve des Lächelns an
- Bedingungen wie
abs(pixel.x+.06) < .194 begrenzen die unendliche Wiederholung
- Lippen und Linien unter den Augen werden gezeichnet, indem SDF-Konturen nach außen verschoben werden
- Eine normale Kontur ist
abs(distance_to_shape) < thickness
- Eine nach außen versetzte Kontur ist
abs(distance_to_shape - outset) < thickness
- Die Linien unter den Augen werden zusätzlich über Positionsbedingungen auf bestimmte Bereiche unter den Augen beschränkt
Bewegung über die Zeit erzeugen
- Die einfachste Animation entsteht, indem
sin(time) in den Code eingebaut wird
sin() faltet den fortlaufend steigenden time-Wert in den Bereich von -1 bis 1 zurück und erzeugt so eine wiederholte Animation
- Mit Skalierung und Offset wie
sin(time)*.5 + .5 wird der Bereich angepasst
- Ricks Kopfdrehung, die Drehung der Zunge und die Höhe der Augenbrauen werden auf diese Weise animiert
- Für die Rotationsberechnung wird die Funktion
rotateAt() ergänzt
- Das Blinzeln wird umgesetzt, indem abhängig von der Zeit das gezeichnete Ziel gewechselt wird
- Wenn
mod(time, 2.) < .09 gilt, wird ein geschlossenes Auge gezeichnet
- Andernfalls werden offene Augen und Pupillen gezeichnet
- Für die unregelmäßige Bewegung der Pupillen wird
noise() verwendet
- Damit die Augen sich nicht durchgehend weich bewegen, wird
round(time) angewendet und anschließend an noise() übergeben
- Die Haare bewegen sich durch Time Domain Warping flexibler
- Statt den Raum zu verzerren, wird der Zeitwert abhängig von der Position verzögert
- Je näher an den Haarspitzen, desto anders ist die Zeitverzögerung, sodass sich das Ganze nicht starr dreht, sondern biegt
Portal-Hintergrund und Abschluss
- Die finale Version wird durch einen Portal-Effekt hinter Rick vervollständigt
- Der Portal-Effekt basiert auf einem Effekt des ShaderToy-Nutzers valena; in einer Fußnote steht, dass es sich um eine gekürzte Version von FabriceNeyret2 handelt
- Der Code priorisiert Lesbarkeit gegenüber Performance
- Das Gesamtresultat bündelt Formkombinationen, SDFs, Warping, Wiederholung, zeitbasierte Übergänge und noise-basierte Bewegung, die für 2D-Shader-Animationen nötig sind, in einem einzigen Beispiel
Workflow für den Videoexport
- Der Editor der Seite kann Animationen derzeit noch nicht als Video exportieren
- Als provisorische Methode wird ein macOS-Workflow mit glslviewer und ffmpeg vorgestellt
- Ein Beispiel zur Installation der Abhängigkeiten sieht so aus
brew install glslviewer ffmpeg
- Das Export-Skript erstellt ein temporäres Verzeichnis und führt
glslViewer im Headless-Modus aus
- Auflösung:
1920x1080
- Sequenz: von
0 Sekunden bis 7 Sekunden
- Framerate:
60
- Ergebnisdatei:
animation.mp4
- Das lokale Live-Coding-Beispiel verwendet folgenden Befehl
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0
Supersampling und Ausgangspunkt des Artikels
- Die Kanten der Formen wirken glatt, weil im Hintergrund Supersampling angewendet wird
- Beim Supersampling wird
color_for_pixel() an 9 Positionen innerhalb eines einzelnen Bildschirmpixels aufgerufen und der Durchschnitt angezeigt
- Wenn
#version 300 es verwendet wird, aktiviert sich der „pro“-Modus des Editors und das automatische Supersampling wird entfernt
- Ausgangspunkt des Artikels war das vor 8 Monaten veröffentlichte Video I Made a 3D Modeler, in C, in a Week
- Dieses Video enthält eine Animation, die den Marching-Cubes-Algorithmus erklärt
- Der Autor hatte das Gefühl, dass sie mit herkömmlicher Animationssoftware nur schwer präzise und schnell zu erstellen wäre, und begann daher, sie per Code zu erzeugen
- Nachdem später Leute nach der Methode zur Erstellung der Animation gefragt hatten, fasste er sie in diesem Artikel zusammen
1 Kommentare
Meinungen auf Hacker News
Im Grunde ersetzt man die Funktion
stepdurchaastep: https://github.com/glslify/glsl-aastepDas ist eine sehr andere, kleinteilige und iterative Arbeitsweise als die Web-, Protokoll- und Anwendungsentwicklung, mit der viele vertraut sind.
Einen einzelnen
floatzu ändern,shift-enterzu drücken und sofort das Ergebnis zu sehen, ist ziemlich befriedigend.canvasoder Abstraktionen wie p5.js darüber ähnlich anfühlt.Ich bin mir nicht sicher, ob damit Grafikprogrammierung allgemein gemeint ist oder konkreter GPU-Shader-Arbeit.
Es gibt auch eine passende YouTube-Playlist von Inigo Quilez: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
Shader-Programmierung ist wirklich eine ganz eigene Welt, und der Artikel ist ebenfalls großartig.
Ich frage mich, wie sich das in Vulkan oder WebGPU/WebGL anfühlen würde.
Bei Vulkan ist es genau genommen der Weg von GLSL nach SPIR-V.
WebGPU im Browser kann technisch gesehen GLSL nicht direkt verwenden, aber native WebGPU-Implementierungen können GLSL akzeptieren, und eine Konvertierung ist ebenfalls möglich.
Oder man verwendet einfach WGSL; abgesehen davon, dass die Syntax statt C-artig eher von Rust inspiriert ist, ist es GLSL ziemlich ähnlich.
240 Zeilen mit passenden Dezimalwerten per Trial-and-Error zu erstellen, dürfte ziemlich zeitaufwendig gewesen sein.
Binäre Suche ist auch von Hand schnell.
Die Uniform wird an den Shader übergeben und kann aktualisiert werden, ohne neu kompilieren zu müssen.