Unity Shaders 3: Nieve realista


Este es el tercero de 6 artículos que fueron publicados originalmente en inglés en la web Unity Gems por Mike Talbot quien me ha autorizado a traducirlos al español.

Si no has leído los anteriores artículos puedes encontrarlos aquí:

El título original de este tercer artículo es:

En la segunda parte de esta serie hemos cubierto la construcción de shader de nieve acumulativo. Esta parte amplía el shader usando matemáticas para crear una mezcla de colores en el límite de la nieve con la roca.

Deberías leer este tutorial si:

  • Quieres entender cómo mezclar colores en un surface shader
  • Quieres construir un shader de nieve más realista

Introducción

En mi deseo de perfeccionar mi shader de nieve, me di cuenta de que el corte entre la nieve y no-nieve era muy brusco. Parecía más una mancha de pintura que algo húmedo acumulándose. Así que el siguiente paso fue dejar un margen donde tanto la nieve como la textura de la roca se renderizaran a la vez.

Esto resulta simplemente en modificar la configuración de los píxeles en el surface shader, pero muestra el uso de la muy útil función saturate.

Planeando el shader

  • Cuando el nivel de la nieve indique que el píxel debería ser del color de la nieve, permitir un margen donde la nieve es semi trasparente, haciéndose más opaca a medida que disminuye el ángulo del píxel con respecto a la dirección de la que cae la nieve. En otras palabras, cuanto más nevado más opaca es la nieve del píxel hasta convertirse en blanco sólido (o el color que hayamos elegido para la nieve).

Implementando el shader

La gran diferencia con este shader es que pasamos de una decisión binaria (nieve o roca) a un rango de valores. Eso hace que tengamos que reescribir la lógica de nuestro shader y empezar a usar matemáticas en lugar de condiciones if.

Primero necesitamos una propiedad que indique cuánto queremos mezclar la nieve, llamaremos a este valor _Wetness:

_Wetness ("Wetness", Range(0, 0.5)) = 0.3

También necesitamos la variable que represente esta propiedad:

float _Wetness;

Ahora calcularemos la diferencia entre el producto escalar entre la normal del píxel y la dirección desde la que cae la nieve y el valor suavizado (lerp) del nivel de la nieve. Esto nos da un valor basado en el coseno del ángulo, que es lo que representa _Wetness.

void surf (Input IN, inout SurfaceOutput o) {
    half4 c = tex2D (_MainTex, IN.uv_MainTex);
    o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

    float dotValue = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) ;
    float lerpValue = lerp(1,-1,_Snow);
    float difference = dotValue - lerpValue;
    difference = saturate(difference / _Wetness);

    o.Albedo = difference*_SnowColor.rgb + (1-difference) *c;
    o.Alpha = c.a;
}

Entonces saturamos la diferencia entre la normal del píxel y el rango de la nieve actual dividido por _Wetness.

  • Al saturarlo obtenemos un valor entre 0 y 1.
  • Así que si estamos fuera del rango que se considera nevado (diferencia menor que 0), el valor será 0.
  • Si _Wetness tuviera su valor por defecto de 0.3
    • Si estuviéramos a 27 grados (30% de 90 grados) el valor caería en el rango 0..1, en otro caso el valor sería 1.

El rango del coseno es de 1 a -1 (una diferencia de 2). Esto representa 180 grados (lo mismo en la dirección opuesta), por lo tanto un valor de 1 en términos de coseno son 90 grados. Nuestro cálculo ha sido 0.3 * 90 = 27 grados.

Entonces tomamos esta diferencia y la multiplicamos por el  color de la nieve, dándonos una proporción del color. También calculamos la proporción inversa para obtener el color de la textura y los sumamos. Esto realiza la mezcla del color de la nieve y la textura de la roca empezando a los 27 grados hasta volverse totalmente opaca.

Corrigiendo los vértices

Nuestro único problema ahora es que si la nieve está muy húmeda o derretida ¡nuestro modelo se podría expandir antes de que esté realmente cubierto de nieve! Esto no es muy realista, por lo que aplicaremos nuestro factor _Wetness al rango de nieve. Esto significa que el modelo se expandirá en función de lo húmedo que esté.


void vert (inout appdata_full v) {
    float dotValue = dot(v.normal, _SnowDirection.xyz);
    float lerpValue = lerp(1, -1, ((1 -_Wetness) * _Snow * 2) / 3);

    if( dotValue >= lerpValue)
    {
        v.vertex.xyz += (_SnowDirection.xyz + v.normal) * _SnowDepth * _Snow;
    }
}

Entonces, la solución consiste en escalar el nivel de la nieve (_Snow) por 1 – _Wetness. Esto significa que con _Wetness=0 nada cambiará y a _Wetness máximo (0.5), escalaremos efectivamente por 1/3 en lugar de 2/3.

Eso es todo, trabajo hecho.

Código fuente

Shader "Custom/SnowShader" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Bump ("Bump", 2D) = "bump" {}
        _Snow ("Snow Level", Range(0,1) ) = 0
        _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)
        _SnowDirection ("Snow Direction", Vector) = (0,1,0)
        _SnowDepth ("Snow Depth", Range(0,0.2)) = 0.1
        _Wetness ("Wetness", Range(0, 0.5)) = 0.3
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert 

        sampler2D _MainTex;
        sampler2D _Bump;
        float _Snow;
        float4 _SnowColor;
        float4 _SnowDirection;
        float _SnowDepth; 
        float _Wetness;

        struct Input {
            float2 uv_MainTex;
            float2 uv_Bump;
            float3 worldNormal;
            INTERNAL_DATA
        };

         void vert (inout appdata_full v) {
                   //Convert the normal to world coortinates
                   float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

                  if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3))
                  {
                      v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
                  }
                }

        void surf (Input IN, inout SurfaceOutput o) { 
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));
            half difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);;
            difference = saturate(difference / _Wetness);
            o.Albedo = difference*_SnowColor.rgb + (1-difference) *c; 
            o.Alpha = c.a;
        }
        ENDCG
    } 
    FallBack "Diffuse"
}

One thought on “Unity Shaders 3: Nieve realista

  1. Pingback: Unity Shaders 3: Nieve realista | David Erosa

Leave a Reply

Your email address will not be published. Required fields are marked *