Simple 2D light shader in Unity

Lighting in 2D games can be achieved by different means. In Unity you can find nice assets in the asset store or use the 3D classic lights by changing your sprite shader from default to diffuse. What I want to achieve here is a very simple light effect by addding a sprite mask on top of my scene and modify the resulting color in a shader to emulate light like the image below:

Light effect shader

The problem

Ok, so if we add a transparent sprite on top of another in unity the last color will be added to the previous.
It means that if the sprite under was black (0,0,0,1) and my ligthing mask sprite was half transparent white (1,1,1,0.5) the resulting color will be gray (0+1*0.5,0+1*0.5,0+1*0.5).

Sprite default material behavior

The sprite below is brighter, this is good but we can do better. The problem with this approach is that we do not expect the light to make everything tend to the white color as we can see in this picture with the same lighting mask sprite on the door sprite:

Lighting sprite mask on the door

To achieve a better light effect we need to enhance colors and contrast but we also need to keep black as black as possible.

Enhancing colors

Since we will use transparent sprites we need to decide what blending we will implement. You can check Unity's documentation to find all the blending operations and factors available: http://docs.unity3d.com/Manual/SL-Blend.html.
Different blending techniques will have very different results. Here we want to add two sprites together and blend them.
With the Add operation, the color we will generate in the shader will be multiplied by a factor and the color already on the screen (the door) multiplied by another factor and the two colors will be added together. These two factors are called SrcFactor and DstFactor.
You can choose both factors in a list of premade factors.

Operation : Blend SrcFactor DstFactor
Resulting color = Light * SrcFactor + Background * DstFactor

The basic blending operation for transparent sprites is Alpha blending : Blend SrcAlpha OneMinusSrcAlpha

  • If we generate transparent (50%) white light and add it to black background (SrcFactor = 0.5, DstFactor = 1-0.5):
    Resulting color = (1,1,1) * 0.5 + (0,0,0) * (1-0.5) we will have gray (0.5,0.5,0.5), but we want to keep the black black so let's look at another operation.

The Multiplicative blending: Blend DstColor Zero

  • If we generate white light and add it to black background (SrcFactor = (0,0,0), DstFactor = 0):
    Resulting color = (1,1,1) * (0,0,0) + (0,0,0) * 0 = (0,0,0) in other words black which is what we wanted to do. Ok let's try with white now.
  • If we generate white light and add it to white background (SrcFactor = (1,1,1), DstFactor = 0):
    Resulting color = (1,1,1) * (1,1,1) + (1,1,1) * 0 = (1,1,1) this is white.
  • Now if we generate black light and add it to white background (SrcFactor = (1,1,1), DstFactor = 0):
    Resulting color = (0,0,0) * (1,1,1) + (1,1,1) * 0 = (0,0,0) black. Well we do not want a dark light to make the background darker, the light is just here to enhance the colors not the opposite, let's change it a little bit.

Custom operation: Blend DstColor One.

  • If we generate white light and add it to black background (SrcFactor = (0,0,0), DstFactor = 1):
    Resulting color = (1,1,1) * (0,0,0) + (0,0,0) * 1 = (0,0,0) this is black.
  • If we generate white light and add it to white background (SrcFactor = (1,1,1), DstFactor = 1):
    Resulting color = (1,1,1) * (1,1,1) + (1,1,1) * 1 = (1,1,1) the maximum of the color value is 1, so this is white, everything we will add to white will be white even black.
  • If we generate white light and add it to gray background (SrcFactor = (0.5,0.5,0.5), DstFactor = 1):
    Resulting color = (1,1,1) * (0.5,0.5,0.5) + (0.5,0.5,0.5) * 1 = (1,1,1) white, the color is brighter.

Ok we've made it, this blending adds to an existing color a percentage of itself, with a white sprite it will be 100%, but with another mask sprite you can achieve nice lighting effects. Let's try this now on our door.

DstColorOne blend effect

The result is much more convincing. Now let's dive into the code. I will not explain how to write shaders in Unity because this is not the subject of the article but it's a pretty basic shader, I added a tint color and you can even manage the resulting color with the sprite component color widget. Since we do not take care of the alpha in the blending I manage it in the resulting rgb colors. The contrast factor is here to adjust the power of the effect.

