Add better stability to Cascaded Shadow Maps projection
This commit is contained in:
@@ -534,21 +534,6 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
|
|||||||
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
|
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
|
||||||
atlasLight.Cache.Set(renderContext.View, light, atlasLight.CascadeSplits);
|
atlasLight.Cache.Set(renderContext.View, light, atlasLight.CascadeSplits);
|
||||||
|
|
||||||
// Select best Up vector
|
|
||||||
Float3 side = Float3::UnitX;
|
|
||||||
Float3 upDirection = Float3::UnitX;
|
|
||||||
Float3 vectorUps[] = { Float3::UnitY, Float3::UnitX, Float3::UnitZ };
|
|
||||||
for (int32 i = 0; i < ARRAY_COUNT(vectorUps); i++)
|
|
||||||
{
|
|
||||||
const Float3 vectorUp = vectorUps[i];
|
|
||||||
if (Math::Abs(Float3::Dot(light.Direction, vectorUp)) < (1.0f - 0.0001f))
|
|
||||||
{
|
|
||||||
side = Float3::Normalize(Float3::Cross(vectorUp, light.Direction));
|
|
||||||
upDirection = Float3::Normalize(Float3::Cross(light.Direction, side));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the different view and projection matrices for each split
|
// Create the different view and projection matrices for each split
|
||||||
float splitMinRatio = 0;
|
float splitMinRatio = 0;
|
||||||
float splitMaxRatio = (minDistance - view.Near) / viewRange;
|
float splitMaxRatio = (minDistance - view.Near) / viewRange;
|
||||||
@@ -564,81 +549,71 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Calculate cascade split frustum corners in view space
|
// Calculate cascade split frustum corners in view space
|
||||||
Float3 frustumCorners[8];
|
Float3 frustumCornersVs[8];
|
||||||
for (int32 j = 0; j < 4; j++)
|
for (int32 j = 0; j < 4; j++)
|
||||||
{
|
{
|
||||||
float overlap = 0.1f * (splitMinRatio - oldSplitMinRatio); // CSM blending overlap
|
float overlapWithPrevSplit = 0.1f * (splitMinRatio - oldSplitMinRatio); // CSM blending overlap
|
||||||
const RenderList* mainCache = renderContext.List;
|
const RenderList* mainCache = renderContext.List;
|
||||||
const auto frustumRangeVS = mainCache->FrustumCornersVs[j + 4] - mainCache->FrustumCornersVs[j];
|
const auto frustumRangeVS = mainCache->FrustumCornersVs[j + 4] - mainCache->FrustumCornersVs[j];
|
||||||
frustumCorners[j] = mainCache->FrustumCornersVs[j] + frustumRangeVS * (splitMinRatio - overlap);
|
frustumCornersVs[j] = mainCache->FrustumCornersVs[j] + frustumRangeVS * (splitMinRatio - overlapWithPrevSplit);
|
||||||
frustumCorners[j + 4] = mainCache->FrustumCornersVs[j] + frustumRangeVS * splitMaxRatio;
|
frustumCornersVs[j + 4] = mainCache->FrustumCornersVs[j] + frustumRangeVS * splitMaxRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform stabilization
|
// Transform the frustum from camera view space to world-space
|
||||||
enum StabilizationMode
|
Float3 frustumCornersWs[8];
|
||||||
{
|
for (int32 i = 0; i < 8; i++)
|
||||||
None,
|
Float3::Transform(frustumCornersVs[i], renderContext.View.IV, frustumCornersWs[i]);
|
||||||
ProjectionSnapping,
|
|
||||||
ViewSnapping,
|
|
||||||
};
|
|
||||||
const StabilizationMode stabilization = ViewSnapping; // TODO: expose to graphics settings maybe
|
|
||||||
Float3 cascadeMinBoundLS;
|
|
||||||
Float3 cascadeMaxBoundLS;
|
|
||||||
Float3 target;
|
|
||||||
{
|
|
||||||
// Make sure we are using the same direction when stabilizing
|
|
||||||
BoundingSphere boundingVS;
|
|
||||||
BoundingSphere::FromPoints(frustumCorners, ARRAY_COUNT(frustumCorners), boundingVS);
|
|
||||||
|
|
||||||
// Compute bounding box center
|
// Calculate the centroid of the view frustum slice
|
||||||
Float3::TransformCoordinate(boundingVS.Center, view.IV, target);
|
Float3 frustumCenter = Float3::Zero;
|
||||||
float boundingVSRadius = (float)boundingVS.Radius;
|
for (int32 i = 0; i < 8; i++)
|
||||||
cascadeMaxBoundLS = Float3(boundingVSRadius);
|
frustumCenter += frustumCornersWs[i];
|
||||||
cascadeMinBoundLS = -cascadeMaxBoundLS;
|
frustumCenter *= 1.0f / 8.0f;
|
||||||
|
|
||||||
if (stabilization == ViewSnapping)
|
// Calculate the radius of a bounding sphere surrounding the frustum corners
|
||||||
{
|
float frustumRadius = 0.0f;
|
||||||
// Snap the target to the texel units (reference: ShaderX7 - Practical Cascaded Shadows Maps)
|
for (int32 i = 0; i < 8; i++)
|
||||||
float shadowMapHalfSize = shadowMapsSize * 0.5f;
|
frustumRadius = Math::Max(frustumRadius, (frustumCornersWs[i] - frustumCenter).LengthSquared());
|
||||||
float x = Math::Ceil(Float3::Dot(target, upDirection) * shadowMapHalfSize / boundingVSRadius) * boundingVSRadius / shadowMapHalfSize;
|
frustumRadius = Math::Ceil(Math::Sqrt(frustumRadius) * 16.0f) / 16.0f;
|
||||||
float y = Math::Ceil(Float3::Dot(target, side) * shadowMapHalfSize / boundingVSRadius) * boundingVSRadius / shadowMapHalfSize;
|
|
||||||
float z = Float3::Dot(target, light.Direction);
|
|
||||||
target = upDirection * x + side * y + light.Direction * z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto nearClip = 0.0f;
|
// Snap cascade center to the texel size
|
||||||
const auto farClip = cascadeMaxBoundLS.Z - cascadeMinBoundLS.Z;
|
float texelsPerUnit = (float)atlasLight.Resolution / (frustumRadius * 2.0f);
|
||||||
|
frustumCenter *= texelsPerUnit;
|
||||||
|
frustumCenter = Float3::Floor(frustumCenter);
|
||||||
|
frustumCenter /= texelsPerUnit;
|
||||||
|
|
||||||
// Create shadow view matrix
|
// Cascade bounds are built around the sphere at the frustum center to reduce shadow shimmering
|
||||||
Matrix shadowView, shadowProjection, shadowVP;
|
Float3 maxExtents = Float3(frustumRadius);
|
||||||
Matrix::LookAt(target - light.Direction * cascadeMaxBoundLS.Z, target, upDirection, shadowView);
|
Float3 minExtents = -maxExtents;
|
||||||
|
Float3 cascadeExtents = maxExtents - minExtents;
|
||||||
|
|
||||||
// Create viewport for culling with extended near/far planes due to culling issues
|
Matrix shadowView, shadowProjection, shadowVP, cullingVP;
|
||||||
Matrix cullingVP;
|
|
||||||
{
|
|
||||||
const float cullRangeExtent = 100000.0f;
|
|
||||||
Matrix::OrthoOffCenter(cascadeMinBoundLS.X, cascadeMaxBoundLS.X, cascadeMinBoundLS.Y, cascadeMaxBoundLS.Y, -cullRangeExtent, farClip + cullRangeExtent, shadowProjection);
|
|
||||||
Matrix::Multiply(shadowView, shadowProjection, cullingVP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create shadow projection matrix
|
// Create view matrix
|
||||||
Matrix::OrthoOffCenter(cascadeMinBoundLS.X, cascadeMaxBoundLS.X, cascadeMinBoundLS.Y, cascadeMaxBoundLS.Y, nearClip, farClip, shadowProjection);
|
Matrix::LookAt(frustumCenter + light.Direction * minExtents.Z, frustumCenter, Float3::Up, shadowView);
|
||||||
|
|
||||||
// Construct shadow matrix (View * Projection)
|
// Create viewport for culling with extended near/far planes due to culling issues (aka pancaking)
|
||||||
|
const float cullRangeExtent = 100000.0f;
|
||||||
|
Matrix::OrthoOffCenter(minExtents.X, maxExtents.X, minExtents.Y, maxExtents.Y, -cullRangeExtent, cascadeExtents.Z + cullRangeExtent, shadowProjection);
|
||||||
|
Matrix::Multiply(shadowView, shadowProjection, cullingVP);
|
||||||
|
|
||||||
|
// Create projection matrix
|
||||||
|
Matrix::OrthoOffCenter(minExtents.X, maxExtents.X, minExtents.Y, maxExtents.Y, 0.0f, cascadeExtents.Z, shadowProjection);
|
||||||
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
|
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
|
||||||
|
|
||||||
// Stabilize the shadow matrix on the projection
|
// Round the projection matrix by projecting the world-space origin and calculating the fractional offset in texel space of the shadow map
|
||||||
if (stabilization == ProjectionSnapping)
|
Float4 shadowOrigin = Float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
{
|
shadowOrigin = Float4::Transform(shadowOrigin, shadowVP);
|
||||||
Float3 shadowPixelPosition = shadowVP.GetTranslation() * (shadowMapsSize * 0.5f);
|
shadowOrigin = shadowOrigin * (shadowMapsSize / 2.0f);
|
||||||
shadowPixelPosition.Z = 0;
|
Float4 roundedOrigin = Float4::Round(shadowOrigin);
|
||||||
const Float3 shadowPixelPositionRounded(Math::Round(shadowPixelPosition.X), Math::Round(shadowPixelPosition.Y), 0.0f);
|
Float4 roundOffset = roundedOrigin - shadowOrigin;
|
||||||
const Float4 shadowPixelOffset((shadowPixelPositionRounded - shadowPixelPosition) * (2.0f / shadowMapsSize), 0.0f);
|
roundOffset = roundOffset * (2.0f / shadowMapsSize);
|
||||||
shadowProjection.SetRow4(shadowProjection.GetRow4() + shadowPixelOffset);
|
roundOffset.Z = 0.0f;
|
||||||
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
|
roundOffset.W = 0.0f;
|
||||||
}
|
shadowProjection.SetRow4(shadowProjection.GetRow4() + roundOffset);
|
||||||
|
|
||||||
|
// Calculate view*projection matrix
|
||||||
|
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
|
||||||
tile.SetWorldToShadow(shadowVP);
|
tile.SetWorldToShadow(shadowVP);
|
||||||
|
|
||||||
// Setup context for cascade
|
// Setup context for cascade
|
||||||
@@ -958,7 +933,7 @@ RETRY_ATLAS_SETUP:
|
|||||||
const ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex];
|
const ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex];
|
||||||
ASSERT(tile.RectTile);
|
ASSERT(tile.RectTile);
|
||||||
auto* packed = shadows.ShadowsBuffer.WriteReserve<Float4>(5);
|
auto* packed = shadows.ShadowsBuffer.WriteReserve<Float4>(5);
|
||||||
packed[0] = Float4(tile.RectTile->Width, tile.RectTile->Height, tile.RectTile->X, tile.RectTile->Y) * atlasResolutionInv; // UV to AtlasUV via a single MAD instruction
|
packed[0] = Float4(tile.RectTile->Width - 1.0f, tile.RectTile->Height - 1.0f, tile.RectTile->X, tile.RectTile->Y) * atlasResolutionInv; // UV to AtlasUV via a single MAD instruction
|
||||||
packed[1] = tile.WorldToShadow.GetColumn1();
|
packed[1] = tile.WorldToShadow.GetColumn1();
|
||||||
packed[2] = tile.WorldToShadow.GetColumn2();
|
packed[2] = tile.WorldToShadow.GetColumn2();
|
||||||
packed[3] = tile.WorldToShadow.GetColumn3();
|
packed[3] = tile.WorldToShadow.GetColumn3();
|
||||||
|
|||||||
Reference in New Issue
Block a user