top of page
Foto del escritorBraulio Madrid

Continuación del agua algo realista.




En el blog anterior me había pasado explicando a detalle cada propiedad, pues era necesario explicar el porqué de todo ese detalle. Esta vez miraremos la implementación de cada una de las características.


Programa de Vértices.


Iniciamos con el programa de vértices, que toma como parámetro de entrada un Struct que explicamos en el anterior blog y el retorno de este programa es un struct de salida llamado vert2frag.


Para poder darle la habilidad a este shader de que se renderice una sola vez para ambos ojos con efecto estereoscópico, hacemos uso de algunas macro, estas son: UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_OUTPUT(vert2frag, o) y UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);


Puede profundizar más en ellos viendo la documentación de unity


vert2frag vert(vertInput v) {					vert2frag o;
UNITY_SETUP_INSTANCE_ID(v); //Insert
UNITY_INITIALIZE_OUTPUT(vert2frag, o); //Insert
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); //Insert

Deformación de vértices.


La primera característica que tiene el agua es la capacidad de movimiento, en este caso desplazamos los vértices de arriba a abajo, pero en secuencia, dando la ilusión de oleaje, además otra característica de este océano y el motivo por el cual se diseñó un disco fue con el propósito de poder tener el mayor detalle en el centro del disco y menor detalle a lo lejos, pues el objetivo de este disco es que se desplace con el personaje.


Para que este disco pueda desplazarse y aun así mantenga la ilusión de que el oleaje se mantiene estático, debemos tomar las coordenadas mundiales del vértice.


Para dar la ilusión de que es el viento el que mueve las olas, hay que hacer una mezcla de posiciones OffsetAByC, para eso tomo la variable _Time.xz y _CosTime.yw, lo multiplico por la propiedad _WaveSpeed.


Creo una variable llamada WorldUV, para poder manipular la textura _VertexDistrotionTex, que será la responsable de alterar los vértices. La variable WorldUV, lo sumo con cada una de las variables OffsetAByC para producir ese efecto de scroll.


Creando así las texturas noiseSampleAByC respectivamente, estas 3 muestras son las que mezclaré en un resultado final tomando un promedio de las 3 muestras. En este caso hago uso de seno hiperbólico, pero un promedio normal también bastaría.

sinh(noiseSampleA + noiseSampleB + noiseSampleC)/3

Por último, resto la posición en y de los vértices con el resultado multiplicando este promedio hacho con las muestras, restándole 0.5 a las muestras, para que se mueva entre -0.5 y 0.5 y el resultado lo multiplico por la propiedad _VertexDistortion, que se encarga de escalar el nivel de distorsión de las olas.


Nota: Trabajar con posiciones de vértices mundiales, da problemas gráficos en una vista ortogonal, así que hay que tener cuidado.


Nota: También se puede trabajar con transformada de Fourier, pero al probarlo me encontré que tiende a inclinar el disco y no me deja ningún tipo de control, así que opté por esta opción en el que tengo mejor control.



//Vertex Deformation
fixed3 worldPos = mul(unity_ObjectToWorld, v.pos);// posicion mundial del objeto
o.pos = UnityObjectToClipPos(v.pos);// posicion local del objeto

fixed2 offsetA = _Time.xz * _WaveSpeed/1000;
fixed2 offsetB = _Time.xy * _WaveSpeed/1000;
fixed2 offsetC = -_CosTime.yw * _WaveSpeed/1000;

fixed2 worldUV = worldPos.xz * (_WaveAmp/1000);

fixed noiseSampleA = tex2Dlod(_VertexDistortionTex, fixed4(worldUV + offsetA,0,0)).r;

fixed noiseSampleB = tex2Dlod(_VertexDistortionTex, fixed4(worldUV/3 + offsetB, 0, 0)).g;

fixed noiseSampleC = tex2Dlod(_VertexDistortionTex, fixed4(worldUV/2 + offsetC, 0, 0)).b;

fixed noiseTex = sinh(noiseSampleA + noiseSampleB + noiseSampleC)/3;
o.pos.y -= sin(_VertexDistortion * (noiseTex -0.5));

Todos los TextureCoord.

En esta sección solo calculamos todos los tipos de salidas a las coordenadas de texturas, la posición mundial que ya fue calculada en la sección anterior, las normales mundiales, los reflejos mundiales, la posición de la cámara y la posición para las texturas grab.


//UVs
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldRefl = reflect(-worldViewDir, worldNormal);
o.uvgrab = ComputeGrabScreenPos(o.pos);
o.screenPos = ComputeScreenPos(o.pos);
o.worldPos = worldPos.xzy;

