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:
- Store the depth, and depth^2 in the depth texture.
- Apply two pass Gaussian blur with a 5 x 5 kernel and 10 passes.
- Sample a depth value, calculate the fragment's distance from the light, and
- Put them in the Chebyshev inequality to determine the maximum probability of the fragment being in shadow
- 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.
Here's how the First moment (depth) looks like, and the second moment is pretty much the same but darker.
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 :
- Is my implementation of VSMs incorrect?
- Why do I not see soft penumbras?
- I don't have a good feeling about how my texture is filtered, is there anything wrong in the Framebuffer generation code?