Jump to content
  • Advertisement
Sign in to follow this  
krausest

Convolution shadow maps - your opinions?

This topic is 3964 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Recently I came across a paper called "Convolution Shadow Maps" (short CSM to allow a clear distinction between them and Cascading or Coherent shadow maps ;-) It sounds interesting: No light bleeding but filtering properties like VSM. Standard 8 bits per channel textures instead of floating buffers and for M=4 they need 2 RGBA8 textures and the artefacts look acceptable. What do you think about it? Has anyone already started implementing? Any opinions or experiences?

Share this post


Link to post
Share on other sites
Advertisement
I'm certainly interested in it, as it certainly falls into the category of interesting ways to parameterize the visibility function, along with deep and variance shadow maps. Using a Fourier decomposition is also useful in that it does filter linearly, just like variance shadow maps.

I haven't had time to implement it myself yet, but I do have a few concerns:

First, even though lower-precision textures can be used, one needs a lot of terms to get reasonable quality. In the paper, they use 16 terms, which is already feeling a tad abusive on texture memory and bandwidth. And it shows: performance is pretty poor (ex. 60fps for a 512^2 shadow map on an 8800GTX for a scene with about a dozen polygons) even with extremely simple scenes.

I'm not *too* concerned about this since this problem will go away as hardware gets better, but note that just brute-forcing a wide PCF kernel will become reasonable as well... in my testing even 8x8 PCF is completely reasonable already so assuming you have a reasonable projection parameterization (which any number of the projection/warping/splitting/etc. algorithms will give you nowadays), the performance benefits of hardware pre-filtering are lost if the alternate technique has to squander these gains in complexity.

Secondly, though the technique does not have the same "light bleeding" artifacts as VSM and other algorithms, it does suffer from significant problems near contact points. In the demo from the authors' page even with 16 Fourier terms (the maximum in their implementation) "fading to light" is clearly visible near contact points, which is pretty unacceptable considering these places are arguably the most important shadows with respect to giving the viewer good depth cues. This is unfortunately a consequence of the Fourier reconstruction, which is arguable not particularly suited to representing step functions like visibility (it was clearly chosen for its linearity and fairly simple reconstruction).

To make matters worse, this problem becomes worse as the light depth ranges increase. In the scene from Figure 6 in the paper if you increase the light depth range from 2 to 10 (i.e. beyond barely covering the tiny scene), the artifacts even with M=16 are as bad or worse than in the paper with M=2! I don't see a good way of getting around this problem; it is a fundamental limitation with the way that truncated Fourier series work.

There are a few other reasons why the technique is interesting with respect to plausible soft shadows (which they did not explore), but those are future work really. IMHO (I'm admittedly somewhat biased ;)), VSMs still have one large theoretical advantage which I believe makes them a good starting point for future work: in the "simplest" case, they get the 100% correct result and only are a bad approximation when the visibility function necessarily must carry more information. Thus they seem like a rather good basis for an adaptive algorithm, as is really required to implement shadows efficiently.

Anyways I don't mean to trash the idea - I actually like it quite a lot and am particularly pleased that people are continuing to come up with new ways to parameterize the shadow function, following in the footsteps of deep, opacity and variance shadow maps to name a few.

To the OP, did you run their demo? I'm surprised that you found the artifacts acceptable with M=4, because I found them to be pretty bad in some cases even with M=16 (as described above). Furthermore I suspect that the quality improvement from adding more terms diminishes quickly, so removing the remaining artifacts would require an unreasonable number of terms.

Share this post


Link to post
Share on other sites
Quote:
Original post by AndyTX
To the OP, did you run their demo?

Yes I tried to, but on Vista with a Geforce 7900 it didn't work so I was just judging from the screenshots in the paper.
VSM seems to be the perfect reason for buying a 8800. I had (a little) hope that CSM might be better doable on older hardware.

Share this post


Link to post
Share on other sites
Quote:
Original post by krausest
Yes I tried to, but on Vista with a Geforce 7900 it didn't work so I was just judging from the screenshots in the paper.

Odd that it doesn't run on a 7900... I wonder what feature(s) of the 8 series it uses. Unfortunately the screenshot in the paper is actually taken at a clever angle that occludes many of the artifacts. I can post some screenshots of what I'm talking about if you'd like.

Quote:
Original post by krausest
VSM seems to be the perfect reason for buying a 8800. I had (a little) hope that CSM might be better doable on older hardware.

fp32 filtering is certainly very nice, and I agree that VSM is pretty unusable on fp16 or lower (except for extremely small light ranges). Summed-area variance shadow maps work on older hardware (they do not require hardware filtering support), although they are more performance intensive than 'stock' VSMs.

Back on topic though, I still have some hope for CSMs as well, and some ideas on how to make them better. In their current state however I think it's unfair for them to claim that their artifacts compare favourably to VSM's.

Share this post


Link to post
Share on other sites
Quote:
Original post by AndyTX
Odd that it doesn't run on a 7900... I wonder what feature(s) of the 8 series it uses.

It uses floating point depth buffers (GL_NV_depth_buffer_float). It didn't run for me either (7950 GT), so after some GLIntercept-ing i found that it uses a depth texture with internal format GL_DEPTH_COMPONENT32F_NV.

HellRaiZer

Share this post


Link to post
Share on other sites
Quote:
Original post by HellRaiZer
It uses floating point depth buffers (GL_NV_depth_buffer_float). It didn't run for me either (7950 GT), so after some GLIntercept-ing i found that it uses a depth texture with internal format GL_DEPTH_COMPONENT32F_NV.

Ah, good find. Probably overkill for shadows (or alternatively, just render to a colour 1f and sacrifice the potential double-speed-z... no biggie), but whatever. My guess would be that it's faster to simply render out all of the Fourier terms in the shadow rendering pass rather than rendering a shadow map and transforming it to the Fourier representation, as using a high-precision depth buffer seems to imply (did you notice whether that's true with your GLintercepting?).

