Tutorial 42: Multiple Light Shadow Mapping

This tutorial will cover how to implement multiple lights when using shadow mapping. The tutorial is written for OpenGL 4.0 using C++ and GLSL. The code in this tutorial is based on the previous shadow mapping tutorial. Also, if you haven't already gone over the multiple light tutorial (Tutorial 11) then I would suggest reviewing that one first as this tutorial assumes you already understand how to illuminate scenes using multiple lights.

Using multiple lights with shadow mapping is an easy extension of the basic shadow mapping technique. Instead of rendering just a single depth map from one light's perspective you now render a depth map for each light in the scene. Then you send a depth map (shadow map) into the shader for each light as well as each light's diffuse color and position. Then for each light you do the exact same shadow process you did for a single light and add all the color results together to get the final pixel value.

To visualize how this works we will setup a scene that has two lights. The following scene has a blue light positioned behind us and to the left. And it also has a second green light positioned behind us and to the right. So, if you look at the sphere you can see the green light to the right illuminating the right side of the sphere and you can see the blue light to the left illuminating the left side of the sphere. The cyan color along the midsection of the sphere is the even combination of the blue and green lights.

Now if we render a depth map of the scene from the blue light's perspective and then render a depth map of the scene from the green light's perspective, we have what we need to determine where all the shadows will located. We send the two depth maps and the position and color of the two lights into the shader and then perform the shadow map algorithm for each light. This will give us the two final color values for the two lights. We then add those two color values together and we get the following result:

You will see that the green light casts a blue shadow since blue light is the only thing that is illuminating the surface when there is an absence of green light. And likewise, the blue light casts a green shadow for the same reason. Where the shadows intersect in the middle of the scene there is only ambient since neither green nor blue light are illuminating the surface.


Framework

The framework remains the same for this tutorial as we have not added any additional classes.

We will start the code section of the tutorial by examining the revised GLSL shadow shader programs.


Shadow.vs

////////////////////////////////////////////////////////////////////////////////
// Filename: shadow.vs
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
in vec3 inputNormal;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;
out vec3 normal;
out vec4 lightViewPosition;
out vec3 lightPos;

The output to the pixel shader now has a second light position and a second light viewing position.

out vec4 lightViewPosition2;
out vec3 lightPos2;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 lightViewMatrix;
uniform mat4 lightProjectionMatrix;
uniform vec3 lightPosition;

We have added a perspective and view matrix for the second light.

uniform mat4 lightViewMatrix2;
uniform mat4 lightProjectionMatrix2;

We have added a position vector for the second light also.

uniform vec3 lightPosition2;


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    vec4 worldPosition;


    // Calculate the position of the vertex against the world, view, and projection matrices.
    gl_Position = vec4(inputPosition, 1.0f) * worldMatrix;
    gl_Position = gl_Position * viewMatrix;
    gl_Position = gl_Position * projectionMatrix;

    // Store the position of the vertice as viewed by the projection view point in a separate variable.
    lightViewPosition = vec4(inputPosition, 1.0f) * worldMatrix;
    lightViewPosition = lightViewPosition * lightViewMatrix;
    lightViewPosition = lightViewPosition * lightProjectionMatrix;

Calculate the second light's viewing position of the vertex.

    // Store the position of the vertice as viewed by the second projection view point in a separate variable.
    lightViewPosition2 = vec4(inputPosition, 1.0f) * worldMatrix;
    lightViewPosition2 = lightViewPosition2 * lightViewMatrix2;
    lightViewPosition2 = lightViewPosition2 * lightProjectionMatrix2;

    // Store the texture coordinates for the pixel shader.
    texCoord = inputTexCoord;

    // Calculate the normal vector against the world matrix only.
    normal = inputNormal * mat3(worldMatrix);

    // Normalize the normal vector.
    normal = normalize(normal);

    // Calculate the position of the vertex in the world.
    worldPosition = vec4(inputPosition, 1.0f) * worldMatrix;

    // Determine the light position based on the position of the light and the position of the vertex in the world.
    lightPos = lightPosition.xyz - worldPosition.xyz;

    // Normalize the light position vector.
    lightPos = normalize(lightPos);

Calculate the position of the second light.

    // Determine the second light position based on the position of the second light and the position of the vertex in the world.
    lightPos2 = lightPosition2.xyz - worldPosition.xyz;

    // Normalize the second light position vector.
    lightPos2 = normalize(lightPos2);
}

Shadow.ps

