15 noviembre 2024

Curso "Shader Development from Scratch for Unity" de Penny de Byl

He de reconocer que una de la cosas que más me está costando aprender del desarrollo de juegos son los shaders. Por mucho que ya sepas desarrollar en Unity, Unreal o Godot, con los shaders tienes que empezar desde cero. Son imprescindibles para que las texturas y los efectos visuales de tu juego sean realmente atractivos, pero para empezar a usarlos tienes que dominar un conjunto de conceptos que parecen tener muy poca relación con el resto de disciplinas. No me extraña nada que en los equipos grandes haya gente completamente especializada en este tema. Mi mayor problema sin embargo no es aprender algo nuevo, sino que todos los conceptos parecen artificialmente crípticos, la documentación opaca y la sintaxis de los shaders artificiosa y poco intuitiva. He de reconocer que lo poco que he aprendido de los shaders de Godot me parece lo más intuitivo que he visto hasta ahora, pero incluso así tiene numerosos puntos "ortopédicos". En el caso de Unity, me consta que sus shaders son extremadamente potentes, pero todo me parece bastante revuelto y confuso.

A pesar de ello, yo no cejo en mi empeño de comprenderlos y obtener un razonable dominio de ellos. Por eso, ya llevo varios cursos sobre el tema con los que poco a poco he ido avanzando con el tema, pero mucho menos de lo que prometían sus autores. La razón es que la mayoría de los cursos se limitan a enumerar los pasos para conseguir un determinado efecto, pero muy pocos se detienen a explicar los conceptos que hay detrás de los shaders que justifican esos pasos.

Afortunadamente, Penny de Byl (a la que conocí por sus excelentes cursos sobre inteligencia artificial para juegos) hace un esfuerzo excepcional por explicar esos conceptos en este curso, "Shader Development from Scratch for Unity", disponible en Udemy. No siempre tiene un éxito completo a la hora de explicarlos, pero al menos lo intenta y lo hace desde los conceptos más básicos hasta los más avanzados.

El curso se enfoca a shaders 3D. Así que la mayor parte del tiempo se dedica a cómo aplicar efectos a las texturas de objetos 3D. Todo ello dejando de lado el lenguaje visual de Unity para los shaders y utilizando el lenguaje de programación específico de este engine. A pesar de lo que pudiera parecer a priori, me he dado cuenta de que entiendo mucho mejor los shaders cuando están implementados en código que cuando lo están en cualquiera de los lenguajes visuales de los engines. Debe ser que, como vengo del mundo de la programación, me resulta más sencillo leer código de arriba a abajo que tener que bucear en el plato de spaguetti resultante de un lenguaje visual.

De las 9 secciones de este curso, creo que se pueden entender bastante bien las 6 primeras. Me resultó especialmente esclarecedora la explicación sobre el uso del Dot Product en los shaders. La sección 7 y 8 se empiezan a enredar y me han dado la sensación de que se explican bastante menos. La última sección, en la que se explican los shaders volumétricos, me debe haber cogido especialmente cansado por que reconozco que la he entendido bien poco. Probablemente me dedique a otros temas un tiempo y revisite esa sección pasado un tiempo. A ver si en una segunda pasada, estando yo más fresco, lo explicado en esa sección cobra más sentido.

A pesar de que haya cosas que no haya comprendido, hay otras muchas que me encontrado en otros cursos y que sólo aquí he alcanzado a entender, gracias a las explicaciones de Penny de Byl. Además, la sección de preguntas y respuestas de cada una de las clases resulta casi tan interesante como la clase en si misma. Te recomiendo que te leas todos las preguntas planteadas por otros alumnos en cada una de las clases, probablemente te encuentres con otros que hayan tenido las mismas dudas que tú. La profesora es rigurosa y responde a casi todas las preguntas lo cual resulta muy esclarecedor. Hay veces que incluso reconoce gazapos en la clase o incluye correcciones o enlaces a recursos externos para resolver dudas, lo que resulta especialmente valioso para acabar de encajar todos los conceptos.

¿El curso merece la pena? Sí, sin duda. Si te encuentras el curso rebajado en Udemy, creo que es una buena oportunidad para mojarse los pies en el mundo de los shaders y una buena oportunidad para conocer a una autora con unos cursos la mar de interesantes.

09 noviembre 2024

Introducción a los sistemas de partículas en Godot (1ª parte) - La caída de la flores del cerezo

