27 octubre 2024

Curso "Make Online Games Using Unity's NEW Multiplayer Framework" de GameDevTV

Tanks
Llevo un tiempo haciendo cursos para aprender las funcionalidades multijugador que ofrecen tanto Godot, como Unity. Se trata de un campo muy complejo, al que hay que acceder cuando ya se tiene un dominio de lo básico del engine, y que además ha recibido importantes novedades en ambos casos, por lo que conviene que el curso que hagamos esté actualizado.

En este caso, le he dedicado tiempo a la propuesta de haciendo el cursos "Make Online Games Using Unity's NEW Multiplayer Framework" que ofrece GameDevTV en la plataforma de Udemy. También lo tienen en la plataforma propia de GameDevTV, pero lo adquirí durante unas rebajas de Udemy.

El curso parte del desarrollo de un sencillo juego de disparos, en el que el tanque de cada jugador se muve por un escenario, en perspectiva cenital, disparando a los tanques del resto de jugadores. Como en otros cursos de GameDevTV, aparte del núcleo central del curso, en este caso centrado en el multijugador, se revisan otras mecánicas típicas de juegos, tales como: movimiento, input del jugador, disparos, gestión de vidas y recogibles, GUI, así como sistemas de partículas (en este caso me gustó mucho cómo implementa las huellas de las cadenas). Todas esas mecánicas se explican con detalle y el código necesario para implementarlos es de calidad. En general, me ha parecido que el profesor tiene experiencia y enfoca el código según las buenas prácticas.

En lo referente a la parte de networking, se trata de un tema muy extenso y lo explica bien. Me hubiera gustado que hubiese se hubiese detenido un poco más en los conceptos fundamentales de RPC, porque después de acabar el curso sigo teniendo la sensación de que no he acabado de coger todos los matices de algunos conceptos. Mi impresión es que en la parte de RPC se limita a explicar cómo se hacen las cosas, pero me hubiera gustado algo más de por qué se hacen las cosas así en RPC. De todos modos, sí que me parece que la parte de RPC es similar a la de Godot, así que si ya la dominas por ese lado, probablemente te resulte fácil el enfoque de Godot. He de reconocer que no es mi caso aún.

Donde sí explica con todo lujo de detalles, y es de agradecer, es toda la integración con los servicios multijugador de Unity Game Services (UGS). Eso es estupendo porque UGS ofrece muchísimos servicios, listos para ser usados, lo que nos ahorra tener que implementarlos desde cero y tener que mantenerlos. Analizando todo lo que ofrece UGS es cuando uno se da cuenta de que Godot está muy verde en este apartado. Incluso comparando lo que ofrece W4 Games con lo que tiene UGS la primera sale perdiendo con sólo un subconjunto de las cosas que ya tiene maduras la segunda.

Al final, la impresión del curso es muy buena. Creo que cubren las temáticas más importantes para un juego que reúna a grupos de hasta varias decenas de jugadores. Si lo que buscas es cómo crear un MMORPG con Unity probablemente esto no te valga, pero tampoco tengo claro si Unity y UGS son las mejores opciones para un juego así.

En resumen, un curso recomendadísimo para los que quieran hacer juegos multijugador, pero mi recomendación es que no lo hagas hasta que te sientas realmente a gusto con el engine. Es de nivel medio-avanzado. 


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


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.

11 octubre 2024

Cómo implementar el reflejo del agua en Godot

La imagen japonesa de nuestro ejemplo
Me encanta la sencillez de los shaders de Godot. Es increíble lo sencillo que resulta aplicar muchísimos de los efectos visuales que dotan de vida a un juego. He estado aprendiendo alguno de esos efectos y los quiero practicar en un escenario de pruebas que combine varios de ellos.

El escenario representará una típica postal japonesa en pixel-art: un cerezo, movido por el viento, soltando flores y enmarcado en una noche estrellada de luna llena, con el monte Fuji al fondo y un cauce de agua en primer plano. Aparte del movimiento del árbol, quiero implementar los efectos de las flores cayendo, lluvia, viento, movimiento de las nubes, rayos y el agua, tanto en su movimiento como en su reflejo. En este primer artículo cubriré como implementar el reflejo en el agua.

Lo primero es que el código fuente de este proyecto está en mi repositorio de GitHub. El enlace apunta a la versión del proyecto que implementa el reflejo. En futuros proyectos, marcaré con etiquetas los commits donde se incluya el código de ese proyecto y serán esos commits los que enlace en el artículo. Conforme avance con los artículos, es posible que toque código anterior, así que es importante que te bajes el commit que enlace para asegurar que veas el mismo código que en el artículo. 

