Fast realtime shadows in Unity

Unity comes with built-in shadow map support, but you shouldn”t expect cutting edge technology. No soft shadows on mobile devices and at least for my current project it is too slow on the GPU. Unfortunately Unity 4.x does not provide many other solutions for realtime shadows. However there is hope that there will be improvements for shadows in Unity 5, but you can’t develop a game on hope can you? I decided to have a fallback solution that is fast and still rendering beautiful shadows.

What kind of shadows do you need?

In my case we are speaking of an outdoor scenario with sunlight casting sharp transparent shadows. These are the summarized requirements:

  • Shadow color tinting
  • Sharp shadows
  • Cheap to calculate (assumption bottleneck is on GPU)
  • Easy and fast workflow (e.g. quick setup, no baking)
  • Harmonizes with other shadow solutions (e.g. shadow map, baked shadows / light maps)
  • Easy to implement / develop (to reduce production costs)

Realtime Shadow Rendering

Here is a small introduction of available realtime shadow solutions I have discovered during my lifetime. There might be many other and better solutions I have not yet discovered. Please let me know if you have heared of another solution.

Shadow map

shadowmap_directional_lightOne could say a shadow map is actually a deep map from a light sources perspective.When rendering the actual screen pixel in forward rendering path a fragment could use the shadow map to decide if the pixel is visible to the light source (illuminated) or not (shadowed) by translating the pixels position to the “shadow map space” and comparing it with the shadow map value. If the shadow map has a higher value than the pixel is behind an object, or in other words the pixel is inside the shadow of that light.

image2014-12-16 9-53-4

RISK / LIMITATIONS

  • rendering a shadow map is expensive on gpu
  • if not optimised the result looks pixelated
    • optimising this is expensive on the gpu
  • each light must have its own shadow map
    • high resolution maps costs memory
    • lots of shadow casting lights could just kill any performance

FURTHER READING

Blob Mesh Shadow

The idea behind this technique is to render a shadow mesh plane on the ground below the shadow caster which represents the shadow caster in all situations. This was used in some old 3d jump and run games like Super Mario 3d Land. This technique is very fast to render, but obviosly not producing realistic results though it is ok for illustrative or fictitious scenes.

Projector Shadow

image2014-12-17 14-32-54Basically this technique is just like a blob shadow, but this time the shadow is not rendered as a mesh on the ground, but rendered per pixel on each object, The renderer has to render an additional pass to achieve this. Technically this should be doable in screen space as post effect if you have access to the z buffer. But I don’t know the details of the implementation.

Screen Space Shadow Tracing

The idea is to render the shadoscreenspace_shadoww from screen space informations. There is a paper from crytek outthere, but I think this solution has many limitations. First of all there is no shadows of objects that are not rendered. And secondly it is not very cheap to raytrace in screenspace.

RISK / LIMITATIONS

  • expensive on gpu bandwith
  • only works for self-shadowing
  • only works with deferred shading path
  • Not much knowledge out there, no working example with code

FURTHER READING

 

Stencil Shadow

Since Doom3 every one should know what stencil volume shadows look like. image2014-12-17 14-56-23

image2014-12-17 14-56-34The idea behind this technique is to calculate a shadow volume mesh and use it to render shadows. This could be achieved by traversing all edges of an object and store them into a list if the edge is between a face pointing towards the light and a face pointing away from that light. This list of edges is a so called Silhouette. The next step is to extrude the Silhouette. You then should have the final shadow volume mesh.

We can use the stencil buffer to detect whether a pixel is inside or outside a shadow volume. We could count +1 in the stencil buffer for succeded deep testing when rendering the all front faces of shadow volume mesh (backface culling) and then -1 for all back faces of the shadow volume mesh (frontface culling). Non zero values in the resulting stencil map indicate shadow.
StencilVolumeShadow (1)

Matrix Shadow

