Uno de los shaders que son muy difíciles de ejecutar es el agua, porque hay que entender como funciona lo que vemos en la vida real como agua y aun así no hay garantía que todas las características que le incluyas funcione bien, como la refracción, la reflexión, movimiento de vértices, la profundidad y la espuma, algunas de estas características funcionan bien para algunos rendering path.
Nuestro objetivo será esta vez lograr entender un poco como funciona el agua en la vida real y lograr implementar varias características, advierto que nos vamos a demorar varios blogs y además, el shader resultante tiene algunos problemas de representación, si en un futuro logro combatir todos estos problemas, corregiré los blogs que hablen de este tema.
Como entender el agua.
El agua es un cuerpo líquido que puede moverse su volumen con facilidad, en el océano el viento es capaz de mover volúmenes de agua, creando patrones muy particulares que han sido analizados por muchos matemáticos. Para aplicar este movimiento de volumen de agua es común usar Transformada de Fourier y ruido perlin, ruido Voronoi o cualquier otro tipo de ruido pseudo aleatorio.
Otra característica es que permite el paso de la luz en gran medida (translucencia), dejándonos ver a través de él lo que hay dentro (Refracción), pero otra parte de la luz rebota y veremos el reflejo del ambiente que lo rodea (Reflexión).
El mismo movimiento del agua distorsiona los reflejos como la visión de los objetos (distorsión o refracción), además si el agua es muy pesada o está muy sucia puede ser tan espesa como la niebla.
La luz que pasa a través de él y sobre todo en las crestas de las olas, forma en el fondo el efecto de las #causticas (proyección), que ya explicamos en otro blog.
El movimiento brusco del agua genera espuma o burbujas tanto en las superficies en las que choca como en las crestas.
El agua también es capaz de producir sombras, pero curiosamente no recibe sombras, sino que las sombras que recibe simplemente deja ver lo que hay en el fondo.
Por último, el agua al tratarse de una superficie lisa capaz de reflejar el entorno, es capaz de producir luz especular, que ya tratamos en el blog de modelos de iluminación.
Preparando la escena.
Cree una nueva escena y agregue en ella algunos objetos de colores variados
Importante añadir el disco del tutorial pasado de blender, ve a ver el tutorial primero antes de continuar.
Cree un nuevo material y añádalo al disco del océano.
Cree un nuevo shader de vértices y fragmentos y añádalo al material.
Configura la iluminación de la escena en: "window/rendering/lighting settings", añadiendo un skybox y una luz direccional.
El resultado debe ser algo como esto:
Creando el shader
Propiedades.
Como advertí que este tutorial iba a ser largo, lo partiré en varias partes. En este caso el objetivo será lograr un shader de vértices y fragmentos capaz que trabaje en coordenadas mundiales que pueda servir como océano, trataré que tenga todas las características que sean posibles y que pueda ser lo más amigable posible como siempre con dispositivos de bajo rendimiento, hay algunas cosas que abra que pulir, en la medida de lo que pueda haré mejora de todo aquello que quede defectuoso como los reflejos.
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
Esta vez iniciamos fuerte con un gran surtido de propiedades, con etiquetas de todo tipo, he optado por separar virtualmente el código en 5 categorías: Reflexión, refracción, profundidad, movimiento de vértices y espuma. He añadido de momento 2 controles para activar o desactivar algunas características y estoy pensando en la posibilidad de añadir quizás un 3.er control, en este caso se trata de la espuma y la profundidad, ya que ambas dependen de la depth texture que envía la cámara al shader y sé que algunos dispositivos podrían no soportarlo.
También pudiera hacer algo similar con la refracción y la reflexión, ya que son texturas que puede ser conflictivas con los dispositivos, pero de momento pienso que eso le quitaría la gracia a este shader.
Tags.
El renderizado que haremos será opaco, ya que no necesitamos ninguna textura con transparencia.
Ignoraremos el efecto de cualquier proyector, porque este shader está pensado para usarse en conjunto con el shader de cáusticas que hicimos en un blog anterior.
Queue puede ignorarse o usar "Transparent" o "Geometry", esto solo representa el orden de dibujado, al tratarse de un objeto sin transparencia, lo lógico es usar "Geometry"
Debe permitir la escritura en el búfer Z, ya que esta información la usaremos para el efecto de profundidad y para la espuma que se forma alrededor de los objetos.
Nunca se va a sacrificar ninguna cara de la malla, ya que deseamos que se pueda ver el efecto del agua en ambas caras.
Añadimos una grabPass para crear el efecto de refracción sobre la textura resultante.
#pragma shader_feature DEPTH_ENABLED, ya lo enseñé en el blog de propiedades extra, por favor ve a ver el blog en cuestión si no sabes el tema de añadir características personalizadas. En este caso habilito o deshabilito las opciones que dependen de la textura de profundidad.
#pragma target 2.0, sirve para limitar las características del shader a ser visualizado en dispositivos antiguos, aumentando así la compatibilidad con dispositivos de gamas bajas
#pragma fragmentoption ARB_procision_hint_fastest, permite que las conversiones en el programa de fragmentos sean muy rápidas a costo de una calidad baja, aunque realmente no es apreciable, pero gana en rendimiento.
SubShader{
Tags { "RenderType" = "Opaque" "IgnoreProjector" = "True"
"Queue" = "Transparent"}
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 debug
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
Propiedades.
He separado las propiedades en grupos, realmente no hay nada misterioso, solo he de destacar, que el sampler2D _Water, viene directamente del grabPass que invocamos antes del pase, sampler2D _NormalMap, lo usaremos para precisamente distorcionar el grabpass y la fuerza de la distorcion, lo controlaremos con un vector.
Esta vez modificaré los vértices del disco usando una textura, lo hago así porque es uso menos cálculos y evito el uso masivo de funciones trigonométricas para producir las olas.
sampler2D _CameraDepthTexture es el búfer de profundidad de la cámara, con esto haremos los cálculos de profundidad de los objetos que se sumergen y que cortan el disco, dando la ilusión de espuma y niebla.
Por último La propiedad DEPTH_ENABLED, contiene las propiedades que se activaran o desactivaran cuando sea necesario, en ella está la textura del búfer de profundidad de la cámara, las opciones que la controlan y el color del agua profunda.
//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 _FoamRamp;
sampler2D _CameraDepthTexture;
fixed _FoamDepth, _FoamPower;
fixed4 _WaterColor;
#endif
Structs.
Los structs son como variables empaquetadas, estos paquetes tienen todo lo necesario para que un programa o "función" logren un resultado, por eso es necesario que los tipos de datos coincidan y que no haya una variable más o una variable menos o empezará a quejarse de que la función no ha sido completamente inicializada.
En este caso es habitual la estructura de entrada y la estructura de salida; de entrada metemos la posición del vértice y las normales del objeto, pero se ha añadido algo mas extra, una macro UNITY_VERTEX_INPUT_INSTANCE_ID que permite en dispositivos de realidad virtual, dibujar una sola vez para ambos ojos.
En la estructura de salida hay varias coordenadas de textura, está la posición de pantalla, que usaremos con la textura del búfer de la cámara para producir el efecto de profundidad y de espuma
La posición uv del grab pass, que lo usaremos para la refracción.
Los reflejos mundiales, que los usaremos para la textura producida por el componente reflection probes.
La posición mundial, que usaremos en conjunto con el búfer de la cámara, para el efecto de la espuma.
UNITY_VERTEX_INPUT_INSTANCE_ID y UNITY_VERTEX_OUTPUT_STEREO, son las macros que permite que dispositivos de realidad virtual pueda dibujar una sola vez para ambos ojos, esta macro se usa para cuestiones de rendimiento, lo que internamente hace esta macro es manipular las coordenadas uv de lo que se ve en pantalla para aprovechar mejor lo que se dibuja en pantalla y generar el mismo efecto estereoscópico.
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
};
Esto ha sido todo esta semana, ya he superado las diez mil palabras y se que el blog se ha hecho muy largo.
En la próxima semana daré continuación a este blog y explicaré que ocurre realmente con cada propiedad a profundidad.
No siendo más nos vemos en el próximo blog.
Comments