Skip to main content
1 of 5
CodeSurgeon
  • 133
  • 1
  • 11

Implementing Shadow Mapping in Python and OpenGL 2.1

Like many beginners working with OpenGL for their first 3D game project, I am struggling with implementing shadow mapping into my game engine. I think I have an understanding of the basic idea behind shadow mapping (rendering the scene to a depth buffer from the light's POV and projecting it onto the scene from the camera's POV). In order to get a better handle on how it works, I am trying to create a little shadow mapping demo.

My code is currently divided into three rendering passes:

  • Pass 1 - Create the depth texture that will be used for shadow mapping on an offscreen framebuffer
  • Pass 2 - (Attempt to) render the scene with shadows using that depth texture
  • Pass 3 - Display the shadow map in the upper right corner (debug purposes)

So far, I have successfully created and rendered a depth texture (passes 1 and 3). However, I am currently struggling to render the scene with shadows from the camera's POV. The results look like this:

enter image description here

As you can see, there is some flickering at the edges of thick floor plane as well as on one cube corner. The results look nothing like shadows and the scene looks fully lit. I am thinking that either my second pass or my shaders for that pass that I am using to display the scene are incorrect, but I cannot seem to find the error. It is a relatively short demo written using python 2.7, opengl 2.1, and GLSL 120. Below is the main method that includes most of the relevant code (aside from some window creation, shader compilations, and primitive matrix math libraries I wrote):

from window import Window
from shader import Shader
from mat4 import Mat4
from vec3 import Vec3

from OpenGL.GL import *
from OpenGL.GLU import *
import math
import numpy as np
from PIL import Image

#Loads my special model file format 
#Basically a super-simplified obj without indexing
def loadAA7(dataUrl):
        vData = []
        tData = []
        nData = []
        inFile = open(dataUrl, "r")
        for line in inFile.readlines():
            lineList = line.strip().split("\t")
            vData.extend([float(v) for v in lineList[0:3]])
            tData.extend([float(v) for v in lineList[3:5]])
            nData.extend([float(v) for v in lineList[5:]])
        vertexData = np.array(vData, dtype=np.float32)
        texCoordData = np.array(tData, dtype=np.float32)
        normalData = np.array(nData, dtype=np.float32)
        return vertexData, texCoordData, normalData