Propiedad RIM y propiedad DATA.


La característica que creo que es más importante es el efecto RIM, en pocas palabras el efecto rim es el completamente opuesto a la iluminación especular y este efecto será el responsable de separar los reflejos de la refraccion.


El efecto rim se calcula multiplicando el vector normal del objeto con el vector de la visión de la cámara, para obtener un vector en medio.


Por último está el vector Data: este es simplemente almacena 3 datos: el resultado de rim, las crestas generadas en las olas para la espuma y el tercer valor sería la posición de la luz especular, pero no funcionó como esperaba, por eso el último valor es cero, por alguna razón la iluminación especular da problemas en este shader.

//Rim
fixed3 viewDir = normalize(ObjSpaceViewDir(v.pos));
float NdotV = saturate(dot(v.normal, abs(viewDir)));
fixed rim = smoothstep(1 - _RimPower, 1, 1 - NdotV);

o.data = fixed3(rim, cos(noiseTex * _FoamPeak), 0);//spec
return o;
}

Programa de fragmentos.


Para efectuar el efecto estereoscópico, hago uso de la macro UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); con esto bastará para que el efecto estereoscópico funcione.



half4 frag(vert2frag i) : COLOR{
					UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
	fixed4 waterColor;
	fixed foam;
	fixed foamData;

Propiedad de Espuma y profundidad.


foamDistortionUV, es el responsable de generar el movimiento de la espuma, en este caso hago uso de la posición mundial y el vector de _Distotion, que también se usa en la distorsión de la refracción.


Como se puede ver, foamDistortion es del tipo fixed4, ya que sirve como coordenada uv para 2 texturas de espuma.


Ahora hago uso de la textura de profundidad de la cámara, para saber como se usa, puede referirse a los ejemplos dados en este blog acerca de los efectos de imagen, donde uso esta propiedad.


De esta textura obtengo una muestra depthSample, que es un valor que va de 0 a 1, esta muestra la uso para tintar el agua con un color que desee y también la aprovecho para calcular la intersección del agua con algún objeto que la corte.


fixed foamLine = 1 - saturate(_FoamDepth * (depthSample - i.screenPos.w));

Pero si lo dejo así, simplemente se vería como un degradado que va de negro a blanco sin ninguna gracia, para esto vuelvo a hacer uso de la variable _SinTime y de la función Frac, que me devuelve el valor fraccional


foamLine *= frac(_SinTime.w + foamLine);

ya simplemente hago uso de este valor para obtener la intensidad de las texturas de la espuma.


fixed foamTex = tex2D(_FoamRamp,  foamDistortionUV.xy);
foamTex += tex2D(_FoamRamp, foamDistortionUV.zw);

Ya se ha completado la espuma que se forma en la orilla del mar, ahora hace falta la espuma que se forma en la cresta de las olas.


//UV Compartido entre la espuma y la distorcion
fixed4 foamDistortionUV = fixed4(i.worldPos * _Distortion.y +
(_Distortion.xz * _SinTime.xz), i.worldPos * _Distortion.y + _CosTime.xy);

#if DEPTH_ENABLED
fixed depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.screenPos);
depthSample = LinearEyeDepth(depthSample);
fixed depth = pow(depthSample,-_WaterColor.a);
waterColor = lerp(_WaterColor, 1, depth);

fixed foamLine = 1 - saturate(_FoamDepth * (depthSample - i.screenPos.w));
foamLine *= frac(_SinTime.w + foamLine);
fixed foamTex = tex2D(_FoamRamp,  foamDistortionUV.xy);
foamTex += tex2D(_FoamRamp, foamDistortionUV.zw);

fixed waveLine = pow(sin(i.data.g)/2 + 0.5, _FoamPower);

foamData = frac(foamLine + waveLine/2);
foam = (foamTex * foamData)/2;

#else
	waterColor = 1;
	foam = 0;
	foamData = 0;
#endif		

Recuerde que en el programa de vértices ya había calculado las crestas en la variable o.data y se había calculado previamente con una función coseno.


cos(noiseTex * _FoamPeak)

Pero aquí de nuevo para darle más detalle y más realismo, vuelvo a mezclar con la función seno.


fixed waveLine = pow(sin(i.data.g)/2 + 0.5, _FoamPower);

Por último mezclo tanto el resultado de la espuma de la cresta de las olas como la espuma de la orilla del mar.


foamData = frac(foamLine + waveLine/2);
foam = (foamTex * foamData)/2;


Propiedad de refracción.


