diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h
index 9fef78f74..26be6534e 100644
--- a/Source/Engine/Core/Config/GraphicsSettings.h
+++ b/Source/Engine/Core/Config/GraphicsSettings.h
@@ -116,6 +116,12 @@ public:
API_FIELD(Attributes="EditorOrder(2120), Limit(50, 1000), EditorDisplay(\"Global Illumination\")")
float GIProbesSpacing = 100;
+ ///
+ /// Enables cascades splits blending for Global Illumination.
+ ///
+ API_FIELD(Attributes="EditorOrder(2125), DefaultValue(false), EditorDisplay(\"Global Illumination\", \"GI Cascades Blending\")")
+ bool GICascadesBlending = false;
+
///
/// The Global Surface Atlas resolution. Adjust it if atlas `flickers` due to overflow (eg. to 4096).
///
diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp
index 020d0ad0e..d60e3d8e8 100644
--- a/Source/Engine/Graphics/Graphics.cpp
+++ b/Source/Engine/Graphics/Graphics.cpp
@@ -21,8 +21,9 @@ Quality Graphics::ShadowMapsQuality = Quality::Medium;
bool Graphics::AllowCSMBlending = false;
Quality Graphics::GlobalSDFQuality = Quality::High;
Quality Graphics::GIQuality = Quality::High;
+bool Graphics::GICascadesBlending = false;
PostProcessSettings Graphics::PostProcessSettings;
-bool Graphics::SpreadWorkload = true;
+bool Graphics::SpreadWorkload = false;
#if GRAPHICS_API_NULL
extern GPUDevice* CreateGPUDeviceNull();
@@ -69,6 +70,7 @@ void GraphicsSettings::Apply()
Graphics::ShadowMapsQuality = ShadowMapsQuality;
Graphics::GlobalSDFQuality = GlobalSDFQuality;
Graphics::GIQuality = GIQuality;
+ Graphics::GICascadesBlending = GICascadesBlending;
Graphics::PostProcessSettings = ::PostProcessSettings();
Graphics::PostProcessSettings.BlendWith(PostProcessSettings, 1.0f);
#if !USE_EDITOR // OptionsModule handles fallback fonts in Editor
diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h
index b7f1fbcd7..a08d78c10 100644
--- a/Source/Engine/Graphics/Graphics.h
+++ b/Source/Engine/Graphics/Graphics.h
@@ -64,6 +64,11 @@ public:
///
API_FIELD() static Quality GIQuality;
+ ///
+ /// Enables cascades splits blending for Global Illumination.
+ ///
+ API_FIELD() static bool GICascadesBlending;
+
///
/// The default Post Process settings. Can be overriden by PostFxVolume on a level locally, per camera or for a whole map.
///
diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
index d74b03672..eb2c9fd33 100644
--- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
+++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp
@@ -221,12 +221,16 @@ bool DynamicDiffuseGlobalIlluminationPass::setupResources()
_csUpdateProbesDistance = shader->GetCS("CS_UpdateProbes", 1);
auto device = GPUDevice::Instance;
auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
- if (!_psIndirectLighting)
+ if (!_psIndirectLighting[0])
{
- _psIndirectLighting = device->CreatePipelineState();
+ _psIndirectLighting[0] = device->CreatePipelineState();
+ _psIndirectLighting[1] = device->CreatePipelineState();
psDesc.PS = shader->GetPS("PS_IndirectLighting");
psDesc.BlendMode = BlendingMode::Add;
- if (_psIndirectLighting->Init(psDesc))
+ if (_psIndirectLighting[0]->Init(psDesc))
+ return true;
+ psDesc.PS = shader->GetPS("PS_IndirectLighting", 1);
+ if (_psIndirectLighting[1]->Init(psDesc))
return true;
}
@@ -246,7 +250,8 @@ void DynamicDiffuseGlobalIlluminationPass::OnShaderReloading(Asset* obj)
_csTraceRays[3] = nullptr;
_csUpdateProbesIrradiance = nullptr;
_csUpdateProbesDistance = nullptr;
- SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting);
+ SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[0]);
+ SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[1]);
invalidateResources();
}
@@ -260,7 +265,8 @@ void DynamicDiffuseGlobalIlluminationPass::Dispose()
_cb0 = nullptr;
_cb1 = nullptr;
_shader = nullptr;
- SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting);
+ SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[0]);
+ SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[1]);
#if USE_EDITOR
_debugModel = nullptr;
_debugMaterial = nullptr;
@@ -534,7 +540,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
// Update probes
{
PROFILE_GPU_CPU_NAMED("Probes Update");
- uint32 threadGroupsX, threadGroupsY;
+ uint32 threadGroupsX;
#if DDGI_DEBUG_STATS
uint32 zero[4] = {};
context->ClearUA(ddgiData.StatsWrite, zero);
@@ -732,7 +738,7 @@ bool DynamicDiffuseGlobalIlluminationPass::Render(RenderContext& renderContext,
context->BindSR(6, ddgiData.Result.ProbesIrradiance);
context->SetViewportAndScissors(renderContext.View.ScreenSize.X, renderContext.View.ScreenSize.Y);
context->SetRenderTarget(lightBuffer);
- context->SetState(_psIndirectLighting);
+ context->SetState(_psIndirectLighting[Graphics::GICascadesBlending ? 1 : 0]);
context->DrawFullscreenTriangle();
}
diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
index c56604255..94560aa13 100644
--- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
+++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h
@@ -47,7 +47,7 @@ private:
GPUShaderProgramCS* _csTraceRays[4];
GPUShaderProgramCS* _csUpdateProbesIrradiance;
GPUShaderProgramCS* _csUpdateProbesDistance;
- GPUPipelineState* _psIndirectLighting;
+ GPUPipelineState* _psIndirectLighting[2] = {};
#if USE_EDITOR
AssetReference _debugModel;
AssetReference _debugMaterial;
diff --git a/Source/Shaders/GI/DDGI.hlsl b/Source/Shaders/GI/DDGI.hlsl
index 330a20420..8b87ffea0 100644
--- a/Source/Shaders/GI/DDGI.hlsl
+++ b/Source/Shaders/GI/DDGI.hlsl
@@ -21,6 +21,9 @@
#define DDGI_PROBE_RESOLUTION_IRRADIANCE 6 // Resolution (in texels) for probe irradiance data (excluding 1px padding on each side)
#define DDGI_PROBE_RESOLUTION_DISTANCE 14 // Resolution (in texels) for probe distance data (excluding 1px padding on each side)
#define DDGI_CASCADE_BLEND_SIZE 2.5f // Distance in probes over which cascades blending happens
+#ifndef DDGI_CASCADE_BLEND_SMOOTH
+#define DDGI_CASCADE_BLEND_SMOOTH 0 // Enables smooth cascade blending, otherwise dithering will be used
+#endif
#define DDGI_SRGB_BLENDING 1 // Enables blending in sRGB color space, otherwise irradiance blending is done in linear space
// DDGI data for a constant buffer
@@ -154,37 +157,8 @@ float2 GetDDGIProbeUV(DDGIData data, uint cascadeIndex, uint probeIndex, float2
return uv;
}
-// Samples DDGI probes volume at the given world-space position and returns the irradiance.
-// bias - scales the bias vector to the initial sample point to reduce self-shading artifacts
-// dither - randomized per-pixel value in range 0-1, used to smooth dithering for cascades blending
-float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, float bias = 0.2f, float dither = 0.0f)
+float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, uint cascadeIndex, float3 probesOrigin, float3 probesExtent, float probesSpacing, float3 biasedWorldPosition)
{
- // Select the highest cascade that contains the sample location
- uint cascadeIndex = 0;
- float probesSpacing = 0;
- float3 probesOrigin = (float3)0, probesExtent = (float3)0, biasedWorldPosition = (float3)0;
- float3 viewDir = normalize(data.ViewPos - worldPosition);
- for (; cascadeIndex < data.CascadesCount; cascadeIndex++)
- {
- // Get cascade data
- probesSpacing = data.ProbesOriginAndSpacing[cascadeIndex].w;
- probesOrigin = data.ProbesScrollOffsets[cascadeIndex].xyz * probesSpacing + data.ProbesOriginAndSpacing[cascadeIndex].xyz;
- probesExtent = (data.ProbesCounts - 1) * (probesSpacing * 0.5f);
-
- // Bias the world-space position to reduce artifacts
- float3 surfaceBias = (worldNormal * 0.2f + viewDir * 0.8f) * (0.75f * probesSpacing * bias);
- biasedWorldPosition = worldPosition + surfaceBias;
-
- // Calculate cascade blending weight (use input bias to smooth transition)
- float cascadeBlendSmooth = frac(max(distance(data.ViewPos, worldPosition) - probesExtent.x, 0) / probesSpacing) * 0.1f;
- float3 cascadeBlendPoint = worldPosition - probesOrigin - cascadeBlendSmooth * probesSpacing;
- float fadeDistance = probesSpacing * DDGI_CASCADE_BLEND_SIZE;
- float cascadeWeight = saturate(Min3(probesExtent - abs(cascadeBlendPoint)) / fadeDistance);
- if (cascadeWeight > dither)
- break;
- }
- if (cascadeIndex == data.CascadesCount)
- return data.FallbackIrradiance;
uint3 probeCoordsEnd = data.ProbesCounts - uint3(1, 1, 1);
uint3 baseProbeCoords = clamp(uint3((worldPosition - probesOrigin + probesExtent) / probesSpacing), uint3(0, 0, 0), probeCoordsEnd);
@@ -208,25 +182,26 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T
{
// Search nearby probes to find any nearby GI sample
for (int searchDistance = 1; searchDistance < 3 && probeState == DDGI_PROBE_STATE_INACTIVE; searchDistance++)
- for (uint searchAxis = 0; searchAxis < 3; searchAxis++)
- {
- int searchAxisDir = probeCoordsOffset[searchAxis] ? 1 : -1;
- int3 searchCoordsOffset = SearchAxisMasks[searchAxis] * searchAxisDir * searchDistance;
- uint3 searchCoords = clamp((int3)probeCoords + searchCoordsOffset, int3(0, 0, 0), (int3)probeCoordsEnd);
- uint searchIndex = GetDDGIScrollingProbeIndex(data, cascadeIndex, searchCoords);
- float4 searchData = LoadDDGIProbeData(data, probesData, cascadeIndex, searchIndex);
- uint searchState = DecodeDDGIProbeState(searchData);
- if (searchState != DDGI_PROBE_STATE_INACTIVE)
+ for (uint searchAxis = 0; searchAxis < 3; searchAxis++)
{
- // Use nearby probe as a fallback (visibility test might ignore it but with smooth gradient)
- probeCoords = searchCoords;
- probeIndex = searchIndex;
- probeData = searchData;
- probeState = searchState;
- break;
+ int searchAxisDir = probeCoordsOffset[searchAxis] ? 1 : -1;
+ int3 searchCoordsOffset = SearchAxisMasks[searchAxis] * searchAxisDir * searchDistance;
+ uint3 searchCoords = clamp((int3)probeCoords + searchCoordsOffset, int3(0, 0, 0), (int3)probeCoordsEnd);
+ uint searchIndex = GetDDGIScrollingProbeIndex(data, cascadeIndex, searchCoords);
+ float4 searchData = LoadDDGIProbeData(data, probesData, cascadeIndex, searchIndex);
+ uint searchState = DecodeDDGIProbeState(searchData);
+ if (searchState != DDGI_PROBE_STATE_INACTIVE)
+ {
+ // Use nearby probe as a fallback (visibility test might ignore it but with smooth gradient)
+ probeCoords = searchCoords;
+ probeIndex = searchIndex;
+ probeData = searchData;
+ probeState = searchState;
+ break;
+ }
}
- }
- if (probeState == DDGI_PROBE_STATE_INACTIVE) continue;
+ if (probeState == DDGI_PROBE_STATE_INACTIVE)
+ continue;
}
float3 probeBasePosition = baseProbeWorldPosition + ((probeCoords - baseProbeCoords) * probesSpacing);
float3 probePosition = probeBasePosition + probeData.xyz * probesSpacing; // Probe offset is [-1;1] within probes spacing
@@ -257,7 +232,8 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T
// Adjust weight curve to inject a small portion of light
const float minWeightThreshold = 0.2f;
- if (weight < minWeightThreshold) weight *= Square(weight) / Square(minWeightThreshold);
+ if (weight < minWeightThreshold)
+ weight *= Square(weight) / Square(minWeightThreshold);
// Calculate trilinear weights based on the distance to each probe to smoothly transition between grid of 8 probes
float3 trilinear = lerp(1.0f - biasAlpha, biasAlpha, (float3)probeCoordsOffset);
@@ -301,3 +277,64 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T
}
return irradiance.rgb;
}
+
+float3 GetDDGISurfaceBias(float3 viewDir, float probesSpacing, float3 worldNormal, float bias)
+{
+ // Bias the world-space position to reduce artifacts
+ return (worldNormal * 0.2f + viewDir * 0.8f) * (0.75f * probesSpacing * bias);
+}
+
+// Samples DDGI probes volume at the given world-space position and returns the irradiance.
+// bias - scales the bias vector to the initial sample point to reduce self-shading artifacts
+// dither - randomized per-pixel value in range 0-1, used to smooth dithering for cascades blending
+float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesIrradiance, float3 worldPosition, float3 worldNormal, float bias = 0.2f, float dither = 0.0f)
+{
+ // Select the highest cascade that contains the sample location
+ uint cascadeIndex = 0;
+ float probesSpacing = 0, cascadeWeight = 0;
+ float3 probesOrigin = (float3)0, probesExtent = (float3)0, biasedWorldPosition = (float3)0;
+ float3 viewDir = normalize(data.ViewPos - worldPosition);
+#if DDGI_CASCADE_BLEND_SMOOTH
+ dither = 0.0f;
+#endif
+ for (; cascadeIndex < data.CascadesCount; cascadeIndex++)
+ {
+ // Get cascade data
+ probesSpacing = data.ProbesOriginAndSpacing[cascadeIndex].w;
+ probesOrigin = data.ProbesScrollOffsets[cascadeIndex].xyz * probesSpacing + data.ProbesOriginAndSpacing[cascadeIndex].xyz;
+ probesExtent = (data.ProbesCounts - 1) * (probesSpacing * 0.5f);
+ biasedWorldPosition = worldPosition + GetDDGISurfaceBias(viewDir, probesSpacing, worldNormal, bias);
+
+ // Calculate cascade blending weight (use input bias to smooth transition)
+ float cascadeBlendSmooth = frac(max(distance(data.ViewPos, worldPosition) - probesExtent.x, 0) / probesSpacing) * 0.1f;
+ float3 cascadeBlendPoint = worldPosition - probesOrigin - cascadeBlendSmooth * probesSpacing;
+ float fadeDistance = probesSpacing * DDGI_CASCADE_BLEND_SIZE;
+#if DDGI_CASCADE_BLEND_SMOOTH
+ fadeDistance *= 2.0f; // Make it even smoother when using linear blending
+#endif
+ cascadeWeight = saturate(Min3(probesExtent - abs(cascadeBlendPoint)) / fadeDistance);
+ if (cascadeWeight > dither)
+ break;
+ }
+ if (cascadeIndex == data.CascadesCount)
+ return data.FallbackIrradiance;
+
+ // Sample cascade
+ float3 result = SampleDDGIIrradianceCascade(data, probesData, probesDistance, probesIrradiance, worldPosition, worldNormal, cascadeIndex, probesOrigin, probesExtent, probesSpacing, biasedWorldPosition);
+
+#if DDGI_CASCADE_BLEND_SMOOTH
+ // Blend with the next cascade
+ cascadeIndex++;
+ if (cascadeIndex < data.CascadesCount && cascadeWeight < 0.99f)
+ {
+ probesSpacing = data.ProbesOriginAndSpacing[cascadeIndex].w;
+ probesOrigin = data.ProbesScrollOffsets[cascadeIndex].xyz * probesSpacing + data.ProbesOriginAndSpacing[cascadeIndex].xyz;
+ probesExtent = (data.ProbesCounts - 1) * (probesSpacing * 0.5f);
+ biasedWorldPosition = worldPosition + GetDDGISurfaceBias(viewDir, probesSpacing, worldNormal, bias);
+ float3 resultNext = SampleDDGIIrradianceCascade(data, probesData, probesDistance, probesIrradiance, worldPosition, worldNormal, cascadeIndex, probesOrigin, probesExtent, probesSpacing, biasedWorldPosition);
+ result = lerp(resultNext, result, cascadeWeight);
+ }
+#endif
+
+ return result;
+}
diff --git a/Source/Shaders/GI/DDGI.shader b/Source/Shaders/GI/DDGI.shader
index 73cac7c26..89d0fdea3 100644
--- a/Source/Shaders/GI/DDGI.shader
+++ b/Source/Shaders/GI/DDGI.shader
@@ -768,6 +768,8 @@ Texture2D ProbesIrradiance : register(t6);
// Pixel shader for drawing indirect lighting in fullscreen
META_PS(true, FEATURE_LEVEL_SM5)
+META_PERMUTATION_1(DDGI_CASCADE_BLEND_SMOOTH=0)
+META_PERMUTATION_1(DDGI_CASCADE_BLEND_SMOOTH=1)
void PS_IndirectLighting(Quad_VS2PS input, out float4 output : SV_Target0)
{
output = 0;