////////////////////////////////////////////////////////////////////////////////
// Filename: shadow.ps
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec3 normal;
in vec4 lightViewPosition;
in vec3 lightPos;
in vec4 lightViewPosition2;
in vec3 lightPos2;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D shaderTexture;
uniform sampler2D depthMapTexture;

We have added a second depth map texture for the second light.

uniform sampler2D depthMapTexture2;
uniform vec4 ambientColor;
uniform vec4 diffuseColor;

We have added the diffuse color for the second light.

uniform vec4 diffuseColor2;
uniform float bias;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    vec4 color;
    vec2 projectTexCoord;
    float depthValue;
    float lightDepthValue;
    float lightIntensity;
    vec4 textureColor;


    // Set the default output color to the ambient light value for all pixels.
    color = ambientColor;

Calculate the color/shadow for the first light as normal except for that we don't saturate a final light color anymore.

    // Calculate the projected texture coordinates.
    projectTexCoord.x = lightViewPosition.x / lightViewPosition.w / 2.0f + 0.5f;
    projectTexCoord.y = lightViewPosition.y / lightViewPosition.w / 2.0f + 0.5f;

    // Determine if the projected coordinates are in the 0 to 1 range.  If so then this pixel is in the view of the light.
    if((clamp(projectTexCoord.x, 0.0, 1.0) == projectTexCoord.x) && (clamp(projectTexCoord.y, 0.0, 1.0) == projectTexCoord.y))
    {
        // Sample the shadow map depth value from the depth texture using the sampler at the projected texture coordinate location.
        depthValue = texture(depthMapTexture, projectTexCoord).r;

        // Calculate the depth of the light.
        lightDepthValue = lightViewPosition.z / lightViewPosition.w;

        // Subtract the bias from the lightDepthValue.
        lightDepthValue = lightDepthValue - bias;

        // Compare the depth of the shadow map value and the depth of the light to determine whether to shadow or to light this pixel.
        // If the light is in front of the object then light the pixel, if not then shadow this pixel since an object (occluder) is casting a shadow on it.
        if(lightDepthValue < depthValue)
        {
            // Calculate the amount of light on this pixel.
            lightIntensity = clamp(dot(normal, lightPos), 0.0f, 1.0f);

            if(lightIntensity > 0.0f)
            {
                // Determine the final diffuse color based on the diffuse color and the amount of light intensity.
                color += (diffuseColor * lightIntensity);
            }
        }
    }

Now do the same thing for the second light using the second light's shadow map and light variables. And instead of having a separate color result we just add the value to the color result from the first light.

    ////////////////
    // Second light: Use all the second light variables and textures for calculations.  Accumulate the additional light value into the color variable.
    ////////////////

    projectTexCoord.x = lightViewPosition2.x / lightViewPosition2.w / 2.0f + 0.5f;
    projectTexCoord.y = lightViewPosition2.y / lightViewPosition2.w / 2.0f + 0.5f;

    if((clamp(projectTexCoord.x, 0.0, 1.0) == projectTexCoord.x) && (clamp(projectTexCoord.y, 0.0, 1.0) == projectTexCoord.y))
    {
        depthValue = texture(depthMapTexture2, projectTexCoord).r;

        lightDepthValue = lightViewPosition2.z / lightViewPosition2.w;
        lightDepthValue = lightDepthValue - bias;

        if(lightDepthValue < depthValue)
        {
            lightIntensity = clamp(dot(normal, lightPos2), 0.0f, 1.0f);

            if(lightIntensity > 0.0f)
            {
                color += (diffuseColor2 * lightIntensity);
            }
        }
    }

And then once we have the colors added together, we perform the saturate.

    // Saturate the final light color of both lights.
    color = clamp(color, 0.0f, 1.0f);

    // Sample the pixel color from the texture using the sampler at this texture coordinate location.
    textureColor = texture(shaderTexture, texCoord);

    // Combine the light and texture color.
    outputColor = color * textureColor;
}

Shadowshaderclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: shadowshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SHADOWSHADERCLASS_H_
#define _SHADOWSHADERCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: ShadowShaderClass
////////////////////////////////////////////////////////////////////////////////
class ShadowShaderClass
{
public:
    ShadowShaderClass();
    ShadowShaderClass(const ShadowShaderClass&);
    ~ShadowShaderClass();

    bool Initialize(OpenGLClass*);
    void Shutdown();

    bool SetShaderParameters(float*, float*, float*, float*, float*, float*, float*, float*, float,
                             float*, float*, float*, float*);

private:
    bool InitializeShader(char*, char*);
    void ShutdownShader();
    char* LoadShaderSourceFile(char*);
    void OutputShaderErrorMessage(unsigned int, char*);
    void OutputLinkerErrorMessage(unsigned int);

private:
    OpenGLClass* m_OpenGLPtr;
    unsigned int m_vertexShader;
    unsigned int m_fragmentShader;
    unsigned int m_shaderProgram;
};

