Cómo dibujar con un formato de grilla, en donde cada una de las celdas dibujan cosas diferentes. Esto se puede utilizar para cosas como gotas de lluvia, en donde se necesita que cada gota se mueva de formas diferentes.

ANTES DE EMPEZAR

Antes de empezar a mostrar código, si no saben NADA de shaders les recomendaría ver un POST ANTERIOR que hice explicando lo básico para dibujar cosas simples. Esto es más que nada para que no sea todo chino básico lo que vean acá.

Vamos empezar creando un Unlit Shader básico y un material vacío para ponerle este shader. Para ello, en Unity, “hacemos click derecho” => “Create” => “Shader” => “Unlit Shader“. De esta forma creamos un shader básico, que ya va a tener bastante código. Les dejo abajo el código de ese shader con una pequeña “limpieza” que básicamente consiste en sacar todo lo que no vamos a usar y dejar sólo que pinte un color plano. Pueden copiarlo y pegarlo, reemplazando el código inicial de Unity, así trabajamos sobre una base más limpia.

 

Shader "MyShadersTutorials/GridShader"
{
    Properties
    {
        
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 finalColor = fixed4(0, 0, 0, 0);
                return finalColor;
            }
            ENDCG
        }
    }
}

 

Con esto tendremos un shader básico, que pinta de negro nada más. Pero con esto es suficiente para empezar.

Lo siguiente sería crear un material con “click derecho” => “Create” => “Material“. Luego asignenle el shader al material recién creado. Y con esto ya tendríamos listo un material base para poder empezar a trabajar.

Lo último y más sencillo que vamos a necesitar es crear un objeto para asignarle nuestro material. Les recomendaría usar, para empezar al menos, un Quad, pero pueden usar el material u objeto que más les guste. Sólo creenlo, y tirenlo a la escena. Para este ejemplo, yo voy a usar un Quad.

Nuestro espacio de trabajo quedaría más o menos así: 

 

CREANDO UNA GRILLA VIRTUAL

No sé si “virtual” sería la palabra correcta, pero a lo que me refiero es que vamos a dividir el espacio en segmentos. Esta división es simplemente matemática para dibujar en diferentes lugares. No dividimos literalmente el material ni nada, pero la idea es que podamos obtener las coordenadas de cada celda en particular, y poder dibujar como si existieran y tuvieran comportamiento propio. Con esto nos evitamos agregar información adicional en el proceso.

Vamos a empezar pintando alguna forma bastante particular, para que la grilla sea notoria al momento de visualizarla. Para ello, podemos asignar un color bastante variante utilizando las coordenadas uv como si fuera un color.

 

fixed4 frag (v2f i) : SV_Target
{
    fixed4 finalColor = fixed4(i.uv, 0, 1);
    return finalColor;
}

 

NOTA IMPORTANTE

Recuerden que la creación de las variables tipo fixed4, float4, half4, se pueden crear de varias maneras. Por ejemplo:

float4(float, float, float, float)
float4(float2, float, float)
float4(float3, float)
float4(float4)

De esta manera, pueden rellenar 2, 3 o 4 valores utilizando otro tipo de dato con menos datos.

 

Con esto, el resultado del color sería el siguiente:

 

 

Por las dudas, y para que quede claro, vamos a explicar un poco por qué se pintó de este color el mesh. Recuerden cuáles son las coordenadas UV representadas en cada parte de la textura:

 

 

Teniendo esto en cuenta, y además teniendo en cuenta que el color final que estamos asignando es un fixed4, o sea, r g b a (red, green, blue, alpha). Así que, si lo analizan, van a ver que los colores de las esquinas son los respectivos equivalentes. O sea:

  • fixed4(0, 0, 0, 1) = Negro
  • fixed4(1, 1, 0, 1) = Amarillo
  • fixed4(1, 0, 0, 1) = Rojo
  • fixed4(0, 1, 0, 1) = Verde

Y todos los colores del medio son las respectivas interpolaciones entre las puntas. Ahora, explicado esto, vamos a empezar a dibujar la matriz. Sí, por ejemplo, dibujamos una matriz de 2×2, deberíamos ver 4 cuadrados con estas interpolaciones de colores exactamente igual. Para esto, vamos a crear una variable para representar las los tiles de la grilla, y utilizarla en el dibujado. Primero muestro el cálculo, luego los resultados y por último, la explicación:

 

