Go to Top

Having covered the basics of surface shader structure in #1, this part shows how to use that to create a bumped, mesh deforming, accumulating snow shader...

You should read this tutorial if you are new to shader programming and you want to learn about:

  • Building an accumulative snow shader
  • Creating a bumped shader
  • Modifying the texture applied for a pixel
  • Modifying the vertices of a model in a surface shader

Introduction

Ok in this second part of my guide to shader's we're going to build something useful! After all of the background work in the first part - we're going to build a simple "Snow" shader.

Here's an example of it in action on a bumped rock available free on the Asset Store.

Planning The Shader

Ok so what we want to do is pretty simple, we can express it like this:

  • As some Snow Level increases we want to turn pixels that face the snow direction to a Snow Color rather than the texture from the material
  • As some Snow Level increases we want to deform the model slightly to be bigger, predominantly on the side that the snow is blowing from.

Step 1 - Bumped Diffuse Shader

So lets start with a new Diffuse shader and add in bump mapping

Shader "Custom/SnowShader" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}

		//New normal map texture
		_Bump ("Bump", 2D) = "bump" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

		CGPROGRAM
		#pragma surface surf Lambert

		sampler2D _MainTex;
		//Must add a sample with the same name
		sampler2D _Bump;

		struct Input {
			float2 uv_MainTex;
			//Get the uv coordinates for the bump map
			float2 uv_Bump;
		};

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

			//Extract the normal map information from the texture
			o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump);

			o.Albedo = c.rgb;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}

This is pretty much the shader that Unity automatically makes for us as a base with just the Bump stuff added.

So we've:

  • Defined a property called _Bump which is a 2D image with a default of "bump" (empty normal map)
  • Created a sampler2D with exactly the same name
  • Created an entry in Input to get the uv coordinates for Bump (again using the same name)
  • Added on line of code calling the UnpackNormal function which takes a normal map texture and converts the result into a normal - we pass it the pixel from the texture, using tex2D and the _Bump variable and uv coorindates from the Input structure

After that we have a pretty unremarkable bumped shader.

Step 2 - Adding Some Snow

Ok for this step we need to actually work out whether the normal of a pixel is pointing roughly in the same direction as as the snow is coming from.

We're going to do that using the dot product. The dot product between two unit length vectors is equal to the cosine of the angle between those vectors.  Usefully CG has a dot function that will calculate it for us.  The great thing about the dot product is that it is helpfully 1 when the vectors are pointing exactly the same way and -1 when then point in exactly opposite directions, with a nice linear scale between them.  So we never need to know the angle for our shader, just the dot of the pixels normal and the snow direction.

A unit vector is one whose magnitude is 1 - so the square root of the squares of it's x, y and z components must be 1.  Don't fall into the trap of thinking a vector like (1,1,1) is a unit vector - it isn't.

To find the angle you must scale any vectors which are greater than unit length so that they become unit length.

Ok so armed with this knowledge let's define some properties for our shader.

	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.3)) = 0.1
	}

We've created:

  • Snow variable which will be the amount of snow that covers the rock, it's always in the range 0..1
  • a color for our snow (avoid yellow) which defaults to white
  • a direction from which the snow is falling (by default it is falling straight down, so our accumulation vector is straight up)
  • a depth for our snow that we will use when we modify the vertices in step 3, which is in the range 0..0.3
Following the information in the #1 of this series - we now go and make sure we have variables with the right names:
		sampler2D _MainTex;
		sampler2D _Bump;
		float _Snow;
		float4 _SnowColor;
		float4 _SnowDirection;
		float _SnowDepth;

Not how we can treat everything as a float of different sizes apart from the texture samplers.

Next we need to update the Input to our shader.  The normal map texture will give us the modification to the normal for a pixel, but for our effect to work we are going to need to work out the actual world normal so we can compare it with our snow direction.

This bit takes a bit of reading in the documentation - basically because we want to write to o.Normal in our shader we need to get the INTERNAL_DATA supplied by Unity and then call a function called WorldNormalVector in our shader program which needs that information.  The practical upshot is we need to put those things in the Input structure.

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

