Add smooth shadows blending between directional light cascades

It was deprecated in 1.9 in favor for dithering between cascades. Bing back that option for games that don't use TAA.
This commit is contained in:
Wojtek Figat
2025-03-27 10:48:35 +01:00
parent fa1469514b
commit 98834131f1
8 changed files with 105 additions and 52 deletions

BIN
Content/Shaders/Shadows.flax (Stored with Git LFS)

Binary file not shown.

View File

@@ -61,10 +61,9 @@ public:
/// <summary>
/// Enables cascades splits blending for directional light shadows.
/// [Deprecated in v1.9]
/// </summary>
API_FIELD(Attributes="EditorOrder(1320), DefaultValue(false), EditorDisplay(\"Quality\", \"Allow CSM Blending\")")
DEPRECATED() bool AllowCSMBlending = false;
bool AllowCSMBlending = false;
/// <summary>
/// Default probes cubemap resolution (use for Environment Probes, can be overriden per-actor).

View File

@@ -69,6 +69,7 @@ void GraphicsSettings::Apply()
Graphics::VolumetricFogQuality = VolumetricFogQuality;
Graphics::ShadowsQuality = ShadowsQuality;
Graphics::ShadowMapsQuality = ShadowMapsQuality;
Graphics::AllowCSMBlending = AllowCSMBlending;
Graphics::GlobalSDFQuality = GlobalSDFQuality;
Graphics::GIQuality = GIQuality;
Graphics::GICascadesBlending = GICascadesBlending;

View File

@@ -55,9 +55,8 @@ public:
/// <summary>
/// Enables cascades splits blending for directional light shadows.
/// [Deprecated in v1.9]
/// </summary>
API_FIELD() DEPRECATED() static bool AllowCSMBlending;
API_FIELD() static bool AllowCSMBlending;
/// <summary>
/// The Global SDF quality. Controls the volume texture resolution and amount of cascades to use.

View File

@@ -24,7 +24,7 @@
#define SHADOWS_MAX_TILES 6
#define SHADOWS_MIN_RESOLUTION 32
#define SHADOWS_MAX_STATIC_ATLAS_CAPACITY_TO_DEFRAG 0.7f
#define SHADOWS_BASE_LIGHT_RESOLUTION(atlasResolution) atlasResolution / MAX_CSM_CASCADES // Allow to store 4 CSM cascades in a single row in all cases
#define SHADOWS_BASE_LIGHT_RESOLUTION(atlasResolution) (atlasResolution / MAX_CSM_CASCADES) // Allow to store 4 CSM cascades in a single row in all cases
#define NormalOffsetScaleTweak METERS_TO_UNITS(1)
#define LocalLightNearPlane METERS_TO_UNITS(0.1f)
@@ -190,6 +190,7 @@ struct ShadowAtlasLight
uint8 TilesNeeded;
uint8 TilesCount;
bool HasStaticShadowContext;
bool BlendCSM;
mutable StaticStates StaticState;
BoundingSphere Bounds;
float Sharpness, Fade, NormalOffsetScale, Bias, FadeDistance, Distance, TileBorder;
@@ -769,6 +770,15 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
const RenderView& view = renderContext.View;
const int32 csmCount = atlasLight.TilesCount;
const auto shadowMapsSize = (float)atlasLight.Resolution;
atlasLight.BlendCSM = Graphics::AllowCSMBlending;
#if USE_EDITOR
// Disable cascades blending when baking lightmaps
if (IsRunningRadiancePass)
atlasLight.BlendCSM = false;
#elif PLATFORM_SWITCH || PLATFORM_IOS || PLATFORM_ANDROID
// Disable cascades blending on low-end platforms
atlasLight.BlendCSM = false;
#endif
// Calculate cascade splits
const float minDistance = view.Near;
@@ -895,7 +905,8 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
Float3 frustumCornersVs[8];
for (int32 j = 0; j < 4; j++)
{
float overlapWithPrevSplit = 0.1f * (splitMinRatio - oldSplitMinRatio); // CSM blending overlap
float csmOverlap = atlasLight.BlendCSM ? 0.2f : 0.1f;
float overlapWithPrevSplit = csmOverlap * (splitMinRatio - oldSplitMinRatio);
const auto frustumRangeVS = frustumCorners[j + 4] - frustumCorners[j];
frustumCornersVs[j] = frustumCorners[j] + frustumRangeVS * (splitMinRatio - overlapWithPrevSplit);
frustumCornersVs[j + 4] = frustumCorners[j] + frustumRangeVS * splitMaxRatio;
@@ -1602,7 +1613,9 @@ void ShadowsPass::RenderShadowMask(RenderContextBatch& renderContextBatch, Rende
}
else //if (light.IsDirectionalLight)
{
context->SetState(_psShadowDir.Get(permutationIndex));
auto* atlasLight = shadows.Lights.TryGet(light.ID);
ASSERT_LOW_LAYER(atlasLight);
context->SetState(_psShadowDir.Get(permutationIndex + (atlasLight->BlendCSM ? 8 : 0)));
context->DrawFullscreenTriangle();
}

View File

@@ -19,11 +19,11 @@ private:
AssetReference<Model> _sphereModel;
GPUPipelineState* _psDepthClear = nullptr;
GPUPipelineState* _psDepthCopy = nullptr;
GPUPipelineStatePermutationsPs<static_cast<int32>(Quality::MAX) * 2> _psShadowDir;
GPUPipelineStatePermutationsPs<static_cast<int32>(Quality::MAX) * 2> _psShadowPoint;
GPUPipelineStatePermutationsPs<static_cast<int32>(Quality::MAX) * 2> _psShadowPointInside;
GPUPipelineStatePermutationsPs<static_cast<int32>(Quality::MAX) * 2> _psShadowSpot;
GPUPipelineStatePermutationsPs<static_cast<int32>(Quality::MAX) * 2> _psShadowSpotInside;
GPUPipelineStatePermutationsPs<int32(Quality::MAX) * 2 * 2> _psShadowDir;
GPUPipelineStatePermutationsPs<int32(Quality::MAX) * 2> _psShadowPoint;
GPUPipelineStatePermutationsPs<int32(Quality::MAX) * 2> _psShadowPointInside;
GPUPipelineStatePermutationsPs<int32(Quality::MAX) * 2> _psShadowSpot;
GPUPipelineStatePermutationsPs<int32(Quality::MAX) * 2> _psShadowSpotInside;
PixelFormat _shadowMapFormat; // Cached on initialization
public:

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#define USE_GBUFFER_CUSTOM_DATA
#define SHADOWS_CSM_BLENDING 1
#define SHADOWS_CSM_DITHERING 1
#include "./Flax/Common.hlsl"
#include "./Flax/GBuffer.hlsl"
@@ -102,14 +102,22 @@ float4 PS_PointLight(Model_VS2PS input) : SV_Target0
// Pixel shader for directional light shadow rendering
META_PS(true, FEATURE_LEVEL_ES2)
META_PERMUTATION_2(SHADOWS_QUALITY=0,CONTACT_SHADOWS=0)
META_PERMUTATION_2(SHADOWS_QUALITY=1,CONTACT_SHADOWS=0)
META_PERMUTATION_2(SHADOWS_QUALITY=2,CONTACT_SHADOWS=0)
META_PERMUTATION_2(SHADOWS_QUALITY=3,CONTACT_SHADOWS=0)
META_PERMUTATION_2(SHADOWS_QUALITY=0,CONTACT_SHADOWS=1)
META_PERMUTATION_2(SHADOWS_QUALITY=1,CONTACT_SHADOWS=1)
META_PERMUTATION_2(SHADOWS_QUALITY=2,CONTACT_SHADOWS=1)
META_PERMUTATION_2(SHADOWS_QUALITY=3,CONTACT_SHADOWS=1)
META_PERMUTATION_3(SHADOWS_QUALITY=0,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=1,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=2,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=3,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=0,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=1,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=2,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=3,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=0)
META_PERMUTATION_3(SHADOWS_QUALITY=0,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=1,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=2,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=3,CONTACT_SHADOWS=0,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=0,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=1,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=2,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=1)
META_PERMUTATION_3(SHADOWS_QUALITY=3,CONTACT_SHADOWS=1,SHADOWS_CSM_BLENDING=1)
float4 PS_DirLight(Quad_VS2PS input) : SV_Target0
{
// Sample GBuffer

View File

@@ -3,10 +3,17 @@
#ifndef __SHADOWS_SAMPLING__
#define __SHADOWS_SAMPLING__
#ifndef SHADOWS_CSM_BLENDING
#define SHADOWS_CSM_BLENDING 0
#endif
#ifndef SHADOWS_CSM_DITHERING
#define SHADOWS_CSM_DITHERING 0
#endif
#include "./Flax/ShadowsCommon.hlsl"
#include "./Flax/GBufferCommon.hlsl"
#include "./Flax/LightingCommon.hlsl"
#ifdef SHADOWS_CSM_BLENDING
#if SHADOWS_CSM_DITHERING
#include "./Flax/Random.hlsl"
#endif
@@ -204,6 +211,43 @@ float SampleShadowMapOptimizedPCF(Texture2D<float> shadowMap, float2 shadowMapUV
#endif
}
// Samples the shadow cascade for the given directional light on the material surface (supports subsurface shadowing)
ShadowSample SampleDirectionalLightShadowCascade(LightData light, Buffer<float4> shadowsBuffer, Texture2D<float> shadowMap, GBufferSample gBuffer, ShadowData shadow, float3 samplePosition, uint cascadeIndex)
{
ShadowSample result;
ShadowTileData shadowTile = LoadShadowsBufferTile(shadowsBuffer, light.ShadowsBufferAddress, cascadeIndex);
// Project position into shadow atlas UV
float4 shadowPosition;
float2 shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, samplePosition, shadowPosition);
// Sample shadow map
result.SurfaceShadow = SampleShadowMapOptimizedPCF(shadowMap, shadowMapUV, shadowPosition.z);
// Increase the sharpness for higher cascades to match the filter radius
const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f };
shadow.Sharpness *= SharpnessScale[cascadeIndex];
#if defined(USE_GBUFFER_CUSTOM_DATA)
// Subsurface shadowing
BRANCH
if (IsSubsurfaceMode(gBuffer.ShadingModel))
{
float opacity = gBuffer.CustomData.a;
shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, gBuffer.WorldPos, shadowPosition);
float shadowMapDepth = shadowMap.SampleLevel(SAMPLE_SHADOW_MAP_SAMPLER, shadowMapUV, 0).r;
result.TransmissionShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth);
result.TransmissionShadow = PostProcessShadow(shadow, result.TransmissionShadow);
}
#else
result.TransmissionShadow = 1;
#endif
result.SurfaceShadow = PostProcessShadow(shadow, result.SurfaceShadow);
return result;
}
// Samples the shadow for the given directional light on the material surface (supports subsurface shadowing)
ShadowSample SampleDirectionalLightShadow(LightData light, Buffer<float4> shadowsBuffer, Texture2D<float> shadowMap, GBufferSample gBuffer, float dither = 0.0f)
{
@@ -242,53 +286,42 @@ ShadowSample SampleDirectionalLightShadow(LightData light, Buffer<float4> shadow
if (viewDepth > shadow.CascadeSplits[i])
cascadeIndex = i + 1;
}
#ifdef SHADOWS_CSM_BLENDING
const float BlendThreshold = 0.05f;
#if SHADOWS_CSM_DITHERING || SHADOWS_CSM_BLENDING
float nextSplit = shadow.CascadeSplits[cascadeIndex];
float splitSize = cascadeIndex == 0 ? nextSplit : nextSplit - shadow.CascadeSplits[cascadeIndex - 1];
float splitDist = (nextSplit - viewDepth) / splitSize;
#endif
#if SHADOWS_CSM_DITHERING && !SHADOWS_CSM_BLENDING
const float BlendThreshold = 0.05f;
if (splitDist <= BlendThreshold && cascadeIndex != shadow.TilesCount - 1)
{
// Blend with the next cascade but with screen-space dithering (gets cleaned out by TAA)
// Dither with the next cascade but with screen-space dithering (gets cleaned out by TAA)
float lerpAmount = 1 - splitDist / BlendThreshold;
if (step(RandN2(gBuffer.ViewPos.xy + dither).x, lerpAmount))
cascadeIndex++;
}
#endif
ShadowTileData shadowTile = LoadShadowsBufferTile(shadowsBuffer, light.ShadowsBufferAddress, cascadeIndex);
// Sample cascade
float3 samplePosition = gBuffer.WorldPos;
#if !LIGHTING_NO_DIRECTIONAL
// Apply normal offset bias
samplePosition += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal);
#endif
result = SampleDirectionalLightShadowCascade(light, shadowsBuffer, shadowMap, gBuffer, shadow, samplePosition, cascadeIndex);
// Project position into shadow atlas UV
float4 shadowPosition;
float2 shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, samplePosition, shadowPosition);
// Sample shadow map
result.SurfaceShadow = SampleShadowMapOptimizedPCF(shadowMap, shadowMapUV, shadowPosition.z);
// Increase the sharpness for higher cascades to match the filter radius
const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f };
shadow.Sharpness *= SharpnessScale[cascadeIndex];
#if defined(USE_GBUFFER_CUSTOM_DATA)
// Subsurface shadowing
BRANCH
if (IsSubsurfaceMode(gBuffer.ShadingModel))
{
float opacity = gBuffer.CustomData.a;
shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, gBuffer.WorldPos, shadowPosition);
float shadowMapDepth = shadowMap.SampleLevel(SAMPLE_SHADOW_MAP_SAMPLER, shadowMapUV, 0).r;
result.TransmissionShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth);
result.TransmissionShadow = PostProcessShadow(shadow, result.TransmissionShadow);
}
#if SHADOWS_CSM_BLENDING
const float BlendThreshold = 0.1f;
if (splitDist <= BlendThreshold && cascadeIndex != shadow.TilesCount - 1)
{
// Sample the next cascade, and blend between the two results to smooth the transition
ShadowSample nextResult = SampleDirectionalLightShadowCascade(light, shadowsBuffer, shadowMap, gBuffer, shadow, samplePosition, cascadeIndex + 1);
float blendAmount = splitDist / BlendThreshold;
result.SurfaceShadow = lerp(nextResult.SurfaceShadow, result.SurfaceShadow, blendAmount);
result.TransmissionShadow = lerp(nextResult.TransmissionShadow, result.TransmissionShadow, blendAmount);
}
#endif
result.SurfaceShadow = PostProcessShadow(shadow, result.SurfaceShadow);
return result;
}