Mirando tutoriales de shaders, me he dado cuenta de que suelen caer en dos extremos: o son demasiado simples o excesivamente complejos. Eso me hizo ver que, en realidad, no estaba entendiendo cómo funcionan los shaders.
Aprovechando que ChatGPT tiene paciencia infinita, me he puesto a analizar el tema con calma para intentar comprender cómo funciona todo esto desde la base.
Esta entrada no va a ser el típico copia y pega. Aquí hay un proceso real de análisis y reflexión. El día que las IAs desaparezcan, no quiero que me pillen al descubierto sin saber hacer ni un “hola mundo”.
Conceptos básicos antes de empezar
Antes de empezar, conviene tener claras algunas ideas básicas sobre cómo funcionan los shaders.
La función fragment() se ejecuta de forma independiente para cada píxel del sprite, incluidos los píxeles transparentes. En cada ejecución, el shader solo trabaja con la información de ese píxel concreto, sin tener en cuenta los píxeles que lo rodean.
La variable UV representa las coordenadas del píxel dentro del sprite. Son coordenadas relativas que van de 0 a 1, con valores decimales, donde (0,0) corresponde a una esquina del sprite y (1,1) a la esquina opuesta.
La variable TEXTURE es, sencillamente, la imagen del sprite. Contiene la información del color de cada píxel.
Usando TEXTURE junto con UV en la función texture(), por ejemplo texture(TEXTURE, UV), obtenemos en cada ejecución de fragment() la información del píxel actual, como el color que tenía originalmente.
Ejercicio 1: un círculo en el centro
Vamos a empezar con un primer shader muy simple, que consiste en dibujar un círculo en el centro de un Sprite2D, para entender cómo se usan las coordenadas UV.
Este es el resultado. Podemos ver que el círculo se dibuja de forma ovalada. Esto ocurre porque, como hemos comentado antes, las coordenadas UV son relativas al tamaño del sprite. Al no ser un cuadrado, el espacio en el que trabaja el shader está estirado en uno de los ejes.
Si ensanchamos el Sprite2D hasta convertirlo en un cuadrado, veremos que el shader dibuja un círculo perfecto. El código no ha cambiado; lo único que ha cambiado es la proporción del sprite sobre el que se aplican las coordenadas UV.
Ejercicio 2: De ejercicio simples a la diana
A continuación, vamos a seguir con una serie de ejercicios, de menor a mayor complejidad, hasta llegar a la diana que se muestra en la cabecera de esta entrada.
Empezaremos creando un shader que dibuje una diana ocupando todo el sprite, ignorando por completo la textura original.
La línea de código int ring = int(dist / ring_size); actúa como un identificador del número de anillo en el que se encuentra el píxel actual. A partir de este valor, el shader puede decidir cómo pintar cada franja de la diana, teniendo en cuenta que cada ejecución de fragment() se realiza de forma independiente para cada píxel.
Un ejemplo que me dio ChatGPT para entenderlo mejor.
Este es el resultado que deberia mostrar el shader.
Ejercicio 3: Controlando el número de anillos
Vamos a añadir dos mejoras. Por un lado, podremos controlar el número de anillos. Por otro, la diana se dibujará únicamente sobre el sprite.
De momento, el ancho de las anillas se mantendrá relativo al número de anillos, de forma que la diana ocupe todo el sprite.
Antes de mostrar el resultado, conviene aclarar el significado de la instrucción float ring_size = 0.5 / float(ring_count);.
El valor 0.5 representa el radio máximo del sprite en coordenadas UV, es decir, la distancia desde el centro hasta uno de los bordes. A partir de ese valor se reparte el espacio disponible entre el número de anillos indicado.
Si utilizamos un valor mayor, como 1.0, los anillos se calculan más allá del área visible del sprite, lo que provoca que el patrón se recorte en los bordes y en las esquinas.
En cambio, si usamos un valor menor, como 0.1, la diana solo ocupa una pequeña zona central del sprite, dejando el resto sin afectar.
Y este es el resultado de este shader.
Ejercicio 4: Una diana que no se expande por todo el Sprite
Finalmente, vamos crear la diana, como la que sale en la cabecera, esta vez queremos que no se expanda y se ajuste a un numero de anillas y al ancho de la diana.
Ejercicio 5: Animando la diana
Como cierre final, quiero mostrar por qué merece la pena aprender a trabajar con shaders en Godot. Su verdadera potencia no está en efectos simples como el parpadeo, que es lo que suelen enseñar muchos tutoriales básicos, sino en la posibilidad de crear animaciones más complejas de forma elegante y controlada.
En este último ejemplo, vamos a preparar el shader para animar la diana, haciendo que las anillas se expandan y se contraigan de forma continua, demostrando que este tipo de efectos se pueden lograr sin recurrir a soluciones más torpes o externas.
La generación de las anillas se basa en la distancia al centro. Mediante el nuevo parámetro offset lo único que hacemos es modificar esa distancia, desplazando el punto desde el que se calcula el patrón:
La generación de las anillas se basa en la distancia al centro. Mediante el parámetro offset, lo único que hacemos es modificar esa distancia, desplazando el punto desde el que se calcula el patrón.
Ahora vamos a animar el shader usando un AnimationPlayer. Desde ahí podemos animar las variables uniform que hemos ido creando, pulsando el icono de la llave. A efectos prácticos, un uniform se comporta de forma muy similar a un @export en GDScript: es un valor externo que podemos modificar sin tocar el código.
Una vez dentro del AnimationPlayer, el flujo es el habitual. No hay ningún misterio especial.
En el segundo 0 el valor de offset es 0.0, y en el segundo 5 pasa a 1.0.
En el segundo 0 el valor de offset es 0.0, y en el segundo 5 pasa a 1.0.
Así es como queda la animación. Ahora ya me dirás tú cómo harías este mismo efecto usando nodos habituales de Godot, sin que acabe siendo un dolor de cabeza innecesario.
¡Pero qué bien me ha quedado la entrada!
Más allá del resultado, me ha servido de verdad para empezar a entender los shaders. La entrada 158 original (posiblemente la siguiente) era demasiado simple, y al hacerla me di cuenta de que no estaba entendiendo absolutamente nada. Podría hacer mil tutoriales de shaders, pero seguiría atascado en el mismo punto.
Todo apunta a que este va a ser el tema de la temporada del blog, porque le veo mucho potencial y, por una vez, siento que estoy avanzando de verdad.
No hay comentarios:
Publicar un comentario