Cerezo soltando flores
En artículos anteriores trabajamos en una estampa japonesa, usando shaders para dotar de reflejo y movimiento al agua, así como al árbol del escenario. En este artículo vamos a dejar a un lado los shaders y vamos a introducir una herramienta con un concepto engañosamente sencillo, pero que bien utilizado nos puede servir para realizar múltiples efectos. Se trata de los sistemas de particulas.

Un sistema de partículas es un componente que emite un flujo continuo de partículas. Los diferentes efectos se consiguen configurando la cantidad de partículas, su velocidad, dirección o apariencia visual. Mi idea es dotar al escenario japonés del artículo de tres sistemas de partículas: uno que reproduzca la caída de las flores del cerezo, otro que simule visualmente el viento y otro que implemente un efecto de lluvia. 

En este artículo veremos cómo implementar la caída de flores del cerezo de la escena. Verás que es un ejemplo sencillo, pero creo que puede servir entender los sistemas de partículas y perderles el miedo. Además, con pocas variaciones puedes aprovechar este ejemplo para implementar efectos similares, como por ejemplo el de la caída de las hojas en otoño.

Como en los ejemplos anteriores, es importante que te bajes el proyecto que tengo en GitHub para que puedas seguir las explicaciones que voy a dar aquí. Asegúrate de bajar el commit exacto o de lo contrario puede ser que veas un código diferente al de este artículo.

El efecto que queremos conseguir es el de las flores del cerezo cayendo suavemente desde la copa del árbol. Para no perder el carácter zen de la escena, la cantidad de flores a caer será reducida, aunque veremos cómo aumentar su cantidad. De hecho, la incrementaremos sutilmente cuando aumente la fuerza del viento. También veremos cómo mover el sistema de partículas al compás del movimiento de la copa del árbol.

Configuración del sistema de partículas

En Godot, un sistema de partículas es un nodo más, de nombre GPUParticles2D (en 3D hay un nodo equivalente). En nuestro caso, dado que está ligado al árbol, parece lógico que hagamos que el nodo del sistema de partículas sea hijo del nodo del árbol.

Una vez que hayamos creado el nodo por debajo del nodo del árbol podremos empezar a configurarlo.

Nodo del emisor de partículas
Nodo del emisor de partículas

Un emisor de partículas tiene dos niveles de configuración: a nivel de nodo y a nivel de material.

La configuración más básica se hace a nivel de nodo:

Configuración de nodo del emisor de partículas.
Configuración de nodo del emisor de partículas

Hay muchísimas cosas que se pueden configurar, así que me voy a centrar en explicar las que yo he usado para este ejemplo.

El campo Emitting es el que activa el emisor de partículas. Si lo desmarcamos el emisor dejará de funcionar.

El campo Amount es el caudal de partículas a emitir por unidad de tiempo. Yo quería un efecto muy sutil, así que lo he fijado en 1, pero si quisieras ver el emisor en todo su esplendor podrías fijarlo a 5 o a 10 y verías al cerezo en todo su apogeo.

El campo Amount Ratio se multiplica en cada ciclo por el valor del campo Amount. Es importante porque nos permite variar el caudal del emisor sin que este se reinicie. Si variases por script el campo Amount verías que el emisor se reiniciaría completamente desde el principio, lo que quedaría poco realista. Sin embargo, si lo que cambias en el Amount Ratio, el emisor no se interrumpirá, pero reducirá su caudal. 

El campo Time--> Lifetime es el que define cuánto tiempo se mantiene visible la partícula, desde si nacimiento, hasta que desaparece. Yo lo he fijado a 3 para que le dé tiempo a las flores a alcanzar el suelo antes de desaparecer.

En cuanto al campo Drawing--> Visibility Rect, nos permite dimensionar un rectángulo dentro del cual serán visibles las partículas del emisor. En cuanto las partículas abandonen el recuadro, dejarán de ser visibles. Lo normal es fijar el rectángulo para que sea algo más grande que la pantalla de manera que las partículas sean visibles dentro de esta, pero dejen de procesarse al salir de ella.

El último campo del nodo que he configurado es el de Texture. En él podemos meter un sprite que le dé apariencia visual a la partícula. En vez de un sprite individual, yo he usado un sprite sheet para generar una animación. 

Un sprite sheet es una imagen compuesta por imágenes individuales. Se usa por comodidad, para que el grupo artístico le pase a los desarrolladores todas las imágenes en unos pocos sprite sheets, en vez de en innumerables ficheros. Por otro lado, los engines están optimizados para leer sprite sheets. Eso quiere decir que es mucho más eficiente que el engine cargue memoria un sprite sheet y "recorte" de él las imágenes individuales que forzarle a abrir y cargar en memoria múltiples ficheros de imagen. 

