2 puntos por GN⁺ 2025-02-08 | 1 comentarios | Compartir por WhatsApp
  • Crea una animación de Rick con solo 240 líneas de código GLSL, donde el shader de GPU calcula el color de cada píxel según el tiempo, sin imágenes ni librerías
  • La clave es que color_for_pixel(pixel, time) se ejecuta en todos los píxeles, y que los campos de distancia con signo (SDF) representan el interior, el exterior y el contorno de las formas mediante valores de distancia
  • El rostro se construye combinando formas SDF como round_rect(), circle(), star(), bezier() y parabola() con desplazamiento, escala, espejo, repetición y unión
  • Las ondas del cabello, la repetición de los dientes, el contorno de los labios, el parpadeo, el movimiento de las pupilas y el balanceo de la cabeza se van sumando paso a paso con domain warping, mod(), sin(time) y noise()
  • La versión final incluye hasta el fondo del portal, pero como el editor no tiene exportación de video, hace falta un flujo de trabajo aparte en macOS con glslviewer y ffmpeg

Estructura del shader que calcula el color por píxel

  • La animación está escrita en OpenGL Shading Language (GLSL)
  • En el editor de live coding de la página se puede ejecutar y modificar el ejemplo al instante
  • El punto de entrada básico es la función color_for_pixel(vec2 pixel, float time)
    • La GPU ejecuta esta función para cada píxel de la vista previa
    • La pregunta que responde la función es: “¿Qué color debería tener este píxel en este momento?”
  • Un ejemplo simple pone los valores pixel.x, pixel.y y time en canales de color para visualizar la posición del píxel y los cambios en el tiempo
  • time son los segundos transcurridos desde la última edición y sigue aumentando continuamente

Cómo se dibujan formas con SDF

  • Un círculo puede dibujarse calculando con length(pixel) la distancia desde el origen hasta el píxel y comparándola con el radio
  • La función circle() no devuelve solo un bool que indique interior o exterior, sino la distancia hasta el borde
    • Dentro de la forma es negativa
    • Fuera de la forma es positiva
    • En el borde es un valor cercano a 0
  • Ese tipo de función es una función de campo de distancia con signo (SDF)
  • Al usar el valor de distancia, el relleno y el contorno se pueden manejar del mismo modo
    • Si dist < -0.01, se usa el color interior
    • Si dist < 0.0, se usa un contorno negro
  • Varias formas SDF se pueden unir con min()
    • Si el contorno de la cabeza y las orejas de Rick se procesa por separado, aparece una línea innecesaria entre ambas
    • Si se combinan ambas distancias con min(), solo queda el contorno de la unión entre cabeza y orejas
  • También hay otras formas de combinación, y se enlazan como referencia los materiales de Inigo Quilez sobre 2D distance functions y combinación de primitivas

Las formas que componen el rostro de Rick

  • La forma de la cabeza se hace con round_rect(), y las orejas se agregan con otro round_rect() aparte
  • Se ajustan los valores superponiendo sobre la vista previa una imagen de Rick del póster de la temporada 1 que parpadea
    • Muchos números se obtuvieron durante ese proceso de prueba y error
    • Los valores de color se sacaron con la herramienta cuentagotas de un editor de imágenes
  • Los ojos se construyen con un SDF circular y un star() de 6 puntas
    • El globo ocular se estira un poco en vertical con una transformación de coordenadas como pixel.y *= .93
    • La pupila redondea sus esquinas restando un pequeño valor de distancia al SDF de estrella de 6 puntas
  • Los dos ojos se hacen con espejo izquierda-derecha usando pixel.x = abs(pixel.x) en vez de duplicar código
  • La nariz, la boca y las cejas se dibujan con bezier()
  • El cabello parte de una estrella de 11 puntas estirada verticalmente
  • El orden del código que dibuja las formas también afecta el resultado
    • El primer color que se devuelve es la forma que queda al frente
    • Los dientes y la lengua solo se dibujan dentro de la condición interna de la forma de la boca, así que no salen fuera de ella

Técnicas para refinar el cabello, los dientes y las líneas decorativas

  • El cabello rígido con forma de estrella se vuelve ondulado mediante domain warping
    • Domain warping es una forma de sacudir las coordenadas con offsets aleatorios basados en la posición del píxel
    • A una misma posición se le aplica el mismo offset, así que puede producir una distorsión consistente a lo largo del tiempo
    • Como referencia se enlaza el material de Inigo Quilez sobre domain warping
  • Los dientes se hacen creando uno con el SDF parabola() y repitiéndolo horizontalmente con mod()
    • mod(pixel.x, width) hace que la coordenada x vuelva a empezar desde 0 a intervalos regulares para repetir la misma forma
    • pixel.y = abs(pixel.y)-.06 refleja los dientes superiores e inferiores
    • Un offset en y basado en pow(pixel.x, 2.) hace que los dientes sigan la curva de una sonrisa
    • Condiciones como abs(pixel.x+.06) < .194 limitan la repetición infinita
  • Los labios y las líneas debajo de los ojos se dibujan empujando el contorno SDF hacia afuera
    • Un contorno normal es abs(distance_to_shape) < thickness
    • Un contorno desplazado hacia afuera es abs(distance_to_shape - outset) < thickness
  • Las líneas bajo los ojos agregan además condiciones de posición para mostrarse solo en ciertas zonas debajo del ojo