fixed4 frag (v2f i) : SV_Target
{
    float2 gridSize = float2(2, 2);
    float2 cellUV = frac(gridSize * i.uv);
    fixed4 finalColor = fixed4(cellUV, 0, 1);
    return finalColor;
}

 

El resultado de esto sería:

 

 

Lo que hice arriba fue crear dos variables nuevas: una para representar el tamaño de la grilla (gridSize), que en este caso es 2×2, y otra para representar las coordenadas UV dentro de cada una de las celdas (cellUV). De esta forma, podemos calcular y trabajar sobre los valores UV, como trabajaríamos en cualquier shader, pero accediendo a las coordenadas de cada una de las celdas de la grilla de manera independiente.

Ahora… La pregunta es… ¿Qué hace ese cálculo? Es algo realmente sencillo. Antes que nada, habrán notado que usamos un método llamado frac(x). Lo que hace este método es, dado un número, devuelve su parte fraccionaria. Como recordarán, el UV son las coordenadas de la textura, que utilizamos para representar cada una de las coordenadas de la misma textura, y son valores entre 0 y 1. Usar el método frac(x) es una manera de mantenernos dentro de esos valores. Pero además también tiene otro motivo.

Piensen en la imagen que explica lo de las coordenadas UV:

 

 

Si quisiéramos pintar las coordenadas del centro, necesitaríamos acceder al punto (0.5, 0.5). Y se supone que las coordenadas del centro de CADA UNA de las celdas, debería ser, también, (0.5, 0.5). Ahora, hagamos algunas cuentas para explicar el algoritmo de recién. Sabemos que el método en el cual estamos trabajando (la función fragment), se ejecuta una vez por cada pixel de textura que va a pintar. Por ende, en cada ejecución nos guarda en la variable i.uv los datos de las coordenadas que está actualmente pintando. O sea que si tenemos 4 cuadrados (en una grilla de 2×2), sabríamos que los centros de cada uno de esos cuadrados son:

  1. (0.25, 0.25)
  2. (0.25, 0.75)
  3. (0.75, 0.25)
  4. (0.75, 0.75)

 

 

O sea que… Cuando el sistema pasa por esas coordenadas, nosotros deberíamos tener en nuestra variable cellUV los valores (0.5, 0.5). Entonces… Hagamos cuentas:

i.uv = (0.25, 0.25) ===> frac(gridSize * i.uv) ===> frac( (2, 2) * (0.25, 0.25)  ) ===> frac(0.5, 0.5) ===> (0.5, 0.5)
i.uv = (0.25, 0.75) ===> frac(gridSize * i.uv) ===> frac( (2, 2) * (0.25, 0.75)  ) ===> frac(0.5, 1.5) ===> (0.5, 0.5)
i.uv = (0.75, 0.25) ===> frac(gridSize * i.uv) ===> frac( (2, 2) * (0.75, 0.25)  ) ===> frac(1.5, 0.5) ===> (0.5, 0.5)
i.uv = (0.75, 0.75) ===> frac(gridSize * i.uv) ===> frac( (2, 2) * (0.75, 0.75) ) ===> frac(1.5, 1.5) ===> (0.5, 0.5)

Como ven, en cada uno de los valores de i.uv, se pueden obtener correctamente cada uno de los centros. Lo que significa que el cálculo puede sacar correctamente también las puntas, y los valores intermedios.

 

NOTA IMPORTANTE

Quizás muchos matemáticos estén volviéndose loco al ver esa multiplicación vectorial. Pero… En realidad no es exactamente una multiplicación vectorial. Lo que hace el lenguaje al multiplicar (x1, y1) * (x2, y2) es (x1 * x2, y1 * y2). O sea… Multiplica cada uno de los términos entre sí.

 

Y eso es todo. Intenten ahora ponerle una cantidad diferente de celdas y vean que sigue funcionando. Por ejemplo: 10×4.

 

 

Ahora lo único que queda es que programen su shader de la misma manera que lo harían normalmente, pero con este pequeño cálculo pueden obtener la coordenada UV de cada una de las “celdas virtuales”.

ASIGNANDO UN ID A CADA CELDA

Ahora vamos a asignar un ID diferente a cada celda de la grilla. Para ello, vamos a hacer sólo una cuenta matemática “simple”. Partamos de la base de que si pudimos fragmentar de alguna manera hacia una grilla, deberíamos poder obtener un “identificador” único para cada una de esas celdas. Para ello, lo que vamos a hacer es la siguiente cuenta:

 