Now we can finally write our shader program

		void surf (Input IN, inout SurfaceOutput o) { 

			//Normal color of a pixel
			half4 c = tex2D (_MainTex, IN.uv_MainTex);

			//Get the normal from the bump map
			o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

			//Get the dot product of the real normal vector and our snow direction
			//and compare it to the snow level
			if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))
			    //If this should be snow pass on the snow color
			    o.Albedo = _SnowColor.rgb;
			else
				o.Albedo = c.rgb;
			o.Alpha = 1;
		}

Ok so we probably want to dissect the if statement which is where all the magic happens:

  • So we are going to get the dot product of two vectors - one is our snow direction and the other is the vector that will actually be used for the normal of the pixel - a combination of the world normal for this point and the bump map.


    We get that normal by calling WorldNormalVector passing it the Input structure with our new INTERNAL_DATA and the normal of the pixel from the bump map.


    After this dot product we will have a value between 1 (the pixel is exactly on the snow direction) and -1 (it is exactly opposite)

  • We then compare the dot value with a lerp - if our Snow level is 0 (no snow) this returns 1 and if the Snow level is 1 it will return -1 (the entire rock is covered).  It's quite normal to only vary the snow level between 0..0.5 when we use this shader so that we only have snow on surfaces that actually face the snow direction.

  • When the dot is greater that the snow level lerp we use the snow color, otherwise we use the texture
This is now a fully working snow shader and looks like this:
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,3)) = 0.1
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

		CGPROGRAM
		#pragma surface surf Lambert

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

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

		void surf (Input IN, inout SurfaceOutput o) { 

			//Normal color of a pixel
			half4 c = tex2D (_MainTex, IN.uv_MainTex);

			//Get the normal from the bump map
			o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

			//Get the dot product of the real normal vector and our snow direction
			//and compare it to the snow level
			if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>=lerp(1,-1,_Snow))
			    //If this should be snow pass on the snow color
			    o.Albedo = _SnowColor.rgb;
			else
				o.Albedo = c.rgb;
			o.Alpha = 1;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}

Deforming The Model

The final step is to deform the model to make it bigger, predominantly (but not completely) in the direction of the snow.

To do this we also need to modify the vertices of the model - this means telling the surface shader that we want to write a function to do just that.

		#pragma surface surf Lambert vertex:vert

At the end of the pragma we add a parameter vertex which provides the name of our vertex function: vert.

Now our vertex function looks like this:

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

Firstly we pass it a parameter - this is the incoming data and we've chosen to use appdata_full (from Unity) which has both texture coordinates, the normal, the vertex position and the tangent.  You can pass extra information to your pixel function by specifying a second parameter with your own Input data structure - where you can add extra values if you want - we don't need to do that.

The snow direction is in world space, but we are working in object space (coordinates of the model) so we have to transpose the snow direction, to object space by multiplying it by a Unity supplied matrix that's designed for that purpose.

We now only have the normal of the vertex so we do the same calculation for snow direction we did before - but we scale the snow level by 2/3 so that only areas well covered in snow already are modified.

Presuming our test passes we then modify the vertex by multiplying its normal + our now direction by the depth factor and the current snow level.  This has the effect of making the vertices move more towards the snow direction and increase this distortion as the snow level increases.

That's it - our job is done!

Source Code

The completed shader code looks like this:

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

		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));
			if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>=lerp(1,-1,_Snow))
			    o.Albedo = _SnowColor.rgb;
			else
				o.Albedo = c.rgb;
			o.Alpha = 1;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}
, , , ,

