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;
}