def createMeshBuffers(vertices, texCoords, normals):
    v, t, n = vertices, texCoords, normals
    vbo, tbo, nbo = glGenBuffers(3)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, len(v)*4, v, GL_STATIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, tbo)
    glBufferData(GL_ARRAY_BUFFER, len(t)*4, t, GL_STATIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, nbo)
    glBufferData(GL_ARRAY_BUFFER, len(n)*4, n, GL_STATIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    return vbo, tbo, nbo

if __name__ == "__main__":
    window = Window("Shadow Mapping Test", 800, 600, 60)
    glClearColor(0.0, 0.0, 0.0, 1.0)
    glEnable(GL_CULL_FACE)
    glEnable(GL_TEXTURE_2D)
    glEnable(GL_DEPTH_TEST)
    time = 0
    
    v, t, n = loadAA7("./data/blockworld.aa7")
    vbo, tbo, nbo = createMeshBuffers(v, t, n)
    shadowMapShader = Shader("./shaders/shadowMap.vert", "./shaders/shadowMap.frag")
    shadowMapShader.compile()
    displayShader = Shader("./shaders/display.vert", "./shaders/display.frag")
    displayShader.compile()
    
    img = Image.open("./data/blockworld.png")
    imgWidth, imgHeight = img.size
    imgData = np.array(img)
    modelTex = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, modelTex)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imgWidth, imgHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData)
    glBindTexture(GL_TEXTURE_2D, 0)
    
    rendertarget = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, rendertarget)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 512, 512, 0, GL_DEPTH_COMPONENT, GL_FLOAT, None)
    fbo = glGenFramebuffers(1)
    glBindFramebuffer(GL_FRAMEBUFFER, fbo)
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, rendertarget, 0)
    glBindFramebuffer(GL_FRAMEBUFFER, 0)
    
    lightPos = Vec3(150, 150, 0)
    cameraPos = Vec3(0, 200, -300)
    
    while True:
        window.update()
        time += 1
        
        #Pass 1: Render to Texture
        shadowMapShader.enable()
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        lightProj = Mat4().perspective(60, float(512)/512, 1, 1000)
        lightView = Mat4().lookAt(lightPos, Vec3(0, 0, 0), Vec3(0, 1, 0))
        modelMatrix = Mat4().rotateY(time)
        glMatrixMode(GL_PROJECTION)
        glLoadMatrixf(lightProj.data)
        glMatrixMode(GL_MODELVIEW)
        glLoadMatrixf(lightView.data)
        glMultMatrixf(modelMatrix.data)
        glBindFramebuffer(GL_FRAMEBUFFER, fbo)
        glViewport(0, 0, 512, 512)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glEnableClientState(GL_VERTEX_ARRAY)
        glVertexPointer(3, GL_FLOAT, 0, v)
        glDrawArrays(GL_TRIANGLES, 0, len(v)/3)
        glDisableClientState(GL_VERTEX_ARRAY)
        shadowMapShader.disable()
        
        #Pass 2: Render the scene with shadows
        bias = Mat4([0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0])
        biasMVPMatrix = bias.mul(lightProj).mul(lightView).mul(modelMatrix)
        glViewport(0, 0, 800, 600)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        cameraProj = Mat4().perspective(30, float(800)/600, 1, 1000)
        cameraView = Mat4().lookAt(cameraPos, Vec3(0, 0, 0), Vec3(0, 1, 0))
        modelMatrix = Mat4().rotateY(time)
        glMatrixMode(GL_PROJECTION)
        glLoadMatrixf(cameraProj.data)
        glMatrixMode(GL_MODELVIEW)
        glLoadMatrixf(cameraView.data)
        glMultMatrixf(modelMatrix.data)
        displayShader.enable()
        glActiveTexture(GL_TEXTURE1)
        glBindTexture(GL_TEXTURE_2D, rendertarget)
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, modelTex)
        displayShader.setUniform("u_modelTexture", "sampler2D", 0)
        displayShader.setUniform("u_shadowMap", "sampler2D", 1)
        displayShader.setUniform("u_biasMVPMatrix", "mat4", biasMVPMatrix.data)
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
        glVertexPointer(3, GL_FLOAT, 0, v)
        glTexCoordPointer(2, GL_FLOAT, 0, t)
        glDrawArrays(GL_TRIANGLES, 0, len(v)/3)
        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        displayShader.disable()
        
        #DEBUG: Display the render texture
        glViewport(0, 0, 800, 600)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(-1, 1, -1, 1, -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, rendertarget)
        glBegin(GL_QUADS)
        glColor3f(1,1,1)
        glTexCoord2f(0, 0); glVertex3f(0.5, 0.5, 0)
        glTexCoord2f(1, 0); glVertex3f(1, 0.5, 0) 
        glTexCoord2f(1, 1); glVertex3f(1, 1, 0)
        glTexCoord2f(0, 1); glVertex3f(0.5, 1, 0)
        glEnd()

I am also including my shadows for pass 1 (shadowMap.vert/frag) and pass 2 (display.vert/frag) in case the error is in one of these, but they seem to make sense to me (pass 1 outputs linearizes fragment depth while pass 2 transforms the vertices with a biased light space matrix before performing a depth comparsion between the depth texture and the scene).

shadowMap.vert

#version 120

void main()
{
    gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
}

shadowMap.frag

#version 120                                                     
                                                                                    
void main()                                                                         
{
    float z = gl_FragCoord.z;
    float n = 1.0;
    float f = 1000.0;
    //convert to linear values   
    //formula can be found at www.roxlu.com/2014/036/rendering-the-depth-buffer 
    float c = (2.0 * n) / (f + n - z * (f - n));                             
    gl_FragDepth = c;          
}

display.vert

#version 120

uniform mat4 u_biasMVPMatrix;
varying vec4 v_shadowCoord;

void main()
{
    mat4 bias = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0);
    gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
    v_shadowCoord = u_biasMVPMatrix * gl_Vertex;
    gl_TexCoord[0] = gl_MultiTexCoord0;
}

display.frag

#version 120                                                     
                                                                                    
void main()                                                                         
{
    float z = gl_FragCoord.z;
    float n = 1.0;
    float f = 1000.0;
    //convert to linear values   
    //formula can be found at www.roxlu.com/2014/036/rendering-the-depth-buffer 
    float c = (2.0 * n) / (f + n - z * (f - n));                             
    gl_FragDepth = c;          
}

I have read many of the tutorials online, many of which are completely deprecated or are for OpenGL 3.2, but it has been a challenge trying to get shadows to work these past two weeks and make progress. Any help with this issue would be greatly appreciated.

CodeSurgeon
  • 133
  • 1
  • 11