top of page
Foto del escritorBraulio Madrid

Cómo hacer cáusticas usando un proyector

Actualizado: 29 sept 2020


¿Qué son las causticas?, es el efecto que se produce cuando la luz atraviesa las crestas de la olas del agua, provocando que se refleje en la superficies duras, generando un patrón como telarañas de luz entrelazados y en movimiento en el caso del agua. Pero el nombre no lo recibe el efecto, sino el patron de red luz que se produce, otro ejemplo seria la luz al rojo vivo que produce la lava de un volcán, cubierto por costras de ceniza, esto genera ese patron de red.


En esta oportunidad te enseñaré como hacer las causticas del agua de una manera relativamente simple, la idea es poder completar en el futuro un shader que pueda simular el agua con relativo realismo.


Por ahora comenzaremos con lo mas sencillo, que es hacer un proyector que nos anime una textura y que esa textura permanezca en el mismo sitio aunque movamos el proyector.


Puedes leer este articulo de programación en CG para ver al detalle como hacer un proyector, también puedes leer este otro articulo de la misma pagina para texturizar en el mundo, en lugar de hacerlo por objeto. Los artículos están en ingles así que acá encontrarás un resumen en español de ambos artículos.


Preparando la escena.


Lo primero que vamos a hacer es crear una escena nueva, le agregamos un plano con cualquier material y algunos objetos distintos como esferas o cubos, estos solo son las superficies donde se reflejará el proyector.


Unity tiene un componente que precisamente se llama projector, que lo que hace es tomar un material y reflejarlo en la superficie de otro, el punto es que normalmente los proyectores agregan luz a las superficies y si se hace con un material común, este solo reemplazará la textura de la superficie con la que contenga el material del proyector.


  • Vamos a crear un objeto vacío y le añadimos el componente projector.

  • En la ventana de assets vamos a crear un material con el nombre que quieran, en mi caso lo llamaré projector.

  • Creamos un shader con el mismo nombre si quieren.


Preparando el shader


Lo primero es descargar una textura de causticas tileada, pueden usar esta imagen para el propósito.


Estas son las propiedades que necesitaremos.