gl_shadowThis technique, also known as “plane shadow”, was available in many old games. Because of a limited use case most of these games decided to disable realtime shadows, though. In quake II you could enable these via “gl_shadow” console command. As you can see there are some “edge-cases” (pun) with this shadow render solution.

The idea behind this solution is to project the geometry on to ground (or a plane in math terms) by just using one matrix multiplication. If light and ground are static such a matrix must be calculated only once.

And then just rendering the mesh with a solid shadow color. Your artist might kill you, but this is probably the fastest way to calculate shadows in realtime. You could even use a lower res shadow geometry to speed things up.

Also this solution is easy to implement. I decided to use this solution because the most of the requirements could be solved. The only thing this technique is not capable of is that it will not harmonize with baked shadows.

Matrix Shadow Implementation

plane_shadow2I wanted to have transparent shadows but just setting the shadow color to a transparent one would not work since there might be overlapping polygons. The shadow would get dimmed twice. If more overlapping the effect would get even worse. But there is a simple solution to that: stencil buffer. We could force the GPU to draw each pixel just once by setting up a proper stencil test.

But there is still that “edge-case” we saw in quake. The problem is that the shadow sticks out, because the projection of the mesh was calculated on a plane ground. If you wanted to extend this solution to be correct even on non planar ground, just think about what you would have to do:

foreach ShadowCaster in Scene do
  foreach Polygon in Scene do
    if Polygon can be affected by ShadowCaster do
      setup a stencil mask that capsules the Polygon
      calculate shadowmatrix
      push shadow matrix to GPU
      render the Shadow Caster (now shadow mesh)

That would be ridiculous! What we could do is to clip the shadow by rendering the ground plane into the stencil buffer. Or if you want to be more flexible, render invisible objects that are using the same stencil buffer like the shadow.

RISKS / LIMITATIONS

If we summarise all these facts we have a small list of what we could not do with this shadow solution:

  • works only on plane grounds
  • no soft shadow
  • shadow gets clipped on geometry that is not coplanar with the ground
  • shadow complexity increases linear with the amount of the vertices of shadow casters

Solution

To get a working solution we need a script calculating the shadow matrix and a shader that does the stencil thing. The script should take at least a light and the ground as values and should be applied to all shadow casters. You might write another script that will add this script to all shadow casters in your scene. The following script will try to find the ground by shooting ray casts along the light direction. Once the script started it will add a shadow material to the material list of your mesh. If your object has sub meshes it will reorder these to be index + 1, so that the shadow material is always at index zero.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
/// <summary>
/// Initializes and Updates the plane shadow material if neccessary.
/// </summary>

public class UpdateShadowMaterial : MonoBehaviour
{
    /// <summary>
    /// The layer mask where to find the floor.
    /// </summary>
    public int LayerMask = 1 << 15;
 
    /// <summary>
    /// A bias value where to start the raycast to find the floor
    /// </summary>
    public float ShadowBias = 1.5f;
 
 
    private GameObject Floor_;
 
    /// <summary>
    /// The current floor detected by the raycast, is used to detect if the floor changed.
    /// </summary>
    public GameObject Floor
    {
        get
        {
            return Floor_;
        }
    }
 
    /// <summary>
    /// If AutoUpdateFloor is false the script will just calculate and update the shadow material once and then destroys itself.
    /// If true it will constantly shot a ray and if ground changed will update the shadow material
    /// </summary>
    public bool AutoUpdateFloor = false;
 
    /// <summary>
    /// Indicates if the shadow should be calculated using all submeshes, if not it will just use the first subMesh.
    /// </summary>
    public bool IncludeAllSubMeshes = false;
 
    /// <summary>
    /// Sets the light which should be used for the light calculation.
    /// </summary>
    private Light LightObject_;
    public Light LightObject
    {
        get
        {
            if (LightObject_ == null)
            {
                LightObject_ = FindLight ();
            }
            return LightObject_;
        }
 
         set
        {
            LightObject_ = value;
        }
    }
 