Sprite sheet de las flores
Sprite sheet de las flores

Puedes hacer un sprite sheet juntando las imágenes individuales en un editor como Gimp o usando una herramienta especializada como TexturePacker. Yo he usado esta última. Lo importante en todo caso es que, si quieres usar un sprite sheet para generar una animación, ordenes las distintas imágenes individuales horizontalmente y de manera equiespaciada a lo largo de la imagen conjunta.

Para animar el sprite tienes que configurar el nodo a nivel de material y, para ello, tienes que dotar al campo Process Material de un recurso ParticleProcessMaterial y entrar a configurar este. Hecho eso, tienes que configurar su parámetro Process Material--> Animation --> Speed a 1 (tanto min como max) y el parámetro Process Material--> Animation --> Offset (con el min en 0 y el max en 1). El resultado es que el sprite sheet se recorrerá a velocidad constante conforme caiga la partícula, pero no siempre empezará desde el mismo punto. Eso hará que la flor cambie de sprite conforme vaya cayendo, como si fuera girando con la caída, pero al no empezar todas las partículas desde la misma imagen individual, habrá más variedad y será más orgánico. Precisamente, para ahondar esa variedad he hecho que las distintas partículas sean de distinto tamaño, haciendo que se generen de un tamaño aleatorio entre 0.03 y 0.05, para ello he fijado el parámetro Process Material--> Display --> Scale en mínimo 0.03 y máximo 0.05.

Para que las flores no estén visibles exactamente el mismo tiempo, lo que sería poco natural, he fijado el parámetro Process Material --> Lifetime Randomness a 0.07. Eso hace que el tiempo de vida de cada partícula varíe ligeramente de manera aleatoria.

Como la escena es 2D, he desactivado la posibilidad de que las partículas se muevan en el eje Z con el parámetro ProcessMaterial --> Particle Flags --> Disable Z.

Otra configuración importante es la forma del generador de partículas, Estas se generan en puntos aleatorios del interior de un volumen con una forma geométrica determinada. Por defecto, esta forma es un cubo. Pero se pueden configurar múltiples formas: un punto o varios, el volumen de una esfera, la superficie de una esfera o un anillo. En mi opinión, la forma de la copa del cerezo se parece más a una esfera que a un cubo, así que he fijado el volumen de generación a la primera forma, en el parámetro Process Material--> Spawn--> Position--> Emission Shape. El radio de la esfera se fija con el parámetro Process Material--> Spawn--> Position--> Emission Shape Radius. También he escorzado ligeramente la esfera a la derecha (ya que la copa del árbol es ligeramente asimétrica hacia ese lado) fijando a X=100, Y=0 y Z=0 el parámetro Process Material--> Spawn--> Position--> Emission Shape Offset.

Para que la partícula caiga, hay que configurar una fuerza de la gravedad, esto se hace fijando un valor para la componente Y del parámetro Process Material --> Accelerations--> Gravity. Mediante prueba y error, he llegado a la conclusión de que el valor que mejor me encajaba era 50.

Movimiento del sistema de partículas

El sistema que hemos configurado se comporta como un volumen esférico en el que se crean aleatoriamente partículas que caen al suelo por efecto de la gravedad. Si nuestro árbol estuviera quieto, ahí podríamos acabar el artículo, pero no es así. En capítulos anteriores implementamos que el árbol pudiera oscilar por efecto del viento. Quedaría raro que la copa del árbol se moviese y que la esfera de creación de las partículas no se moviese en la copa. En un caso extremo, la copa podría oscilar tanto que la esfera de creación de las partículas podría quedar fuera de ella haciendo que las flores pareciesen surgir del aire. No, la esfera de creación de las partículas se tiene que mover junto con la copa, al compas de la oscilación de esta. Si la oscilación de la copa fuera física, quizás podríamos aprovechar que el sistema de partículas es hijo del árbol, pero la oscilación es un mero efecto visual obtenido a través de un shader. Más allá de la apariencia visual, lo cierto es que el árbol no altera su posición por lo que el su sistema de partículas hijo tampoco lo hará.

En vez de eso, lo que vamos a hacer es dotar al nodo del sistema de partículas de su propio script de tal manera que sea este el que se encargue de desplazar su posición. Este script reproducirá el mismo algoritmo que el del shader de manera que el desplazamiento del sistema de partículas sea similar al desplazamiento de la copa.