Share this post


Link to post
Share on other sites
I tried this out on my 8800, and it ran OK, but fairly slow using 512*512 shadow maps.

As for the method, I've seen better. The fact that light bleeding is avoided is offset by other problems, and the slow speed. Also, since no true penumbra is created, i dont really see why you'd use this much power to blur the shadow like this. Just use a good PCF filter..with textured surfaces the shadow edge pixels are barely detectable. Frankly I'm not a big fan of VSM either for similar reasons, but its better than this in my opinion.

Share this post


Link to post
Share on other sites
Quote:
Original post by AndyTX
My guess would be that it's faster to simply render out all of the Fourier terms in the shadow rendering pass rather than rendering a shadow map and transforming it to the Fourier representation, as using a high-precision depth buffer seems to imply (did you notice whether that's true with your GLintercepting?).

It doesn't look like they render the Fourier terms directly. All the rendering commands for the incomplete FBO (the one with the D32F depth map) use the following shader, which as far as i can tell outputs fragment depth as color.


//== PROGRAM LINK STATUS = TRUE
//== PROGRAM VALIDATE STATUS = TRUE

//======================================================
// Vertex Shader 22
//======================================================

//== SHADER COMPILE STATUS = TRUE
//=====================================================================
// This shader maps z-values in a regular hyperbolic range!
//=====================================================================
void main()
{
gl_Position = ftransform();
gl_TexCoord[5] = gl_MultiTexCoord5;
}

//======================================================
// Fragment Shader 23
//======================================================

//== SHADER COMPILE STATUS = TRUE
//=====================================================================
// Pixel shader to pass z-values.
//=====================================================================
void main()
{
gl_FragColor.x = gl_FragCoord.z;
}



When the D32F shadowmap is used the shader seems to use it as a regular shadow map (a simple shadow2DProj). I don't know if this is the correct way (i haven't read the paper to be honest). The only shader which uses the shadow map is this :


//== PROGRAM LINK STATUS = TRUE
//== PROGRAM VALIDATE STATUS = TRUE

//======================================================
// Vertex Shader 10
//======================================================

//== SHADER COMPILE STATUS = TRUE
//=========================================================================
//
// lighting_lin.vtx
//
// OpenGL per vertex lighting with linear depth values in texture
// coordinate z.
//
//=========================================================================
varying vec4 diffuse, ambient;
varying vec3 normal, light_dir, half_vector;
varying float dist;

void main()
{
vec4 ec_pos;
vec3 aux;

normal = normalize(gl_NormalMatrix * gl_Normal);

// These are the new lines of code to compute the light's direction.
ec_pos = gl_ModelViewMatrix * gl_Vertex;
light_dir = (gl_LightSource[0].position - ec_pos).xyz;
dist = length(light_dir);
aux = normalize(light_dir);

// Do not use the light source state half-vector! It is wrong!
half_vector = -ec_pos.xyz + light_dir;

// Compute the diffuse, ambient and globalAmbient terms.
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;

// The ambient terms have been separated since one of them
// suffers attenuation.
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;

gl_Position = ftransform();

// Do a linear mapping by pre-multiplying perspective division.
/* OLD
vec4 eye_pos = gl_TextureMatrix[0] * gl_Vertex;
vec4 lin_pos = gl_TextureMatrix[2] * eye_pos;
lin_pos.z = (gl_TextureMatrix[1] * eye_pos).z;
lin_pos.z *= lin_pos.w;
*/

vec4 eye_pos = gl_TextureMatrix[0] * gl_Vertex;
vec4 lin_pos = gl_TextureMatrix[2] * eye_pos;
lin_pos.z = (gl_TextureMatrix[1] * eye_pos).z;

gl_TexCoord[0] = lin_pos;
gl_TexCoord[5] = gl_MultiTexCoord5;
}


//======================================================
// Fragment Shader 11
//======================================================

