2 points par GN⁺ 2025-02-08 | 1 commentaires | Partager sur WhatsApp
  • Créer une animation de Rick avec seulement 240 lignes de code GLSL, sans image ni bibliothèque, où un shader GPU calcule la couleur de chaque pixel au fil du temps
  • Le cœur du système : color_for_pixel(pixel, time) s’exécute pour chaque pixel, et un champ de distance signé (SDF) représente l’intérieur, l’extérieur et les contours des formes sous forme de valeurs de distance
  • Le visage est construit en combinant des formes SDF comme round_rect(), circle(), star(), bezier() et parabola() via déplacement, mise à l’échelle, miroir, répétition et union
  • Les ondulations des cheveux, la répétition des dents, le contour des lèvres, le clignement, le mouvement des pupilles et le hochement de tête sont ajoutés progressivement avec du domain warping, mod(), sin(time) et noise()
  • La version finale inclut même un fond de portail, mais l’éditeur ne proposant pas d’export vidéo, un workflow macOS séparé avec glslviewer et ffmpeg est nécessaire

Structure du shader qui calcule la couleur pixel par pixel

  • L’animation est écrite en OpenGL Shading Language (GLSL)
  • Les exemples peuvent être exécutés et modifiés directement dans l’éditeur de live coding intégré à la page
  • Le point d’entrée de base est la fonction color_for_pixel(vec2 pixel, float time)
    • Le GPU exécute cette fonction pour chaque pixel de l’aperçu
    • La question à laquelle répond la fonction est : « quelle couleur doit avoir ce pixel à cet instant ? »
  • Un exemple simple place les valeurs pixel.x, pixel.y et time dans les canaux de couleur afin de visualiser la position des pixels et l’évolution dans le temps
  • time correspond au nombre de secondes écoulées depuis la dernière modification et augmente en continu

Dessiner des formes avec des SDF

  • Un cercle peut être dessiné en calculant la distance entre le pixel et l’origine avec length(pixel), puis en la comparant au rayon
  • La fonction circle() ne renvoie pas un simple bool indiquant l’intérieur ou l’extérieur, mais la distance jusqu’au contour
    • À l’intérieur de la forme : valeur négative
    • À l’extérieur de la forme : valeur positive
    • Sur la frontière : valeur proche de 0
  • Ce type de fonction est une fonction de champ de distance signé (SDF)
  • Les valeurs de distance permettent de traiter le remplissage et le contour de la même manière
    • dist < -0.01 : couleur intérieure
    • dist < 0.0 : contour noir
  • Plusieurs formes SDF peuvent être fusionnées avec min()
    • Si la tête et les oreilles de Rick sont traitées séparément pour le contour, une ligne inutile apparaît entre elles
    • En fusionnant les deux distances avec min(), il ne reste que le contour de l’union de la tête et des oreilles
  • Il existe aussi d’autres modes de combinaison ; les ressources d’Inigo Quilez sur les 2D distance functions et la combinaison de primitives sont citées en référence

Les formes qui composent le visage de Rick

  • La forme de la tête est créée avec round_rect(), et les oreilles sont ajoutées avec un autre round_rect()
  • Une image de Rick issue de l’affiche de la saison 1 est superposée à l’aperçu en clignotant afin d’ajuster les valeurs
    • Plusieurs nombres sont issus de ce processus d’essais et d’erreurs
    • Les valeurs de couleur sont récupérées avec l’outil pipette d’un éditeur d’images
  • Les yeux sont composés d’un SDF circulaire et d’une star() à 6 branches
    • Le globe oculaire est légèrement étiré verticalement par une transformation de coordonnées comme pixel.y *= .93
    • La pupille utilise un SDF d’étoile à 6 branches auquel on soustrait une petite distance pour arrondir les coins
  • Les deux yeux sont créés sans dupliquer le code, grâce à un miroir gauche-droite avec pixel.x = abs(pixel.x)
  • Le nez, la bouche et les sourcils sont dessinés avec bezier()
  • Les cheveux partent d’une star() à 11 branches étirée verticalement
  • L’ordre du code qui dessine les formes influence aussi le résultat
    • La première couleur renvoyée devient la forme visible au premier plan
    • Les dents et la langue ne sont dessinées qu’à l’intérieur du bloc conditionnel de la forme de la bouche, afin de ne pas dépasser à l’extérieur

Techniques pour affiner les cheveux, les dents et les lignes décoratives

  • Les cheveux rigides en forme d’étoile sont transformés en ondulations grâce au domain warping
    • Le domain warping consiste à perturber les coordonnées avec un décalage aléatoire basé sur la position du pixel
    • Le même décalage est appliqué à une même position, ce qui produit une distorsion cohérente dans le temps
    • La ressource d’Inigo Quilez sur le domain warping est citée en lien
  • Les dents sont créées à partir d’un SDF parabola(), puis répétées horizontalement avec mod()
    • mod(pixel.x, width) fait repartir la coordonnée x de 0 à intervalles réguliers, ce qui répète la même forme
    • pixel.y = abs(pixel.y)-.06 applique un miroir entre les dents du haut et du bas
    • Un décalage y basé sur pow(pixel.x, 2.) aligne les dents sur la courbe du sourire
    • Une condition comme abs(pixel.x+.06) < .194 limite la répétition infinie
  • Les lèvres et les lignes sous les yeux sont dessinées en décalant les contours SDF vers l’extérieur
    • Un contour classique : abs(distance_to_shape) < thickness
    • Un contour décalé vers l’extérieur : abs(distance_to_shape - outset) < thickness
  • Les lignes sous les yeux ajoutent des conditions de position pour n’apparaître que dans une zone précise sous les yeux

