Stable Cascaded Shadow Maps

Started by
14 comments, last by Fulg 14 years, 3 months ago
I'm trying to improve my ShaderX5 cascaded shadows by following the ShaderX6 article 'Stable Rendering of Cascacded Shadow Maps' - where the shadow map for each cascade doesn't rotate with the camera (I was doing this anyway) and where the cascade translates/pans with camera rotation and translation, with sub-texel movement cancelled out. It's this bit I just can't grasp... even though the way the article is written suggests this is the easy bit (!). A summary of the ShaderX6 approach is: (1). Compute the projection of the world space origin using the shadow map matrix (origin_shadow) (2). Round out the projected X and Y coords: origin_rounded = round(origin_shadow * shadowmapsize/2) (3). use (origin_rounded - origin_shadow) to create a 2D transformation matrix (by which the cascade shadow map is multiplied). It's (2) i can't grasp. Why multiply by half the shadow map size? Elsewhere I found someone saying "fmod the sphere center by the size of a shadow-map pixel in world space" which seems to make sense - conceptually, at least. So maybe all I need to know is how to calculate the size of a shadow-map pixel in world space... but how do I do that?
Advertisement
Managed to figure this out myself.

The multiply-by-half-the-shadow-map-size at step (2) is to go from projected homogenous light space coords to shadow texture space.

What the ShaderX6 article doesn't make explicit is that at step (3), the origin_rounded - origin_shadow offset must be expressed in homogenous light space.

Just on the off-chance I'm not just talking to myself, here's how I calculate the 'rounding' matrix (DirextX):

// xShadowMatrix is the light view projection matrix
D3DXVECTOR3 ptOriginShadow(0,0,0);
D3DXVec3TransformCoord(&ptOriginShadow, &ptOriginShadow, &xShadowMatrix);

// Find nearest shadow map texel. The 0.5f is because x,y are in the
// range -1 .. 1 and we need them in the range 0 .. 1
float texCoordX = ptOriginShadow.x * SHADOW_MAP_SIZE * 0.5f;
float texCoordY = ptOriginShadow.y * SHADOW_MAP_SIZE * 0.5f;

// Round to the nearest 'whole' texel
float texCoordRoundedX = round(texCoordX);
float texCoordRoundedY = round(texCoordY);

// The difference between the rounded and actual tex coordinate is the
// amount by which we need to translate the shadow matrix in order to
// cancel sub-texel movement
float dx = texCoordRoundedX - texCoordX;
float dy = texCoordRoundedY - texCoordY;

// Transform dx, dy back to homogenous light space
dx /= SHADOW_MAP_SIZE * 0.5f;
dy /= SHADOW_MAP_SIZE * 0.5f;

D3DXMATRIX xRounding;
D3DXMatrixTranslation(&xRounding, dx, dy, 0);
Thank you for posting your solution...I was planning on tackling this particular problem at some point in the near future and it's nice to have a reference. [smile]
Thanks for sharing this.

There will be a ShaderX7 article in which I will describe a slight improvement to Michal's approach. Michal picks the right shadow map with a rather cool trick. Mine is a bit different but it might be more efficient. So what I do to pick the right map is send down the sphere that is constructed for the light view frustum. I then check if the pixel is in the sphere. If it is I pick that shadow map, if it isn't I go to the next sphere. I also early out if it is not in a sphere by returning white.
At first sight it does not look like a trick but if you think about the spheres lined up along the view frustum and the way they intersect, it is actually pretty efficient and fast.
On my target platforms, especially on the one that Michal likes a lot, this makes a difference.
Hi guys,

Thanks for posting this - I guess this proves I was not clear enough in the article.

cheers,
Michal
Thanks for your replies.

The ShaderX6 article is probably fine - I wouldn't read too much into my amateurish fumblings (Michal).

I look forward to your ShaderX7 article (Wolf). Being new to 3D graphics (though an experienced programmer) I've been less interested in rendering efficiency so far and more interested in achieving a decent looking result. I figure by the time I actually have something 'finished' the hardware will make any optimisations I attempt today redundant anyway.

Well... that's how I console myself when I fail to understand the ShaderX articles anyway ;-)






Hi,

I have a few questions about the stable cascaded shadow maps.

Maybe someone could explain how in the Shaderx6 article right shadow map is picked? As I don't have this book.

How does the light position and direction quantization work with this method? Does it produce nice results?

And the last one, maybe someone knows how to quantize the light direction?

I haven't implemented the ShaderX6 technique for picking the right shadow map (I've plumped for a straight-forward depth check), but conceptually it starts by noticing that the n'th shadow matrix is identical to the shadow matrix at the first cascade, only shifted and scaled. Then determine the xyz shift and scale required to compute the 2nd, 3rd and 4th cascades, and pass these to the shader. In the shader perform some cunning masking and component-wise multiplication of these values with the depths of the splits to select the correct shift and scale to apply for the current screen pixel. Can you tell I don't really understand it yet?

I don't follow what you mean about quantizing the light direction... I'm using a directional light source (orthographic projection) so there's no quantization to worry about. All I do is render 4 separate shadow maps (one for each cascade), combine them into a single screen-space shadow coefficient map, then pass this map to the lighting stage. All very simple, robust and hideously inefficient.

The quality of the result mostly boils down to the quality at each cascade (and you can plug-in just about any shadow mapping algorithm you want). And after all... don't many leading current games use the technique?
Revived by request.
SlimDX | Ventspace Blog | Twitter | Diverse teams make better games. I am currently hiring capable C++ engine developers in Baltimore, MD.
I have two questions regared to topic:

first:
Quote:
// Find nearest shadow map texel. The 0.5f is because x,y are in the
// range -1 .. 1 and we need them in the range 0 .. 1
float texCoordX = ptOriginShadow.x * SHADOW_MAP_SIZE * 0.5f;
float texCoordY = ptOriginShadow.y * SHADOW_MAP_SIZE * 0.5f;

After mul by 0.5, the coords are in range -0.5 and 0.5, not 0 and 1. Yet it works. So I suppose that we actually don't need the have values in range 0..1 but it is about having range *spanned* across the "length" of 1.0, because shadow map tex coords are spanned across such a "length". Correct me if I'm wrong.

second:
I also have some really small problem with stability. Indvidual texels of my shadow map just flicker. It's really few of them (literally - "individual") but I'm a perfectionist and it annoys me :). I suppose it's due to floats' stability, isn't it?

I've just noticed another problem. Points that are farther from the origin, with greater shadow map size, flicker much more. With shadow map's size 4096 and at some small distance from the origin, almost all of the texels flicker. If I change the "input point" and choose not the origin but some point near the camera it's all fine. So I have to update this starting point from time to time and it causes some visible translations of shadow map texels.
How to get around that problem? Use doubles?

[Edited by - Maxest on May 24, 2009 7:02:15 AM]

This topic is closed to new replies.

Advertisement