Shader "Custom/Light"
{
    Properties
    {
        _MainTex ("Diffuse Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _ContrastFactor ("Contrast Factor", Float) = 1.0
    }
    
    SubShader
    {
         Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "ForceNoShadowCasting"="True"
        }
        
        Pass
        {
            AlphaTest Greater 0.0 // Pixel with an alpha of 0 should be ignored
            Blend DstColor One // Keep black values
            
            CGPROGRAM
             
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
             
            // User-specified properties
            uniform sampler2D _MainTex;
            uniform float4 _Color;
            uniform float _ContrastFactor;
             
            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            struct VertexOutput
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            VertexOutput vert(VertexInput input)
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = input.uv;
                output.color = input.color;
                
                return output;
            }
             
            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                // Retrieve color from texture and multiply it by tint color and by sprite color
                // Multiply everything by texture alpha to emulate transparency
                diffuseColor.rgb = diffuseColor.rgb * _Color.rgb * input.color.rgb;
                diffuseColor.rgb *= diffuseColor.a * _Color.a * input.color.a;
                diffuseColor *= _ContrastFactor;
                
                return float4(diffuseColor);
             }
         
             ENDCG
        }
    }
}

There is one important issue though.

  • If we generate white and add it to red (SrcFactor = (1,0,0), DstFactor = 1):
    Resulting color = (1,1,1) * (1,0,0) + (1,0,0) * 1 = (1,0,0) this is the same red, the light has no effect if the value is already at is maximum.

Enhancing issue with max value

We need to change slightly the color of the sprite we are lighting.

Adding light color

This part is easier we will just add a new pass with an alpha blending shader to add some color to the previous pass. The color factor will limit the percentage of color we will add to the previous effect.

        Pass
        {
            AlphaTest Greater 0.0 // Pixel with an alpha of 0 should be ignored
            Blend SrcAlpha One // Add colours to the previous pixels

            CGPROGRAM
             
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
             
            // User-specified properties
            uniform sampler2D _MainTex;
            uniform float4 _Color;
            uniform float _ColorFactor;
             
            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            struct VertexOutput
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            VertexOutput vert(VertexInput input)
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = input.uv;
                output.color = input.color;
                
                return output;
            }
             
            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                diffuseColor.rgb = _Color.rgb * diffuseColor.rgb * input.color.rgb * diffuseColor.a;
                diffuseColor *= _ColorFactor;
                diffuseColor.a = _Color.a * input.color.a;

                return float4(diffuseColor);
            }
         
            ENDCG
        }

Ok putting everything together and adding a slight orange tint to the light we can see the result with a contrast factor of 1 and a color factor of 0.5:

Light effects combined

Give it some life

Let's add some flying particles in the light and change the intensity over time to make it more real.
I added two textures for the particles and an intensity factor.
The particles textures are added in the second alpha blending pass. To make them move I use the TRANSFORM_TEX macro from UnityCG.cginc to make sure texture offset is applied correctly and give some sine variation as parameters in the vertex shader. Then I add these two textures into the fragment shader to compute the final color. You can specify the "black" default string in the properties of the shader so the shader can work without these two particles textures.

                float u = input.uv.x + _SinTime.x * 0.1f;
                float v = input.uv.y + _SinTime.x * _CosTime.y * _SinTime.x * 0.1f;
                output.uv_part = TRANSFORM_TEX(float2(u, v), _Particles);

For the ligth intensity variation you can add a intensity variable to ouput from the vertex shader and use it to multiply the resulting color in the fragment shader.

                output.intensity = _IntensityFactor * _ContrastFactor * cos(_Time.z) * sin(_Time.w) * _CosTime.w
                                + 1.0 * _ContrastFactor;

This is the final shader, keep in mind that it is not optimized for the clarity of the article. We can replace some float by the fixed data type and do some optimization on calculations too.

