Shader Part 2

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

In the second part of this 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.

We need to 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;

Note how we can treat everything as a float of different sizes apart from the texture samplers. Remember that the Cg section between CGPROGRAM and ENDCG, it is actually independent from the rest of the shader which is using ShaderLab which is Unity shader system. The properties defined in the Properties section belongs to ShaderLab and they need to be connected with Cg. This is the reason why we need to declare them again with same naming, ShaderLab compiler will make the link.

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

Moving on to part 3.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s