Cómo crear movimiento con el tiempo

  • La animación más simple se logra metiendo sin(time) en el código
    • sin() envuelve el valor creciente de time entre -1 y 1 para crear una animación repetitiva
    • Con escalado y offset como sin(time)*.5 + .5 se ajusta el rango
  • La rotación de la cabeza de Rick, la rotación de la lengua y la altura de las cejas se mueven de esta manera
    • Para los cálculos de rotación se agrega la función rotateAt()
  • El parpadeo se implementa cambiando qué se dibuja según el tiempo
    • Si mod(time, 2.) < .09, se dibujan los ojos cerrados
    • En caso contrario, se dibujan los ojos abiertos y las pupilas
  • Para el movimiento irregular de las pupilas se usa noise()
    • Para evitar que los ojos se muevan de forma suave todo el tiempo, se aplica round(time) antes de pasarlo a noise()
  • El cabello se mueve de forma más flexible con time domain warping
    • En vez de deformar el espacio, se retrasa el valor de tiempo según la posición
    • Cuanto más cerca de la punta del cabello, más cambia ese retraso, así que todo no rota rígidamente y más bien se dobla

Fondo de portal y cierre

  • La versión final se completa agregando un efecto de portal detrás de Rick
  • El efecto del portal está basado en uno creado por el usuario de ShaderToy valena, y en una nota al pie se indica que es una versión reducida por FabriceNeyret2
  • El código prioriza la legibilidad por encima del rendimiento
  • El resultado completo reúne en un solo ejemplo la combinación de formas, SDF, warping, repetición, transiciones basadas en tiempo y movimiento basado en ruido necesarios para una animación 2D con shaders

Flujo de trabajo para exportar video

  • El editor de la página todavía no puede exportar la animación como video
  • Como método temporal se propone un flujo de trabajo en macOS con glslviewer y ffmpeg
  • Un ejemplo para instalar dependencias es el siguiente
brew install glslviewer ffmpeg
  • El script de exportación crea un directorio temporal y ejecuta glslViewer en modo headless
    • Resolución: 1920x1080
    • Secuencia: de 0 segundos a 7 segundos
    • Frame rate: 60
    • Archivo de salida: animation.mp4
  • Un ejemplo de live coding local usa el siguiente comando
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

Supersampling y punto de partida del artículo

  • La razón por la que los bordes de las formas se ven suaves es que por detrás se aplicó supersampling
  • El supersampling llama a color_for_pixel() en 9 posiciones dentro de un solo píxel de pantalla y muestra el promedio
  • Si se usa #version 300 es, se activa el modo “pro” del editor y se elimina el supersampling automático
  • El punto de partida del artículo fue el video publicado hace 8 meses I Made a 3D Modeler, in C, in a Week
    • Ese video incluye una animación que explica el algoritmo marching cubes
    • Sintió que en un programa de animación normal sería difícil hacerlo con precisión y rapidez, así que empezó a hacerlo con código
    • Después, como la gente le preguntó cómo hacía las animaciones, lo reunió en este artículo

1 comentarios

 
GN⁺ 2025-02-08
Opiniones en Hacker News
  • Si quieres obtener bordes suaves con antialiasing sin una segunda pasada de supersampling, puedes usar derivadas estándar en SDF.
    Básicamente es reemplazar la función step por aastep: https://github.com/glslify/glsl-aastep
  • Realmente genial. Los desarrolladores de shaders parecen estar en otra dimensión.
    Es una forma de trabajo densa e iterativa, muy distinta al desarrollo web, de protocolos o de aplicaciones al que mucha gente está acostumbrada.
    Cambiar un solo float y presionar shift-enter para ver el resultado de inmediato es bastante satisfactorio.
    • Me pregunto si trabajar con abstracciones como canvas de JavaScript o p5.js encima de eso se siente así.
      No sé bien si se refieren a la programación gráfica en general o más específicamente al trabajo con shaders en GPU.
  • Es un artículo muy bien estructurado como introducción a GLSL.
    También hay una playlist relacionada de Inigo Quilez en YouTube: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • La técnica de hacer parpadear la imagen de referencia sobre la vista previa para compararla con el original mientras se modifica el código es exactamente la misma que se usa en la animación dibujada a mano.
    La programación de shaders es realmente otro mundo, y el artículo también es excelente.
  • Estaba muy bien estructurado como introducción a GLSL.
    Me da curiosidad cómo se sentiría hacer esto en Vulkan o WebGPU/WebGL.
    • Es casi lo mismo. Tanto Vulkan como WebGL pueden usar GLSL directamente.
      En Vulkan, para ser precisos, es una ruta de GLSL a SPIR-V.
      WebGPU ejecutándose en el navegador técnicamente no puede usar GLSL directamente, pero las implementaciones nativas de WebGPU pueden aceptarlo, y también se puede convertir.
      O simplemente puedes usar WGSL, que es casi igual a GLSL salvo que su sintaxis está inspirada en Rust en lugar de tener una sintaxis tipo C.
  • Que haya tomado 8 meses hacer esta animación demuestra una perseverancia impresionante.
  • Me pregunto si el proceso de desarrollo consistió en ajustar valores decimales una y otra vez, o qué editor usó.
    Crear 240 líneas con valores decimales adecuados mediante prueba y error debe haber tomado bastante tiempo.
    • Simplemente usó el editor de código integrado en esa página.
      La búsqueda binaria es rápida incluso a mano.
    • Para este tipo de trabajo, normalmente se toma un slider o un valor de entrada y se conecta a un uniform.
      El uniform se pasa al shader y puede actualizarse sin recompilar.