#endif

Shadowshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: shadowshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "shadowshaderclass.h"


ShadowShaderClass::ShadowShaderClass()
{
    m_OpenGLPtr = 0;
}


ShadowShaderClass::ShadowShaderClass(const ShadowShaderClass& other)
{
}


ShadowShaderClass::~ShadowShaderClass()
{
}


bool ShadowShaderClass::Initialize(OpenGLClass* OpenGL)
{
    char vsFilename[128];
    char psFilename[128];
    bool result;


    // Store the pointer to the OpenGL object.
    m_OpenGLPtr = OpenGL;

    // Set the location and names of the shader files.
    strcpy(vsFilename, "../Engine/shadow.vs");
    strcpy(psFilename, "../Engine/shadow.ps");

    // Initialize the vertex and pixel shaders.
    result = InitializeShader(vsFilename, psFilename);
    if(!result)
    {
        return false;
    }

    return true;
}


void ShadowShaderClass::Shutdown()
{
    // Shutdown the shader.
    ShutdownShader();

    // Release the pointer to the OpenGL object.
    m_OpenGLPtr = 0;

    return;
}


bool ShadowShaderClass::InitializeShader(char* vsFilename, char* fsFilename)
{
    const char* vertexShaderBuffer;
    const char* fragmentShaderBuffer;
    int status;


    // Load the vertex shader source file into a text buffer.
    vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
    if(!vertexShaderBuffer)
    {
        return false;
    }

    // Load the fragment shader source file into a text buffer.
    fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
    if(!fragmentShaderBuffer)
    {
        return false;
    }

    // Create a vertex and fragment shader object.
    m_vertexShader = m_OpenGLPtr->glCreateShader(GL_VERTEX_SHADER);
    m_fragmentShader = m_OpenGLPtr->glCreateShader(GL_FRAGMENT_SHADER);

    // Copy the shader source code strings into the vertex and fragment shader objects.
    m_OpenGLPtr->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
    m_OpenGLPtr->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);

    // Release the vertex and fragment shader buffers.
    delete [] vertexShaderBuffer;
    vertexShaderBuffer = 0;

    delete [] fragmentShaderBuffer;
    fragmentShaderBuffer = 0;

    // Compile the shaders.
    m_OpenGLPtr->glCompileShader(m_vertexShader);
    m_OpenGLPtr->glCompileShader(m_fragmentShader);

    // Check to see if the vertex shader compiled successfully.
    m_OpenGLPtr->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
    if(status != 1)
    {
        // If it did not compile then write the syntax error message out to a text file for review.
        OutputShaderErrorMessage(m_vertexShader, vsFilename);
        return false;
    }

    // Check to see if the fragment shader compiled successfully.
    m_OpenGLPtr->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
    if(status != 1)
    {
        // If it did not compile then write the syntax error message out to a text file for review.
        OutputShaderErrorMessage(m_fragmentShader, fsFilename);
        return false;
    }

    // Create a shader program object.
    m_shaderProgram = m_OpenGLPtr->glCreateProgram();

    // Attach the vertex and fragment shader to the program object.
    m_OpenGLPtr->glAttachShader(m_shaderProgram, m_vertexShader);
    m_OpenGLPtr->glAttachShader(m_shaderProgram, m_fragmentShader);

    // Bind the shader input variables.
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 1, "inputTexCoord");
    m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 2, "inputNormal");

    // Link the shader program.
    m_OpenGLPtr->glLinkProgram(m_shaderProgram);

    // Check the status of the link.
    m_OpenGLPtr->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
    if(status != 1)
    {
        // If it did not link then write the syntax error message out to a text file for review.
        OutputLinkerErrorMessage(m_shaderProgram);
        return false;
    }

    return true;
}


void ShadowShaderClass::ShutdownShader()
{
    // Detach the vertex and fragment shaders from the program.
    m_OpenGLPtr->glDetachShader(m_shaderProgram, m_vertexShader);
    m_OpenGLPtr->glDetachShader(m_shaderProgram, m_fragmentShader);

    // Delete the vertex and fragment shaders.
    m_OpenGLPtr->glDeleteShader(m_vertexShader);
    m_OpenGLPtr->glDeleteShader(m_fragmentShader);

    // Delete the shader program.
    m_OpenGLPtr->glDeleteProgram(m_shaderProgram);

    return;
}