    /// <summary>
    /// Internal storage for the current shadowmatrix
    /// </summary>
    private Matrix4x4 shadowMatrix;
 
    /// <summary>
    /// Returns the renderComponent of this gameObject
    /// </summary>
    private Renderer RenderComponent_;
    Renderer RenderComponent
    {
        get{
            if (null == RenderComponent_)
            {
                RenderComponent_ = gameObject.GetComponent<Renderer>();
            }
            return RenderComponent_;
        }
    }
 
    // internal
    private Vector3 FloorNormal_;
    private Vector3 FloorPoint_;
    private bool FloorDirty_ = false;
 
    /// <summary>
    /// Gets or sets the floor normal used for the calculation of the shadow matrix.
    /// </summary>
    /// <value>The floor normal.</value>
    public Vector3 FloorNormal
    {
        set
        {
            FloorNormal_ = value;
            FloorDirty_ = true;
        }
         
        get
        {
            return FloorNormal_;
        }
    }
 
    /// <summary>
    /// Gets or sets the floor point used for the calculation of the shadow matrix.
    /// </summary>
    /// <value>The floor point.</value>
    public Vector3 FloorPoint
    {
        set
        {
            FloorPoint_ = value;
            FloorDirty_ = true;
        }
         
        get
        {
            return FloorPoint_;
        }
    }
 
    // Update is called once per frame
    void LateUpdate ()
    {
        if (AutoUpdateFloor || ! Floor)
        {
            CheckFloor();
        }
 
        if (FloorDirty_)
        {
            UpdateShadowMatrix();
        }
 
        if (!AutoUpdateFloor && Floor)
        {
            Destroy (this);
        }
    }
 
    // Use this for initialization
    void Start ()
    {
        AddShadowMaterialToList ();
        UpdateShadowMatrix ();
    }
 
    /// <summary>
    /// Updates the shadow matrix and uploads the result to the material.
    /// </summary>
    void UpdateShadowMatrix()
    {
        LightObject.shadows = LightShadows.None;
 
        calculateShadowMatrix(LightObject, FloorPoint_, FloorNormal_);
        RenderComponent_.sharedMaterials[0].SetMatrix("worldToShadow", shadowMatrix);
    }
 
 
    /// <summary>
    /// Adds the shadow material to material list. SubMeshes will be reconfigured so that each subMeshIndex is increased by one.
    /// Tht is done, because shadow material must be the first submesh.
    /// </summary>
    void AddShadowMaterialToList()
    {
        Material SharedShadowMaterial = Resources.Load<Material>("Profiling/ShadowPlane/PlaneShadow");
 
        Shader ShadowPlaneShader = SharedShadowMaterial.shader;
 
        if (RenderComponent.sharedMaterials[0] &&
            RenderComponent.sharedMaterials[0].shader.Equals(ShadowPlaneShader))
        {
            return;
        }
 
        MeshFilter mf = gameObject.GetComponent<MeshFilter>();
 
        if (null != mf && mf.mesh.subMeshCount > 1)
        {
            Mesh mesh = mf.mesh;
            mesh.subMeshCount++;
 
 
            List<int> triangles = new List<int>();
            List<int> indices = new List<int>();
 
            MeshTopology mt = MeshTopology.Triangles;
 
            for (int j = mesh.subMeshCount - 2; j >= 0; --j )
            {
                if ( j == 0 )
                {
                    mt =  mf.mesh.GetTopology(j);
                }
                else
                {
                    if ( mt != mf.mesh.GetTopology(j) )
                    {
                        Debug.LogError("Inconsitent Mesh Topology");
                    }
                }
                int [] subMesh = mf.sharedMesh.GetTriangles(j);
                int [] subIndices = mf.sharedMesh.GetIndices(j);
                triangles.AddRange(subMesh);
                indices.AddRange (subIndices);
                mesh.SetTriangles(subMesh, j+1);
                mesh.SetIndices(subIndices, mt, j+1);
            }
 
            if (IncludeAllSubMeshes)
            {
                mesh.SetTriangles(triangles.ToArray(), 0);
                mesh.SetIndices(indices.ToArray(), mt, 0);
            }
              
            mf.mesh = mesh;
        }
 
  
 
        Material [] newList = new Material[RenderComponent.sharedMaterials.Length + 1];
          
        newList[0] = new Material(SharedShadowMaterial);           
        for (int i = 1; i < newList.Length; ++i)
        {
            newList[i] = RenderComponent_.sharedMaterials[i - 1];
        }    
        RenderComponent_.sharedMaterials = newList;   
    }
 