Te recomiendo que bajes el proyecto y lo abras en Godot para que puedas ver cómo he conformado las escena. Verás que lo he hecho superponiendo Sprites2D en el siguiente orden de Z-Index:

  1. Cielo estrellado
  2. Luna
  3. Monte Fuji
  4. Nube grande negra
  5. Nube media gris
  6. Nube pequeña
  7. Césped
  8. Árbol
  9. Agua
  10. Occluder del cesped
Recuerda que los sprites con un Z-Index menor se pintan antes que los que tienen uno mayor, lo que permite que estos últimos tapen a los primeros.

El occluder del césped es una especie de parche que uso para tapar parte del sprite del agua y que no se note que este es rectangular. Más adelante veremos por qué lo he usado.

Configuración del agua


Vamos a centrarnos en el agua. Su Sprite2D (Water) no es más que un cuadrado en blanco. Lo he movido y redimensionado para que ocupase todo el tercio inferior de la imagen. Por dejar más ordenada la escena, lo he puesto en su propia capa (la 6) fijando tanto su "Visibility Layer", como su "Light Mask" en su configuración de CanvasItem. 

En esa misma sección, la de CanvasItem, he fijado su Z-Index a 1000 para asegurarme que está por encima del resto de sprites de la escena, salvando el occluder. Esto tiene sentido no sólo desde el punto de vista artístico, ya que el agua se encuentra en primer plano de la escena, sino también porque la técnica que vamos a ver en este artículo sólo permite reflejar las imágenes de aquellos objetos que tengan un Z-Index inferior al del nodo del shader. Ahora veremos la razón.

Por último, he dotado a la propiedad "Material" del agua con un ShaderMaterial basado en GDShader.

Vamos a ver el código de ese shader.

Shader del reflejo del agua


Este shader se encuentra en el fichero Shaders/WaterShader.gdshader del repositorio. Como no puede ser de otra manera, ya que la escena es 2D, se trata de un shader del tipo canvas_item.

Ofrece los siguientes parámetros al exterior:

Uniforms del shader

Todos ellos se pueden fijar desde el inspector, salvando el de screen_texture:

Inspector del shader

Recuerda que, como ya he contado en artículos anteriores, un uniform Sampler2D que tenga el atributo de hint_screen_texture se tratará como una textura que se actualizará constantemente con la imagen dibujada en la pantalla. Eso nos permitirá acceder a los colores presentes en la pantalla, para replicarlos en el reflejo del agua.

Este shader en concreto no deforma la imagen, sólo juega con sus colores, así que sólo tiene componente fragment():

Método fragment
Método fragment()

Como se pude ver en el código anterior, el código del shader se basa en tres pasos.

El primer paso es el de la línea 33. En él se calcula qué punto de la imagen en pantalla es el que se refleja en el punto del agua que está renderizando el shader.

Una vez que sabemos qué punto reflejar, se muestra la imagen de la pantalla en ese punto para obtener su color (línea 38).

Por último, el color a reflejar se mezcla con el que deseamos para el agua para dar lugar al color definitivo a mostrar al renderizar. Recuerda que, en GDShader, el color que guardes en la variable COLOR será el que finalmente se muestre en el pixel a renderizar.

El segundo paso (línea 38) ya lo hemos visto en otros shaders de artículos anteriores. Básicamente es cómo usar la herramienta de cuentagotas de Photoshop o Gimp: coge el color de un punto determinado de una imagen. Aquí sólo hay que tener en cuenta que, en el momento que ejecutamos el shader, la imagen está conformada sólo por aquellos sprites que se hayan dibujado hasta ese momento. Es decir, por los sprites con Z-Index inferiores al sprite con nuestro shader. Si lo piensas tiene sentido: no puedes muestrear un color de la pantalla que aún no se ha pintado en ella. Si echas en falta objetos en el reflejo, no pienses que se han vuelto vampíricos, los más probable es que su Z-Index sea superior al del nodo del shader.

Entendido lo anterior, vamos a ver cómo calcular la coordenada de la pantalla a reflejar:

Código del método get_mirrored_coordinate.
Método get_mirrored_coordinate()

En este método se pone a prueba tu comprensión de lo que son los distintos tipos de coordenadas que se usan en un shader 2D.

Recuerda que en 2D tienes dos tipos de coordenadas:
  • Coordenadas UV: Estas coordenadas tiene componentes X e Y y ambas van de 0 a 1. En el rectángulo del sprite del agua que queremos renderizar con nuestro shader,  el origen de coordenadas corresponde a la esquina superior izquierda del rectángulo. Hacia la izquierda crece el eje X y hacia abajo el eje Y. La esquina inferior derecha del rectángulo del agua se corresponde con el límite de coordenadas, (1, 1).
  • Coordenadas SCREEN_UV: Su orientación es la misma que la de las coordenadas UV, con la diferencia de que el origen de coordenadas está en la esquina superior izquierda de la pantalla y el límite de coordenadas en las esquina inferior derecha de esta. 
