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:

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.

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.

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.

 

7 thoughts on “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 *

*