Shader "Unlit/Projector"
{
Properties{
    _MainTex ("Texture", 2D) = "white" {}
    _Scale ("Scale", Float) = 1.0	}
    ...
    uniform sampler2D _MainTex;
    fixed _Scale;

Estas son algunos de las propiedades que necesitaremos para indicarle como debe comportarse nuestro subshader.


En primer lugar la mezcla, debe ser One One o SrcAlpha One, esto le dará un pase aditivo a nuestro shader, un proyector siempre se inicia un paso después de que los objetos sólidos ya han sido renderizados.


Como no necesitamos escribir nada en profundidad, desactivamos ZWrite

Para evitar el Z Fighting, o la pelea que se produce cuando 2 caras de un objeto se encuentran en la misma posición, para evitar esto usamos la propiedad Offset

SubShader	{
	Blend One One
	ZWrite Off
	Offset -1, -1  // evite las peleas en profundidad (debe ser "Offset -1, -1")
	Tags { "RenderType" = "Opaque" }
	LOD 100

Aquí me voy a detener a explicar algo sobre los espacios en que se mueven los vertices o las texturas. Usted puede hacer una función de vértice donde no posicione nada en el, este automáticamente asume que el objeto en cuestión estará frente a la cámara todo el tiempo, luego ya hay otros espacios como el espacio del mundo que es una matriz inamovible, pero que en realidad si lo hace, la que es inamovible es la cámara, la que se mueve es el mundo ante nuestros ojos, o la matriz de ese mundo, luego hay otra matriz que es la matriz del objeto, que por lo general resta su posición y su rotación de esta matriz mundial, por eso unity tiene algunas variables que ya contienen estas matrices calculadas, posiblemente no quedó muy claro, la idea que quiero que entiendas es que los motores de videojuegos les es mas fácil calcularlo todo con matrices, en lugar de usar vectores, las tarjetas gráficas se especializan en multiplicar matrices.


Unity_ObjectToWorld; //translada la matriz del objeto al mundo,
Unity_WorldToObject; //translada la matriz del mundo al objeto,

Si quiere indagar mas de este tema vaya al manual de shaders de unity Lo que ahora nosotros necesitamos es usar usar la matriz mundial y la matriz del objeto proyector.


Necesitamos la estructura de entrada que solo contiene la posición de los vertices en verInput, como salida necesitamos mandarle la posición del objeto, y las coordenadas uv del mundo, con esto ya es suficiente para hacer el calculo de la posición de nuestro objeto y la posición del mundo, por alguna razón aunque no necesitemos la posición del objeto, este se hace necesario o el proyector se perderá, no termino por entender porque, a veces los shaders tiene algunos detalles inexplicables, pero eso hace parte de la magia.

#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
...
uniform float4x4 unity_Projector; // transformation matrix 
...
struct vertInput {
	fixed4 pos : POSITION;};

struct vert2frag {
	fixed4 pos : SV_POSITION;
	fixed4 worldPos : TEXCOORD1;
	};
	
vert2frag vert(vertInput v) {
	vert2frag o;
	// posicion local del objeto
	o.pos = UnityObjectToClipPos(v.pos);
	// posicion mundial del objeto
	o.worldPos = mul(unity_ObjectToWorld, v.pos);
	return o;	}

Por ultimo solo queda hacer la función de fragmento, donde usaremos nuestra textura 3 veces para generar esa sensación de movimiento, fíjese que uso las coordenadas globales en X y Z para ubicar la textura y proyectar hacia el suelo, también es posible proyectarla al techo, pero por eso está ese if que evita que se proyecte al techo, el componente w de la matriz solo puede valer 1 o -1, 1 es hacia el frente, -1 es detrás.


Uso el plano X y Z a la que le sumo la variable de unity _SinTime o _CosTime en sus distintos componente, lo importante es que sean distintos para que dé la ilusión de movimiento aleatorio, tomo 3 muestras distintas de la misma imagen y las promedio, pero usted puede optar por multiplicarlo por algún flotante.


half4 frag(vert2frag i) : COLOR{
if (i.worldPos.w > 0.0) // frente al proyector?
{
	fixed4 caustic = tex2D(_MainTex , (i.worldPos.xz + _SinTime.zw) * _Scale);
	fixed4 caustic2 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.xy) * _Scale);
	fixed4 caustic3 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.zw) * _Scale);
	return (caustic + caustic2 + caustic3)/3;			}
else // detrás del proyector
{
	return float4(0.0, 0.0, 0.0, 0.0);
}
}

Actualizacion.



Un error del que no me di cuenta mucho despues fue cuando proyecté sobre un terreno el shader que se habia creado y me di cuenta que la proyeccion se hacia dobre todo el objeto, entonces habia que pensar en algo para limitarlo, hacer una especie de mascara y eso hice.



Pueden usar esta textura o hacer ustedes mismos una a su gusto, lo importante es saber que dicha textura no podrá repetirse y debe limitarse, al tratarse de una textura de control, pueden incluso bajarle aun mas la calidad, incluso usar un filtro mas bajo que bilinear y usar el filtro point, esto para que no ocupe mucho espacio en memoria.


ahora proceda a abrir el shader para editarlo, pues agregaremos una propiedad que nos falta.


float4x4 unity_Projector;
....
vert2frag vert(vertInput v) {
    o.posProj = mul(unity_Projector, v.pos);
....
}

En varios ejemplos que he visto de proyectores en unity usan la variable unity_Projector, que antiguamente se llamaba _Projector, unity te corrige si usas este ultimo, el asunto es que normalmente se pensaría que hacer uso de esta variable, seria como usar "unity_ObjectToWorld" o alguna similar que ya está dentro de la documentación y seria como una variable integrada dentro de unity.cg, pero si guardas verás que directamente no funciona y deberás crear manualmente la variable.