char* ShadowShaderClass::LoadShaderSourceFile(char* filename)
{
    FILE* filePtr;
    char* buffer;
    long fileSize, count;
    int error;


    // Open the shader file for reading in text modee.
    filePtr = fopen(filename, "r");
    if(filePtr == NULL)
    {
        return 0;
    }

    // Go to the end of the file and get the size of the file.
    fseek(filePtr, 0, SEEK_END);
    fileSize = ftell(filePtr);

    // Initialize the buffer to read the shader source file into, adding 1 for an extra null terminator.
    buffer = new char[fileSize + 1];

    // Return the file pointer back to the beginning of the file.
    fseek(filePtr, 0, SEEK_SET);

    // Read the shader text file into the buffer.
    count = fread(buffer, 1, fileSize, filePtr);
    if(count != fileSize)
    {
        return 0;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        return 0;
    }

    // Null terminate the buffer.
    buffer[fileSize] = '\0';

    return buffer;
}


void ShadowShaderClass::OutputShaderErrorMessage(unsigned int shaderId, char* shaderFilename)
{
    long count;
    int logSize, error;
    char* infoLog;
    FILE* filePtr;


    // Get the size of the string containing the information log for the failed shader compilation message.
    m_OpenGLPtr->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);

    // Increment the size by one to handle also the null terminator.
    logSize++;

    // Create a char buffer to hold the info log.
    infoLog = new char[logSize];

    // Now retrieve the info log.
    m_OpenGLPtr->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);

    // Open a text file to write the error message to.
    filePtr = fopen("shader-error.txt", "w");
    if(filePtr == NULL)
    {
        cout << "Error opening shader error message output file." << endl;
        return;
    }

    // Write out the error message.
    count = fwrite(infoLog, sizeof(char), logSize, filePtr);
    if(count != logSize)
    {
        cout << "Error writing shader error message output file." << endl;
        return;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        cout << "Error closing shader error message output file." << endl;
        return;
    }

    // Notify the user to check the text file for compile errors.
    cout << "Error compiling shader.  Check shader-error.txt for error message.  Shader filename: " << shaderFilename << endl;

    return;
}


void ShadowShaderClass::OutputLinkerErrorMessage(unsigned int programId)
{
    long count;
    FILE* filePtr;
    int logSize, error;
    char* infoLog;


    // Get the size of the string containing the information log for the failed shader compilation message.
    m_OpenGLPtr->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);

    // Increment the size by one to handle also the null terminator.
    logSize++;

    // Create a char buffer to hold the info log.
    infoLog = new char[logSize];

    // Now retrieve the info log.
    m_OpenGLPtr->glGetProgramInfoLog(programId, logSize, NULL, infoLog);

    // Open a file to write the error message to.
    filePtr = fopen("linker-error.txt", "w");
    if(filePtr == NULL)
    {
        cout << "Error opening linker error message output file." << endl;
        return;
    }

    // Write out the error message.
    count = fwrite(infoLog, sizeof(char), logSize, filePtr);
    if(count != logSize)
    {
        cout << "Error writing linker error message output file." << endl;
        return;
    }

    // Close the file.
    error = fclose(filePtr);
    if(error != 0)
    {
        cout << "Error closing linker error message output file." << endl;
        return;
    }

    // Pop a message up on the screen to notify the user to check the text file for linker errors.
    cout << "Error linking shader program.  Check linker-error.txt for message." << endl;

    return;
}

The SetShaderParameters function now takes as input a view matrix, a projection matrix, a depth map texture, a position, and a diffuse color for the second light.

bool ShadowShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float* lightViewMatrix, float* lightProjectionMatrix,
                                            float* diffuseColor, float* ambientColor, float* lightPosition, float bias,
                                            float* lightViewMatrix2, float* lightProjectionMatrix2, float* diffuseColor2, float* lightPosition2)
{
    float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16], tpLightViewMatrix[16], tpLightProjectionMatrix[16], tpLightViewMatrix2[16], tpLightProjectionMatrix2[16];
    int location;


    // Transpose the matrices to prepare them for the shader.
    m_OpenGLPtr->MatrixTranspose(tpWorldMatrix, worldMatrix);
    m_OpenGLPtr->MatrixTranspose(tpViewMatrix, viewMatrix);
    m_OpenGLPtr->MatrixTranspose(tpProjectionMatrix, projectionMatrix);

    // Transpose the second view and projection matrices.
    m_OpenGLPtr->MatrixTranspose(tpLightViewMatrix, lightViewMatrix);
    m_OpenGLPtr->MatrixTranspose(tpLightProjectionMatrix, lightProjectionMatrix);

