26 octubre 2024

Cómo implementar la oscilación de la vegetación en Godot

Árbol
En artículos anteriores expliqué cómo utilizar shaders para dotar de realismo el agua de una escena 2D. No acaban ahí las cosas que podemos lograr con los shaders. Otra que se suele hacer es que la vegetación se mueva mecida por el viento. Eso dota de vida a la escena haciendo más orgánicos sus elementos.

Para explicar cómo conseguirlo, haré evolucionar el escenario de los artículos anteriores. Como entonces, te recomiendo que te bajes el repositorio en el commit específico de este artículo, para estar seguros de que el código que estudies sea exactamente la misma versión que la que te voy a mostrar yo.

En los shaders anteriores manipulamos los colores a mostrar desde el método fragment(), pero antes que ese los shaders de Godot ejecutan otro método denominado vertex(). Mientras que fragment() se utiliza para manipular el color de los pixels de la superficie a renderizar, vertex() se emplea para deformarla manipulando los vértices de esa superficie. Eso da mucho juego en 3D porque permite deformar mallas de manera dinámica, simulando el movimiento del mar o la deformación que se produce al abrir un surco en la nieve. En 2D también tiene su utilidad, aunque algo mas limitada ya que cualquier sprite sólo tiene 4 vértices que podamos manipular:
  • Superior izquierdo: VERTEX(0,0)
  • Superior derecho: VERTEX(Ancho,0)
  • Inferior izquierdo: VERTEX(0,Alto)
  • Inferior derecho: VERTEX(Ancho,Alto)
El concepto de VERTEX es equivalente al de UV que usábamos en el método fragment(). Si el método fragment() se ejecutaba para cada pixel, de la superficie a renderizar, recogiendo su posición relativa en la variable UV; en el caso del método vertex() este se ejecuta para cada vértice de la superficie, recogiendo su posición en la variable VERTEX. La gran diferencia es que las coordenadas UV están normalizadas entre los valores 0 y 1, mientras que las de VERTEX no lo están y oscilan entre el ancho y el alto en pixels del sprite al que estemos aplicando el shader. Si aun así no reconoces lo que es una coordinada UV, te recomiendo que revises el artículo donde lo expliqué.

No importa que un sprite tenga una forma irregular. En términos del shader su forma es rectángular, con los cuatro vértices anteriores en cada una de las esquinas, sólo que relleno de color transparente (alpha=0) allá donde no haya dibujo (si la imagen del sprite tiene canal alpha).

Si bien un sprite 3D carece de la resolución de vértices de una malla 3D, manipulando la posición de sus vértices podemos estirar y comprimir el sprite. Eso nos viene fenomenal para simular un vegetación comprimida, estirada y, en definitiva, movida por el viento.

Si nos fijamos en el código del ejemplo del repositorio, veremos que he dotado al árbol de un shader cuyo código se encuentra en Shaders/Tree.gdshader. Al ser un shader 2D es del tipo canvas_item, como el de los artículos anteriores. 

Configuración del shader
Configuración del shader


El shader exporta las siguientes variables para que puedan ser editadas a través del inspector:  

Variables exportadas por el shader
Variables exportadas por el shader


La primera variable, wind_strength, sirve para modelar la fuerza del viento. Cuanto mayor sea, más se desplazará la copa del árbol en su oscilación.

La segunda variable, horizontal_offset, se utiliza para que la oscilación no sea simétrica. Cuanto más cerca esté este valor de 1.0 la copa tenderá más a ceñir sus oscilaciones hacia el lado derecho; mientras que lo hará hacia el izquierdo conforme el calor se acerque a -1.0. Si el valor se queda en cero, las oscilaciones serán simétricas.

La variable maximum_oscillation_amplitude marca el valor máximo de desplazamiento en pixels que deseamos para la copa del árbol. Es el desplazamiento que alcanzará la copa del árbol cuando la fuerza del viento sea 1.0.

Por último, la variable time no está pensada para que la manipule el usuario, sino un script externo. La uso para sincronizar la oscilación de la copa del árbol con el desplazamiento de la fuente de partículas que desprende las flores. Lo de la fuente de partículas lo veremos en algún artículo posterior, así que por favor ignóralo. Si no existiese esa fuente de partículas, podría haber usado la variable TIME, que es interna al shader y que ofrece un contador con el tiempo que el shader lleva activo.

Analicemos ahora el método vertex():

Método vertex()
Método vertex()

La línea 11 es clave, porque restringe la manipulación a los vértices superiores. ¿Por qué restringirnos a los vértices superiores? porque la vegetación está firmemente sujeta al suelo por sus raíces, así que el viento sólo agita su parte superior. Por eso, solo modificamos los vértices superiores y dejamos los inferiores en su posición por defecto. En teoría, debería haber podido filtrar comparando VERTEX.y y no UV.y, pero cuando lo he intentado no he conseguido el efecto que quería porque la imagen entera oscilaba, no sólo los vértices superiores.

Recuerda también que la comparación de la línea 11 no puede ser de igualdad, porque estas no funcionan en coma flotante al no ser exactos los números de este tipo. Por eso, la comparación no es "igual a cero" sino "pequeñito hasta casi cero".

La oscilación se genera en la línea 14 usando una sinusoide con una amplitud máxima fijada por wind_strength. Recuerda que una sinusoide te da valores entre -1 y +1, por lo que, al multiplicar por su amplitud, su oscilación resultante estará entre -wind_strength y +wind_strength. Como wind_strength está limitado entre 0 y 1, la oscilación no superará tampoco -1 y +1 en el valor máximo de wind_strength.

Observa que el interior de la sinusoide valora dos sumandos. Por un lado está time, lo que permite que la sinusoide cambie su valor conforme avance el tiempo. Por otro lado está VERTEX.x, dado que este valor es diferente para cada uno de los vértices superiores, esto permite que su oscilación no vaya sincronizada, lo que genera el efecto de estiramiento y aplastamiento. Si estuviésemos renderizando vegetación sin follaje, por ejemplo hierba, y no nos interesase este efecto de estiramiento y aplastamiento, podríamos eliminar el sumando de VERTEX.x, lo que haría que los dos vértices se moviesen de manera acompasada.

En la línea 15 es donde multiplicamos por el valor máximo de desplazamiento, para obtener una oscilación acorde a la amplitud que queríamos obtener.

En la línea 16 es donde añadimos el desplazamiento a la posición horizontal del vértice. 

Y por último, en la línea 17 es donde registramos la nueva posición del vértice.

Al mover sólo los dos vértices superiores y dejar fijos los inferiores, el efecto es como si pinchases con chinchetas las dos esquinas inferiores de un rectángulo de plástico flexible y te dedicases a mover las dos esquinas superiores. 

Deformación de la superficie
Deformación de la superficie obtenida con el shader


Cuando superpones la imagen de un árbol a una superficie deformada de esta manera se obtiene el efecto que queríamos lograr:

Oscilación resultante del árbol
Oscilación resultante del árbol