openglshadow-mapping

Why does VSM Depth Map Blurring produces strange results?


I am trying to implement Variance Shadow Mapping for directional shadows in my rendering engine with OpenGL.

I have read multiple articles such as - https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps, https://graphics.stanford.edu/~mdfisher/Shadows.html to develop this.

The basic flow of the algorithm is as follows:

  1. Store the depth, and depth^2 in the depth texture.
  2. Apply two pass Gaussian blur with a 5 x 5 kernel and 10 passes.
  3. Sample a depth value, calculate the fragment's distance from the light, and
  4. Put them in the Chebyshev inequality to determine the maximum probability of the fragment being in shadow
  5. Use the result to make the fragment dark.

Here's my Depth Shader for the directional light with a orthographic projection matrix:

#version 440 core

uniform float farPlane;
uniform vec3 lightPos;
uniform mat4 directional_light_space_matrix;

in vec4 FragPos;
out vec2 depth;

void main()
{   

     vec4 FragPosLightSpace = directional_light_space_matrix * FragPos;
     float d = FragPosLightSpace.z / FragPosLightSpace.w;

     d = d * 0.5 + 0.5;

     float m1 = d;
     float m2 = d * d;

     float dx = dFdx(depth.x);
     float dy = dFdx(depth.y);

     m2 += 0.25 * (dx * dx + dy * dy);

     depth.r = m1;
     depth.g = m2;

}

Here's the snippet of the fragment shader that check's how much a fragment is lit.

float linstep(float mi, float ma, float v)
{
    return clamp ((v - mi)/(ma - mi), 0, 1);
}

float ReduceLightBleeding(float p_max, float Amount) 
{
     return linstep(Amount, 1, p_max); 
} 

float chebyshevUpperBound(float dist, vec2 moments)
{
    float p_max;
    if(dist <= moments.x)
    {
         return 1.0;
    }

    float variance = moments.y - (moments.x * moments.x);
    variance = max(variance, 0.1);
    float d = moments.x - dist;

    p_max = variance / (variance + d * d);

    return ReduceLightBleeding(p_max, 1.0);
}

float CheckDirectionalShadow(float bias, vec3 lightpos, vec3 FragPos)
{       
     vec3 projCoords = FragPosLightSpace.xyz / FragPosLightSpace.w;
     projCoords = projCoords * 0.5 + 0.5;

     vec2 closest_depth = texture(shadow_depth_map_directional, projCoords.xy).rg;

     return chebyshevUpperBound(projCoords.z, closest_depth);
}

Here's the Two Pass Gaussian Blur shader.

#version 440 core

layout (location = 0) out vec2 out_1;

in vec2 TexCoords;

uniform sampler2D inputTexture_1;

uniform bool horizontal;
float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);

void main()
{
    vec2 tex_offset = 1.0 / textureSize(inputTexture_1,0);
    vec2 o1 = texture(inputTexture_1, TexCoords).rg * weights[0];

    if(horizontal)
    {
        for(int i=1; i<4; i++)
        {
            o1 += texture(inputTexture_1, TexCoords + vec2(tex_offset.x * i, 0.0)).rg * weights[i];
            o1 += texture(inputTexture_1, TexCoords - vec2(tex_offset.x * i, 0.0)).rg * weights[i];
        }
    }
    else
    {
        for(int i=1; i<4; i++)
        {
            o1 += texture(inputTexture_1, TexCoords + vec2(0.0, tex_offset.y * i)).rg * weights[i];
            o1 += texture(inputTexture_1, TexCoords - vec2(0.0, tex_offset.y * i)).rg * weights[i];
        }
    }

    out_1 = o1;
}

I am putting my framebuffer generation code for information about how I store the moments.

// directional ----------------------------------------------------------------------------------------------------------------------------------------------
glGenFramebuffers(1, &directional_shadow_framebuffer);

glGenTextures(1, &directional_shadow_framebuffer_depth_texture);
glBindTexture(GL_TEXTURE_2D, directional_shadow_framebuffer_depth_texture); 

glTexImage2D(GL_TEXTURE_2D, 0, GL_RG32F, shadow_map_width, shadow_map_height, 0, GL_RG, GL_FLOAT, NULL);

float border_color[] = { 0.0f,0.0f,0.0f,1.0f };

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

glBindFramebuffer(GL_FRAMEBUFFER, directional_shadow_framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, directional_shadow_framebuffer_depth_texture, 0);

glGenRenderbuffers(1, &directional_shadow_framebuffer_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, directional_shadow_framebuffer_renderbuffer);

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, shadow_map_width, shadow_map_height);

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, directional_shadow_framebuffer_renderbuffer);


if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    LOGGER->log(ERROR, "Renderer : createShadowMapBuffer", "Directional Shadow Framebuffer is incomplete!");

glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// ----------------------------------------------------------------------------------------------------------------------------------------------

The results of the above operations is far from expectations. Instead of getting soft penumbra shadows, I get blob like sharp shadows.

Shadows

Here's how the First moment (depth) looks like, and the second moment is pretty much the same but darker.

enter image description here

I have tried experimenting with the minimum variance, shadow kernel size, gaussian samples, blur passes.. but I haven't come any closer to the solution.

I have a feeling I maybe doing something wrong with how I have set the texture filtering parameters in the Framebuffer generation code given above.

My final questions are :

  1. Is my implementation of VSMs incorrect?
  2. Why do I not see soft penumbras?
  3. I don't have a good feeling about how my texture is filtered, is there anything wrong in the Framebuffer generation code?

Solution

  • So, I had solved the problem.

    The implementation is perfectly fine, but the min variance and the amount parameter of the ReduceLightBleeding required tuning.

    I discovered that reducing the minimum variance parameter would soften the shadows more, but would greatly increase Light Bleeding. To counter this side effect we can tune the p_max value to become 0 when below a certain threshold, otherwise rescale between 0 and 1. This is exactly what the ReduceLightBleeding function does, which is also described in the same site linked above. But, increasing the amount parameter in ReduceLightBleeding would make the shadows look blob-like, which can be seen in the screenshots that I posted above.

    I managed to tweak the min variance and the light bleeding reduction amounts to find an optimal spot. However, I could never completely get rid of this artifact.

    A better alternative to Variance Shadow Mapping is its extension - Exponential Variance Shadow Maps.

    I do not understand the math properly, but I still managed to implement it quite easily. Check this question on gamedev.stackexchange for hints - EVSM.

    ESVM did a great job by reducing bleeding to the point that it can either not be noticed or just ignored.