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

One Response 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.

Leave a Reply