Fíjate que cuando el sprite del agua se muestra por completo en la pantalla, el rango que cubren las coordenadas UV resultan ser un subconjunto de las coordenadas SCREEN_UV.

Para entender mejor cómo funcionan los dos tipos de coordenadas UV, fíjate en el siguiente diagrama:

Tipos de coordenadas UV


El diagrama pretende representar de manera esquemática la escena con la que estamos trabajando. La zona roja representa la imagen mostrada en la pantalla; en nuestro caso comprendería: el cielo, las nubes, la luna, la montaña, el árbol, la hierba y el agua. Por su parte el rectángulo azul representa al agua, y en concreto al sprite rectangular al que se aplica el shader que estamos implementando.

Ambos sistemas de coordenadas comienzan en la esquina superior izquierda. El sistema de SCREEN_UV en la esquina superior izquierda de la pantalla; y el sistema UV en la esquina superior izquierda del sprite al que se le aplica el render. En ambos casos, el extremo del sistema de coordinadas, el punto (1,1), se encuentra en la esquina inferior derecha de la pantalla y del sprite respectivamente. Fíjate que se trata de coordenadas normalizadas, así que siempre nos moveremos entre el 0 y el 1, independientemente del tamaño real del elemento.

Para explicar cómo calcular el reflejo, he incluido un triángulo azul para representar el monte Fuji. El de la zona roja es la montaña en sí, mientras que el triángulo de la zona azul representa su correspondiente reflejo.

Supongamos que nuestro shader está renderizando la coordenada (0,66, 0,66), representado en la figura (por favor, no te tomes demasiado en serio las medidas de la figura, todo está colocado a ojo). El shader no sabe qué color mostrar como reflejo, para ello tiene que muestrear el color de un punto de la zona roja ¿pero cual?. 

Calcular la coordenada X del punto reflejado es fácil, porque es la misma que la del punto de reflejo: 0,66.

El truco está en la coordenada Y. Si el punto de nuestro reflejo está en la coordenada UV.Y = 0,66 significa que se encuentra a una distancia se 1 - 0,66 = 0,33 del borde inferior (vamos a redondear al segundo decimal para entendernos). En la composición que nos ocupa, en la que la imagen que se refleja está arriba y su correspondiente reflejo abajo, lo natural es esperar que la imagen aparezca verticalmente invertida al reflejarse. Por eso, si a al punto de reflejo le quedaba una distancia de 0,33 para llegar al borde inferior del rectángulo, eso significa que al punto reflejado se encontraba a 0,33 del borde superior de la pantalla. Por tanto la coordenada Y del punto reflejado será 0,33. Este es precisamente el cálculo que se hace en la línea 11 del listado del método get_mirrored_coordinate().

Por tanto, conforme el shader vaya recorriendo el rectángulo de izquierda a derecha y de arriba a abajo, para renderizar sus puntos, irá muestreando también la pantalla de izquierda a derecha y de abajo a arriba (observa la diferencia) para adquirir los colores de los puntos a reflejar.

Este proceso tiene dos efectos colaterales a tener en cuenta.

El primero es que si la superficie de reflejo (el rectángulo de nuestro shader) tiene menos anchura vertical que la superficie reflejada (la pantalla), como es nuestro caso, el reflejo será una versión "aplastada" de la imagen original. Se puede ver a qué me refiero en la imagen que abre el artículo. En nuestro caso, eso no es un problema, y es incluso deseable porque da más sensación de profundidad, ya que ese efecto es el que se daría si el plano del agua fuese longitudinal a nuestro eje de visión.

El segundo efecto colateral es que, si vamos recorriendo la pantalla para coger los colores a reflejar, habrá un momento en el que muestreemos el tercio inferior de la pantalla en el que está el mismo rectángulo del agua. Se producirá entonces un fenómeno interesante: ¿Qué pasará cuando el shader del reflejo empiece a muestrear pixels donde aparece el área sobre el que pinta el shader del reflejo?. Bueno, en el mejor de los casos, el color que se muestree será negro ya que estaremos intentando muestrear pixels sobre los que no se ha pintado aún (precisamente es en eso en lo que se ocupa nuestro shader), así que lo que se reflejará será una mancha negra. Para evitar eso, tenemos que asegurarnos de que nuestra labor de muestreo no baje de la altura de la pantalla donde empiece el rectángulo del agua.

Usando como ejemplo nuestro caso, el de la imagen que da comienzo al artículo, podemos estimar que el rectángulo del agua ocupa el tercio inferior de la pantalla, por lo que la labor de muestreo debería desarrollarse sólo entre SCREEN_UV.Y = 0 y SCREEN_UV.Y = 0.66. Para lograrlo, uso la línea 13 de get_mirrored_coordinate(). Ese método mix() te permite obtener el valor equivalente a su tercer parámetro dentro de un intervalo definido por los dos primeros. Por ejemplo, mix (0, 0.66, 0,5) señalaría a la mitad del intervalo entre 0 y 0,66, arrojando un resultado de 0,33.