El script del sistema de partículas se encuentra en la ruta Scripts/LeafEmitter.cs.

Se configura desde el inspector a través de 4 campos exportados:

Campos exportados por LeafEmitter.cs
Campos exportados por LeafEmitter.cs

El primer campo, el de _minimumFlowerAmount es la cantidad de flores emitidas por el sistema cuando el árbol se encuentra en reposo,

El segundo campo, OscillationMultiplier, sirve para definir la amplitud máxima del movimiento oscilante del sistema de particulas.

El tercer campo es WindStrength, es la fuerza del viento.

Por último, el campo HorizontalOffset es similar al uniform del mismo nombre que usamos en el shader del árbol para sesgar la oscilación hacia la derecha o hacia la izquierda.

Una vez fijados esos campos, se ejecuta el siguiente algoritmo en cada fotograma visual:

Método _Process() de LeafEmitter.cs
Método _Process() de LeafEmitter.cs

En sí mismo el algoritmo es muy sencillo: en la línea 55 se calcula la oscilación y en la línea 57 se usa esa oscilación para desplazar la posición horizontal del sistema de partículas.

La oscilación se calcula a partir del método get_oscillation():

Método get_oscilation() en LeafEmitter.cs.
Método get_oscilation() en LeafEmitter.cs.

Este método se limita reproducir el algoritmo que ya usamos en el shader del árbol para definir el movimiento de los vértices superiores de la textura.

Claro, para que el movimiento oscilatorio del sistema de partículas coincida con el de la copa no sólo se necesita que su algoritmo sea el mismo, sino que lo alimentemos con las mismas variables. Eso lo podemos hacer manualmente desde el inspector, asegurándonos de que los valores del shader del movimiento del árbol y los del movimiento del sistema de partículas sean los mismos, pero es mucho más sencillo si implementamos un mecanismo de sincronización entre el shader del árbol y el script del sistema de partículas.

Sincronización del shader con el sistema de partículas

Esa sincronización se puede hacer desde un script situado en el árbol y que pueda acceder simultáneamente al shader del árbol y al script del sistema de partículas. 

El script se encuentra en la ruta Scripts/Tree.cs.

Los campos de configuración que exporta al inspector son los siguientes:

Campos exportados por Tree.cs
Campos exportados por Tree.cs

El truco está en que esos campos son en realidad propiedades que aplican sus modificaciones tanto al shader de la oscilación del árbol, como al script LeafEmitter del emisor de partículas.

Propiedad de WindStrength de Tree.cs
Propiedad de WindStrength de Tree.cs

Como se puede ver en el código anterior, cuando se le pide un valor a la propiedad, esta devuelve el valor fijado en el shader de la oscilación del árbol (línea 20). Cuando, por el contrario, se le pide fijar un valor, la propiedad se lo fija al shader (línea 27) y también al script del emisor de partículas (línea 30).

Las otras dos propiedades, HorizontalOffset y MaximumOscillationAmplitude, tienen un funcionamiento similar.

De esta manera, cualquier cambio que hagamos en las propiedades de Tree.cs se replicarán automáticamente en las propiedades equivalentes de LeafEmitter.

Eso sirve para configurar todas las propiedades de la oscilación menos una: el tiempo. ¿Cómo podemos hacer para que tanto Tree como LeafEmitter usen la misma referencia de tiempo para sus respectivas oscilaciones? generando la cuenta de tiempo nosotros y pasándosela tanto al shadel del árbol como al script del emisor de partículas.

El método _Process() del script del árbol es el que lleva la cuenta:

Método Process() de Tree.cs
Método Process() de Tree.cs

En la línea 102 del método nos limitamos a sumar el tiempo desde el último fotograma (delta).

Time es en realidad una propiedad, y es la que se encarga de actualizar tanto el shader de la oscilación del árbol, como el emisor de partículas.

Propiedad Time de Tree.cs
Propiedad Time de Tree.cs

La línea 83 es la que actualiza el shader y la 84 el emisor de partículas. Precisamente por esto exponíamos un uniform para el tiempo desde el shader del árbol.

Con el mismo algoritmo, los mismos parámetros y la misma variable de tiempo, el árbol y el emisor de partículas pueden oscilar al compás. En la siguiente captura he seleccionado el nodo del emisor de partículas. Su posición se señala con una cruz. Fíjate cómo esta se desplaza de manera acompasada con el árbol.


Aquí acaba esta introducción a los emisores de partículas. En futuros artículos explicaré cómo implementar un efecto lluvia, así como una representación visual del viento.