diff --git a/Source/Editor/Windows/GraphicsQualityWindow.cs b/Source/Editor/Windows/GraphicsQualityWindow.cs
index 59cd61f25..a1b6b3eab 100644
--- a/Source/Editor/Windows/GraphicsQualityWindow.cs
+++ b/Source/Editor/Windows/GraphicsQualityWindow.cs
@@ -1,6 +1,5 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
-using System.Collections.Generic;
using System.ComponentModel;
using FlaxEditor.CustomEditors;
using FlaxEngine;
@@ -96,14 +95,6 @@ namespace FlaxEditor.Windows
set => Graphics.ShadowMapsQuality = value;
}
- [DefaultValue(false)]
- [EditorOrder(1320), EditorDisplay("Quality", "Allow CSM Blending"), Tooltip("Enables cascades splits blending for directional light shadows.")]
- public bool AllowCSMBlending
- {
- get => Graphics.AllowCSMBlending;
- set => Graphics.AllowCSMBlending = value;
- }
-
[NoSerialize, DefaultValue(1.0f), Limit(0.05f, 5, 0)]
[EditorOrder(1400), EditorDisplay("Quality")]
[Tooltip("The scale of the rendering resolution relative to the output dimensions. If lower than 1 the scene and postprocessing will be rendered at a lower resolution and upscaled to the output backbuffer.")]
diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h
index 97c5f81cc..a3a5e7ded 100644
--- a/Source/Engine/Core/Config/GraphicsSettings.h
+++ b/Source/Engine/Core/Config/GraphicsSettings.h
@@ -61,9 +61,10 @@ 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\")")
- bool AllowCSMBlending = false;
+ DEPRECATED 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 1e834ff55..954fc7c73 100644
--- a/Source/Engine/Graphics/Graphics.cpp
+++ b/Source/Engine/Graphics/Graphics.cpp
@@ -65,7 +65,6 @@ void GraphicsSettings::Apply()
Graphics::VolumetricFogQuality = VolumetricFogQuality;
Graphics::ShadowsQuality = ShadowsQuality;
Graphics::ShadowMapsQuality = ShadowMapsQuality;
- Graphics::AllowCSMBlending = AllowCSMBlending;
Graphics::GlobalSDFQuality = GlobalSDFQuality;
Graphics::GIQuality = GIQuality;
Graphics::PostProcessSettings = ::PostProcessSettings();
diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h
index 7f49d450c..55ded56e5 100644
--- a/Source/Engine/Graphics/Graphics.h
+++ b/Source/Engine/Graphics/Graphics.h
@@ -50,8 +50,9 @@ public:
///
/// Enables cascades splits blending for directional light shadows.
+ /// [Deprecated in v1.9]
///
- API_FIELD() static bool AllowCSMBlending;
+ API_FIELD() DEPRECATED static bool AllowCSMBlending;
///
/// The Global SDF quality. Controls the volume texture resolution and amount of cascades to use.
diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp
index f43fc8c75..edd30af7a 100644
--- a/Source/Engine/Graphics/RenderTools.cpp
+++ b/Source/Engine/Graphics/RenderTools.cpp
@@ -552,6 +552,14 @@ void RenderTools::ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascad
}
}
+float RenderTools::ComputeTemporalTime()
+{
+ const float time = Time::Draw.UnscaledTime.GetTotalSeconds();
+ const float scale = 10;
+ const float integral = roundf(time / scale) * scale;
+ return time - integral;
+}
+
void RenderTools::CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal)
{
// Calculate tangent
diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h
index 06b059975..07fcab89a 100644
--- a/Source/Engine/Graphics/RenderTools.h
+++ b/Source/Engine/Graphics/RenderTools.h
@@ -121,6 +121,10 @@ public:
return (frameIndex % updateFrequency == updatePhrase) || updateForce;
}
+ // Calculates temporal offset in the dithering factor that gets cleaned out by TAA.
+ // Returns 0-1 value based on unscaled draw time for temporal effects to reduce artifacts from screen-space dithering when using Temporal Anti-Aliasing.
+ static float ComputeTemporalTime();
+
static void CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal);
static void CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal, const Float3& tangent);
};
diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
index 56d1194d5..e488bab81 100644
--- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
+++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
@@ -13,13 +13,13 @@
#include "Engine/Engine/Engine.h"
#include "Engine/Content/Content.h"
#include "Engine/Debug/DebugDraw.h"
-#include "Engine/Engine/Time.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/RenderTargetPool.h"
+#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/Shaders/GPUShader.h"
#include "Engine/Level/Actors/BrushMode.h"
#include "Engine/Renderer/GBufferPass.h"
@@ -480,18 +480,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
auto& cascade = ddgiData.Cascades[cascadeIndex];
data.ProbeScrollClears[cascadeIndex] = Int4(cascade.ProbeScrollClears, 0);
}
- if (renderContext.List->Setup.UseTemporalAAJitter)
- {
- // Use temporal offset in the dithering factor (gets cleaned out by TAA)
- const float time = Time::Draw.UnscaledTime.GetTotalSeconds();
- const float scale = 10;
- const float integral = roundf(time / scale) * scale;
- data.TemporalTime = time - integral;
- }
- else
- {
- data.TemporalTime = 0.0f;
- }
+ data.TemporalTime = renderContext.List->Setup.UseTemporalAAJitter ? RenderTools::ComputeTemporalTime() : 0.0f;
GBufferPass::SetInputs(renderContext.View, data.GBuffer);
context->UpdateCB(_cb0, &data);
context->BindCB(0, _cb0);
diff --git a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp
index b3d0ea6ba..5e758d148 100644
--- a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp
+++ b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp
@@ -11,7 +11,6 @@
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Graphics/RenderBuffers.h"
-#include "Engine/Engine/Time.h"
#include "Engine/Platform/Window.h"
#include "Utils/MultiScaler.h"
#include "Engine/Engine/Engine.h"
@@ -247,13 +246,7 @@ void ScreenSpaceReflectionsPass::Render(RenderContext& renderContext, GPUTexture
data.TemporalEffect = useTemporal ? 1.0f : 0.0f;
if (useTemporal)
{
- const float time = Time::Draw.UnscaledTime.GetTotalSeconds();
-
- // Keep time in smaller range to prevent temporal noise errors
- const double scale = 10;
- const double integral = round(time / scale) * scale;
- data.TemporalTime = static_cast(time - integral);
-
+ data.TemporalTime = RenderTools::ComputeTemporalTime();
buffers->LastFrameTemporalSSR = Engine::FrameCount;
if (!buffers->TemporalSSR || buffers->TemporalSSR->Width() != temporalWidth || buffers->TemporalSSR->Height() != temporalHeight)
{
diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp
index 64f043d28..aa304d43b 100644
--- a/Source/Engine/Renderer/ShadowsPass.cpp
+++ b/Source/Engine/Renderer/ShadowsPass.cpp
@@ -27,7 +27,8 @@ PACK_STRUCT(struct Data{
ShaderLightData Light;
Matrix WVP;
Matrix ViewProjectionMatrix;
- Float2 Dummy0;
+ float Dummy0;
+ float TemporalTime;
float ContactShadowsDistance;
float ContactShadowsLength;
});
@@ -62,7 +63,6 @@ struct ShadowAtlasLight
int32 ContextCount;
uint16 Resolution;
uint16 TilesNeeded;
- bool BlendCSM;
float Sharpness, Fade, NormalOffsetScale, Bias, FadeDistance;
Float4 CascadeSplits;
ShadowsAtlasTile* Tiles[MaxTiles];
@@ -294,15 +294,7 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
Float3 lightDirection = light.Direction;
float shadowsDistance = Math::Min(view.Far, light.ShadowsDistance);
int32 csmCount = Math::Clamp(light.CascadeCount, 0, MAX_CSM_CASCADES);
- bool blendCSM = Graphics::AllowCSMBlending;
const auto shadowMapsSize = (float)atlasLight.Resolution;
-#if USE_EDITOR
- if (IsRunningRadiancePass)
- blendCSM = false;
-#elif PLATFORM_SWITCH || PLATFORM_IOS || PLATFORM_ANDROID
- // Disable cascades blending on low-end platforms
- blendCSM = false;
-#endif
// Views with orthographic cameras cannot use cascades, we force it to 1 shadow map here
if (view.Projection.M44 == 1.0f)
@@ -397,7 +389,6 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
// Init shadow data
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = csmCount;
- atlasLight.BlendCSM = blendCSM;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
// Create the different view and projection matrices for each split
@@ -413,9 +404,7 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
// Calculate cascade split frustum corners in view space
for (int32 j = 0; j < 4; j++)
{
- float overlap = 0;
- if (blendCSM)
- overlap = 0.2f * (splitMinRatio - oldSplitMinRatio);
+ float overlap = 0.1f * (splitMinRatio - oldSplitMinRatio); // CSM blending overlap
const auto frustumRangeVS = mainCache->FrustumCornersVs[j + 4] - mainCache->FrustumCornersVs[j];
frustumCorners[j] = mainCache->FrustumCornersVs[j] + frustumRangeVS * (splitMinRatio - overlap);
frustumCorners[j + 4] = mainCache->FrustumCornersVs[j] + frustumRangeVS * splitMaxRatio;
@@ -927,6 +916,7 @@ void ShadowsPass::RenderShadowMask(RenderContextBatch& renderContextBatch, Rende
else if (light.IsSpotLight)
((RenderSpotLightData&)light).SetShaderData(sperLight.Light, true);
Matrix::Transpose(view.ViewProjection(), sperLight.ViewProjectionMatrix);
+ sperLight.TemporalTime = renderContext.List->Setup.UseTemporalAAJitter ? RenderTools::ComputeTemporalTime() : 0.0f;
sperLight.ContactShadowsDistance = light.ShadowsDistance;
sperLight.ContactShadowsLength = EnumHasAnyFlags(view.Flags, ViewFlags::ContactShadows) ? light.ContactShadowsLength : 0.0f;
if (isLocalLight)
diff --git a/Source/Shaders/Shadows.shader b/Source/Shaders/Shadows.shader
index aff46ac8e..92c2d0815 100644
--- a/Source/Shaders/Shadows.shader
+++ b/Source/Shaders/Shadows.shader
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#define USE_GBUFFER_CUSTOM_DATA
+#define SHADOWS_CSM_BLENDING 1
#include "./Flax/Common.hlsl"
#include "./Flax/GBuffer.hlsl"
@@ -12,7 +13,8 @@ GBufferData GBuffer;
LightData Light;
float4x4 WVP;
float4x4 ViewProjectionMatrix;
-float2 Dummy0;
+float Dummy0;
+float TemporalTime;
float ContactShadowsDistance;
float ContactShadowsLength;
META_CB_END
@@ -115,7 +117,7 @@ float4 PS_DirLight(Quad_VS2PS input) : SV_Target0
GBufferSample gBuffer = SampleGBuffer(gBufferData, input.TexCoord);
// Sample shadow
- ShadowSample shadow = SampleDirectionalLightShadow(Light, ShadowsBuffer, ShadowMap, gBuffer);
+ ShadowSample shadow = SampleDirectionalLightShadow(Light, ShadowsBuffer, ShadowMap, gBuffer, TemporalTime);
#if CONTACT_SHADOWS
// Calculate screen-space contact shadow
diff --git a/Source/Shaders/ShadowsSampling.hlsl b/Source/Shaders/ShadowsSampling.hlsl
index d8510011a..20806daf6 100644
--- a/Source/Shaders/ShadowsSampling.hlsl
+++ b/Source/Shaders/ShadowsSampling.hlsl
@@ -6,6 +6,9 @@
#include "./Flax/ShadowsCommon.hlsl"
#include "./Flax/GBufferCommon.hlsl"
#include "./Flax/LightingCommon.hlsl"
+#ifdef SHADOWS_CSM_BLENDING
+#include "./Flax/Random.hlsl"
+#endif
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM5
#define SAMPLE_SHADOW_MAP(shadowMap, shadowUV, sceneDepth) shadowMap.SampleCmpLevelZero(ShadowSamplerLinear, shadowUV, sceneDepth)
@@ -77,7 +80,7 @@ float SampleShadowMap(Texture2D shadowMap, float2 shadowMapUV, float scen
}
// 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)
+ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer, float dither = 0.0f)
{
#if !LIGHTING_NO_DIRECTIONAL
// Skip if surface is in a full shadow
@@ -114,6 +117,19 @@ ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadow
if (viewDepth > shadow.CascadeSplits[i])
cascadeIndex = i + 1;
}
+#ifdef SHADOWS_CSM_BLENDING
+ const float BlendThreshold = 0.05f;
+ float nextSplit = shadow.CascadeSplits[cascadeIndex];
+ float splitSize = cascadeIndex == 0 ? nextSplit : nextSplit - shadow.CascadeSplits[cascadeIndex - 1];
+ float splitDist = (nextSplit - viewDepth) / splitSize;
+ if (splitDist <= BlendThreshold && cascadeIndex != shadow.TilesCount - 1)
+ {
+ // Blend 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);
float3 samplePosition = gBuffer.WorldPos;
@@ -241,11 +257,11 @@ GBufferSample GetDummyGBufferSample(float3 worldPosition)
}
// Samples the shadow for the given directional light at custom location
-ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, float3 worldPosition, float viewDepth)
+ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, float3 worldPosition, float viewDepth, float dither = 0.0f)
{
GBufferSample gBuffer = GetDummyGBufferSample(worldPosition);
gBuffer.ViewPos.z = viewDepth;
- return SampleDirectionalLightShadow(light, shadowsBuffer, shadowMap, gBuffer);
+ return SampleDirectionalLightShadow(light, shadowsBuffer, shadowMap, gBuffer, dither);
}
// Samples the shadow for the given spot light at custom location