1
\$\begingroup\$

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)

I have successfully created and rendered a depth texture (passes 1 and 3). However, I am struggling to render the scene with shadows from the camera's POV. 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
uniform sampler2D u_modelTexture;
uniform sampler2D u_shadowMap;
varying vec4 v_shadowCoord;

void main()                                                                         
{
    vec3 projCoords = v_shadowCoord.xyz/v_shadowCoord.w;
    float closestDepth = texture2D(u_shadowMap, projCoords.xy).r;
    float currentDepth = projCoords.z;  
    float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;
    gl_FragColor = shadow * texture2D(u_modelTexture, gl_TexCoord[0].xy);
}

Update: Here is a picture of the results achieved when using this as the pass 2 fragment shader:

gl_FragColor = vec4(v_shadowCoord.xyz/v_shadowCoord.w, 1.0);

enter image description here

I also found it interesting that I get "shadows" when I use this as the shader instead (but I have no idea what it mean):

#version 120                                                     

uniform sampler2D u_modelTexture;
uniform sampler2D u_shadowMap;
varying vec4 v_shadowCoord;

void main()                                                                         
{
    vec3 projCoords = v_shadowCoord.xyz/v_shadowCoord.w;
    float closestDepth = texture2D(u_shadowMap, projCoords.xy).z;
    gl_FragColor.rgb = vec3(closestDepth);
    gl_FragColor.a = 1.0;
}

enter image description here

\$\endgroup\$
  • \$\begingroup\$ Where's your Pass2 fragment shader? Looks like you have pasted the Pass1 shader twice. \$\endgroup\$ – Kromster Jan 22 '16 at 5:14
  • \$\begingroup\$ Start off by writing Pass2 shader as gl_FragColor = vec4(v_shadowCoord.xyz, 1.0); to see what is going on. \$\endgroup\$ – Kromster Jan 22 '16 at 5:15
  • 1
    \$\begingroup\$ Ok, sorry about that. I have updated the post to include my current pass 2 shader. I went ahead and tested your pass 2 shader and all of the geometry was displayed as completely white. This means that the transformation that I was performing on gl_Vertex to produce v_shadowCoord is incorrect as the displayed r,g,b values outputted should be somewhere between 0 and 1 instead of all greater than 1, right? \$\endgroup\$ – CodeSurgeon Jan 22 '16 at 16:08
  • \$\begingroup\$ I took a look at it some more and I don't believe that the biasMVPMatrix I am passing into the shader is wrong. I get a range of colors when I use gl_FragColor = vec4(v_shadowCoord.xyz / v_shadowCoord.w, 1.0); instead, normalizing by the w component. I think the issue is in the shader itself. What is the depth value that I need to compare v_shadowCoord.xyz / v_shadowCoord.w to and how do I obtain it in the shader? \$\endgroup\$ – CodeSurgeon Jan 23 '16 at 5:13
  • \$\begingroup\$ Your latest screen means shadows get mapped properly. What is left to do is to fix that currentDepth > closestDepth comparison you have. You were converting to linear in vertex shader, do you need to do the same in fragment? \$\endgroup\$ – Kromster Jan 23 '16 at 6:55
0
\$\begingroup\$

As Krom Stern pointed out in the comments, the issues with my shadow mapping stemmed from two issues in the pass 2 fragment shader. First, I needed to divide the v_shadowCoord by the w component in addition to performing the correct bias model-view-projection multiplication. This division would normalize my coordinates between 0 and 1 and provide valid texture coordinates for looking up depths in my shadow map. Second, I needed to fix my comparison between current and closest depth by performing the same linearization function that I did on closestDepth in pass 1 to currentDepth in pass 2.

I have reorganized the code to somewhat separate the lighting and shadowing calculations in the shader. Here is my updated shader (display.frag) for the shadow mapping demo:

#version 120                                                     

