- 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
Opiniones en Hacker News
Básicamente es reemplazar la función
stepporaastep: https://github.com/glslify/glsl-aastepEs 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
floaty presionarshift-enterpara ver el resultado de inmediato es bastante satisfactorio.canvasde 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.
También hay una playlist relacionada de Inigo Quilez en YouTube: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
La programación de shaders es realmente otro mundo, y el artículo también es excelente.
Me da curiosidad cómo se sentiría hacer esto en Vulkan o WebGPU/WebGL.
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.
Crear 240 líneas con valores decimales adecuados mediante prueba y error debe haber tomado bastante tiempo.
La búsqueda binaria es rápida incluso a mano.
El uniform se pasa al shader y puede actualizarse sin recompilar.