From 98834131f10be5aec36e2a952aa5d8e604332929 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 27 Mar 2025 10:48:35 +0100 Subject: [PATCH] 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. --- Content/Shaders/Shadows.flax | 4 +- Source/Engine/Core/Config/GraphicsSettings.h | 3 +- Source/Engine/Graphics/Graphics.cpp | 1 + Source/Engine/Graphics/Graphics.h | 3 +- Source/Engine/Renderer/ShadowsPass.cpp | 19 +++- Source/Engine/Renderer/ShadowsPass.h | 10 +-- Source/Shaders/Shadows.shader | 26 ++++-- Source/Shaders/ShadowsSampling.hlsl | 91 +++++++++++++------- 8 files changed, 105 insertions(+), 52 deletions(-) diff --git a/Content/Shaders/Shadows.flax b/Content/Shaders/Shadows.flax index ddd2cd83a..5e4098fd4 100644 --- a/Content/Shaders/Shadows.flax +++ b/Content/Shaders/Shadows.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:594bfd3a29298f746c19058b1d8d80b929e89ebe05e5fe211f8b78e4bb9d83cd -size 6571 +oid sha256:bb9d5d974ef1ce956eb898fe70af919c2b81d30fe2f51133fe70d23a9a9d77cc +size 7388 diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h index 8e42748e3..0e11d171e 100644 --- a/Source/Engine/Core/Config/GraphicsSettings.h +++ b/Source/Engine/Core/Config/GraphicsSettings.h @@ -61,10 +61,9 @@ public: /// /// Enables cascades splits blending for directional light shadows. - /// [Deprecated in v1.9] /// API_FIELD(Attributes="EditorOrder(1320), DefaultValue(false), EditorDisplay(\"Quality\", \"Allow CSM Blending\")") - DEPRECATED() bool AllowCSMBlending = false; + bool AllowCSMBlending = false; /// /// Default probes cubemap resolution (use for Environment Probes, can be overriden per-actor). diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp index c189b6e50..f6e3d417f 100644 --- a/Source/Engine/Graphics/Graphics.cpp +++ b/Source/Engine/Graphics/Graphics.cpp @@ -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; diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h index fdcc706f7..0910c08a8 100644 --- a/Source/Engine/Graphics/Graphics.h +++ b/Source/Engine/Graphics/Graphics.h @@ -55,9 +55,8 @@ public: /// /// Enables cascades splits blending for directional light shadows. - /// [Deprecated in v1.9] /// - API_FIELD() DEPRECATED() static bool AllowCSMBlending; + API_FIELD() static bool AllowCSMBlending; /// /// The Global SDF quality. Controls the volume texture resolution and amount of cascades to use. diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index b2a4f749c..dcefd8e26 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -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(); } diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index be52f4acc..66ecd5ace 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -19,11 +19,11 @@ private: AssetReference _sphereModel; GPUPipelineState* _psDepthClear = nullptr; GPUPipelineState* _psDepthCopy = nullptr; - GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowDir; - GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowPoint; - GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowPointInside; - GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowSpot; - GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowSpotInside; + GPUPipelineStatePermutationsPs _psShadowDir; + GPUPipelineStatePermutationsPs _psShadowPoint; + GPUPipelineStatePermutationsPs _psShadowPointInside; + GPUPipelineStatePermutationsPs _psShadowSpot; + GPUPipelineStatePermutationsPs _psShadowSpotInside; PixelFormat _shadowMapFormat; // Cached on initialization public: diff --git a/Source/Shaders/Shadows.shader b/Source/Shaders/Shadows.shader index 510c68c09..2e6ca62e9 100644 --- a/Source/Shaders/Shadows.shader +++ b/Source/Shaders/Shadows.shader @@ -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 diff --git a/Source/Shaders/ShadowsSampling.hlsl b/Source/Shaders/ShadowsSampling.hlsl index 598a22748..36a57545a 100644 --- a/Source/Shaders/ShadowsSampling.hlsl +++ b/Source/Shaders/ShadowsSampling.hlsl @@ -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 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 shadowsBuffer, Texture2D 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 shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer, float dither = 0.0f) { @@ -242,53 +286,42 @@ ShadowSample SampleDirectionalLightShadow(LightData light, Buffer 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; }