    /// <summary>
    /// Returns the direction of the light used for the raycast when trying to detect the floor.
    /// </summary>
    /// <returns>The light dir_.</returns>
    Vector3 GetLightDir_()
    {
        if (!LightObject) return Vector3.down;
         
        Vector4 lightPos;
         
        if (LightObject.type == LightType.Directional)
        {
            lightPos =  LightObject.transform.TransformDirection(Vector3.back);
            lightPos.w = 0.0f;
            return -Vector3.Normalize(lightPos);
        }
        else
        {
            lightPos =  LightObject.transform.position;
            Vector3 tmp = transform.position - new Vector3(lightPos.x, lightPos.y, lightPos.z) ;
            return Vector3.Normalize(tmp);
            //lightPos.w = 1.0f;
        }
    }
 
    /// <summary>
    ///  returns the first directional light
    /// </summary>
    /// <returns>The light.</returns>
    Light FindLight()
    {
        Light [] lights = Light.GetLights(LightType.Directional,~0x0);
         
        foreach (Light light in lights)
        {
            if ( light.type == LightType.Directional )
            {
                Debug.Log ("Light Found:" + light.name);
                return light;
            }
        }   
        Debug.LogError("LightObject is null");
        return null;
    }
 
    /// <summary>
    /// Using a raycast from the direction of the light to detect the floor.
    /// </summary>
    void CheckFloor()
    {
        Vector3 lightDir = GetLightDir_();
        RaycastHit hit;
        Vector3 objectPosition = transform.position + Vector3.up * ShadowBias;
 
 
 
        if (Physics.Raycast(objectPosition, lightDir, out hit, Mathf.Infinity, LayerMask))
        {
            //Debug.DrawLine(hit.point, hit.point+ hit.normal * 3.0f, Color.cyan);
            //Debug.DrawLine(hit.point, objectPosition, Color.red);
            if (Floor_ != hit.collider.gameObject)
            {
                Floor_ = hit.collider.gameObject;
            }
             
            FloorNormal = hit.normal;
            FloorPoint = hit.point;
//            Debug.Log ("Found Surface at " + hit.ToString() );
//            Debug.Log ("Shadow Matrix is now :\n\r" + shadowMatrix.ToString());
        }
        else
        {
            Debug.Log ("Error: could not find the surface");
            calculateShadowMatrix(LightObject, Vector3.zero,Vector3.up);
        }
    }
 
 
    /// <summary>
    /// Actual function to calculate the shadow matrix used in the material.
    /// A shadow matrix is a transform matrix to transform worldSpace vertices to shadowSpace (plane projection) on floor.
    /// </summary>
    /// <param name="theLight">The light.</param>
    /// <param name="point">Point.</param>
    /// <param name="normal">Normal.</param>
    void calculateShadowMatrix(Light theLight, Vector3 point, Vector3 normal)
    {
        Vector3 L, n, E;
        float c, d;
         
         
        switch(theLight.type)
        {
        case LightType.Point:
            // Calculate the projection matrix
            // Let L be the position of the light
            // P the position of a vertex of the object we want to shadow
            // E a point of the plane (not seen in the figure)
            // n the normal vector of the plane
             
            L = theLight.transform.position;
            n = -normal; 
            E = point;
             
            d = Vector3.Dot(L, n);
            c = Vector3.Dot(E, n) - d;
             
            shadowMatrix[0, 0] = L.x * n.x + c;
            shadowMatrix[1, 0] = L.y * n.x;
            shadowMatrix[2, 0] = L.z * n.x;
            shadowMatrix[3, 0] = n.x;
             
            shadowMatrix[0, 1] = L.x * n.y;
            shadowMatrix[1, 1] = L.y * n.y + c;
            shadowMatrix[2, 1] = L.z * n.y;
            shadowMatrix[3, 1] = n.y;
             
            shadowMatrix[0, 2] = L.x * n.z;
            shadowMatrix[1, 2] = L.y * n.z;
            shadowMatrix[2, 2] = L.z * n.z + c;
            shadowMatrix[3, 2] = n.z;
             
            shadowMatrix[0, 3] = -L.x * (c + d);
            shadowMatrix[1, 3] = -L.y * (c + d);
            shadowMatrix[2, 3] = -L.z * (c + d);
            shadowMatrix[3, 3] = -d;
            break;
             
        case LightType.Spot:
            goto case LightType.Directional;
             
        case LightType.Directional:
            // Calculate the projection matrix
            // Let L be the direction of the light
            // P the position of a vertex of the object we want to shadow
            // E a point of the plane (not seen in the figure)
            // n the normal vector of the plane
             
            L = theLight.transform.forward;
            n = -normal;
            E = point;
             
            d = Vector3.Dot(L, n);
            c = Vector3.Dot(E, n);
             
            shadowMatrix[0, 0] = d - n.x * L.x;
            shadowMatrix[1, 0] =   - n.x * L.y;
            shadowMatrix[2, 0] =   - n.x * L.z;
            shadowMatrix[3, 0] = 0;
             
            shadowMatrix[0, 1] =   - n.y * L.x;
            shadowMatrix[1, 1] = d - n.y * L.y;
            shadowMatrix[2, 1] =   - n.y * L.z;
            shadowMatrix[3, 1] = 0;
             
            shadowMatrix[0, 2] =   - n.z * L.x;
            shadowMatrix[1, 2] =   - n.z * L.y;
            shadowMatrix[2, 2] = d - n.z * L.z;
            shadowMatrix[3, 2] = 0;
             
            shadowMatrix[0, 3] = c * L.x;
            shadowMatrix[1, 3] = c * L.y;
            shadowMatrix[2, 3] = c * L.z;
            shadowMatrix[3, 3] = d;
            break;
        }
    }
}

