Posted on Categories Games, Software DevelopmentTags ,

My Quest for a “Blend Shader” – An Introduction to Unity Shaders, Part 2

  • Part I – Creating a basic shader
  • Part II – Creating a blend shader (You are here)
  • Part III – Extending the blend shader (normal map, metallic map, specular map, etc.)  (Coming Soon)

    Part II – Creating a blend shader

Hello, and welcome back to our series on writing Unity Standard Surface Shaders.

Last time we went over the basics of writing a Unity Standard Surface Shader that would control the surface color of an object. Today we are going to add support for transparency, add support for applying a bitmap texture, and then add support for blending between two bitmap textures.

Adding Transparency

If you recall from last time, we ended up with a simple shader that can only control an object’s surface color, as seen below.

The basic shader from Part 1 controls surface color.
The basic shader from Part 1 controls surface color.

The shader code we ended up with last time was this:

Shader "Custom/TestShader2" {
     Properties {
          _Color ("My Color", Color) = (1,1,1,1) // The input color exposed in the Unity Editor, defined as type "Color" and set to rgba 1,1,1,1 (solid white) 
     }

     SubShader {
          Tags { "RenderType"="Opaque" } 

          CGPROGRAM
          // Physically based Standard lighting model,
          // enable shadows on all light types
          #pragma surface surf Standard  

          // This Input struct is required for the "surf" function
          struct Input {
               float2 x;
          };

          fixed4 _Color; // A variable to store rgba color values
          // This "surf" surface function with this signature is required
          // It is executed for each pixel of the objects with this shader
          void surf (Input IN, inout SurfaceOutputStandard o) {
               // Albedo comes is tinted by a color
               fixed4 c = _Color; // Get the _Color value
               o.Albedo = c.rgb; // Set the "Albedo" or diffuse color rgb

          }
          ENDCG
     }
     FallBack "Diffuse" // Revert to legacy shader if this shader-type not supported
}

Let’s add support for transparency by changing a few lines our code. First, update the “Tags” line and change from using the “Opaque” queue to the “Transparent” queue:

Tags {"Queue"="Transparent" "RenderType"="Transparent" }

This lets the rendering engine know the order in which to render this object and that it should be rendered with the transparent objects after the opaque objects. Next, update the #pragma surface… directive to include the “alpha” keyword.

#pragma surface surf Standard alpha

Finally, update the surf() surface function to apply the alpha value of the _Color input property:

o.Alpha = _Color.a;

We now have the following shader code:

Shader "Custom/TestShader3" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" } 
        
        CGPROGRAM
        // The "alpha" keyword is added to the surface directive
        #pragma surface surf Standard alpha
          
        struct Input {
            float2 x;
        };
         
        fixed4 _Color;
          
        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = _Color;
            o.Albedo = c.rgb;  
            o.Alpha = _Color.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Change the name to “TestShader3” and then save this file as TestShader3.shader. When create a new Material with this shader and apply it to an object, we can now control the object’s transparency by manipulating the alpha value of the Material’s color property.

The shader now supports transparency.
The shader now supports transparency.

Adding a bitmap texture

Colors are great and all, but surfaces look much more interesting when we can apply bitmap textures to them, so let’s do that next.

Within the Properties block at the top, add a new input property called _MainTex:

_MainTex ("Albedo (RGB)", 2D) = "white" {}

We have given the var a type of “2D” (which is an 2D image texture) and set the default value as a solid white image. The name of this property in the Unity Inspector will be “Albedo (RGB)”. Remember, albedo is the term used for the un-shaded surface color of an object. If you save this file as a new shader and then apply it to a Material in unity, you can see there is now an “Albedo (RBG)” property that accepts an image.

We now have the "Albedo (RBG)" property exposed in the editor.
We now have the “Albedo (RBG)” property exposed in the editor.

However, this image will not be visible on your material because it is not being applied in the surface function. In order to do this, first update the Input struct to handling the input texture:

struct Input {
     float2 uv_MainTex;
};

Now the input structure of a surface shader requires that the texture coordinates be named starting with either “uv” or “uv2” and then should correspond to your input property name.

We then must add a variable to handle input property, just as we did for _Color. Since this main texture property is an image, we will use the type sampler2D:

sampler2D _MainTex;

Great! Now we are ready to alter the heart of the surf() surface function in order to apply this bitmap.

void surf (Input IN, inout SurfaceOutputStandard o) {
     fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
     o.Albedo = c.rgb; 
     o.Alpha = c.a;
}

Take a look at the line fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; which is what applies the texture. As you can see, we pass the texture property _MainTex and the uv_MainTex input struct to the built-in tex2D() function and then multiply that by the _Color property. The tells the shader that for each pixel on the object, get the image pixel color at this location and apply the chosen color. This value is stored in the c variable, which is then applied to the o.Albedo and o.Alpha properties. Simple!

Now we can apply a bitmap texture to our Material.
Now we can apply a bitmap texture to our Material.