struct vert2frag {
	fixed4 pos : SV_POSITION;
	fixed4 worldPos : TEXCOORD1;
	float4 posProj : TEXCOORD0;
	};
...
half4 frag(vert2frag i) : COLOR {

    fixed4 caustic = tex2D(_MainTex , (i.worldPos.xz + _SinTime.zw) / _Scale);
    fixed4 caustic2 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.xy) / _Scale);
    fixed4 caustic3 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.zw) / _Scale);
    fixed4 causticAverage = ((caustic + caustic2 + caustic3) / 2);
    
    fixed4 mask = tex2D(_Mask, UNITY_PROJ_COORD(i.posProj));
    fixed4 color = causticAverage * mask;
    
    return saturate(color/2 * (i.posProj.z + 2));
    }

Por último ha que incluir en la estructura de salida la variable o.posProj para ser usada en la máscara que tendrá como coordenadas uv, la macro de UNITY_PROJ_COORD


Note que he organizado un poco las imágenes que generan las cáusticas, en lugar de multiplicar por la escala, resulta mejor dividir para controlar la escala. A continuación el resultado.


Actualizacion 2


Hay que tener cuidado con un detalle que se me escapó y es que si bien el proyector refleja las causticas en la direccion que indica, tambien lo hace hacia arriba en la direccion opuesta,

por eso es necesario limitarlo

saturate(color/2 * (i.posProj.z + 2));

Multiplicarlo con la proyeccion en z, limita que si el valor es menor a la posicion del proyectos, solo lo haga en ese en esa direccion, yo le sumo 2 metros para que las causticas se reflejen un poco mas arriba, generando causticas por encima del nivel del agua.




Eso es todo por esta vez, como siempre les dejaré el código completo. No se preocupen si el resultado sale distinto, ya que yo modifiqué la textura arrastrando los canales 5 pixeles, bajo mi punto de vista considero que las crestas de las olas se vuelven como un prisma que divide la luz en los distintos espectros, pero ustedes pueden dejarlo como quieran.


Shader "Unlit/Projector"
{
Properties
{
	_MainTex ("Caustic", 2D) = "white" {}
	_Mask("Mask", 2D) = "white" {}
	_Scale ("Scale", Range(1,30)) = 1.0
}
SubShader
{
	Blend One DstAlpha//One One
	ZWrite Off
	Offset -1, -1  // evite las peleas en profundidad (debe ser "Offset -1, -1")
	Tags { "RenderType" = "Opaque" }
	LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

	uniform sampler2D _MainTex, _Mask;
	fixed _Scale;
	float4x4 unity_Projector;

	struct vertInput {
		fixed4 pos : POSITION;
	};

	struct vert2frag {
		fixed4 pos : SV_POSITION;
		fixed4 worldPos : TEXCOORD1;
		float4 posProj : TEXCOORD0;
	};

	vert2frag vert(vertInput v) {
		vert2frag o;
		// posicion local del objeto
		o.pos = UnityObjectToClipPos(v.pos);
		// posicion mundial del objeto
		o.worldPos = mul(unity_ObjectToWorld, v.pos);
		o.posProj = mul(unity_Projector, v.pos);

		return o;
	}

	half4 frag(vert2frag i) : COLOR
	{
fixed4 caustic = tex2D(_MainTex , (i.worldPos.xz + _SinTime.zw) / _Scale);
fixed4 caustic2 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.xy) / _Scale);
fixed4 caustic3 = tex2D(_MainTex, (i.worldPos.xz + _CosTime.zw) / _Scale);
fixed4 causticAverage = ((caustic + caustic2 + caustic3) / 2);

fixed4 mask = tex2D(_Mask, UNITY_PROJ_COORD(i.posProj));

fixed4 color = causticAverage * mask;
return saturate(color/2 * (i.posProj.z + 2));
	}
		ENDCG
		}
	}
}

Nos vemos en otro Blog.

103 visualizaciones0 comentarios

Commentaires


bottom of page