27 Responses to "Noobs Guide To Shaders #2 – Accumulative Snow Shader"

  • Sam
    November 12, 2012 - 12:04 am Reply

    The mesh deformation isn’t necessarily from the same direction as _SnowDirection. While the colour of the snow corrects itself if you rotate the object in the Scene, the mesh deformation is only correct if the object’s rotation is (0,0,0,0). Any suggestions to fix this?

    Also, you mention that you can pass extra information to the pixel function by adding a second Input parameter, but when I try to add this my compiler complains that it’s invalid. How would you do this?

    • whydoidoit
      November 12, 2012 - 8:17 am Reply

      Sam

      I’ve updated the article (I forgot to convert the snow direction to object space, so you are right it only worked at 0,0,0) now it should be fine. We basically multiply the snow direction out by the UNITY_MATRIX_IT_MV.

      As for a second parameter – well you do it like this:

      	struct Input {
      		    float snowDir;
      			float2 uv_MainTex;
      			float2 uv_Bump;
      			INTERNAL_DATA
      		};
      		
      		void vert (inout appdata_full v, out Input o) {
      		
      		  //Convert the normal to world coortinates
      		  float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
      		  
      		
      		  if(dot(v.normal, sn) >= lerp(1,-1, ((1-_Wetness) * _Snow*2)/3))
      		  {
      		    
                	v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; 
                }
                o.snowDir = sn;
              
                
              }
      

      So I’ve added snow direction to the input for the surface function and passed that structure to the vert program as an out parameter. We can then fill in the values we need.

      • Sam
        November 12, 2012 - 8:56 am Reply

        Ah, that makes sense. Thanks a lot for your help!

        I’m also now getting these compiler warnings:

        Shader warning in ‘Custom/SnowShader’: Program ‘vert_surf’, Temporary register limit of 8 exceeded; 9 registers needed to compile program at line 14
        Shader warning in ‘Custom/SnowShader’: Not enough temporary registers, needs 9 (compiling for flash) at line 15

        Where line 15 is where CGPROGRAM starts. The shader is running fine, but I can’t figure out what these warnings mean.

  • whydoidoit
    November 12, 2012 - 9:21 am Reply

    Yeah at the moment it isn’t compatible with Flash due to the amount of processing it is doing. I’m going to look at fixing that later – but I would guess it’s due to the effort of multiplying out the snow level by 2/3 and the 1- Wetness – I guess that should really be done outside the shader and made another property – it’s a bit wasteful in there.

  • whydoidoit
    November 12, 2012 - 9:24 am Reply

    Just a point – clearly the surface shader magic is also creating a vertex shader behind the scenes with more instructions in it than our vert function here.

  • Marco
    December 3, 2012 - 10:35 pm Reply

    The code in your example threw the following error at me:
    Program ‘vert_surf’, ambiguous overloaded function reference “mul(float4x4, float3)”

    Changing the line in the vertex function to the following code solved the issue for me (in case somebody is having the very same problem):

    void vert (inout appdata_full v) {
    // Convert the normal to world coordinates/world space
    float3 sn = mul((float3x3)_World2Object, _SnowDirection);

  • whydoidoit
    December 3, 2012 - 11:43 pm Reply

    That’s odd – I have _SnowDirection defined as a float4 I believe – so it should be fine…

    • Marco
      December 4, 2012 - 5:19 pm Reply

      Yes, and your code is totally fine. The issue was definitely on my end. I had it defined as float3 accidentely. So, no real problem, just a typo. :)

  • David
    December 19, 2012 - 7:11 pm Reply

    Thanks for this tutorial mate, I’m new to shaders in general and yours is pretty much exactly what I’m looking for. Just wondering, is it possible to add a different bump to the snow? lets say I want to have my snow covered ground, I love how the bump map of my ground help define where the snow lands,but once the snow completely covers the ground it still shows off my small ground features, where as i’d imagine the snow would provide a smoother surface (aka a different bump map) is this possible?

    On a slightly harder note (I think it’d be hard anyway) is it possible to use the mixing you’ve got happening between the snow and the rock texture and have it as a mix between two actual shaders? so if you spent time having a really life like rock shader with nice specular and all that jazz, then have the snow as its own shader, eg different spec. is this possible?

  • Omer
    December 28, 2012 - 9:42 am Reply

    I’ve just started doing this tutorial and it’s progressing really good. But I have a problem with the final part.

    The deformed parts of the model changes when I rotate the camera. Is this an expected behaviour?

  • whydoidoit
    December 28, 2012 - 10:37 am Reply

    No that’s wrong – I thought that I’d fixed that though. Hmmm. Will take a look.

    • Georges Paz
      January 1, 2013 - 10:42 pm Reply

      Quick dirty solution:
      Replace the entire vert part with this, it is faster:
      v.vertex.xyz += (_SnowDirection.xyz + v.normal) * _SnowDepth * _Snow;
      It will inflate the whole mesh uniformly while keeping in mind the _SnowDirection.

      Actually, this vertex modification makes tilled-mapped meshes (or UVs that are separated in islands) explodes (creating holes everywhere).
      The best bet would be to transform the vertex pos to the _SnowDirection space and scale it on the Y axis (once transformed).

      • Omer
        January 2, 2013 - 7:35 am Reply

        Thanks for the help. But even though the view direction problem is solved, this introduces another problem. While the whole mesh is inflated, I think some parts are more inflated. For me, it was a rock that didn’t have the pivot on center and the bigger half inflated more, while the smaller half moved slightly.

        As a side question, is there any resource that I can read to understand what is possible with a fragment, pixel or any other shader and what’s not?

        • Georges Paz
          January 2, 2013 - 8:38 am Reply

          Your vertex shader should look like this:

          void vert (inout appdata_full v)
          {
          v.vertex.xyz += (_SnowDirection.xyz + v.normal) * _SnowDepth * _Snow;
          }

          • Omer
            January 2, 2013 - 8:53 am

            I’m already using it like that. Snow direction is (0,1,0,1). But when I increase Snow Depth, the mesh inflates to the -z direction. I’ve uploaded two images so you can see how it inflates. First one is snow depth at min point, second one is at max point.

            min:
            http://i46.tinypic.com/16qo7m.jpg

            max:
            http://i45.tinypic.com/vr7s47.jpg

          • Maven
            April 21, 2013 - 3:14 pm

            float4 sn = mul(transpose(_Object2World) , _SnowDirection);

            I seem to achieve the desired effect with this line of code. What I did was, instead of multiplying the normal with UNITY_MATRIX_IT_MV (Inverse transpose of model*view matrix ), I multiplied it with transpose of _Object2World Matrix.

            So, the camera dependent snow depth problem should be solved as the object normals from object space are converted to world space(Independent of eye-space).

    • Omer
      January 2, 2013 - 8:33 am Reply

      I’ve started reading on it but I’m still a bit confused about all the different languages used in shaders. So far I’ve heard of HLSL, GLSL, cg and ShaderLab. Do these complete each other in a shader or are they substitutes? Because as far as I can see, the tutorial on Unity Shader Reference, the tutorial here and the examples on your link are completely different, even though they do mostly the same thing.

      • whydoidoit
        January 2, 2013 - 8:52 am Reply

        You are programming in ShaderLab and CG – you can choose to use surface shaders (like this article does) or you can use Vertex/Fragment shaders like the link I provided and the later articles in this series – it depends on what you want to do. Surface shaders are compiled into Vertex/Fragment shaders and have the benefit of being a lot simpler to write, with the downside of not having so much control. ShaderLab is the stuff around the outside of the CGPROGRAM which is the actual shader code. The core code of a surface shader and a vertex/fragment shader are the same – but surface shaders basically have a different set of steps which simplify the process of applying lighting meaning that you don’t have to do it yourself.

        I should also say that GLSL and CG are effectively the same thing.

        • Omer
          January 2, 2013 - 9:13 am Reply

          Thanks, that cleared a lot of confusion for me. I guess, I’ll just go for learning surface shaders first then.

  • Ilan
    March 3, 2013 - 5:18 pm Reply

    Hey, what object do I attach the shader to so that the snow does build up on the objects (terrain, buildings, rocks, houses….)?

  • Jonno
    August 26, 2013 - 3:53 am Reply

    Small syntax issue. In step 1, you forgot a bracket at the end of this line.

    29 o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump);

  • Sam
    August 31, 2013 - 7:39 am Reply

    Thats so fantastically awesome. Thanks man. You just got me super motivated to learn more about shaders.

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

    How can I integrate the _Time.y within the code so that the snow increases over time?

  • Markus
    December 6, 2013 - 11:21 pm Reply

    Awesome tutorial. But I’m having problems with transparency on the enlarged parts. I can see other objects through that area of the object, but they are blocked by the original shape of the object.
    Why is this happening?

  • S.
    December 8, 2013 - 2:44 pm Reply

    Is this snow accumulation as in it getting bigger, the height of the snow rises?
    Or just a surface will get more white?

Leave a Reply