Sorry, but I really do not know how to add a “collapse / expand” to crayons syntax highlighting. Anyways here comes the shadow shader. It has only two Inputs: shadow color and shadow matrix. The stencil part will setup the stencil test so that each pixel will be rendered just once without overdraw so to speak. If you just want a solid shadow color you might want to remove that stencil part to speed things up. Also I have added a polygon offset to avoid z fight. I have queued this shader to be rendered just after other objects have been rendered in transparent forward base, so shadows will be drawn after other transparent objects, like if you want to have shadows on a water surface.

Shader "HansHamm/PlaneShadow"
{
    Properties
    {   
          _ShadowColor ("Shadow Color", Color) = (0,0,0,1)
    }
    
    SubShader
    {
        Pass
        {
            Tags
            {
                "LightMode" = "ForwardBase"
                "RenderType"="Transparent"
                "Queue" = "Transparent+10"        
            }
               
            ZTest on             
            Blend SrcAlpha OneMinusSrcAlpha
            Offset -3.0, -1.0
                             
            Stencil
             {
                Ref 1
                Comp NotEqual
                Pass Replace
                Fail Keep
                ZFail Keep
                ReadMask 1
                WriteMask 1
            }
             
            CGPROGRAM
 
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
 
            #include "UnityCG.cginc"
 
            uniform float4x4 worldToShadow; //will be set by a script
            uniform fixed4 _ShadowColor;
              
            float4 vert(appdata_base v) : POSITION
            {
                float4 posW = mul(_Object2World, v.vertex);
                float4 posWS = mul(worldToShadow, posW);
                return mul (UNITY_MATRIX_VP, posWS);
            }
 
            fixed4 frag(float4 sp:WPOS) : COLOR {                
                return _ShadowColor;
            }
 
            ENDCG
        } // Pass        
    } // SubShader    
} // Shader

