Shadows

This month I’ve been learning and using real-time shadowing techniques including Drop Shadows, Stencil Shadows and Shadow Mapping. The three key reasons to implement good shadowing techniques in my opinion are

Depth Perception

Gameplay

Realism

In the case of depth perception, creating it through shadows makes it easier for players to judge relative distances which is very useful for platformer style games. There are certain styles of games that have gameplay relying on the use of lighting and shadow, usually to create puzzles and challenges. Lastly realism which has a varying level of importance dependent on the project. Realistic shadowing/lighting can be costly so balance of performance and desired result has be considered in the implementation.

For my study I implemented a basic Shadow Map using the Stanford Bunny model (Download) without using soft edge calculations, also know as the penumbra. Shadow Mapping works by rendering all of the items that cast a shadow in to a render target that stores the depth information of the scene, rendered from the perspective of a shadow-casting light, which requires us to set up multiple items:

  •  A Render Target that uses a Depth Buffer as its only attachment,
    set up as a texture so that we can sample it
  • A Projection and View matrix from the perspective of the light
  • Shaders that simply pass-through the data so that only the depth is rendered

This is a look at what it takes in code to do the above three items, starting with the shadow map buffer.

// setup shadow map buffer 
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); 
glGenTextures(1, &m_fboDepth); 
glBindTexture(GL_TEXTURE_2D, m_fboDepth); 

// texture uses a 16-bit depth component format 
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 1024, 1024, 0,
                                   GL_DEPTH_COMPONENT, GL_FLOAT, 0);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 

// attached as a depth attachment to capture depth not colour 
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, m_fboDepth, 0); 

// no colour targets are used 
glDrawBuffer(GL_NONE); 

GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); 
if (status != GL_FRAMEBUFFER_COMPLETE) printf("Framebuffer Error!\n");
glBindFramebuffer(GL_FRAMEBUFFER, 0); 

The next step of getting the projection view will look something like this.


m_lightDirection = glm::normalize(glm::vec3(1, 2.5f, 1)); 

glm::mat4 lightProjection = glm::ortho(-10, 10, -10, 10, -10, 10); 

glm::mat4 lightView = glm::lookAt(m_lightDirection, glm::vec3(0), glm::vec3(0,1,0)); 

m_lightMatrix = lightProjection * lightView; 

Next I created a fragment and vertex shader to generate the shadow, saving and loading these in as .frag and .vert files.


// FRAGMENT SHADER – GENERATE SHADOW 
#version 410 
out float fragDepth;

 void main() {
     fragDepth = gl_FragCoord.z;
 }

// VERTEX SHADER – GENERATE SHADOW 
#version 410 

layout(location = 0) in vec4 Position; 
uniform mat4 lightMatrix;
 
void main() { 
    gl_Position = lightMatrix * Position;
 } 

I combined the two shaders above into a shader program I named m_shadowGeneratProgram, then used that program.

// shadow pass: bind our shadow map target and clear the depth 
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glViewport(0, 0, 1024, 1024); 
glClear(GL_DEPTH_BUFFER_BIT); 
glUseProgram(m_shadowGenProgram); 

// bind the light matrix 
int loc = glGetUniformLocation(m_shadowGenProgram,"lightMatrix");
glUniformMatrix4fv(loc, 1, GL_FALSE, &(m_lightMatrix[0][0])); 

The shadow map is now filled with the depth of all pixels from the light. The two other shaders that were created are bound to the m_useShadowProgram and I did them like this.

// VERTEX SHADER – USE SHADOW 
#version 410 

layout(location = 0) in vec4 Position; 
layout(location = 1) in vec4 Normal; 

out vec4 vNormal; 
out vec4 vShadowCoord;

uniform mat4 projectionView; 
uniform mat4 modelMatrix;
uniform mat4 lightMatrix;

void main() { 
    vNormal = Normal; 

    mat4 PVM = projectionView * modelMatrix;

    gl_Position = PVM * Position;

    // new transform to the shadow space 
    vShadowCoord = lightMatrix * Position;
}
// FRAGMENT SHADER – USE SHADOW 
#version 410 

in vec4 vNormal; 
in vec4 vShadowCoord;

out vec4 fragColour; 

uniform vec3 lightDir; 
uniform sampler2D shadowMap;
uniform float shadowBias;


void main() { 
    float d = max(0, dot(normalize(vNormal.xyz), lightDir)); 

    if (texture(shadowMap, vShadowCoord.xy).r < 
        vShadowCoord.z - shadowBias) { 
        d = 0;
     }

    fragColour = vec4(d, d, d, 1);
}

Finally it’s time to use the m_useShadowProgram.

// final pass: bind back-buffer and clear colour and depth glBindFramebuffer(GL_FRAMEBUFFER, 0); 
glViewport(0, 0, 1280, 720); // screen resolution 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glUseProgram(m_useShadowProgram); 

// bind the camera 
loc = glGetUniformLocation(m_useShadowProgram, "projectionView");
glUniformMatrix4fv(loc, 1, GL_FALSE, &(m_camera->getProjectionView()[0][0])); 

// bind the light matrix 
glm::mat4 textureSpaceOffset( 0.5f, 0.0f, 0.0f, 0.0f,
                              0.0f, 0.5f, 0.0f, 0.0f,
                              0.0f, 0.0f, 0.5f, 0.0f,
                              0.5f, 0.5f, 0.5f, 1.0f ); 

glm::mat4 lightMatrix = textureSpaceOffset * m_lightMatrix; 
loc = glGetUniformLocation(m_useShadowProgram, "lightMatrix"); 
glUniformMatrix4fv(loc, 1, GL_FALSE, &lightMatrix[0][0]); 

loc = glGetUniformLocation(m_useShadowProgram, "lightDir"); 
glUniform3fv(loc, 1, &m_lightDirection[0]); 

loc = glGetUniformLocation(m_useShadowProgram, "shadowMap");
glUniform1i(loc, 0);
glActiveTexture(GL_TEXTURE0); 
glBindTexture(GL_TEXTURE_2D, m_fboDepth); 

Note: the textureSpaceOffset is used to convert from clip-space [-1, 1] to texture-space [0, 1]
by applying a 0.5 scale to all axis.

Running the program with the above shaders and OpenGL code I achieved this result.
Standford Bunny with Shadow Map

Thank you for reading, I know this was a long post but it was really beneficial to break down exactly what I did and why. I feel it helps tremendously with retaining knowledge, but hopefully the next blog is more concise.

Leave a Reply

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