float2 cellID = floor(i.uv * gridSize);

 

El método “floor(x)” lo que hace es devolvernos la parte entera del número. Básicamente hace “x – frac(x)“. Lo que quiere decir que si tuviéramos 5.8 nos devolvería 5. Básicamente lo opuesto al método “frac(x)“. Explicado esto, veamos un poco por qué hacemos ese cálculo. Partamos desde el hecho de que i.uv va a ser SIEMPRE un valor entre (0, 0) y (1, 1). También tengamos en cuenta que la variable gridSize almacena cuáles son las proporciones de nuestra grilla. Ahora… ¿Cuáles son las posiciones que ocupan cada una de nuestras celdas? Lo voy a explicar con la imagen en la que se pueden ver las coordenadas de las celdas:

 

 

Partiendo de esa imagen, y teniendo en cuenta los valores posibles de i.uv, vamos a determinar cuáles serían las distintas áreas que van a ocupar cada una de nuestras celdas:

  1. X => Desde 0 hasta 0.5       Y => Desde 0 hasta 0.5
  2. X => Desde 0.5 hasta 1       Y => Desde 0 hasta 0.5
  3. X => Desde 0 hasta 0.5       Y => Desde 0.5 hasta 1
  4. X => Desde 0.5 hasta 1       Y => Desde 0.5 hasta 1

Pueden visualizar estas áreas?

 

 

Entonces, sabemos con esto que cuando i.uv esté entre (0, 0) y (0.5 0.5) vamos a estar dentro de la celda 1. Vamos a ver si es así asignándole un valor cualquiera a i.uv. Si funciona, cualquier valor que le pongamos en el rango que dijimos, debería darnos como resultado (0, 0), puesto que estamos en la primer celda.

  • i.uv = (0, 0) ======> (0, 0) * (2, 2) = (0, 0) ========> floor( (0, 0) ) = (0, 0)
  • i.uv = (0.1, 0.3) === > (0.1, 0.3) * (2, 2) = (0.2, 0.6) === > floor( (0.2, 0.6) ) = (0, 0)

Ahora, vamos a ver si al hacer que i.uv tenga valores entre (0.5, 0) y (1, 0.5) nos devuelve (1, 0), que representaría la celda número 2.

  • i.uv = (0.5, 0) =====> (0.5, 0) * (2, 2) = (1, 0) =======> floor( (1, 0) ) = (1, 0)
  • i.uv = (0.7, 0.3) ===  > (0.7, 0.3) * (2, 2) = (1.4, 0.6) ===> floor( (1.4, 0.6) ) = (1, 0)

Noten que en ambos casos voy obteniendo cada una de las celdas. Continuemos con la celda 3 que va entre (0, 0.5) y (0.5, 1), y tendría que devolver (0, 1)

  • i.uv = (0, 0.5) ======> (0, 0.5) * (2, 2) = (0, 1) ========> floor( (0, 1) ) = (0, 1)
  • i.uv = (0.4, 0.7) ====  > (0.4, 0.7) * (2, 2) = (0.8, 1.4) ====> floor( (0.8, 1.4) ) = (0, 1)

Y por último, vamos a comprobar la última celda, que sería la que va desde (0.5, 0.5) a (1, 1). En cuyo caso el resultado debería ser (1, 1).

  • i.uv = (0.5, 0.5) =====> (0.5, 0.5) * (2, 2) = (1, 1) ======> floor( (1, 1) )
  • i.uv = (0.7, 0.8) =====> (0.7, 0.8) * (2, 2) = (1.4, 1.6) === > floor( (1.4, 1.6) ) = (1, 1)

De esta manera entonces es que podemos obtener un ID para cada una de las celdas.Y una vez obtenido, podemos utilizar este ID en cualquier cálculo matemático que hagamos en el proceso de pintado para obtener resultados diferentes para cada celda. Por ejemplo, lo podemos asignar al color.

 

fixed4 frag (v2f i) : SV_Target
{
    float2 gridSize = float2(7, 4);
    float2 cellUV = frac(gridSize * i.uv);
    float2 cellID = floor(i.uv * gridSize);
    fixed4 finalColor = fixed4(cellID / 10, 0, 1);
    return finalColor;
}

 

NOTA IMPORTANTE:

En el último bloque de código hice una división por 10 para asegurarme de que los valores nunca den mayores a (1, 1), puesto que ese sería un UV inválido.

 

Y el resultado final sería el siguiente: