Variance shadow maps

Variance shadow maps

Variance shadow maps

Variance shadow maps propose a way to soften shadow edges by allowing the use of standard filtering methods such as hardware linear interpolation and gaussian blur directly on shadow maps. The results are convincing and many options are available to tune the quality / performance ratio.

Concept

The variance shadow map technique have a statistical approach to shadow filtering. The problem is formulated this way: For a given point with a depth z, what is the percentage of points in a filtered shadow map that have a depth superior or equal to this z. The answer to this can be found with the  Chebyshev inequality:

Chebyshev

P() is the percentage of points that will fail the depth test.
is the depth.
is the fixed depth we are comparing to.
σ2 is the variance (the standard deviation squared).
µ is the mean.

The first important observation is that we are considering our shadow map to be filtered so after the filtering the depth value will become the mean.  Now, we need to find how to retrieve the variance from a filtered shadow map. For this we need to remember that the variance can also be expressed as the mean of squares minus the squared mean.

σ2 = E[x2] - µ2

Knowing this, if we store the depth squared into our shadow maps, this value will become the mean of squares after filtering. Now that we know how to get all the terms needed for this inequation let’s see how the implementation will look like.

Basic implementation (opengl)

For the implementation I will assume that you already have basic shadow maps working. If you don’t, this tutorial is a very good starting point.  So I will only highlight the implementation changes specific to VSM.

The first thing we need to change is the type of attachment for our shadow FBO. Since our depth pass now need to record both the depth and the squared depth we will attach a texture of type GL_RGB16F_ARB. Also, our depth attachment is not a texture but a RenderBuffer.

glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glGenTextures (1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F_ARB, 1024, 1024, 0, GL_RGB, GL_FLOAT, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0);
glGenRenderbuffers(1, &depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 1024);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);

Don’t forget to disable the compare function from your shadow map texture parameters and to switch your sampler from sampler2DShadow to sampler2D:

//glTexParameteri(mGLTexturetype, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
//glTexParameteri(mGLTexturetype, GL_TEXTURE_COMPARE_FUNC, GL_LESS);

The shader for your depth pass will look like this:

vec4 recordDepth()
{ 
    float depth = gl_FragCoord.z;
    float moment2 = depth * depth;
    return vec4(depth, moment2, 0.0, 0.0);
}

The you compute the percentage of shadow using the Chebyshev inequality this way:

vec4 shadowCoordNDC = vShadowCoord / vShadowCoord.w;
vec2 moments = texture2D(ShadowMap, shadowCoordNDC.xy).rg;

if (distance <= moments.x)
    return 1.0;

float variance = moments.y - (moments.x * moments.x);
variance = max(variance, 0.0000005);
float d = shadowCoordNDC.z - moments.x;
float shadowPCT = variance / (variance + d*d);

Note that since we are not using the GLSL textureProj function, we need to apply the division by vShadowCoord.w ourself to transform to normalized device coordinate.

Results and options

(All results are rendered with a 1024×1024 shadow map)

No filtering

VSM no filtering.

This is what we get if we don’t perform any filtering on our shadow map. Not very interesting, we see very aliased shadow edges and some weird artifacts (near the base of the cube).

Linear interpolation

VSM linear interpolation

Slightly better but still not what we expect. After all variance shadow maps allow us to use any filtering, we should try to add more serious filtering.

Linear interpolation and 5×5 Gaussian blur

VSM linear interpolation and gaussian blur

Way better, the 5×5 Gaussian blur produce soft edges. But we still have this artifact at the base of the cube.

Linear interpolation and 5×5 Gaussian blur, floor value

VSM-linear+gaussian+floor

We can soften the depth precision artifact by setting a greater floor value to the variance in the shader. (variance = max(variance, 0.0005) )

Linear interpolation and 5×5 Gaussian blur, 32bit texture

VSM-linear+gaussian+32bit

By changing the shadow texture internal format to GL_RGB32F_ARB we can set back our floor value to a lower value and get rid of the artifact.

Far away, no correction

At some angle, the farther shadows can show aliasing artifacts.

At some angle, the farther shadows have aliasing artifacts.

Far away, anisotropic filtering

Using anisotropic filtering (x16) improve farther shadows a little bit.

Using anisotropic filtering (x16) improve farther shadows a little bit. You could also try to turn on mipmap generations.

Limitations and improvements

VSM_light-bleeding

VSM light-bleeding.

Variance shadow maps suffer from light-bleeding when multiple shadows start to layer each other’s. This can be corrected by using a threshold on the returned shadowPCT value. In GLSL this can be done using the smoothstep function. To do so you can modify your shadow shader this way:
//float p_max = variance / (variance + d*d);
 float p_max = smoothstep(0.20, 1.0, variance / (variance + d*d));
VSM_light-bleeding-correction

VSM light-bleeding correction.

For this particular scene, a minimum value of 0.20 was just enough to get rid of the light-bleeding. We can also observe that applying a threshold on the shadowPCT also have unfortunate effect of hardening the transition from shadow to light, creating less soft shadows. So you will need to experiment with your scene to find the right minimum value.  Finally, even if it’s not directly related to VSM, we can add that the Gaussian blur we use create uniformly soft edges that don’t take into account the distance between the occluder and the receiver.

References

www.punkuser.net—vsm_paper.pdf

GPU Gems 3

fabiensanglard.net—shadowmappingVSM

Opengl Development Cookbook

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s