La #nieve en el desarrollo de videojuegos es siempre un reto en todos los sentidos, desde como abordar las mecánicas hasta como hacer que luzca bien, en muchas ocasiones se aborda el problema duplicando modelos y texturas para que incluyan la nieve directamente, pero que pasa por ejemplo con esos objetos que tienen que ser girados a último minuto y ya la nieve no encaja mucho.
Esta vez trataré el tema desde una perspectiva visual, las mecánicas y los sonidos se los dejo a otro. Debo de aclarar que no soy el primero en tratar este tema, Alan Zucconi ya ha tratado este tema y de el me base para hacer este tutorial, Los ejemplos de shaders de unity también trata este tema, pero yo quiero llegar un poco más lejos.
Trataré de hacer una serie de 3 tutoriales en los que haré 3 shaders para distintos problemas comunes que se presentan, pretendo alcanzar un poco más de realismo al que otros shaders comunes alcanzan.
Creando el shader.
La solución es poder agregar una capa de color blanco en una dirección global, algunos otros enfoque le añaden una extrusión de los vértices en el mismo sentido de la dirección, pero no aplicaremos ese enfoque, porque me parece que daña mucho la geometría y pudiera terminar viéndose muy feo, así que solo nos haremos cargo del color.
Estos serian los parámetros que vamos a utilizar, note que tendrá etiquetas [ShowIf(nombre)], esto es porque usaremos la nieve de forma opcional, para entender que son estas etiquetas vé al blog donde trato el tema de los Drawers
_MainTex("Base (RGB)", 2D) = "white" {}
[Normal]_Bump("Bump", 2D) = "bump" {}
[MaterialToggle(IF_SNOW)] _IfSnow("if Snow", Float) = 1 [ShowIf(IF_SNOW)]_Snow("Snow Level", Range(0,1)) = 1 _Color("Color", Color) = (1.0,1.0,1.0,1.0) [ShowIf(IF_SNOW)]_SnowDirection("Snow Direction", Vector) = (0,1,0) [ShowIf(IF_SNOW)]_SnowDepth("Snow Depth", Range(0,0.2)) = 0.1 [ShowIf(IF_SNOW)]_Wetness("Wetness", Range(0, 0.5)) = 0.3 [ShowIf(IF_SNOW)]_SnowLevel("Snow level", Float) = 1
A continuación, crearé las variables para que sean enviadas a través de los parámetros. Los que serán las variables fijas son sampler2D _MainTex y _Bump y el fixed4 _Color, luego las que dependen del condicional son las líneas comprendidas entre los #preprocesadores #if IF_SNOW y #endif, que dependen del drawer [MaterialToggle] anunciado en los parámetros y que no podrían activarse sin la ayuda del preproesador #pragma shader_feature, esto ya está explicado a profundidad en el capítulo de los Drawers
#pragma surface surf Lambert
#pragma shader_feature IF_SNOW
sampler2D _MainTex;
sampler2D _Bump;
fixed4 _Color;
#if IF_SNOW
float _Snow, _SnowDepth, _Wetness;
float4 _SnowDirection;
float _SnowLevel;
#endif
Como este es un shader de superficie, para unity hay tres modelos comunes, el modelo Standard, Lambert y BlinnPhong, yo he escogido Lambert, pero nada te impide escoger el que gustes o crear tu propio modelo, tienes el capitulo que hablo acerca de los modelos de iluminación.
Los shaders de superficie por lo general necesitan una estructura llamada Input, donde tomará algunos datos del programa de vértices, aunque no lo veas existe el programa de vértices, solo viene camuflada y es llamada internamente cuando invocas la línea #pragma surface surf x
Normalmente solo llama a uv_MainTex y uv_Bump, pero es raro que usemos worldNormal y worldPos, normalmente los shaders de nieve solo usan world normal, pero quiero agregarle una característica que creo que será útil y es poder activar la nieve dependiendo de la altura, esto hará que la nieve se manifieste por encima de determinados metros hacia arriba, esto hará que debajo de ese valor la nieve no sea necesario y a determinada altura se vea el efecto, esto nos ahorrará tener que gastar días duplicando modelos 3D para agregarle nieve
struct Input {
float2 uv_MainTex;
float2 uv_Bump;
float3 worldNormal;
float3 worldPos;
INTERNAL_DATA
};
void surf(Input IN, inout SurfaceOutput o) {
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));
#if IF_SNOW
half difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);
difference = saturate(difference / _Wetness);
fixed worldPos = dot(IN.worldPos, fixed3(0, 1 / _SnowLevel, 0)); o.Albedo = lerp(c, difference + (1 - difference) * c, worldPos);
#else
o.Albedo = c;
#endif
o.Alpha = c.a;
}
half difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);
Primero empezamos calculando la dirección, para eso usamos un vector que ya está calculado que es la dirección normal mundial, este vector viene siendo una constante dada por unity, usamos las normales del objeto, WorldNormalVector() es una función que multiplica la dirección constante de unity y la normal del objeto, dándonos como resultado la dirección mundial de las normales de ese objeto, a su vez multiplicamos un producto punto con la dirección que nosotros deseamos para la nieve y del total, que es el punto superior de la normal del objeto, podría decirse que sería su coronilla, le restamos el nivel al que estará cubierto de nieve.
difference = saturate(difference / _Wetness);
Luego pulimos este valor, dividiéndolo por el valor de humedad, que lo que hace es suavizar los bordes de la nieve, para que no se vea cortante la transición entre el color real del objeto y la nieve, luego limitamos ese valor para que no salga del rango entre 0 y 1
fixed worldPos = dot(IN.worldPos, fixed3(0, 1 / _SnowLevel, 0));
Ahora la parte especial de este shader, hacemos uso del vector mundial de posición y lo usamos en un producto punto, donde lo multiplicaremos con un vector que construimos que apunta siempre hacia arriba. El único componente de este vector que tiene algún valor es el Y, que dividimos por la altura a la que deseamos "_SnowLevel".
o.Albedo = lerp(c, difference + (1 - difference) * c, worldPos);
El color resultante es una interpolación entre el color del objeto y el color con la nieve ya aplicada y quien controla este valor es la posición mundial que hemos calculado, esto quiere decir que entre más alto esté el objeto más se va notando la capa de nieve sobre este.
Aquí el shader completo:
Shader "Surface/Lambert/WikiBook/WorldSnowShader" {
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
[Normal]_Bump("Bump", 2D) = "bump" {}
[MaterialToggle(IF_SNOW)] _IfSnow("if Snow", Float) = 1
[ShowIf(IF_SNOW)]_Snow("Snow Level", Range(0,1)) = 1
_Color("Color", Color) = (1.0,1.0,1.0,1.0) [ShowIf(IF_SNOW)]_SnowDirection("Snow Direction", Vector) = (0,1,0) [ShowIf(IF_SNOW)]_SnowDepth("Snow Depth", Range(0,0.2)) = 0.1 [ShowIf(IF_SNOW)]_Wetness("Wetness", Range(0, 0.5)) = 0.3 [ShowIf(IF_SNOW)]_SnowLevel("Snow level", Float) = 1
}
SubShader{
Tags { "RenderType" = "Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
#pragma shader_feature IF_SNOW
sampler2D _MainTex;
sampler2D _Bump;
fixed4 _Color;
#if IF_SNOW
float _Snow, _SnowDepth, _Wetness;
float4 _SnowDirection;
float _SnowLevel;
#endif
struct Input
{
float2 uv_MainTex;
float2 uv_Bump;
float3 worldNormal;
float3 worldPos;
INTERNAL_DATA
};
void surf(Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));
#if IF_SNOW
half difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);
difference = saturate(difference / _Wetness);
fixed worldPos = dot(IN.worldPos, fixed3(0, 1 / _SnowLevel, 0));
o.Albedo = lerp(c, difference + (1 - difference) * c, worldPos);
#else
o.Albedo = c;
#endif
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Esto es todo por ahora, sé qué este shader se puede mejorar, por ejemplo limitando el valor de la posición mundial, quizás mejorando la lectura de algunas variables, tengo planeado hacer algunas mejoras sutiles en el futuro, como hacer uso de la instanciación si es posible, pero eso es tema para otro blog.
Comentarios