Transpose the second light matrices.

    // Do the same for the second light matrices.
    m_OpenGLPtr->MatrixTranspose(tpLightViewMatrix2, lightViewMatrix2);
    m_OpenGLPtr->MatrixTranspose(tpLightProjectionMatrix2, lightProjectionMatrix2);

    // Install the shader program as part of the current rendering state.
    m_OpenGLPtr->glUseProgram(m_shaderProgram);

    // Set the world matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "worldMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpWorldMatrix);

    // Set the view matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "viewMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpViewMatrix);

    // Set the projection matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpProjectionMatrix);

    // Set the light view matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightViewMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpLightViewMatrix);

    // Set the light projection matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightProjectionMatrix");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpLightProjectionMatrix);

Set the second light matrices in the vertex shader.

    // Set the second light view matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightViewMatrix2");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpLightViewMatrix2);

    // Set the second light projection matrix in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightProjectionMatrix2");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpLightProjectionMatrix2);

    // Set the light position in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightPosition");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform3fv(location, 1, lightPosition);

Set the second light position in the vertex shader.

    // Set the second light position in the vertex shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightPosition2");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform3fv(location, 1, lightPosition2);

    // Set the texture in the pixel shader to use the data from the first texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "shaderTexture");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 0);

    // Set the projection texture in the pixel shader to use the data from the second texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "depthMapTexture");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 1);

Set the second depth map texture in the pixel shader.

    // Set the second light projection texture in the pixel shader to use the data from the thrid texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "depthMapTexture2");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1i(location, 2);

    // Set the diffuse light color in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "diffuseColor");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform4fv(location, 1, diffuseColor);

Set the second light's diffuse color in the pixel shader.

    // Set the second diffuse light color in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "diffuseColor2");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform4fv(location, 1, diffuseColor2);

    // Set the ambient light in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "ambientColor");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform4fv(location, 1, ambientColor);

    // Set the shadow map bias in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "bias");
    if(location == -1)
    {
        return false;
    }
    m_OpenGLPtr->glUniform1f(location, bias);

    return true;
}

Applicationclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_NEAR = 1.0f;
const float SCREEN_DEPTH = 100.0f;
const int SHADOWMAP_WIDTH = 1024;
const int SHADOWMAP_HEIGHT = 1024;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "openglclass.h"
#include "cameraclass.h"
#include "modelclass.h"
#include "lightclass.h"
#include "rendertextureclass.h"
#include "depthshaderclass.h"
#include "shadowshaderclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class Name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
    ApplicationClass();
    ApplicationClass(const ApplicationClass&);
    ~ApplicationClass();

    bool Initialize(Display*, Window, int, int);
    void Shutdown();
    bool Frame(InputClass*);

private:
    bool RenderDepthToTexture();

There is now an additional function for rendering the scene from the second light's perspective onto a second depth map render texture.

    bool RenderDepthToTexture2();
    bool Render();

private:
    OpenGLClass* m_OpenGL;
    CameraClass* m_Camera;
    ModelClass *m_CubeModel, *m_SphereModel, *m_GroundModel;

We have added a second light and a second render to texture object for the second light.

    LightClass *m_Light, *m_Light2;
    RenderTextureClass *m_RenderTexture, *m_RenderTexture2;
    DepthShaderClass* m_DepthShader;
    ShadowShaderClass* m_ShadowShader;
    float m_shadowMapBias;
};

#endif

Applicationclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "applicationclass.h"

Initialize the second light and second render to texture to null in the class constructor.

ApplicationClass::ApplicationClass()
{
    m_OpenGL = 0;
    m_Camera = 0;
    m_CubeModel = 0;
    m_SphereModel = 0;
    m_GroundModel = 0;
    m_Light = 0;
    m_Light2 = 0;
    m_RenderTexture = 0;
    m_RenderTexture2 = 0;
    m_DepthShader = 0;
    m_ShadowShader = 0;
}


ApplicationClass::ApplicationClass(const ApplicationClass& other)
{
}


ApplicationClass::~ApplicationClass()
{
}


bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
    char modelFilename[128], textureFilename[128];
    bool result;


    // Create and initialize the OpenGL object.
    m_OpenGL = new OpenGLClass;

    result = m_OpenGL->Initialize(display, win, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, VSYNC_ENABLED);
    if(!result)
    {
        cout << "Could not initialize the OpenGL object." << endl;
        return false;
    }