Limitando así el rango vertical de los píxeles de pantalla, a muestrear para ser reflejados, conseguiremos que sólo se refleje la zona de la pantalla que nos interesa realmente.

Con todo lo anterior, ya tendremos la coordenada de la pantalla que debemos muestrear para obtener el color a reflejar en el pixel que nuestro shader está renderizando (línea 15 de get_mirrored_coordinate()).

Esa coordenada será por tanto la que se use en la línea 38 del método fragment() para muestrear la pantalla.

Obtenido el color de ese punto de la pantalla, podríamos volcarlo directamente en la propiedad COLOR del pixel que esté renderizando nuestro shader, pero eso crearía un reflejo con unos colores exactos al objeto reflejado, lo cual sería poco realista ya que lo normal es que la superficie de reflejo superponga cierta tonalidad sobre los colores reflejados, debido a suciedad en la superficie de reflejo o al mismo color de esta. En nuestro caso, vamos a suponer que el agua es azul y por tanto tendremos que superponer cierta tonalidad azul a los colores reflejados. De ello se encarga el método get_resulting_water_color() al que se llama desde la línea 40 del método principal fragment().

Método get_resulting_water_color()


El efecto principal que persigue este método es que el agua sea más azul conforme se acerque al borde inferior del rectángulo del agua. Por contra, cuanto más cerca estemos del borde superior deben predominar los colores originales reflejados. Por ese motivo, se utiliza el método mix() en la línea 29 de get_resulting_water_color(). Cuanto mayor sea el tercer parámetro (water_color_mix) más cerca estará el color resultante del valor marcado por el segundo parámetro (water_color, definido desde el inspector). Si el tercer parámetro fuera cero, mix() devolvería el color del primer parámetro (highlighted_color).

Partiendo de ese funcionamiento básico hay un par de matizaciones. En muchas implementaciones usan UV.Y como el tercer parámetro de mix(). Sin embargo, yo he preferido añadir la posibilidad de configurar un límite en cuanto al máximo de intensidad que queremos que tenga el color del agua. Eso lo he hecho en las líneas 25  28, usando el metodo clamp(). Ese método devolverá el valor de currentUV.y mientras este se encuentre en el intervalo limitado por 0.0 y water_color_intensity. En cambio de que el valor esté por debajo del límite inferior entonces el método devolverá 0.0, y si estuviera por encima del límite superior devolverá el valor de water_color_intensity. Como es el valor resultante de clamp() el que le pasemos a mix() tendremos que el valor del tercer parámetro nunca podrá ser superior al límite que marquemos en el inspector, a través del uniform water_color_intensity.

La siguiente matización, es que he añadido un realce del brillo de las imágenes reflejadas. Se hace entre las líneas 20 a 22. Para ello he configurado un uniform, mirrored_colors_intensity, para poder definir el valor de realce desde el inspector. En la línea 22 se usa ese valor para incrementar las tres componentes del color a reflejar, lo que supone en la práctica subirle el brillo al color. En la línea 22 me aseguro de que el valor resultante no sobrepasa los valores límites de los colores, aunque sospecho que es una comprobación redundante.

El occluder


Recuerda que decíamos que este shader sólo puede reflejar sprites que se hayan dibujado antes que el del shader. Es decir, sólo puede reflejar sprites que tienen un Z Index inferior al del shader. Dado que queremos que queremos reflejar todos los sprites (incluso, parcialmente, el de la hierba), eso supone que el rectángulo del agua debe quedar por encima de todos los demás. 

Si nos limitamos a poner el rectángulo del agua por encima de todo lo demás, el resultado sería como este:

El agua sin occluder

Quedaría raro que la orilla fuese perfectamente rectilínea. Por ese motivo he superpuesto un occluder.

Un occluder es un fragmento de sprite que actúa, a modo de parche, para tapar algo. Generalmente comparte el color del elemento sobre el que se superpone para que parezcan la misma pieza. En mi caso, he utilizado el siguiente occluder:

Occluder


El occluder que he usado tiene el mismo color que la hierba, por lo que, superpuesto sobre la parte inferior de la hierba, no parece que sean dos piezas distintas. Por otro lado, al tapar el borde superior del rectángulo del agua, hace la orilla más irregular, lo que da una apariencia más natural.

El resultado es el de la imagen que abría este hilo, que vuelvo a reproducir aquí para que se vea con todo detalle.

El agua sin occluder

Con esto doy por terminado este artículo. En otro de los que están por venir quiero implementar cierto movimiento en el agua, para dotarlo de vida.