//== SHADER COMPILE STATUS = TRUE
//=========================================================================
//
// shadow_mapping_pcf.pxl
//
//=========================================================================
varying vec4 diffuse, ambient, light_dir, half_vector;
varying vec3 normal;
varying float dist;

uniform int use_diffuse;
uniform int use_opacity;
uniform sampler2D diffuse_tex;
uniform sampler2D opacity_tex;
uniform sampler2DShadow shadow_map;

vec4 GetDiffuseTex() { return texture2DProj(diffuse_tex, gl_TexCoord[5]); }
float GetOpacityTex() { return texture2DProj(opacity_tex, gl_TexCoord[5]).x; }

//=========================================================================
//
// Reconstruct shadow test signal. We only use linear depth.
//
//=========================================================================
vec4 ShadowTerm()
{
vec4 tc = gl_TexCoord[0];
tc.z *= gl_TexCoord[0].w; // Undo perspective division for z.
return vec4(shadow2DProj(shadow_map, tc).x);
}


vec4 DiffusePart(const vec3 p_n, const vec3 p_ldir)
{
// Compute the dot product between normal and normalized lightdir.
// Support two-sided lighting for dragon wings.
float n_dot_l = dot(p_n, normalize(p_ldir));
vec4 diff_tex = (1 == use_diffuse)? GetDiffuseTex() : vec4(1.0);
return diffuse * diff_tex * n_dot_l;
}


vec4 SpecularPart(const vec3 p_n, const vec3 p_hv)
{
float n_dot_hv = dot(p_n, normalize(p_hv));
return gl_FrontMaterial.specular * gl_LightSource[0].specular *
pow(n_dot_hv,gl_FrontMaterial.shininess);
}


//=========================================================================
//
// Main function to compute per pixel lighting and modulate the finale
// pixel color by the shadow term.
//
//=========================================================================
void main()
{
vec4 color = gl_LightModel.ambient * gl_FrontMaterial.ambient;
float att;

// A fragment shader can't write a varying variable, hence we need
// a new variable to store the normalized interpolated normal.
vec3 n = normalize(normal);

float spot_effect = dot(normalize(gl_LightSource[0].spotDirection),
normalize(-light_dir.xyz));

if(spot_effect > gl_LightSource[0].spotCosCutoff)
{
att = 1.0 / (gl_LightSource[0].constantAttenuation +
gl_LightSource[0].linearAttenuation * dist +
gl_LightSource[0].quadraticAttenuation * dist * dist);

color += vec4(att) * DiffusePart(n, normalize(light_dir.xyz));
color += vec4(att) * SpecularPart(n, normalize(half_vector.xyz));

// Finally, apply the spot effect.
color *= pow(spot_effect, gl_LightSource[0].spotExponent);
color *= ShadowTerm();
}

// Compute shadow term and multiply current color with it.
gl_FragColor = color;

if(1 == use_opacity)
gl_FragColor.w = GetOpacityTex();
}



HellRaiZer

Share this post


Link to post
Share on other sites
Quote:
Original post by Matt Aufderheide
As for the method, I've seen better. The fact that light bleeding is avoided is offset by other problems, and the slow speed.

I do agree with you here, although perhaps the technique can be developed further.

Quote:
Original post by Matt Aufderheide
Also, since no true penumbra is created, i dont really see why you'd use this much power to blur the shadow like this. Just use a good PCF filter..with textured surfaces the shadow edge pixels are barely detectable. Frankly I'm not a big fan of VSM either for similar reasons, but its better than this in my opinion.

The big problem with PCF is once you start using it "properly" (i.e. filtering over the actual sample extents in texture space using ddx/ddy of the texture coordinates), it requires dynamic branching and gets very slow since these filter regions can get rather big. The more common "neighborhood sampled" PCF works in simple cases but really looks quite terrible in some pretty common cases that require anisotropic filtering, such as a first-person camera viewing shadows projected onto the ground. When compared to VSM/CSM with mipmapping PCF starts to look even worse (the video on that site demonstrates nicely what happens, even with PCF)... These poorly-filtered shadows were acceptable when we couldn't do any better, but those times have passed IMHO. The other big problem with PCF is biasing which gets unmanageable with large, dynamic filter sizes.

Anyways I'm happy that people are still researching on this front as "stock PCF" is pretty unacceptable moving forward IMHO - it just scales terribly and if you avoid the poor scaling you must make a significant sacrifice to image quality. I don't think CSM (or even VSM) is the "final answer", but I would not be surprised if a killer robust, probably-adaptive algorithm emerges based on the same ideas.

Share this post


Link to post
Share on other sites
Quote:
Original post by HellRaiZer
When the D32F shadowmap is used the shader seems to use it as a regular shadow map (a simple shadow2DProj). I don't know if this is the correct way (i haven't read the paper to be honest). The only shader which uses the shadow map is this:

From a glance, that looks almost like the standard PCF filter - did you set it to "CSM" before intercepting this stuff (for some reason the default mode is PCF)?

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!