2 Punkte von GN⁺ 2025-02-08 | 1 Kommentare | Auf WhatsApp teilen
  • 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

 
GN⁺ 2025-02-08
Meinungen auf Hacker News
  • Wenn man weiche Antialiasing-Kanten aus einem SDF ohne einen zweiten Supersampling-Pass erhalten möchte, kann man die Standardableitungen verwenden.
    Im Grunde ersetzt man die Funktion step durch aastep: https://github.com/glslify/glsl-aastep
  • Wirklich beeindruckend. Shader-Entwickler wirken wie aus einer anderen Dimension.
    Das ist eine sehr andere, kleinteilige und iterative Arbeitsweise als die Web-, Protokoll- und Anwendungsentwicklung, mit der viele vertraut sind.
    Einen einzelnen float zu ändern, shift-enter zu drücken und sofort das Ergebnis zu sehen, ist ziemlich befriedigend.
    • Ich frage mich, ob sich die Arbeit mit JavaScript-canvas oder 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.
  • Ein sehr gut aufgebauter Einstieg in GLSL.
    Es gibt auch eine passende YouTube-Playlist von Inigo Quilez: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • Die Methode, ein Referenzbild über der Vorschau blinken zu lassen, um es während Codeänderungen mit dem Original zu vergleichen, ist exakt dieselbe wie bei handgezeichneter Animation.
    Shader-Programmierung ist wirklich eine ganz eigene Welt, und der Artikel ist ebenfalls großartig.
  • Als Einstieg in GLSL war das sehr gut strukturiert.
    Ich frage mich, wie sich das in Vulkan oder WebGPU/WebGL anfühlen würde.
    • Fast genauso. Vulkan und WebGL können beide direkt GLSL verwenden.
      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.
  • Dass es 8 Monate gedauert hat, diese Animation zu erstellen, zeigt enorme Ausdauer.
  • Ich frage mich, ob der Entwicklungsprozess darin bestand, immer wieder Dezimalwerte anzupassen, oder welchen Editor er verwendet hat.
    240 Zeilen mit passenden Dezimalwerten per Trial-and-Error zu erstellen, dürfte ziemlich zeitaufwendig gewesen sein.
    • Ich habe einfach den auf der Seite eingebauten Code-Editor verwendet.
      Binäre Suche ist auch von Hand schnell.
    • Bei solcher Arbeit nimmt man normalerweise einen Slider oder einen Eingabewert und verbindet ihn mit einer Uniform.
      Die Uniform wird an den Shader übergeben und kann aktualisiert werden, ohne neu kompilieren zu müssen.