Cascaded shadow map bug between splits


I’m still new in the world of shadows and I implemented a cascaded shadow maps. It works fine in general, but I’m being puzzled with a stitched line that sometime happens between my splits.

You can see in this image the bug in between the 2 splits:

This isn’t the depth maps, this is the actual amount of shadows applied to the rendered scene.

This bug is seen in PCF, using shadow2DArray but it won’t happen if I do the depth comparison myself using texture2DArray, so I know my depth map is ok.

Here is the shader code I use:

float getShadowCoef(int nIndex, float fDist)
	vec4 shadow_coord = shadTextMat[nIndex]*eyepos;
	shadow_coord.w = shadow_coord.z;
	shadow_coord.z = float(nIndex);
	float ret = shadow2DArray(nTexUnit8, shadow_coord).x;
	return ret;

float shadowCoef()
	int index = 0;
	float fDist = length(eyepos);
	for (int ii=0; ii<nNumSplits; ii++)
		if(fDist < far_d[ii])
			index = ii;

	float coef = getShadowCoef(index, fDist);
	return coef;

I tried forcing shadow_coord.w and shadow_coord.z to constant values, the stitching still occur. Making more tests has proven me the stitch is shared between the 2 splits (forcing a split to given value reduces the stitch, but doesn’t completely eliminates it.

FYI, I’m using a geForce GTX 580 with an up to date driver.

Has anyone seen something like this?


The link doesn’t work for me.

A shot in the dark, but I have two ideas:

  1. It might be that you’re sampling slightly over the edge of the cascade’s shadow texture. Try setting GL_CLAMP_TO_EDGE as texture wrap mode, so you don’t end up sampling the opposite side of the texture as your texture coordinates go off the edge.

  2. If you’re using mipmapped shadowmaps, a jump in the texture coordinates occurs as you go from one cascade to another. This jump results in huge gradients for the texture coordinates which in turn makes the sample sample a far too small mipmap level. If this is the case, the artifacts should show up as small 2x2 blocks. If that is the case, either make your texture coordinate spaces for the cascades in a way that the gradients appear smooth
    (like cascade 1 goes from 0…1, second from 1…0 and third again from 0…1). GL_MIRRORED_REPEAT might help you here. Or you produce continuous texture coordinate (cascade #1 0.0…1.0, cascade#2 1.0…2.0 and cascade #3 2.0…3.0) and use GL_REPEAT (which might give you problems as in 1)

Hi Skynet,

My bad for the link, I forgot to make it public. Should work now!

Your ideas are good, but I already tried those before writing:

  1. The textures are already clamped_to_edge. I checked the texture coordinates, and they don’t go outside [0,1] anyway. I also tried forcing the use of wrong split. This gets rid of the stitches (my depth textures are actually a little larger then what is used).

  2. I’m not using mipmaps. But somehow, I feel this is not that far. The impression I have when I look at the image is the stitches is texture sampling happening at the wrong coordinates. But All the test I made shows me it’s not the case. I though of multisampling having a role to play, but yet, I don’t see how that would happen.


Have seen that too but haven’t traced through it yet so I only have a guess right now. More likely to occur with front-face casting (which unfortunately is often necessary to prevent light leaks), hyper-tight fitting of split maps, and using hardware PCF (which can’t be smart about sampling across split boundaries).

Envision sun at elevation 45 deg shining onto a flat surface (e.g. ground). And just for instance, eyepoint is looking toward the horizon with sun behind you. So your light frusta are inclined ~45 deg w.r.t. ground. Shadow map distances increase within each split as you get further from the sun at some rate (say, near depth = 500, far depth = 600 in split 1). What happens when you hit the far end of the split map? CLAMP_TO_EDGE creates a virtual wall at the constant depth value (e.g. 600) at that edge of the map, even though depth samples just outside of the map actually should have a slightly greater distance. So you inadvertently end up creating this ghost shadow-casting sliver at the edge of the map that only gets pulled in by PCF (assuming you’re chosing the split only based on the point sampled eye-space frag pos backprojected into light space with no padding).

I agree, it’s an annoying problem that needs fixed. You can hide this with some light-space bias, but of course that can lead to peter panning. Assuming my guess is correct… Not using hardware PCF and rolling your own filtering may be the ticket (where taps can span maps). Otherwise you apparently need to pad the map so PCF never samples outside of it. However, this is all shooting from the hip because I haven’t actually fixed it myself yet.

Reaching the borders of my depth map was the first thing I thought of. Unfortunately, it is not the problem. My depth map is larger then what is actually used. I made sure of it by adding an offset in the distance to select what split is used. The stitches always follow the splits separation, regardless of where I put it. I also made sure my texture coordinates aren’t close to 0 or 1, so it’s definitely not a clamping issue.

However, I ran more tests, and I noticed reducing anisotropic filtering reduces it. Setting it to 1 or 2 eliminates the problem (but cause other problems, so it’s not a solution). It appears like anisotropic filtering causes it to sample outside the current fragment/pixel. But I don’t have nVidia’s algorithm for anisotropic filtering, so that’s just an assumption. I could be completely wrong.

It appears like anisotropic filtering causes it to sample outside the current fragment/pixel.

Not necessarily! At least not alone. The footprint is heavily dependent on the gradients used to sample the texture. In principle what you’re seeing is the same problem as with mipmaps: jumping texture coordinates cause the footprint of the anisotropic sample to be larger than necessary, causing(?) the artifact.

Can you tell me more about that footprint? I discovered since I last wrote that using the GLSL function textureGrad() instead of texture(), with any gradient I tried, fixes the stitching.

I’ve been trying to find what that function does exactly, but the documentation is helpless. It only says it allows to manually set the gradients. But I found nowhere what the gradient is used for. It certainly has to do with multisampling/ anisotropic sampling, but I don’t understand how exactly.

I discovered since I last wrote that using the GLSL function textureGrad() instead of texture(), with any gradient I tried, fixes the stitching.

Choosing the same gradient for s and t across all polygons counteracts anisotropic filtering :slight_smile: You may as well disable it for the shadow map.

Is the only information available that explains how the footprints gets calulated. If you stare long enough at those equations, you’ll eventually find out that the footprint might stick up to 0.5*max_anisotropy texels out of the texture coordindate region you specified :slight_smile:
Be aware though, that this might be only vaguely related to whats actually implemented in hardware.

Ah! Good insight. Same here. Found the modeler’s had overridden aniso to be 12 on all textures, and this was being picked up by the shadow map textures as well.

I think that whenever the pixel projection onto the shadow texture causes the aniso integration to bump up against the edge of the map, the CLAMP_TO_EDGE “virtual sliver caster” thing kicks in.

(but cause other problems, so it’s not a solution)

Care to share?

Need to do more testing, but guessing the aniso multi-sample integration (presumed) of the shadow map probably helps reduce some edge-on shadow acne, even without MIPs. I guess I’m a little surprised that aniso does anything without MIPs. Though on second thought, while it can’t average large areas quickly (as with MIPs), I suppose it could still multi-sample the base map and take pot-shots at the integration.

Possibly more indication that custom filtering is the way to go (to keep the filter taps inside the splits, possibly mapping each to the appropriate splits).

It certainly has to do with multisampling/ anisotropic sampling, but I don’t understand how exactly.

Extreme example: Think about standing on flat textured ground looking off toward the horizon along the texture space S axis. While a given pixel’s width might span 1 texel in your shadow map along the texture space T axis (at some distance), the pixel’s “height” might span “512” texels along the texture space S axis. This is a case where aniso helps.

That’s the ground texture, so who cares right? Now envision the sun directly overhead, so the shadow map is parallel with the ground, and you’ve got a very similar problem. How to integrate a region of the map 512 texels tall (and varying widths based on distance from eyepoint for a perspective frustum).

So what are we feeding the PCF aniso shadow lookup in this case? Just the center point for the lookup (essentially 256 texels up from the bottom of the projected region on the texture, and 256 texels down from the top) plus gradients. With this, the GPU could easily sample outside the map with full-up aniso filtering. (Would be nice to know exactly what the card is doing though…)

You know, I just had another thought on this. And I vaguely remember reading about it. Think maybe ShaderX series. Possibly 7, and the article on VSMs.

Texture derivatives are computed on pixel quads, right? When different pixels in a quad map to different splits, what happens to the texcoord derivatives? They’re potentially nonsense. Could yield large gradients, which, with aniso, can cause it to multi-sample over a large range of the map (as it tries to pot-shot integrate a large area of the map), causing sampling off the map as discussed (pulling in ghost sliver shadow casters from the CLAMP_TO_EDGE). Disable aniso, and the gradients are less important without MIPs (if used at all).

(…which now that I re-read skynet’s post above, is prob exactly what he was thinking about, though he wasn’t sure how this was kicking in.)

I think on split boundaries we probably need to compute our own tex derivatives, especially if we want to use any aniso. IIRC, there’s a slick trick from Andrew Lauritzen in one of the ShaderX books (probably VSM chapter) covering this I think. Now to dig it out…

Just to wrap up this thread, that is in-fact the underlying problem (bogus texture gradients on split boundaries). Two solutions out there for this:

  1. Tweak the split indices in the shader so that pixels in the same GPU pixel quad always use the same split.
  2. Compute analytic texcoord gradients in the shader.

Both are described in:

  • Shader X7, Chapter 4.1 (Practical Cascaded Shadow Maps), “Filtering Across Splits” section, pp. 321-327
    …but the code they list for #1 (derived from a snippet by Andrew Lauritzen) is buggy.

You can find the correct, original snippet here:

  • Variance Shadow Maps Demo (D3D10) (Andrew Lauritzen, Beyond3D post, 4/25/07)
    Use log2 or his lookup table as desired. Also the dot trick for computing the split saves a surprisingly number of frag instructions.

And as we’ve both discovered, apparently texture gradients are important not only when you have MIPs (e.g. when using shadow map pre-filtering methods such as VSM), but also when you have aniso enabled on a standard, non-MIPed depth map.

Also, one final note on that. Using aniso on a standard shadow map doesn’t work very well at all. While it does soften shadow edges a bit, it essentially means integrating over a larger area of the map, which (if there’s a a depth gradient in the shadow map) causes depth texels further and further from the desired depth sample to potentially shadow it. This leads to having to apply more and more bias with increasing aniso to avoid self shadowing. And that isn’t good.