Shader "Custom/Light"
{
    Properties
    {
        _MainTex ("Diffuse Texture", 2D) = "white" {}
        _Particles ("Particles Texture even", 2D) = "black" {}
        _Particles2 ("Particles Texture odd", 2D) = "black" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _ContrastFactor ("Contrast Factor", Float) = 1.0
        _ColorFactor ("Color Factor", Float) = 0.5
        _IntensityFactor ("Intensity Variation Factor", Float) = 0.5
        _ParticleFactor ("Particle Factor", Float) = 1.0
    }
    
    SubShader
    {
         Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "ForceNoShadowCasting"="True"
        }
        
        Pass
        {
            AlphaTest Greater 0.0     // Pixel with an alpha of 0 should be ignored
            Blend DstColor One // Keep deep black values
            
            CGPROGRAM
             
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
             
            // User-specified properties
            uniform sampler2D _MainTex;
            uniform float4 _Color;
            uniform float _ContrastFactor;
            uniform float _IntensityFactor;
             
            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            struct VertexOutput
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
                float intensity : TEXCOORD1;
            };
             
            VertexOutput vert(VertexInput input)
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = input.uv;
                output.color = input.color;
                output.intensity = _IntensityFactor * _ContrastFactor * cos(_Time.z) * sin(_Time.w) * _CosTime.w
                                + 1.0 * _ContrastFactor; 
                return output;
            }
             
            float4 frag(VertexOutput input) : COLOR
            {
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                // Retrieve color from texture and multiply it by tint color and by sprite color
                // Multiply everything by texture alpha to emulate transparency
                diffuseColor.rgb = diffuseColor.rgb * _Color.rgb * input.color.rgb;
                diffuseColor.rgb *= diffuseColor.a * _Color.a * input.color.a;
                diffuseColor *= input.intensity;
                
                return float4(diffuseColor);
              }
         
               ENDCG
           }
           
        Pass
        {
            AlphaTest Greater 0.0     // Pixel with an alpha of 0 should be ignored
            Blend SrcAlpha One // Add colours to the previous pixels

            CGPROGRAM
             
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
             
            // User-specified properties
            uniform sampler2D _MainTex;
            uniform sampler2D _Particles;
            uniform sampler2D _Particles2;
            uniform float4 _Particles_ST;
            uniform float4 _Particles2_ST;
            uniform float4 _Color;
            uniform float _ColorFactor;
            uniform float _ParticleFactor;
             
            struct VertexInput
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };
             
            struct VertexOutput
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
                float2 uv_part : TEXCOORD1;
                float2 uv_part2 : TEXCOORD2;
                float4 color : COLOR;
            };
             
            VertexOutput vert(VertexInput input)
            {
                VertexOutput output;
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
                output.uv = input.uv;
                
                float u = input.uv.x + _SinTime.x * 0.1f;
                float v = input.uv.y + _SinTime.x * _CosTime.y * _SinTime.x * 0.1f;
                output.uv_part = TRANSFORM_TEX(float2(u, v), _Particles);

                float u2 = input.uv.x + _SinTime.x * 0.05f;
                float v2 = input.uv.y + _SinTime.y * _CosTime.x * _SinTime.x * 0.1f;
                output.uv_part2 = TRANSFORM_TEX(float2(u2, v2), _Particles2);
                output.color = input.color;
                
                return output;
            }
             
            float4 frag(VertexOutput input) : COLOR
            {
                
                float4 diffuseColor = tex2D(_MainTex, input.uv);
                diffuseColor.rgb = _Color.rgb * diffuseColor.rgb * input.color.rgb * diffuseColor.a;
                diffuseColor *= _ColorFactor;
                
                float4 partColor = tex2D(_Particles, input.uv_part);
                float4 part2Color = tex2D(_Particles2, input.uv_part2);
                
                partColor.rgb = partColor.rgb * input.color.rgb * partColor.a;
                part2Color.rgb = part2Color.rgb * input.color.rgb * part2Color.a;
                
                float4 finalColor;
                finalColor = diffuseColor + (partColor + part2Color) * _ParticleFactor;
                finalColor.a = _Color.a * input.color.a;
                
                return float4(finalColor);
             }
         
             ENDCG
        }
    }
}

Hope you enjoyed this article. Here is a last picture of the lights in action.

Light shader final

GitHub mark Download it on GitHub.

Menu