16 octubre 2024

Cómo implementar el movimiento del agua en Godot

Estampa japonesa con agua en movimiento
En mi anterior artículo expliqué cómo crear un shader para reflejar una escena sobre la superficie de un sprite. Sin embargo, un reflejo de ese tipo sólo es posible en una lámina de agua absolutamente inmóvil. Lo normal es que el agua tenga perturbaciones superficiales que distorsionen ese reflejo. En este artículo vamos a enriquecer el shader anterior para que muestre esa distorsión y que esta vaya cambiando con el tiempo.

Como en otros artículos, puedes encontrar el código de este en mi repositorio de GitHub. Te recomiendo que te lo bajes y lo habrás localmente en tu editor de Godot para seguir las explicaciones, pero asegúrate de bajarte el código del commit concreto que he enlazado. Estoy haciendo constantes cambios en el proyecto del repositorio, así que si te bajas el código de un commit posterior al que he enlazado es posible que lo que veas sea diferente a lo que te muestre aquí. 

No quiero repetir explicaciones, así que si aún no dominas lo que son las coordenadas UV de un shader de Godot, te recomiendo que leas el artículo que enlazaba al comienzo de este.

Por tanto, vamos a evolucionar el shader que aplicábamos al rectángulo del sprite del agua. El código de ese shader está en el fichero Shaders/WaterShader.gdshader.

Para parametrizar el efecto y poder configurarlo desde el inspector, he añadido las siguientes variables uniform:

Parámetros del shader configurables desde el inspector
Parámetros del shader configurables desde el inspector


Los parámetros wave_strength y wave_speed son intuitivos, el primero define la amplitud de las distorsión de la imagen reflejada, mientras que el segundo define la velocidad del movimiento de la distorsión.

Sin embargo, el parámetro wave_threshold necesita cierta explicación. Si pones un espejo a los pies de una persona esperas que el reflejo de dichos pies esté en contacto con los pies mismos. El reflejo es como la sombra, lo normal es que dé comienzo a partir de los pies. El problema es que el algoritmo que vamos a ver aquí puede distorsionar la imagen desde el mismo borde superior del reflejo haciendo que este no esté en contacto con el borde inferior de la imagen reflejada. Eso puede restarle realismo al efecto, así que he añadido el parámetro wave_threshold para definir a partir de qué tanto por uno del sprite se empieza a aplicar el efecto de distorsión. En mi ejemplo está fijado en 0.11, lo que significa que la distorsión se empezará a aplicar a partir de una distancia del borde superior del sprite del agua equivalente al 11% de su anchura (o sea, su UV.y = 0.11).

Por último, está el parámetro wave_noise. Se trata de una imagen. En un parámetro como este podríamos poner cualquier clase de imagen, lo que pasa es que para nuestro algoritmo de distorsión nos interesa una muy concreta. Nos interesa meter una imagen de ruido. Una imagen de ruido contiene manchas blancas y negras aleatorias. Los que ya pinten canas se acordarán de la imagen que se veía en las televisiones analógicas cuando se estropeaba la antena o esta no tenía buena cobertura; en este caso se trataría de una imagen muy similar. Podríamos buscar por internet una imagen de ruido, pero afortunadamente son tan comunes que Godot nos permite generarlas usando un recurso NoiseTexture2D.

Configuración de la imagen de ruido
Configuración de una imagen de ruido.

Una configuración como la de la figura bastará para nuestro caso. En realidad, sólo he cambiado dos parámetros respecto a la configuración por defecto. He desactivado la generación de Mipmaps, porque esta imagen no se verá en la lejanía: sí que he pedido que sea Seamless porque, como veremos más adelante, la recorreremos de manera continua y no querremos que haya saltos perceptibles cuando alcancemos el borde; y, por último, he configurado el algoritmo de generación de ruido a FastNoiseLite, aunque esto da más igual.

La utilidad de la imagen de ruido se va a ver ahora, cuando entremos al código del shader.

Para que te hagas una idea de las magnitudes a configurar para las variables anteriores, yo he usado los siguientes valores (no he incluido en la captura los parámetros que se configuraron en el artículo anterior, para el reflejo estático):

