T3D stable PSSM shadows
by Manoel Neto · 10/02/2011 (9:10 am) · 16 comments
T3D's real time sunlight shadows are nice, but the excessive shimmering (pixel-crawling) in the low resolution PSSM splits is a huge drawback. It makes any shadows cast by the last PSSM split more of a glitch than a feature and it's a pain to minimize it (reduce shadow distance, increase shadow resolution, etc). So many games have completely stable shadowmaps for static objects, why can't T3D?
Looking at the shadowmapping code, it turns out T3D already tries to prevent shimmering by "rounding off" the shadow projection matrix to make it "snap" to the shadowmap's texels. So something was still missing (or wrong). The ShadowViz debug window obviously shows what's up: the scene seems to zoom in and out from the sun's point of view when the camera moves. This is a big NO: for stable shadows, rotations to the frustum should not change the size of the rendered shadow area.
Thankfully, this only requires two small changes to fix! In source/lighting/shadowMap/pssmLightShadowMap.cpp, find this code:
And replace it by this:
Now we need to fix a bug when using 4 PSSM splits. In the same file, in the method PSSMLightShadowMap::_roundProjection() below, find this code:
And replace it by this:
This makes the shadows for static objects completely stable. If T3D filtered them using bilinear (and not the dithered pattern filter), they would look no different than baked shadows. With this it's now possible to increase shadow distance or even reduce shadow map resolution without major visual drawbacks.
Here's a video of it in action (shadow filtering is off to make it more obvious):
And here's a video of the same scene in stock T3D:
Looking at the shadowmapping code, it turns out T3D already tries to prevent shimmering by "rounding off" the shadow projection matrix to make it "snap" to the shadowmap's texels. So something was still missing (or wrong). The ShadowViz debug window obviously shows what's up: the scene seems to zoom in and out from the sun's point of view when the camera moves. This is a big NO: for stable shadows, rotations to the frustum should not change the size of the rendered shadow area.
Thankfully, this only requires two small changes to fix! In source/lighting/shadowMap/pssmLightShadowMap.cpp, find this code:
Box3F PSSMLightShadowMap::_calcClipSpaceAABB(const Frustum& f, const MatrixF& transform, F32 farDist)
{
Box3F result;
for (U32 i = 0; i < 8; i++)
{
const Point3F& pt = f.getPoints()[i];
// Lets just translate to lightspace
// We need the Point4F so that we can project the w
Point4F xformed(pt.x, pt.y, pt.z, 1.0f);
transform.mul(xformed);
F32 absW = mFabs(xformed.w);
xformed.x /= absW;
xformed.y /= absW;
xformed.z /= absW;
Point3F xformed3(xformed.x, xformed.y, xformed.z);
if (i != 0)
{
result.minExtents.setMin(xformed3);
result.maxExtents.setMax(xformed3);
} else {
result.minExtents = xformed3;
result.maxExtents = xformed3;
}
}
result.minExtents.x = mClampF(result.minExtents.x, -1.0f, 1.0f);
result.minExtents.y = mClampF(result.minExtents.y, -1.0f, 1.0f);
result.maxExtents.x = mClampF(result.maxExtents.x, -1.0f, 1.0f);
result.maxExtents.y = mClampF(result.maxExtents.y, -1.0f, 1.0f);
return result;
}And replace it by this:
Box3F PSSMLightShadowMap::_calcClipSpaceAABB(const Frustum& f, const MatrixF& transform, F32 farDist)
{
// Calculate frustum center
Point3F center(0,0,0);
for (U32 i = 0; i < 8; i++)
{
const Point3F& pt = f.getPoints()[i];
center += pt;
}
center /= 8;
// Calculate frustum bounding sphere radius
F32 radius = 0.0f;
for (U32 i = 0; i < 8; i++)
radius = getMax(radius, (f.getPoints()[i] - center).lenSquared());
radius = mFloor( mSqrt(radius) );
// Now build box for sphere
Box3F result;
Point3F radiusBox(radius, radius, radius);
result.minExtents = center - radiusBox;
result.maxExtents = center + radiusBox;
// Transform to light projection space
transform.mul(result);
return result;
}Now we need to fix a bug when using 4 PSSM splits. In the same file, in the method PSSMLightShadowMap::_roundProjection() below, find this code:
// Convert to texture space (0..shadowMapSize) F32 t = mShadowMapTex->getWidth() / mNumSplits; Point2F texelsToTexture(t / 2.0f, mShadowMapTex->getHeight() / 2.0f); originShadow.convolve(texelsToTexture);
And replace it by this:
// Convert to texture space (0..shadowMapSize)
F32 t = mNumSplits < 4 ? mShadowMapTex->getWidth() / mNumSplits : mShadowMapTex->getWidth() / 2;
Point2F texelsToTexture(t / 2.0f, mShadowMapTex->getHeight() / 2.0f);
if (mNumSplits >= 4)
texelsToTexture.y *= 0.5f;
originShadow.convolve(texelsToTexture);This makes the shadows for static objects completely stable. If T3D filtered them using bilinear (and not the dithered pattern filter), they would look no different than baked shadows. With this it's now possible to increase shadow distance or even reduce shadow map resolution without major visual drawbacks.
Here's a video of it in action (shadow filtering is off to make it more obvious):
And here's a video of the same scene in stock T3D:
About the author
#2
10/02/2011 (10:17 am)
Thanks, this is very useful.
#3
10/02/2011 (11:18 am)
Thanks for sharing this gem, Manoel!
#4
10/02/2011 (11:37 am)
very neat; much thanks for this.
#5
The code change cutoff for T3D 1.2 is today... i will try to get this fix in.
In trade here is a patch file for a bilinear filter which i was experimenting with earlier this year. Its hard coded to 9x9, but can be changed in the code to fewer samples... in many cases you get shadow acne from it which is why it never was integrated.
10/02/2011 (11:55 am)
Fantastic fix Manoel.The code change cutoff for T3D 1.2 is today... i will try to get this fix in.
In trade here is a patch file for a bilinear filter which i was experimenting with earlier this year. Its hard coded to 9x9, but can be changed in the code to fewer samples... in many cases you get shadow acne from it which is why it never was integrated.
#6
The problem is that there are currently two massive wastes of depth in the current PSSM implementation:
First, most of the depth range is spent in empty space outside the view frustum to account for "potential" shadows casters behind the camera. This is completely unnecessary: all I care about objects outside the frustum is whether they cast a shadow on the frustum or not. The self-shadowing of geometry outside the view frustum is irrelevant, since I'm not rendering them anyway.
The idea is to have the depth range as tight as the view frustum and clamp the post-projection Z coordinates of all vertices which are below the minimum depth range. This is called "pancaking": the geometry behind the near plane will be flattened on it and will still cast shadows on visible geometry (ideally it's good to not simply clamp, but to allocate a small part of the depth range for this so early z rejection isn't compromised).
Second problem: all PSSM splits share a single depth range. Each split should have it's own near plane. This would boost depth precision closer to the camera and prevent a lot of acne. It should also make it possible to use lower-precision formats for shadows.
Fixing these two problems is necessary for my ultimate plan: adding support for hardware PCF and fetch-4 shadows. As it is the PSSM takes lots of samples for the dithered filtering. It doesn't look too good and performance degrades quickly on lower-end GPUs.
Hardware PCF is very fast even on low end NVidia and AMD GPUs like the 9400M and the HD3200. It also works on *every* shader-capable NVidia card (and AMD cards starting from the HD2000 series). Fetch-4 would open support for older AMD cards.
My aim is to reduce the GPU requirements for advanced lighting. The deferred lighting itself is surprising usable even on low-end hardware (I get great framerates on a 8400M), but the shadows kill it.
10/02/2011 (3:22 pm)
@Tom: Thanks! I'm also working on a fix for the acne... but that'll take longer. The problem is that there are currently two massive wastes of depth in the current PSSM implementation:
First, most of the depth range is spent in empty space outside the view frustum to account for "potential" shadows casters behind the camera. This is completely unnecessary: all I care about objects outside the frustum is whether they cast a shadow on the frustum or not. The self-shadowing of geometry outside the view frustum is irrelevant, since I'm not rendering them anyway.
The idea is to have the depth range as tight as the view frustum and clamp the post-projection Z coordinates of all vertices which are below the minimum depth range. This is called "pancaking": the geometry behind the near plane will be flattened on it and will still cast shadows on visible geometry (ideally it's good to not simply clamp, but to allocate a small part of the depth range for this so early z rejection isn't compromised).
Second problem: all PSSM splits share a single depth range. Each split should have it's own near plane. This would boost depth precision closer to the camera and prevent a lot of acne. It should also make it possible to use lower-precision formats for shadows.
Fixing these two problems is necessary for my ultimate plan: adding support for hardware PCF and fetch-4 shadows. As it is the PSSM takes lots of samples for the dithered filtering. It doesn't look too good and performance degrades quickly on lower-end GPUs.
Hardware PCF is very fast even on low end NVidia and AMD GPUs like the 9400M and the HD3200. It also works on *every* shader-capable NVidia card (and AMD cards starting from the HD2000 series). Fetch-4 would open support for older AMD cards.
My aim is to reduce the GPU requirements for advanced lighting. The deferred lighting itself is surprising usable even on low-end hardware (I get great framerates on a 8400M), but the shadows kill it.
#7
Seems like your taking half the radius there. That can work out ok at certain fov and split log weights... it doesn't work out well at fov 55 and log 0.9. Even without that change you can get clipped shadows at various fov's and log weights.
10/02/2011 (3:56 pm)
Just i noticed this...// Calculate frustum bounding sphere radius
F32 radius = 0.0f;
for (U32 i = 0; i < 8; i++)
radius = getMax(radius, (f.getPoints()[i] - center).lenSquared());
radius = mFloor( mSqrt(radius) * 0.5f ); // half the radius?Seems like your taking half the radius there. That can work out ok at certain fov and split log weights... it doesn't work out well at fov 55 and log 0.9. Even without that change you can get clipped shadows at various fov's and log weights.
#8
10/02/2011 (3:56 pm)
Putting this in now... Thanks!
#9
I guess if there is one you'll run into it. :)
Doing a simple bilinear filter in the shader always seemed to take alot more samples and look worse than the current poisson disk implementation... even without the early out for non shadow edge samples.
10/02/2011 (4:12 pm)
Quote:"pancaking"I investigated this a year ago and found a downside to it... but i cannot remember the details or find my notes.
I guess if there is one you'll run into it. :)
Quote:hardware PCF and fetch-4 shadowsI'd be interested to see the results.
Doing a simple bilinear filter in the shader always seemed to take alot more samples and look worse than the current poisson disk implementation... even without the early out for non shadow edge samples.
#10
Wouldn't a simple bilinear need 4 samples? I remember implementing something like that many years ago.
Of course, it looks no different than having a non-antialised B&W texture projected over everything (which is the same visual result of single-sample hardware PCF and Fetch4). But I find that better than having no shadows at all. As a bonus, hw-PCF and Fetch4 could be used to make additional shadow filtering faster as well.
First I need to get more depth. Then it's a matter of modifying a lot of places to plug the PCF and Fetch4 D3D9 hacks in a not-so-horrible way.
I don't know what the Croteam guys do, but the Serious Sam HD games have absolutely fantastic shadowmapping and I can play with shadows on on my ION netbook. They do some strange filtering that makes shadows look as if they have a larger resolution than they really do, but I have no idea how it works (it kinda looks like Valve's distance field alpha-test technique).
10/02/2011 (4:47 pm)
You found my dirty hack... I should have tested that one more thoroughly. Ideally I should be drawing the frustum into the ShadowViz for better debugging, but I got lazy. =/Wouldn't a simple bilinear need 4 samples? I remember implementing something like that many years ago.
Of course, it looks no different than having a non-antialised B&W texture projected over everything (which is the same visual result of single-sample hardware PCF and Fetch4). But I find that better than having no shadows at all. As a bonus, hw-PCF and Fetch4 could be used to make additional shadow filtering faster as well.
First I need to get more depth. Then it's a matter of modifying a lot of places to plug the PCF and Fetch4 D3D9 hacks in a not-so-horrible way.
I don't know what the Croteam guys do, but the Serious Sam HD games have absolutely fantastic shadowmapping and I can play with shadows on on my ION netbook. They do some strange filtering that makes shadows look as if they have a larger resolution than they really do, but I have no idea how it works (it kinda looks like Valve's distance field alpha-test technique).
#11
Using the full radius should never clip the shadows, as it generates a bigger frustum bounding box than the original code and actually loses a bit of shadowmap resolution.
I also just tested with different log weights, shadow distance azimuths and elevations. No problems so far. Also, DeathBall looks amazing with low sun elevations now that the shadows are stable. Long shadows that stretch as far as the eye can see.
10/02/2011 (5:17 pm)
I took the hack out and I cannot find clipped shadows, at least not in Deathball. Do you have a .mis that displays this?Using the full radius should never clip the shadows, as it generates a bigger frustum bounding box than the original code and actually loses a bit of shadowmap resolution.
I also just tested with different log weights, shadow distance azimuths and elevations. No problems so far. Also, DeathBall looks amazing with low sun elevations now that the shadows are stable. Long shadows that stretch as far as the eye can see.
#12
10/02/2011 (7:31 pm)
They have a pretty tight log weight (0.91), a short shadow distance (50 meters), and an fov of 55. Could be some other code in my repo... in theory the full radius shouldn't do that. Or it could be a bad bounding box in the art. I need to investigate it further.
#13
10/03/2011 (2:52 pm)
Just checking, what is the shadow fade distance? I was testing and saw the shadows disappear and thought "finally found a bug!", only to realize I had the shadow fade distance greater than the shadow distance =/
#14
10/04/2011 (10:08 am)
Yea... the "shadow fade distance" is the start distance for shadows to begin to fade. We've had a long standing bug where if you have the start fade distance longer than the shadow distance nothing renders.
#15
10/05/2011 (3:02 pm)
nice resource, ty
#16
Thank you very much!
10/10/2011 (11:49 am)
Very cool! The shimmer on the edges of shadows has been majoring driving me up the wall! I've spent a lot of time in games like Crysis and Farcry 2, so I couldn't help but think "Man, those games don't have the shadow edge shimmer, WTF is wrong with T3D???"Thank you very much!

Torque Owner TheGasMan
G.A.S. [+others]