And finally a shader to draw into the stencil buffer to mask a shadow. I used Transparent+9 to make sure those objects will be rendered before any shadow object gets rendered. I setup the color mask to zero to prevent drawing into the color frame buffer, we want the object to be invisible.

Shader "HansHamm/MaskShadow" 
{
    Properties 
    {
	 
    }
   
    SubShader 
    {
    
        Tags 
        { 
            "LightMode" = "ForwardBase"
            "RenderType"="Transparent" 
            "IgnoreProjector"="True"
            "Queue" = "Transparent+9" 
        }
    
        Pass 
        {            
            Offset -1.0, -3.0
            ColorMask 0
            ZTest on
      		
            Stencil 
            {
                Ref 1
                Comp NotEqual
                Pass Replace 
                Fail Keep 
                ZFail Keep
                ReadMask 1
                WriteMask 1
            }
            
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            #include "UnityCG.cginc" 
         	
            float4 vert(appdata_base v) : POSITION 
            {
                return mul (UNITY_MATRIX_MVP, v.vertex);
            }

            fixed4 frag(float4 sp:WPOS) : COLOR {
                return fixed4(1.0, 1.0, 1.0, 1.0);
            }
            ENDCG
        }
    }
}

 

7 Replies to “Fast realtime shadows in Unity”

    • Simply assign the script to any object of your scene that should cast those shadows. The line ” Material SharedShadowMaterial = Resources.Load(“Profiling/ShadowPlane/PlaneShadow”);” will create the material for you. Just make sure the shader can be found under this name.
      This is actually false. You have to create the shadow material manually and put it in this directory: Profiling/ShadowPlane/PlaneShadow.mat
      You can setup the shadow color there as well. Sorry for this wrong post in first. It’s been a long time when I actually wrote this script.

      • I have Android game with open area. There are about 30 skinned meshes, two helicopters and two tanks.
        Does my directional light has to have some spesific setup?
        I`m bad at coding and know nothing about shader scripting. I copied Shader code from this article and made two standart surface shaders called: PlainShadow and MaskShadow.
        Do i have to make two New materials, with theese shaders draged on to them?
        Did i named them right?
        I have no clue how to make it work(
        Could you make short video tutor man please.
        Thanks!

        • There is no limitation to your light, It can be either directional or an positional.

          The filenames of the shaders shouldn’t matter since they are not accessed by the code. Instead the code will load the predefined shadow material you created.

          So, the issue might be that you have to create the shadow material manually and put it in the correct folder that is used by the script. I think it is Profiling/ShadowPlane/PlaneShadow.mat

          You could use another folder and name, but then you have to change this in the script as well (look for AddShadowMaterialsToList() function)

          Everything should then just work like a charm 🙂

          The mask shader is not used by the script. How ever, if you want to add invisible geometry that blocks the shadow, you may manually create such a material and assign it to your shadow blocker objects as described in the article.

          Actually, I don’t have the time to make a tutorial video right now. But l am going to add this as soon as possible. Meanwhile I hope this helps you out.

  1. Thanks!!!
    I’ll try to make good use of your post.
    Hope to make it happen eventually)
    I’ll keep youinformed of my progress.
    Cheers!

Leave a Reply

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

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.