El tema de la refracción ya fue tratado en otro blog donde aplico un efecto de imagen que distorsiona lo que ve la cámara, ve a ver el blog antes de continuar.



fixed3 norm = UnpackNormal(tex2D(_NormalMap, foamDistortionUV.xy));

i.uvgrab.xy += norm * _Distortion.w;
fixed4 col = tex2Dproj(_Water, UNITY_PROJ_COORD(i.uvgrab)) * waterColor ;

Propiedad de Reflejos y mezclando todo.


Tampoco hay mucho que agregar acá, ya he escrito un blog acerca de como aplicar reflejos a los materiales.


El truco especial es el vector data que habíamos calculado para el efecto rim, pues la mezcla entre reflejos y refracción depende de la variable i.data.r

fixed4 reflData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
half3 refl = DecodeHDR(reflData, unity_SpecCube0_HDR);

fixed4 result = lerp(col, fixed4(refl, 1), i.data.r);
result += foam; 
return result;

}
ENDCG
    }
}
FallBack "Diffuse"

Conclusiones.


Este shader no es perfecto, tiene algunos pequeños glitches, como por ejemplo que a veces lo que se ve en la ventana del juego, resulta verse distinto en la ventana de escena.


Errores de vista en perspectiva ortográfica o en la vista 2D.


Sé que hay otros enfoques de reflejos, como el uso de render textures, pero prefiero hacer uso de rénder probes, aunque advierto que según que plataforma a la que se compile, puede activarse o no el componente reflection probe.


Si ha llegado hasta aca, se puede decir que usted ya es un experto en el lenguaje de shaders de unity y que prácticamente puede crear cualquier estilo visual que necesite.


No siendo más, nos vemos en el próximo blog.