Créer du mouvement avec le temps

  • L’animation la plus simple consiste à insérer sin(time) dans le code
    • sin() replie la valeur time, qui augmente continuellement, entre -1 et 1 pour créer une animation répétée
    • La plage est ajustée avec une mise à l’échelle et un décalage, par exemple sin(time)*.5 + .5
  • La rotation de la tête de Rick, la rotation de la langue et la hauteur des sourcils sont animées ainsi
    • La fonction rotateAt() est ajoutée pour calculer les rotations
  • Le clignement est implémenté en changeant ce qui est dessiné au fil du temps
    • Si mod(time, 2.) < .09, on dessine les yeux fermés
    • Sinon, on dessine les yeux ouverts et les pupilles
  • noise() est utilisé pour le mouvement irrégulier des pupilles
    • Pour éviter que les yeux bougent en continu de manière fluide, round(time) est appliqué avant de passer la valeur à noise()
  • Les cheveux bougent plus souplement grâce au time domain warping
    • Au lieu de déformer l’espace, on retarde la valeur du temps selon la position
    • Plus on se rapproche de la pointe des cheveux, plus le retard temporel varie, de sorte que l’ensemble se courbe au lieu de tourner rigidement

Fond de portail et finition

  • La version finale ajoute un effet de portail derrière Rick
  • L’effet de portail est basé sur un effet créé par l’utilisateur ShaderToy valena, et une note indique qu’il s’agit d’une version réduite par FabriceNeyret2
  • Le code privilégie la lisibilité plutôt que les performances
  • Le résultat complet rassemble, dans un seul exemple, la combinaison de formes, les SDF, le warping, la répétition, les transitions basées sur le temps et les mouvements basés sur le bruit nécessaires à une animation shader 2D

Workflow d’export vidéo

  • L’éditeur de la page ne permet pas encore d’exporter l’animation en vidéo
  • Une méthode provisoire avec glslviewer et ffmpeg sur macOS est proposée
  • Exemple d’installation des dépendances :
brew install glslviewer ffmpeg
  • Le script d’export crée un répertoire temporaire et lance glslViewer en mode headless
    • Résolution : 1920x1080
    • Séquence : de 0 à 7 secondes
    • Framerate : 60
    • Fichier de sortie : animation.mp4
  • L’exemple de live coding local utilise la commande suivante :
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

Super sampling et point de départ de l’article

  • Les bords des formes sont lisses parce qu’un super sampling est appliqué en arrière-plan
  • Le super sampling appelle color_for_pixel() à 9 positions à l’intérieur d’un même pixel de l’écran, puis affiche la moyenne
  • Utiliser #version 300 es active le mode « pro » de l’éditeur et supprime le super sampling automatique
  • Le point de départ de l’article était la vidéo publiée il y a 8 mois, I Made a 3D Modeler, in C, in a Week
    • Cette vidéo contient une animation expliquant l’algorithme marching cubes
    • L’auteur a estimé qu’il serait difficile de la réaliser précisément et rapidement avec un logiciel d’animation classique, et a commencé à la créer en code
    • Comme des personnes lui ont ensuite demandé comment il créait ses animations, il a synthétisé sa méthode dans cet article

1 commentaires

 
GN⁺ 2025-02-08
Commentaires sur Hacker News
  • Si vous voulez obtenir des bords anti-aliasés lisses depuis un SDF sans second passage de superéchantillonnage, vous pouvez utiliser les dérivées standard dans le SDF
    En gros, il suffit de remplacer step par aastep : https://github.com/glslify/glsl-aastep
  • Vraiment magnifique. Les développeurs de shaders semblent venir d’une autre dimension
    C’est une manière de travailler très dense et itérative, très différente du développement web, des protocoles ou des applications auquel beaucoup de gens sont habitués
    Modifier un simple float et appuyer sur shift-enter pour voir immédiatement le résultat, c’est assez satisfaisant
    • Je me demande si travailler avec JavaScript canvas ou des abstractions par-dessus comme p5.js procure la même sensation
      Je ne sais pas très bien si cela parle de programmation graphique en général, ou plus précisément du travail sur les shaders GPU
  • C’est un article très bien structuré pour débuter avec GLSL
    Il y a aussi une playlist YouTube pertinente d’Inigo Quilez : https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • La méthode qui consiste à faire clignoter l’image de référence au-dessus de l’aperçu pour comparer avec l’original pendant les modifications du code est exactement la même que celle utilisée en animation dessinée à la main
    La programmation de shaders est vraiment un autre monde, et l’article est excellent
  • Très bonne structure pour une introduction à GLSL
    Je me demande ce que cela donnerait avec Vulkan ou WebGPU/WebGL
    • C’est presque pareil. Vulkan et WebGL permettent tous deux d’écrire directement en GLSL
      Avec Vulkan, c’est plus précisément un passage de GLSL vers SPIR-V
      Le WebGPU exécuté dans le navigateur n’utilise techniquement pas directement GLSL, mais les implémentations WebGPU natives peuvent accepter du GLSL, et il est aussi possible de le convertir
      Sinon, on peut simplement utiliser WGSL : c’est presque identique à GLSL, sauf que la syntaxe est inspirée de Rust plutôt que d’une syntaxe de type C
  • Le fait qu’il ait fallu 8 mois pour créer cette animation montre une persévérance impressionnante
  • Je me demande si le processus de développement consistait à ajuster sans cesse de petites valeurs, ou quel éditeur était utilisé
    Construire 240 lignes avec les bonnes valeurs décimales par essais et erreurs a dû prendre énormément de temps
    • J’ai simplement utilisé l’éditeur de code intégré à la page
      La recherche binaire reste rapide, même à la main
    • Pour ce genre de travail, on prend généralement un slider ou une valeur d’entrée qu’on relie à un uniform
      L’uniform est transmis au shader, puis peut être mis à jour sans avoir à recompiler