Go to Top

In #2 of this series we covered building an accumulative snow shader. This part expands on that by using mathematics to create blended colours on the margin of the snow to rock boundary.

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.

Source Code

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

6 Responses to "Noobs Guide To Shaders #3 – More Realistic Snow"

  • Lovrenc
    January 20, 2013 - 4:34 am Reply

    In the end source code you forgot to add the changes in vert function. Vert function is same as in tutorial 2 and does not consider wetness at all.

  • Chris
    November 21, 2013 - 5:34 pm Reply

    How can I use _Time.y to have the snow increase on the object over time?

  • MM
    February 27, 2014 - 7:51 pm Reply

    Hi,
    very nice effect, but vertex manipulations doesn’t work well for me.
    I’m not sure about those space transformations in vertex program:
    ” //Convert the normal to world coortinates
    float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection); ”

    _SnowDirection is already in WorldCoords and it is v.normal that needs to be transformed into WorldCoords (which is not that obvious). Only then it is possible to compare _SnowDirection and v.normal dot product.

    Here is what works for me:

    //in vert
    fixed3 sn = normalize(_SnowDirection.xyz);
    //normalize let us change direction of snow properly from inspector

    fixed3 normalWorld = normalize(mul(v.normal,float3x3(_World2Object)));

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

    • MM
      February 27, 2014 - 8:11 pm Reply

      some modified version:
      //in vert
      fixed3 sn = normalize(_SnowDirection.xyz);
      //normalize let us change direction of snow properly from inspector

      fixed3 normalWorld = normalize(mul(v.normal,float3x3(_World2Object)));
      fixed NdotS = dot(normalWorld, sn);//computed once for reuse
      if(NdotS >= lerp(1,-1,(1-_Wetness)*(_Snow*2)/3))
      v.vertex.xyz +=NdotS*v.normal*_SnowDepth*_Snow;//NdotS for smoother vertex transitions

  • Quinn
    March 20, 2014 - 2:25 am Reply

    Could you perhaps do a tutorial on how to do this shader with an additional texture and normalmap – like for a base texture / normalmap and also a transparent overlay texture / normalmap and then the snow on top? I’ve been following these tutorials in an attempt to learn how to do just that but I can’t seem to get a grasp on adding in the second layer of textures and also the snow. I can get the two layers by themselves, or I can get one layer and the snow, but combined doesn’t seem to be working – and 90% of the models I want to use this on use two sets of textures. Thanks either way, these tutorials are awesome!

Leave a Reply