Valores de los parámetros del shader
Valores de los parámetros del shader.

Teniendo en cuenta lo anterior, si vemos la función principal del shader, la de fragment(), comprobaremos que hay muy poca diferencia respecto al shader de los reflejos.

Método fragment() del shader.
Método fragment() del shader.

Si comparamos con el código del artículo de los reflejos veremos que la novedad es que en la línea 50 se ha incluido una llamada a un nuevo método, get_wave_offset(), que devuelve un vector bidimensional que luego se suma a la coordenada UV utilizada para muestrear (en la línea 58) el color a reflejar.

¿Qué está pasando aquí? bueno, el efecto de distorsión del reflejo se consigue no reflejando el color que toca en un determinado punto, sino un color de un punto cercano. Lo que vamos a hacer es ir recorriendo la imagen del ruido. Esta imagen tiene colores que oscilan entre el negro y el blanco puro; es decir, de un valor 0 a 1 (simultáneamente en sus tres canales). Para cada coordenada UV que queramos renderizar del rectángulo del agua muestrearemos un punto equivalente en la imagen de ruido y la cantidad de blanco de ese punto la utilizaremos para desviar el muestreo del color del punto a reflejar en una cantidad equivalente en sus coordenadas X e Y. Dado que la imagen de ruido no tiene cambios bruscos entre negro y blanco, el resultado es que las desviaciones cambiarán gradualmente conforme recorramos las coordenadas UV, lo que hará que la distorsión resultante cambie gradualmente también.

Revisemos el código del método get_wave_offset() para entender mejor lo anterior:

Código del método get_wave_offset()
Código del método get_wave_offset()

Al método se le pasa como parámetro la coordenada UV que estemos renderizando y el número de segundos que han pasado desde el arranque del juego.

La línea 42 del método tiene que ver con el parámetro wave_threshold del que hablábamos antes. Al renderizar el rectángulo del agua desde su borde superior no queremos que la distorsión actué con toda su fuerza desde ese mismo borde porque podría generar aberraciones que llamarían la atención. Imagina por ejemplo, una persona que esté de pie en la orilla, justo al borde del agua; si la distorsión actuase con toda su fuerza desde el borde superior del agua, el reflejo de los pies de la persona aparecería separado de esos pies, lo cual sería poco natural. Así que la línea 42 sirve para hacer que la fuerza de la distorsión se incrementase gradualmente desde 0, hasta que UV.y alcanza el wave_threshold y, a partir de ahí, la fuerza se mantiene en 1.

La línea 43 es la que muestrea la imagen de ruido, con una llamada al método texture(). Si nos limitásemos a muestrear usando exclusivamente la coordenada UV, sin tener en cuenta ni el tiempo ni la velocidad, el resultado sería un reflejo con una imagen distorsionada estática. Vamos a suponer por un momento que fuese así, y que no muestreásemos teniendo en cuenta el tiempo ni la velocidad, lo que pasaría entonces es que para coordenada UV siempre muestrearíamos el mismo punto de la imagen de ruido y por tanto siempre obtendríamos el mismo vector de distorsión (para ese punto). Sin embargo, al añadir el tiempo, el punto de muestreo de la imagen de ruido sería diferente cada vez que renderizásemos la misma coordenada UV lo que provocaría que la imagen de distorsión fuese variando con el paso del tiempo. Entendiendo eso, los factores multiplicativos serían fáciles de entender: wave_speed hace que la variación del muestro de la imagen de ruido cambie con más rapidez lo que acelera también el cambio de la imagen distorsionada; al multiplicar el vector de distorsión resultante por wave_strength_by_distance reducimos la distorsión cuando estemos cerca del borde superior (tal y como explicábamos antes); y multiplicar por wave_strength hace que la amplitud de la distorsión sea mayor al aumentar el vector que devuelve el método.

Y ya está, el vector devuelto por get_wave_offset() es el que se usa luego en la línea 58 del método fragment() para desviar el muestreo del punto a reflejar en el agua. el efecto sería el de la parte inferior de esta figura:

Reflejo distorsionado en el agua.