Search code examples
c++openglshadow

OpenGL Cubemap FrameBuffer Depth Comparison


I am trying to implement shadow maps for point lights. Basically I'm creating a framebuffer and then render all shadow casters on each side of a cubemap texture (which is 6 times) and then read it in the regular rendering pass and determine which pixel is in shadow. I have several questions:

  1. Why do I have to include a color attachment in addition to a depth component in order for my cubemap to get anything rendered to? I tried it without the color attachment and it did not work.

  2. After adding the color attachment, I can see my shadow casters in the cubemap but it seems the shadow comparison is wrong. I am suspecting that one is in NDC while the other isn't.

Here's how I initialize my framebuffer containing the shadow cubemap:

// Create the depth buffer
    glGenTextures(1, &mDepthTextureID);
    glBindTexture(GL_TEXTURE_2D, mDepthTextureID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_2D, 0);

    //Create the cubemap texture
    glGenTextures(1, &mCubemapTextureID);
    glBindTexture(GL_TEXTURE_CUBE_MAP, mCubemapTextureID);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    for (GLuint i = 0; i < 6; ++i)
    {
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_R32F, width, height, 0, GL_RED, GL_FLOAT, 0);
    }
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

    //Create the framebuffer and attach the cubemap texture to it
    glGenFramebuffers(1, &mFrameBufferObjectID);
    glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferObjectID);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDepthTextureID, 0);

    //Disable writes to the color buffer
    glDrawBuffer(GL_NONE);
    //Disable reads from the color buffer
    glReadBuffer(GL_NONE);

    GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (Status != GL_FRAMEBUFFER_COMPLETE)
    {
        switch(Status)
        {
            case GL_FRAMEBUFFER_UNSUPPORTED:
                printf("FrameBuffer unsupported error");
                return false;
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                printf("FrameBuffer incomplete attachement");
                return false;
                break;
            default:
                printf("GLShadowCubemap error, status: 0x%x\n", Status);
                return false;
        }
    }

    //Unbind this
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

Here's my shadow's vertex shader: (Only the Position attribute is used)

#version 330 core

layout (location = 0) in vec3 Position;
layout (location = 1) in vec3 Normal;
layout (location = 2) in vec2 TexCoord;
layout (location = 3) in vec3 Tangent;

uniform mat4 gModelMatrix;
uniform mat4 gModelViewProjectionMatrix;

out vec3 WorldPosition;

/*
 * Below needs a GS and using layered rendering
void main()
{
    gl_Position = gModelMatrix * vec4(Position, 1.0);
}
*/

void main()
{
    vec4 pos4 = vec4(Position, 1.0);
    gl_Position = gModelViewProjectionMatrix * pos4;
    WorldPosition = (gModelMatrix * pos4).xyz;
}

Here's my shadow fragment shader:

#version 330 core

in vec3 WorldPosition;

uniform vec3 gLightPosition;

out float Fragment;

void main()
{
    // get distance between fragment and light source
    float dist_to_light = length(WorldPosition - gLightPosition);
    //gl_FragDepth = dist_to_light;
    Fragment = dist_to_light;
}

Additional question here:

I saw that many have said that overriding gl_FragDepth is a bad idea. I kind of know why but what's strange here is that if I were to override the gl_FragDepth manually, nothing gets written to the cubemap. Why?

Here's how I render all the regular stuff (the variable i is an index to my lights array)

    mShadowCubemapFBOs[i].ViewportChange();
    mShadowMapTechnique.SetLightPosition(light.Position);
    const float shadow_aspect = (static_cast<float>(mShadowWidth) / mShadowHeight);
    const mat4 shadow_projection_matrix = glm::perspective(90.f, shadow_aspect, 1.f, mShadowFarPlane);
    const vector<MeshComponent>& meshes = ComponentManager::Instance().GetMeshComponentPool().GetPool();
                for(int layer = 0; layer < 6; ++layer)
                {
                    GLenum cubemap_face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer;
                    mShadowCubemapFBOs[i].Bind(cubemap_face);
                    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
                    for(const MeshComponent& mesh : meshes)
                    {
//the transform_component is referenced ahead of time.
                        const mat4 model_transform = transform_component->GetTransformMatrix();
                                 mShadowMapTechnique.SetModelViewProjectionMatrix(light.Position, cubemap_face, shadow_projection_matrix, model_transform);
                        mShadowMapTechnique.SetModelMatrix(model_transform);
                        mesh.Render();
                    }
                }

Finally here's the regular rendering shader:

#version 330 core