struct Light
{
    vec3 color;
    vec3 direction;
    vec3 position;
    float innerAngle;
    float outerAngle;
};

uniform sampler2D u_modelTexture;
uniform sampler2D u_shadowMap;
varying vec4 v_shadowCoord;
varying vec3 v_position;
varying vec3 v_normal;
varying mat4 v_mvMatrix;
uniform Light u_light;//spotlight in the demo!

vec4 calcShadow()
{
    vec3 projCoords = v_shadowCoord.xyz/v_shadowCoord.w;
    float closestDepth = texture2D(u_shadowMap, projCoords.xy).z;
    float z = projCoords.z;
    float n = 1.0;
    float f = 1000.0;
    float currentDepth = (2.0 * n) / (f + n - z * (f - n));
    vec3 shadow = u_light.color;
    float bias = 0.00005;//simple bias works here well enough, but needs to be tweaked based on the scene to avoid peter-panning.

    if(closestDepth + bias < currentDepth)
    {
        shadow = vec3(0.5, 0.5, 0.5);//ambient light
    }

    //We have artifacts on sides that should be in shadow. 
    //Let's remove those with a partial lighting calculation.
    vec3 normal = normalize(v_normal);
    vec3 lightPos = vec3(v_mvMatrix * vec4(u_light.position, 1.0));
    vec3 lightDir = normalize(lightPos - v_position);
    float diffCont = max(0.0, dot(normal, lightDir));
    if (diffCont == 0.0)
    {
        shadow = vec3(0.5, 0.5, 0.5);//ambient light
    }
    return vec4(shadow, 1.0);
}

vec3 calcLight()
{
    vec3 normal = normalize(v_normal);
    vec3 outLight = vec3(0.0, 0.0, 0.0);
    vec3 lightPos = vec3(v_mvMatrix * vec4(u_light.position, 1.0));
    vec3 spotDir = normalize(vec3(v_mvMatrix * vec4(u_light.direction, 0.0)));
    float innerAngle = u_light.innerAngle;
    float outerAngle = u_light.outerAngle;
    vec3 lightDiffuse = u_light.color;
    vec3 lightDir = normalize(lightPos - v_position);
    float diffCont = max(0.0, dot(normal, lightDir));
    if (diffCont > 0.0)
    {
        float angle = degrees(acos(dot(-lightDir, spotDir)));
        if (angle < outerAngle)
        {
            if (angle < innerAngle)
            {
                vec3 diffuse = diffCont * lightDiffuse;
                outLight = diffuse;
            }

            else
            {
                float att = 1 - pow(((angle - innerAngle) / (outerAngle - innerAngle)), 2);//squaring to approximate inverse-square law falloff
                vec3 diffuse = diffCont * lightDiffuse * att;
                outLight = diffuse;
            }
        }
    }
    return outLight;
}

void main()                                                                         
{
    vec3 ambientLight = vec3(0.5, 0.5, 0.5);
    vec3 diffuseLight = calcLight();
    vec4 totalLight = vec4(ambientLight + diffuseLight, 1.0);
    vec4 shadow = calcShadow();
    vec4 color = texture2D(u_modelTexture, gl_TexCoord[0].st);
    gl_FragColor = shadow * color * totalLight;
}

I have also made several changes to the rest of the code. These include:

  1. Sending light uniforms to the pass 2 shader.
  2. Actually using the vertex buffer objects I had created during initialization instead of just using vertex array objects.
  3. Applying culling to move artifacts to the faces that should be in shadow by the lighting calculations.
  4. Changing the model I am displaying (just for fun).

Below is a screenshot of the result:

enter image description here

There is still room for improvements such as PCF soft shadows and removing deprecated OpenGL variables and function calls, but I think it is a good starting point. The complete code for the demo can be found here. Just unzip the contents and run main.py. Note that there are several dependencies, which include pysdl2 (used for window management), PIL/pillow, PyOpenGL, and numpy.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.