• Advertisement
Sign in to follow this  

Stable Cascaded Shadow Maps

This topic is 2931 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

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?

Share this post


Link to post
Share on other sites
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);

Share this post


Link to post
Share on other sites
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]

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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 ;-)






Share this post


Link to post
Share on other sites
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?

Share this post


Link to post
Share on other sites
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?

Share this post


Link to post
Share on other sites
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]

Share this post


Link to post
Share on other sites
first figure out where the problem is... float values inherently have issues when using large integer + small fraction... further look towards using more temporaries and ensuring the math you do maintains as much accuracy as possible.

double will likely help you in this area but it would be best to figure out the issue before going there.

Share this post


Link to post
Share on other sites
It must be some numerical problem. I've just rescaled my world down by 0.1 in each axis and flickering is much smaller farther from the camera. But now "farther" means not 300 units but 30 units from the origin. Of course I cannot continue to scale down cause small values causes other problems :)
So right now my code looks like this:

static CVector3 focusPoint = eye;
if (getDistanceBetweenPoints(focusPoint, eye) > 100.0f)
focusPoint = eye;
CVector4 anyPointOnShadowMap(focusPoint);
anyPointOnShadowMap *= sunLightViewProjTransform;

// find nearest shadow map texel. The 0.5 is because texCoords are spanned
// across a value of 2.0 (-1..1) and they need to be spanned across a value of 1.0
float texCoordX = shadowMapSize * 0.5f * anyPointOnShadowMap.x;
float texCoordY = shadowMapSize * 0.5f * anyPointOnShadowMap.y;

// round to the nearest "whole" texel
float roundedTexCoordX = round(texCoordX);
float roundedTexCoordY = round(texCoordY);

// the difference between the rounded and actual texCoord is the
// amount by which we need to translate the shadow matrix in order to
// cancel sub-texel movement
float dx = roundedTexCoordX - texCoordX;
float dy = roundedTexCoordY - texCoordY;

// transform dx, dy back to homogenous light space
dx /= shadowMapSize * 0.5f;
dy /= shadowMapSize * 0.5f;

CMatrix roundingTransform;
roundingTransform.loadTranslate(dx, dy, 0);

sunLightViewProjTransform *= roundingTransform;

Share this post


Link to post
Share on other sites
I've managed to solve my issue. The problem was.. few code lines up:

    sunLightProjTransform.loadOrthoRH(shadowMapFocusingSphereDiameterRescale*2.0f*sphereRadius, shadowMapFocusingSphereDiameterRescale*2.0f*sphereRadius, zNear, zFar); 


Every frame I was computing center and radius of view frustum's bounding sphere. And because of the numerical problems the radius was every frame slightly different. This was causing slightly different projection's plane width and height every frame, what further means that some texels were unable to do exact one-texel movement. After I have computed the radius once, everything works fine.

Share this post


Link to post
Share on other sites
Hey guys,

I've also hit the shadow stability problem with our pssm implementation. However in our case the pssm is combined with the post perspective transformation from the trapezoidal shadow maps paper for better texture coverage - as it turns out this invalidates the above fix for us (which works nicely with a simple bounding box based test version).
Unfortunately I don't have access to the ShaderX articles so I'm not sure if anything in them could point me to a possible solution. I'm hoping someone here has faced this problem before and can help :)

Thanx in advance,
BoyC

Share this post


Link to post
Share on other sites
Wolf, what happened to your ShaderX7 article? I have the book on my desk but there is nothing on the "bounding spheres" trick. I don't really like having conditionals in my shaders (on a certain console they cost a lot!) and was curious to see how you resolved this.

Am I going mad? :)

Share this post


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

  • Advertisement