const int MAX_LIGHTS = 8;
const int LIGHT_TYPE_DIRECTIONAL = 0;
const int LIGHT_TYPE_POINT = 1;
const int LIGHT_TYPE_SPOT = 2;

in vec2 TexCoord0;
in vec3 WorldNormal0;
in vec3 WorldPos0;
in vec3 WorldTangent0;

out vec4 FragmentColor;

struct Material
{
    vec4 Emissive;
    vec4 Ambient;
    vec4 Diffuse;
    vec4 Specular;
    float SpecularPower;
    bool UseTexture;
};

struct Light
{
    vec3 Position;
    vec3 Direction;
    vec4 Color;                 //RGBA
    float SpotAngle;
    float ConstantAttenuation;
    float LinearAttenuation;
    float QuadraticAttenuation;
    int LightType;
    samplerCube ShadowMap;  //Cubemap shadows
    bool Enabled;
};

struct LightingResult
{
    vec4 Diffuse;
    vec4 Specular;
};

uniform Material gMaterial;
uniform Light gLights[MAX_LIGHTS];
uniform sampler2D gTextureSampler0;
uniform sampler2D gNormalMap;
uniform bool gEnableNormalMap;
uniform vec3 gEyeWorldPos;

float CalculateShadowFactor(vec3 frag_pos, Light light)
{
    vec3 fragment_to_light = frag_pos - light.Position;
    float sample_distance = texture(light.ShadowMap, fragment_to_light).r;
    float distance = length(fragment_to_light);
    if (distance < sample_distance + 0.001)
    {
        return 1.0; // Inside the light
    }
    else
    {
        return 0.5; // Inside the shadow
    }
}

//L - Light direction vector from pixel to light source
//N - Normal at the pixel
vec4 CalculateDiffuse(Light light, vec3 L, vec3 N)
{
    float n_dot_l = max(0, dot(N, L));
    return light.Color * n_dot_l;
}

//V - View vector
//L - Light direction vector from pixel to light source
//N - Normal at the pixel
vec4 CalculateSpecular(Light light, vec3 V, vec3 L, vec3 N)
{
    //Phong lighting
    vec3 R = normalize(reflect(-L, N));
    float r_dot_v = max(0, dot(R, V));
    return light.Color * pow(r_dot_v, max(0.4, gMaterial.SpecularPower));
}

float CalculateAttenuation(Light light, float distance)
{
    return 1.0 / (light.ConstantAttenuation + light.LinearAttenuation * distance + light.QuadraticAttenuation * distance * distance);
}

//V - View vector
//P - Position of pixel
//N - Normal of pixel
LightingResult CalculatePointLight(Light light, vec3 V, vec3 P, vec3 N)
{
    LightingResult result;
    result.Diffuse = vec4(0.0, 0.0, 0.0, 1.0);
    result.Specular = vec4(0.0, 0.0, 0.0, 1.0);

    vec3 L = light.Position - P;
    float distance = length(L);
    L = normalize(L);

    float attenuation = CalculateAttenuation( light, distance );
    result.Diffuse = CalculateDiffuse(light, L, N) * attenuation;
    result.Specular = CalculateSpecular(light, V, L, N) * attenuation;

    return result;
}

//V - View vector
//P - Position of pixel
//N - Normal of pixel
LightingResult CalculateDirectionalLight(Light light, vec3 V, vec3 P, vec3 N)
{
    LightingResult result;
    result.Diffuse = vec4(0.0, 0.0, 0.0, 1.0);
    result.Specular = vec4(0.0, 0.0, 0.0, 1.0);

    vec3 L = -light.Direction;

    result.Diffuse = CalculateDiffuse(light, L, N);
    result.Specular = CalculateSpecular(light, V, L, N);

    return result;
}

//L - Light vector
//Smoothness increases as angle gets larger
float CalculateSpotCone(Light light, vec3 L)
{
    //cos are in radians
    float min_cos = cos(light.SpotAngle);
    float max_cos = (min_cos + 1.0f) / 2.0f;
    float cos_angle = dot(light.Direction, -L); //negated L such that as we move towards the edge, intensity decreases
    return smoothstep(min_cos, max_cos, cos_angle);
}