Cool, we now have a basic surface shader that allows us to support color, transparency, and a bitmap texture.

Shader "Custom/TestShader3" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" } 
         
        CGPROGRAM

        // Physically based Standard lighting model
        #pragma surface surf Standard alpha
        struct Input {
            float2 uv_MainTex;
        };
         
        fixed4 _Color; 
        sampler2D _MainTex;

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb; 
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Blending between two bitmaps

Now let’s start add a second bitmap texture to the shader for blending. In the Properties block of the shader add new properties for _BlendColor and _BlendTex, which are basically duplicates of the existing _Color and _MainTex properties.

// The Blend texture
_BlendColor ("Blend Texture Color", Color) = (1,1,1,1)
_BlendTex ("Blend Texture Albedo (RGB)", 2D) = "white" {}

If we save this to a new shader file and apply that to a Material in Unity, we can see the new properties in the Inspector.

The new Blend properties are visible now.
The new Blend properties are visible, now.

We then need to add the new bitmap variable texture coordinates into the Input struct:

struct Input {
     float2 uv_MainTex;   // This line was already present
     float2 uv_BlendTex;
};

Next we need to make sure we have variables inside the shader to handle the new properties. After the line fixed4 _Color; add variables to handle the new Properties.

fixed4 _Color;      // This line was already present
sampler2D _MainTex; // This line was already present
fixed4 _BlendColor; 
sampler2D _BlendTex;

Finally, in our surf() function, we need to calculate the the new blend inputs and then add them to the original texture and color values.

void surf (Input IN, inout SurfaceOutputStandard o) {
      
     fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
     fixed4 b = tex2D (_BlendTex, IN.uv_BlendTex) * _BlendColor;
    
     o.Albedo =  (c.rgb) + (b.rgb);  
     o.Alpha = (c.a) + (b.a); 
}

But wait! Something is wrong. The textures don’t blend properly and when we make the object semi-transparent, we see shadow color is bright:

Something isn't quite right here!
Something isn’t quite right here!

Simply adding the two colors together doesn’t produce the desired effect, which is a nice blend between two textures. What we need is a way to control the amount of contribution from each texture so we can smoothly transition between 100% Texture A to 100% Texture B. We will do that by adding another Property called _Blend that will be a float value from 0-1.

_Blend ("Blend Textures Amount", Range(0.0,1.0)) = 0.0

This will create a range slider in the Inspector allowing the user to input values between 0.0-1.0. For our purposes 0 will mean show 100% of the main texture and 1.0 will show 100% of the blend texture. A value of 0.5 will display a 50/50 mix between both textures.

Finally, we need to add a half _Blend;declaration to handle this new variable and then update our surf() function to make sure it properly combines the main texture with the blend texture at the specified percentages:

void surf (Input IN, inout SurfaceOutputStandard o) {
     // Albedo comes from a texture tinted by color
     fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
     fixed4 b = tex2D (_BlendTex, IN.uv_BlendTex) * _BlendColor;
     
     // Set the Albedo and alpha to the main texture combined with the blend texture based on the blend amount
     o.Albedo =  (c.rgb * (1-_Blend)) + (b.rgb * (_Blend)); 
     o.Alpha = (c.a * (1- _Blend)) + (b.a * _Blend);
}

Our function now blends the proper percentages between the main and blend textures depending on the slider value. Let’s test it by blending a semi-transparent, green-tinted checkerboard texture with an opaque, non-tinted wood texture.

The updated shader properly blends between the two textures.
The updated shader properly blends between the two textures.

The completed (basic) blend shader

We now have a shader that allows us to blend smoothly between two bitmap textures including individual color and alpha value support!

A shader that allows blending between two textures.
A shader that allows blending between two textures.

The completed shader code:

Shader "Custom/TestShader4" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}

        // The blending texture blend amount
         _Blend ("Blend Textures Amount", Range(0.0,1.0)) = 0.0

        // The Blend texture
        _BlendColor ("Blend Texture Color", Color) = (1,1,1,1)
        _BlendTex ("Blend Texture Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" } 
        
        CGPROGRAM

        // Physically based Standard lighting model
        #pragma surface surf Standard alpha  

        struct Input {
            float2 uv_MainTex;
            float2 uv_BlendTex;
        };
         
        fixed4 _Color;
        fixed4 _BlendColor;
        sampler2D _MainTex;
        sampler2D _BlendTex;
        half _Blend;

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            fixed4 b = tex2D (_BlendTex, IN.uv_BlendTex) * _BlendColor;
            o.Albedo =  (c.rgb * (1-_Blend)) + (b.rgb * (_Blend));
            o.Alpha = (c.a * (1- _Blend)) + (b.a * _Blend);
        }
        ENDCG
    }
    FallBack "Diffuse"
}

That’s pretty awesome! Join in next time when we will add support for all the shader features you know and love like normal maps, specular maps, emission maps, etc. See you soon!

Leave a Reply

Your email address will not be published. Required fields are marked *