GLSL Programming/Unity/Shadows on Planes

From Wikibooks, open books for an open world
Jump to: navigation, search
A cartoon character with a drop shadow.

This tutorial covers the projection of shadows onto planes.

It is not based on any particular tutorial; however, some understanding of Section “Vertex Transformations” is useful.

Projecting Hard Shadows onto Planes[edit]

Computing realistic shadows in real time is difficult. However, there are certain cases that are a lot easier. Projecting a hard shadow (i.e. a shadow without penumbra; see Section “Soft Shadows of Spheres”) onto a plane is one of these cases. The idea is to render the shadow by rendering the shadow-casting object in the color of the shadow with the vertices projected just above the shadow-receiving plane.

Illustration of the projection of a point P in direction L onto a plane in the coordinate system of the plane.

Projecting an Object onto a Plane[edit]

In order to render the projected shadow, we have to project the object onto a plane. In order to specify the plane, we will use the local coordinate system of the default plane game object. Thus, we can easily modify the position and orientation of the plane by editing the plane object. In the coordinate system of that game object, the actual plane is just the y=0 plane, which is spanned by the x and z axes.

Projecting an object in a vertex shader means to project each vertex. This could be done with a projection matrix similar to the one discussed in Section “Vertex Transformations”. However, those matrices are somewhat difficult to compute and debug. Therefore, we will take another approach and compute the projection with a bit of vector arithmetics. The illustration to the left shows the projection of a point P in the direction of light L onto a shadow-receiving plane. (Note that the vector L is in the opposite direction than the light vectors that are usually employed in lighting computations.) In order to move the point P to the plane, we add a scaled version of L. The scaling factor turns out to be the distance of P to the plane divided by the length of L in the direction of the normal vector of the plane (because of similar triangles as indicated by the gray lines). In the coordinate system of the plane, where the normal vector is just the y axis, we can also use the ratio of the y coordinate of the point P divided by the negated y coordinate of the vector L.

Thus, the vertex shader could look like this:

         GLSLPROGRAM
 
         // User-specified uniforms
         uniform mat4 _World2Receiver; // transformation from 
            // world coordinates to the coordinate system of the plane
 
         // The following built-in uniforms  
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object 
         uniform vec4 _WorldSpaceLightPos0; 
            // position or direction of light source
 
         #ifdef VERTEX
 
         void main()
         {            
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
            modelMatrixInverse[3][3] = 1.0; 
            mat4 viewMatrix = gl_ModelViewMatrix * modelMatrixInverse;
 
            vec4 lightDirection;
            if (0.0 != _WorldSpaceLightPos0.w) // point or spot light?
            {
               lightDirection = normalize(
                  modelMatrix * gl_Vertex - _WorldSpaceLightPos0);
            } 
            else // directional light
            {
               lightDirection = -normalize(_WorldSpaceLightPos0); 
            }
 
            vec4 vertexInWorldSpace = modelMatrix * gl_Vertex;
            float distanceOfVertex = 
               (_World2Receiver * vertexInWorldSpace).y; 
               // = height over plane 
            float lengthOfLightDirectionInY = 
               (_World2Receiver * lightDirection).y; 
               // = length in y direction
 
            lightDirection = lightDirection 
               * (distanceOfVertex / (-lengthOfLightDirectionInY));
 
            gl_Position = gl_ProjectionMatrix * (viewMatrix 
               * (vertexInWorldSpace + lightDirection));
         }
 
         #endif
         ...

The uniform _World2Receiver is best set with the help of a small script that should be attached to the shadow-casting object:

@script ExecuteInEditMode()
 
public var plane : GameObject;
 
function Update () 
{
   if (null != plane)
   {
      renderer.sharedMaterial.SetMatrix("_World2Receiver", 
         plane.renderer.worldToLocalMatrix);
   }
}

The script requires the user to specify the shadow-receiving plane object and sets the uniform _World2Receiver accordingly.

Complete Shader Code[edit]

For the complete shader code we improve the performance by noting that the y coordinate of a matrix-vector product is just the dot product of the second row (i.e. the first when starting with 0) of the matrix and the vector. Furthermore, we improve the robustness by not moving the vertex when it is below the plane, neither when the light is directed upwards. Additionally, we try to make sure that the shadow is on top of the plane with this instruction:

Offset -1.0, -2.0

This reduces the depth of the rasterized triangles a bit such that they always occlude other triangles of approximately the same depth.

The first pass of the shader renders the shadow-casting object while the second pass renders the projected shadow. In an actual application, the first pass could be replaced by one or more passes to compute the lighting of the shadow-casting object.

Shader "GLSL planar shadow" {
   Properties {
      _Color ("Object's Color", Color) = (0,1,0,1)
      _ShadowColor ("Shadow's Color", Color) = (0,0,0,1)
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // rendering of object
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
 
         #ifdef VERTEX
 
         void main()
         {                                
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            gl_FragColor = _Color;
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {   
         Tags { "LightMode" = "ForwardBase" } 
            // rendering of projected shadow
         Offset -1.0, -2.0 
            // make sure shadow polygons are on top of shadow receiver
 
         GLSLPROGRAM
 
         // User-specified uniforms
         uniform vec4 _ShadowColor;
         uniform mat4 _World2Receiver; // set by script
 
         // The following built-in uniforms ) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object 
         uniform vec4 _WorldSpaceLightPos0; 
            // position or direction of light source
 
         #ifdef VERTEX
 
         void main()
         {            
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
            modelMatrixInverse[3][3] = 1.0; 
            mat4 viewMatrix = gl_ModelViewMatrix * modelMatrixInverse;
 
            vec4 lightDirection;
            if (0.0 != _WorldSpaceLightPos0.w) 
            {
               // point or spot light
               lightDirection = normalize(
                  modelMatrix * gl_Vertex - _WorldSpaceLightPos0);
            } 
            else 
            {
               // directional light
               lightDirection = -normalize(_WorldSpaceLightPos0); 
            }
 
            vec4 vertexInWorldSpace = modelMatrix * gl_Vertex;
            vec4 world2ReceiverRow1 = 
               vec4(_World2Receiver[0][1], _World2Receiver[1][1], 
               _World2Receiver[2][1], _World2Receiver[3][1]);
            float distanceOfVertex = 
               dot(world2ReceiverRow1, vertexInWorldSpace); 
               // = (_World2Receiver * vertexInWorldSpace).y 
               // = height over plane 
            float lengthOfLightDirectionInY = 
               dot(world2ReceiverRow1, lightDirection); 
               // = (_World2Receiver * lightDirection).y 
               // = length in y direction
 
            if (distanceOfVertex > 0.0 && lengthOfLightDirectionInY < 0.0)
            {
               lightDirection = lightDirection 
                  * (distanceOfVertex / (-lengthOfLightDirectionInY));
            }
            else
            {
               lightDirection = vec4(0.0, 0.0, 0.0, 0.0); 
                  // don't move vertex
            }
 
            gl_Position = gl_ProjectionMatrix * (viewMatrix 
               * (vertexInWorldSpace + lightDirection));
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            gl_FragColor = _ShadowColor;
         }
 
         #endif
 
         ENDGLSL
      }
   }
}

Further Improvements of the Fragment Shader[edit]

There are a couple of things that could be improved, in particular in the fragment shader:

  • Fragments of the shadow that are outside of the rectangular plane object could be removed with the discard instruction, which was discussed in Section “Cutaways”.
  • If the plane is textured, this texturing could be integrated by using only local vertex coordinates for the texture lookup (also in the shader of the plane object) and specifying the texture of the plane as a shader property of the shadow-casting object.
  • Soft shadows could be faked by computing the lighting of the plane in this shader and attenuating it depending on the angle of the surface normal vector of the shadow-casting object to the light direction similar to the approach in Section “Silhouette Enhancement”.

Summary[edit]

Congratulations, this is the end of this tutorial. We have seen:

  • How to project a vertex in the direction of light onto a plane.
  • How to implement this technique to project a shadow onto a plane.

Further Reading[edit]

If you still want to learn more

  • about the model transformation, the view transformation, and the projection, you should read the description in Section “Vertex Transformations”.
  • about setting up a projection matrix to project the shadow, you could read Section 9.4.1 of the SIGGRAPH '98 Course “Advanced Graphics Programming Techniques Using OpenGL” organized by Tom McReynolds, which is available online.


< GLSL Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.