Shader part 3

You should read this tutorial if:

  • You want to understand how to blend colours in a surface shader
  • You want to build a more realistic snow shader

Introduction

In my desire to perfect my snow shader, I realised that the cut off between snow and no snow was a bit abrupt – more like paint splatter than actual accumulating the wet stuff! So the next step was to allow a margin where both the snow and the texture were rendered at the same time.

This ends up being just a modification to the pixel settings of the surface shader program – but it demonstrates the highly useful saturate function.

Planning The Shader

When the Snow level dictates that a pixel should be snowy, allow a margin where the snow is semi transparent white, getting more opaque the closer the angle of the pixel is to the snow direction modified by the level of snow. In other words the more snowy it is, the more opaque the snow on a pixel gets before it becomes solid white (or in fact, snow color).
Implementing The Shader

So the big difference with this shader is that we move from a binary switch to a range of values. That make us rewrite our shader logic and start to use mathematics rather than if based switches.

First we need a property to indicate how much we should blend the snow – we’ll call that _Wetness for want of a better name:

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

We then need a variable to represent the property:

float _Wetness;

We now calculate the difference between the dot of the pixels normal against the snow directions with the currently lerped value based on the level of snow. This gives us a value in terms of the cosine of the angle, which is also what the _Wetness represents.

void surf (Input IN, inout SurfaceOutput o) {
    half4 c = tex2D (_MainTex, IN.uv_MainTex);
    o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));
    float 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;
}

We then saturate the difference between the normal of the pixel and the range of our current snow divided by _Wetness.

Saturating gives us a value clamped between 0 and 1.
So if we were out of range for being snowy (the difference was < 0), the value will be 0. If _Wetness were its default 0.3 if we were within 27 degrees (30% of 90 degrees) then the value will lie somewhere between 0..1, otherwise the value will be 1. The range of a cosine is 1 to -1 – a difference of 2, this represents 180 degrees (same direction to opposite direction), hence a value of 1 in cosine terms is 90 degrees. Our calculation is 0.3 * 90 = 27 degrees. We then take this difference and multiply it by the snow color – giving us a proportion of that color, we then take the inverse proportion of the texture color and add them together. This effectively blends the snow color into the texture color over 27 degrees of angle between the start of the snow and it becoming totally opaque. Fixing the Vertices Our only problem now is that if the snow is very wet then our model may expand before it is actually snowy! That’s not very realistic. So we apply our wetness factor to the snow range, this means the model will expand later depending on how wet it is.

void vert (inout appdata_full v) { 
    if(dot(v.normal, _SnowDirection.xyz) >= lerp(1,-1, ((1-_Wetness) * _Snow*2)/3)){
        v.vertex.xyz += (_SnowDirection.xyz + v.normal) * _SnowDepth * _Snow;
    }
}

So the modification is to scale the _Snow level by 1 – _Wetness – this means that at 0 wetness nothing changes and at our full (0.5) wetness, we are effective scaling the snow factor by 1/3 rather than 2/3 – making the model modify 50% later.

That’s it, job done.

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"
}

Moving on to part 4.

3 Comments

  1. Hi,

    First of all great posts on shaders, I am really enjoying learning these and I am for the most part getting how it all works (minus some of the Maths :P). With this example I am having difficulty seeing what the Wetness actually does when compared with the previous version of the Shader. I have swapped between them and played with values on the shader and can see no difference.

    Also I am having some issues with the shader applying the snow to the top facing polys on a rock, I am not sure if this is something to do with the model I am using or if there is something funny happening in the shader, but basically the snow seems to be starting from the side of my rocks rather than the top. See images below for examples.

    http://tinypic.com/r/143gbdg/9
    http://tinypic.com/r/2jaht75/9

    Any ideas what I might be missing here?

    Like

  2. Hei, first off, unfortunately, the shader articles were from the other author that is no longer participating in Unitygems. I will have a closer look at what the wetness is meant to do but I am not the writer. I just moved them from the old unitygems to the new one since people seem to like those.

    For the snow issue, is your model rotated? This may required to convert the normal of the vertex to world position before using it in the dot.

    Like

  3. Great tutorial. I really like starting off with something exciting like additive snow. I have one question about said feature though. When the snow accumulates seams start to open up on the edges of the snow. How could I “patch” those seams so there aren’t any holes?

    Like

Leave a comment