//V - View vector
//P - Position of pixel
//N - Normal of pixel
LightingResult CalculateSpotLight(Light light, vec3 V, vec3 P, vec3 N)
{
    LightingResult result;
    result.Diffuse = vec4(0.0, 0.0, 0.0, 1.0);
    result.Specular = vec4(0.0, 0.0, 0.0, 1.0);

    vec3 L = light.Position - P;
    float distance = length(L);
    L = normalize(L);

    float attenuation = CalculateAttenuation(light, distance);
    float spot_intensity = CalculateSpotCone(light, L);
    result.Diffuse = CalculateDiffuse(light, L, N) * attenuation * spot_intensity;
    result.Specular = CalculateSpecular(light, V, L, N) * attenuation * spot_intensity;

    return result;
}

//P - Position of pixel
//N - Normal of pixel
LightingResult CalculateLighting(vec3 P, vec3 N)
{
    vec3 V = normalize(gEyeWorldPos - P);

    LightingResult total_result;
    total_result.Diffuse = vec4(0, 0, 0, 1.0);
    total_result.Specular = vec4(0, 0, 0, 1.0);

    for(int i = 0; i < MAX_LIGHTS; ++i)
    {
        if(!gLights[i].Enabled)
        {
            continue;
        }

        LightingResult result;
        result.Diffuse = vec4(0, 0, 0, 1.0);
        result.Specular = vec4(0, 0, 0, 1.0);
        float shadow_factor = 1.0;

        switch(gLights[i].LightType)
        {
            case LIGHT_TYPE_DIRECTIONAL:
                result = CalculateDirectionalLight(gLights[i], V, P, N);
                break;
            case LIGHT_TYPE_POINT:
                result = CalculatePointLight(gLights[i], V, P, N);
                shadow_factor = CalculateShadowFactor(P, gLights[i]);
                break;
            case LIGHT_TYPE_SPOT:
                result = CalculateSpotLight(gLights[i], V, P, N);
                shadow_factor = CalculateShadowFactor(P, gLights[i]);
                break;
        }
        total_result.Diffuse += (result.Diffuse * shadow_factor);
        total_result.Specular += (result.Specular * shadow_factor);
    }

    total_result.Diffuse = clamp(total_result.Diffuse, 0, 1);
    total_result.Specular = clamp(total_result.Specular, 0, 1);

    return total_result;
}

vec3 CalculateNormalMapNormal()
{
    vec3 normal = normalize(WorldNormal0);
    vec3 tangent = normalize(WorldTangent0);
    tangent = normalize(tangent - dot(tangent, normal) * normal);   //remove components from the normal vector. This is needed for non-uniform scaling
    vec3 bi_tangent = cross(tangent, normal);
    vec3 bump_map = texture(gNormalMap, TexCoord0).xyz;
    bump_map = 2.0 * bump_map - vec3(1.0, 1.0, 1.0);    //Remaps the values
    mat3 TBN = mat3(tangent, bi_tangent, normal);
    vec3 actual_normal = TBN * bump_map;
    return normalize(actual_normal);
}

void main()
{
    vec3 pixel_normal = normalize(WorldNormal0);
    vec4 texture_color = vec4(0, 0, 0, 1);

    if(gMaterial.UseTexture)
    {
        texture_color = texture( gTextureSampler0, TexCoord0 );
    }

    if(gEnableNormalMap)
    {
        pixel_normal = CalculateNormalMapNormal();
    }

    LightingResult light_result = CalculateLighting(WorldPos0, pixel_normal);
    vec4 diffuse_color = gMaterial.Diffuse * light_result.Diffuse;
    vec4 specular_color = gMaterial.Specular * light_result.Specular;

    FragmentColor = (gMaterial.Emissive + gMaterial.Ambient + diffuse_color + specular_color) * texture_color;
    //FragmentColor = texture_color;

    //temp test
    //vec3 fragment_to_light = WorldPos0 - gLights[1].Position;
    //FragmentColor = vec4(vec3(texture(gLights[1].ShadowMap, fragment_to_light).r / gFarPlane), 1.0);
}

What am I doing wrong? I see that I am storing the distance from fragment to light in world space and it is written to a color buffer (not the depth buffer) and so it shouldn't be in NDC. Finally when I am comparing it, it's also in world space .... Why are the shadows off? It appears as if the shadows are way larger than they should be so the entire scene is covered with shadow and it appears that what should be the size of shadow is actually covered in light.

Picture of the shadow cubemap:

enter image description here

Picture of the scene (only the helicopter will cast shadow):

enter image description here

Thanks!


Solution

  • After some debugging, I found out my problems:

    1. glPerspective takes fov as radians, not degrees even though it's documentation says it's only in radians if FORCE_RADIANS is defined (I did not define that)

    2. The cubemap for shadow require the clear color to be (FLT_MAX, FLT_MAX, FLT_MAX, 1.0) such that everything is NOT in shadow by default.