We will position the camera behind the scene so we can clearly see the double light effect.

    // Create and initialize the camera object.
    m_Camera = new CameraClass;

    // Position camera to view scene from behind to see the shadow effect better.
    m_Camera->SetPosition(-8.0f, 7.0f, 8.0f);
    m_Camera->SetRotation(35.0f, 135.0f, 0.0f);
    m_Camera->Render();

    // Create and initialize the cube model object.
    m_CubeModel = new ModelClass;

    strcpy(modelFilename, "../Engine/data/cube.txt");
    strcpy(textureFilename, "../Engine/data/wall01.tga");

    result = m_CubeModel->Initialize(m_OpenGL, modelFilename, textureFilename, false);
    if(!result)
    {
        cout << "Could not initialize the cube model object." << endl;
        return false;
    }

    // Create and initialize the sphere model object.
    m_SphereModel = new ModelClass;

    strcpy(modelFilename, "../Engine/data/sphere.txt");
    strcpy(textureFilename, "../Engine/data/ice.tga");

    result = m_SphereModel->Initialize(m_OpenGL, modelFilename, textureFilename, true);
    if(!result)
    {
        cout << "Could not initialize the sphere model object." << endl;
        return false;
    }

    // Create and initialize the ground model object.
    m_GroundModel = new ModelClass;

    strcpy(modelFilename, "../Engine/data/plane01.txt");
    strcpy(textureFilename, "../Engine/data/metal001.tga");

    result = m_GroundModel->Initialize(m_OpenGL, modelFilename, textureFilename, false);
    if(!result)
    {
        cout << "Could not initialize the ground model object." << endl;
        return false;
    }

    // Create and initialize the light object.
    m_Light = new LightClass;

    m_Light->SetAmbientLight(0.15f, 0.15f, 0.15f, 1.0f);
    m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
    m_Light->SetLookAt(0.0f, 0.0f, 0.0f);
    m_Light->GenerateProjectionMatrix(SCREEN_DEPTH, SCREEN_NEAR);

Setup the second light object.

    // Create and initialize the second light object.
    m_Light2 = new LightClass;

    m_Light2->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
    m_Light2->SetLookAt(0.0f, 0.0f, 0.0f);
    m_Light2->GenerateProjectionMatrix(SCREEN_DEPTH, SCREEN_NEAR);

    // Create and initialize the render to texture object.
    m_RenderTexture = new RenderTextureClass;

    result = m_RenderTexture->Initialize(m_OpenGL, SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT, SCREEN_NEAR, SCREEN_DEPTH, 0);
    if(!result)
    {
        cout << "Could not initialize the render texture object." << endl;
        return false;
    }

Setup the second render to texture object.

    // Create and initialize the second render to texture object.
    m_RenderTexture2 = new RenderTextureClass;

    result = m_RenderTexture2->Initialize(m_OpenGL, SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT, SCREEN_NEAR, SCREEN_DEPTH, 0);
    if(!result)
    {
        cout << "Could not initialize the second render texture object." << endl;
        return false;
    }

    // Create and initialize the depth shader object.
    m_DepthShader = new DepthShaderClass;

    result = m_DepthShader->Initialize(m_OpenGL);
    if(!result)
    {
        cout << "Could not initialize the depth shader object." << endl;
        return false;
    }

    // Create and initialize the shadow shader object.
    m_ShadowShader = new ShadowShaderClass;

    result = m_ShadowShader->Initialize(m_OpenGL);
    if(!result)
    {
        cout << "Could not initialize the shadow shader object." << endl;
        return false;
    }

    // Set the shadow map bias to fix the floating point precision issues (shadow acne/lines artifacts).
    m_shadowMapBias = 0.0022f;

    return true;
}

The second light object and second render to texture object are released in the Shutdown function.

void ApplicationClass::Shutdown()
{
    // Release the shadow shader object.
    if(m_ShadowShader)
    {
        m_ShadowShader->Shutdown();
        delete m_ShadowShader;
        m_ShadowShader = 0;
    }

    // Release the depth shader object.
    if(m_DepthShader)
    {
        m_DepthShader->Shutdown();
        delete m_DepthShader;
        m_DepthShader = 0;
    }

    // Release the second render texture object.
    if(m_RenderTexture2)
    {
        m_RenderTexture2->Shutdown();
        delete m_RenderTexture2;
        m_RenderTexture2 = 0;
    }

    // Release the render texture object.
    if(m_RenderTexture)
    {
        m_RenderTexture->Shutdown();
        delete m_RenderTexture;
        m_RenderTexture = 0;
    }

    // Release the second light object.
    if(m_Light2)
    {
        delete m_Light2;
        m_Light2 = 0;
    }

    // Release the light object.
    if(m_Light)
    {
        delete m_Light;
        m_Light = 0;
    }

    // Release the ground model object.
    if(m_GroundModel)
    {
        m_GroundModel->Shutdown();
        delete m_GroundModel;
        m_GroundModel = 0;
    }

    // Release the sphere model object.
    if(m_SphereModel)
    {
        m_SphereModel->Shutdown();
        delete m_SphereModel;
        m_SphereModel = 0;
    }

    // Release the cube model object.
    if(m_CubeModel)
    {
        m_CubeModel->Shutdown();
        delete m_CubeModel;
        m_CubeModel = 0;
    }

    // Release the camera object.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    // Release the OpenGL object.
    if(m_OpenGL)
    {
        m_OpenGL->Shutdown();
        delete m_OpenGL;
        m_OpenGL = 0;
    }

    return;
}