Shader "Vertex Fragment/Custom/WaterGrabPass" {

Properties{
[Header(Reflection)]
_RimPower("rim power", Range(0.0,1.0)) = 1.0

[Header(Refraction)]
_Distortion("XZ displace | Y Scale | W Distortion", Vector) = (1,1,0.05,0)
[NoScaleOffset]_NormalMap("normal map", 2D) = "bump"{}

[Header(Depth)]
[MaterialToggle(DEPTH_ENABLED)]_ifDepth("Depth enabled", Float) = 1
[ShowIf(DEPTH_ENABLED)]_WaterColor("Water color| Alpha = depth", Color) = (0.5,0.5,0.5,1)

[Header(Foam)]
[NoScaleOffset]_FoamRamp("Foam Ramp", 2D) = "white"{}
[ShowIf(DEPTH_ENABLED)]_FoamDepth("Foam depth", Range(0.1,5)) = 1.0
[ShowIf(DEPTH_ENABLED)]_FoamPeak("Foam Peak", Range(1,50)) = 1.0
[ShowIf(DEPTH_ENABLED)]_FoamPower("Foam Power", Range(1,10)) = 1.0

[Header(Vertex modification)]
[NoScaleOffset]_VertexDistortionTex("Water distortion",2D) = "white"{}
_WaveSpeed("Wave speed", Float) = 1.0
_WaveAmp("Wave Amplitude", Range(0,10)) = 0.1
_VertexDistortion("WaterDistortion", Range(0,100)) = 1.0
}

SubShader{
Tags { "RenderType" = "Opaque" "IgnoreProjector" = "True"  "Queue" = "Geometry"}

ZWrite On
Cull Off
LOD 200

GrabPass{"_Water"}
Pass
	{
	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#pragma shader_feature DEPTH_ENABLED
	#pragma target 2.0
	#pragma fragmentoption ARB_precision_hint_fastest
	#include "UnityCG.cginc"
	
	//Reflection
	fixed _RimPower;
	
	//Refraction
	sampler2D _Water;//GrabPass
	sampler2D _NormalMap;
	fixed4 _Distortion;
	
	//Vertex modification
	sampler2D _VertexDistortionTex;
	
	fixed _WaveSpeed, _WaveAmp;
	fixed _VertexDistortion;
	fixed _FoamPeak;
#if DEPTH_ENABLED
	sampler2D _CameraDepthTexture;
	fixed _FoamDepth, _FoamPower;
	sampler2D _FoamRamp;
	fixed4 _WaterColor;
#endif
	struct vertInput {
		fixed4 pos: POSITION;
		fixed3 normal : NORMAL;
		//VR Single pass
		UNITY_VERTEX_INPUT_INSTANCE_ID
		};
		
	struct vert2frag {
		fixed4 screenPos : TEXCOORD0;
		fixed4 uvgrab: TEXCOORD1;
		fixed3 worldRefl : TEXCOORD2;
		fixed3 worldPos : TEXCOORD3;
		fixed3 data : COLOR;//R = rim, G = Foam, B = Specular
		//Used to pass fog amount around number should be a free texcoord.
		fixed4 pos : SV_POSITION; //Es obligatorio o los vertices no apareceran.
		//VR Single pass
		UNITY_VERTEX_INPUT_INSTANCE_ID
		UNITY_VERTEX_OUTPUT_STEREO
		};
		
vert2frag vert(vertInput v) {
	vert2frag o;
	UNITY_SETUP_INSTANCE_ID(v); //Insert
	UNITY_INITIALIZE_OUTPUT(vert2frag, o); //Insert
	UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); //Insert
	
	//Vertex Deformation
	fixed3 worldPos = mul(unity_ObjectToWorld, v.pos);// posicion mundial del objeto
	o.pos = UnityObjectToClipPos(v.pos);// posicion local del objeto
	fixed2 offsetA = _Time.xz * _WaveSpeed/1000;
	fixed2 offsetB = _Time.xy * _WaveSpeed/1000;
	fixed2 offsetC = -_CosTime.yw * _WaveSpeed/1000;
	
	fixed2 worldUV = worldPos.xz * (_WaveAmp/1000);
	fixed noiseSampleA = tex2Dlod(_VertexDistortionTex, fixed4(worldUV + offsetA,0,0)).r;
	fixed noiseSampleB = tex2Dlod(_VertexDistortionTex, fixed4(worldUV/3 + offsetB, 0, 0)).g;
	fixed noiseSampleC = tex2Dlod(_VertexDistortionTex, fixed4(worldUV/2 + offsetC, 0, 0)).b;
	fixed noiseTex = sinh(noiseSampleA + noiseSampleB + noiseSampleC)/3;
	o.pos.y -= sin(_VertexDistortion * (noiseTex -0.5));
	
	//UVs
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
	o.worldRefl = reflect(-worldViewDir, worldNormal);
	o.uvgrab = ComputeGrabScreenPos(o.pos);
	o.screenPos = ComputeScreenPos(o.pos);
	o.worldPos = worldPos.xzy;
	
	//Rim
	fixed3 viewDir = normalize(ObjSpaceViewDir(v.pos));
	float NdotV = saturate(dot(v.normal, abs(viewDir)));
	fixed rim = smoothstep(1 - _RimPower, 1, 1 - NdotV);
	
	o.data = fixed3(rim, cos(noiseTex * _FoamPeak), 0);//spec
	return o;
}

//UNITY_DECLARE_SCREENSPACE_TEXTURE(_MainTex); //Insert

half4 frag(vert2frag i) : COLOR{

	UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
	fixed4 waterColor;
	fixed foam;
	fixed foamData;
	
	//UV Compartido entre la espuma y la distorcion
fixed4 foamDistortionUV = fixed4(i.worldPos * _Distortion.y + (_Distortion.xz * _SinTime.xz), i.worldPos * _Distortion.y + _CosTime.xy);
#if DEPTH_ENABLED
	fixed depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.screenPos);
	depthSample = LinearEyeDepth(depthSample);
	fixed depth = pow(depthSample,-_WaterColor.a);
	waterColor = lerp(_WaterColor, 1, depth);
	fixed foamLine = 1 - saturate(_FoamDepth * (depthSample - i.screenPos.w));
	foamLine *= frac(_SinTime.w + foamLine);
	fixed foamTex = tex2D(_FoamRamp,  foamDistortionUV.xy);
	foamTex += tex2D(_FoamRamp, foamDistortionUV.zw);
	
	fixed waveLine = pow(sin(i.data.g)/2 + 0.5, _FoamPower);
	foamData = frac(foamLine + waveLine/2);
	foam = (foamTex * foamData)/2;
#else
	waterColor = 1;
	foam = 0;
	foamData = 0;
#endif		
	fixed3 norm = UnpackNormal(tex2D(_NormalMap, foamDistortionUV.xy));
	i.uvgrab.xy += norm * _Distortion.w;
	fixed4 col = tex2Dproj(_Water, UNITY_PROJ_COORD(i.uvgrab)) * waterColor ;
	fixed4 reflData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
	half3 refl = DecodeHDR(reflData, unity_SpecCube0_HDR);
	
	fixed4 result = lerp(col, fixed4(refl, 1), i.data.r);
	result += foam;
	return result;
}

ENDCG
}
}
FallBack "Diffuse"
}

24 visualizaciones0 comentarios

Comments


bottom of page