bool ApplicationClass::Frame(InputClass* Input)
{
    bool result;


    // Check if the escape key has been pressed, if so quit.
    if(Input->IsEscapePressed() == true)
    {
        return false;
    }

Set the first light behind the camera and to the right. Set the second light behind the camera and to the left.

    // Set the position of the two lights and generate their view matrices.
    m_Light->SetPosition(5.0f, 8.0f, -5.0f);
    m_Light->GenerateViewMatrix();

    m_Light2->SetPosition(-5.0f, 8.0f, -5.0f);
    m_Light2->GenerateViewMatrix();

    // Render the scene depth for the first light to it's render texture.
    result = RenderDepthToTexture();
    if(!result)
    {
        return false;
    }

The scene is rendered from the second light's perspective onto the second render to texture.

    // Render the scene depth for the second light to it's render texture.
    result = RenderDepthToTexture2();
    if(!result)
    {
        return false;
    }

    // Render the final graphics scene.
    result = Render();
    if(!result)
    {
        return false;
    }

    return true;
}


bool ApplicationClass::RenderDepthToTexture()
{
    float translateMatrix[16], lightViewMatrix[16], lightProjectionMatrix[16];
    bool result;


    // Set the render target to be the render texture and clear it.
    m_RenderTexture->SetRenderTarget();
    m_RenderTexture->ClearRenderTarget(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the view and orthographic matrices from the light object.
    m_Light->GetViewMatrix(lightViewMatrix);
    m_Light->GetProjectionMatrix(lightProjectionMatrix);

    // Setup the translation matrix for the cube model.
    m_OpenGL->MatrixTranslation(translateMatrix, -2.0f, 2.0f, 0.0f);

    // Render the cube model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_CubeModel->Render();

    // Setup the translation matrix for the sphere model.
    m_OpenGL->MatrixTranslation(translateMatrix, 2.0f, 2.0f, 0.0f);

    // Render the sphere model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_SphereModel->Render();

    // Setup the translation matrix for the ground model.
    m_OpenGL->MatrixTranslation(translateMatrix, 0.0f, 1.0f, 0.0f);

    // Render the ground model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_GroundModel->Render();

    // Reset the render target back to the original back buffer and not the render to texture anymore.  And reset the viewport back to the original.
    m_OpenGL->SetBackBufferRenderTarget();
    m_OpenGL->ResetViewport();

    return true;
}

This is the new function for rendering the scene from the second light's perspective onto the second render to texture object.

bool ApplicationClass::RenderDepthToTexture2()
{
    float translateMatrix[16], lightViewMatrix[16], lightProjectionMatrix[16];
    bool result;


    // Set the render target to be the render texture and clear it.
    m_RenderTexture2->SetRenderTarget();
    m_RenderTexture2->ClearRenderTarget(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the view and orthographic matrices from the light object.
    m_Light2->GetViewMatrix(lightViewMatrix);
    m_Light2->GetProjectionMatrix(lightProjectionMatrix);

    // Setup the translation matrix for the cube model.
    m_OpenGL->MatrixTranslation(translateMatrix, -2.0f, 2.0f, 0.0f);

    // Render the cube model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_CubeModel->Render();

    // Setup the translation matrix for the sphere model.
    m_OpenGL->MatrixTranslation(translateMatrix, 2.0f, 2.0f, 0.0f);

    // Render the sphere model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_SphereModel->Render();

    // Setup the translation matrix for the ground model.
    m_OpenGL->MatrixTranslation(translateMatrix, 0.0f, 1.0f, 0.0f);

    // Render the ground model using the depth shader and the light matrices.
    result = m_DepthShader->SetShaderParameters(translateMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    m_GroundModel->Render();

    // Reset the render target back to the original back buffer and not the render to texture anymore.  And reset the viewport back to the original.
    m_OpenGL->SetBackBufferRenderTarget();
    m_OpenGL->ResetViewport();

    return true;
}


bool ApplicationClass::Render()
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16], lightViewMatrix[16], lightProjectionMatrix[16];
    float diffuseColor[4], ambientColor[4], lightPosition[3];
    float lightViewMatrix2[16], lightProjectionMatrix2[16], diffuseColor2[4], lightPosition2[3];
    bool result;


    // Clear the buffers to begin the scene.
    m_OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // Get the world, view, and projection matrices from the opengl and camera objects.
    m_OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_OpenGL->GetProjectionMatrix(projectionMatrix);

    // Get the light view and projection matrices.
    m_Light->GetViewMatrix(lightViewMatrix);
    m_Light->GetProjectionMatrix(lightProjectionMatrix);

    // Get the light properties.
    m_Light->GetDiffuseColor(diffuseColor);
    m_Light->GetAmbientLight(ambientColor);
    m_Light->GetPosition(lightPosition);

Get the view matrix, projection matrix, diffuse light color, and light position from the second light. The shadow map shader now requires all of these as input for doing the second light rendering.

    // Get the second light data.
    m_Light2->GetViewMatrix(lightViewMatrix2);
    m_Light2->GetProjectionMatrix(lightProjectionMatrix2);
    m_Light2->GetDiffuseColor(diffuseColor2);
    m_Light2->GetPosition(lightPosition2);

    // Setup the translation matrix for the cube model.
    m_OpenGL->MatrixTranslation(worldMatrix, -2.0f, 2.0f, 0.0f);

Now perform all the rendering passing in the additional second light parameters into the shadow shader. Note that we must also set the second depth texture as shader texture unit input 2.

    // Set the shadow shader as the current shader program and set the parameters that it will use for rendering.
    result = m_ShadowShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, lightProjectionMatrix, diffuseColor, ambientColor, lightPosition, m_shadowMapBias,
                                                 lightViewMatrix2, lightProjectionMatrix2, diffuseColor2, lightPosition2);
    if(!result)
    {
        return false;
    }

    // Set the render texture in texture unit 1.
    m_RenderTexture->SetTexture(1);

    // Set the second light render texture in texture unit 2.
    m_RenderTexture2->SetTexture(2);

    // Render the cube model using the shadow shader.
    m_CubeModel->SetTexture1(0);
    m_CubeModel->Render();

    // Setup the translation matrix for the sphere model.
    m_OpenGL->MatrixTranslation(worldMatrix, 2.0f, 2.0f, 0.0f);

    // Set the shadow shader as the current shader program and set the parameters that it will use for rendering.
    result = m_ShadowShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, lightProjectionMatrix, diffuseColor, ambientColor, lightPosition, m_shadowMapBias,
                                                 lightViewMatrix2, lightProjectionMatrix2, diffuseColor2, lightPosition2);
    if(!result)
    {
        return false;
    }

    // Set the render texture in texture unit 1.
    m_RenderTexture->SetTexture(1);

    // Set the second light render texture in texture unit 2.
    m_RenderTexture2->SetTexture(2);

    // Render the sphere model using the shadow shader.
    m_SphereModel->SetTexture1(0);
    m_SphereModel->Render();

    // Setup the translation matrix for the ground model.
    m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 1.0f, 0.0f);

    // Set the shadow shader as the current shader program and set the parameters that it will use for rendering.
    result = m_ShadowShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, lightProjectionMatrix, diffuseColor, ambientColor, lightPosition, m_shadowMapBias,
                                                 lightViewMatrix2, lightProjectionMatrix2, diffuseColor2, lightPosition2);
    if(!result)
    {
        return false;
    }

    // Set the render texture in texture unit 1.
    m_RenderTexture->SetTexture(1);

    // Set the second light render texture in texture unit 2.
    m_RenderTexture2->SetTexture(2);

    // Render the ground model using the shadow shader.
    m_GroundModel->SetTexture1(0);
    m_GroundModel->Render();

    // Present the rendered scene to the screen.
    m_OpenGL->EndScene();

    return true;
}

Summary

We can now illuminate the scene with multiple lights that all cast shadows.


To Do Exercises

1. Recompile and run the program.

2. Modify the color and position of the lights to see the effect on the scene.

3. Rewrite the code to handle three lights. Change the program flow to use for loops to loop through each light. Set the lights, render textures, and such to be arrays.


Source Code

Source Code and Data Files: gl4linuxtut42_src.tar.gz

Back to Tutorial Index