From e7fd9018073f15bc634b8322b45ca5a039b56f2f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 7 May 2026 16:21:17 +0200 Subject: [PATCH] Add improved fallback GI outside the DDGI range to use a special ambient probe for stability --- Content/Shaders/GI/DDGI.flax | 4 +-- Source/Shaders/GI/DDGI.hlsl | 30 ++++++++++++++++++--- Source/Shaders/GI/DDGI.shader | 50 +++++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Content/Shaders/GI/DDGI.flax b/Content/Shaders/GI/DDGI.flax index 6d1431236..486f23dd9 100644 --- a/Content/Shaders/GI/DDGI.flax +++ b/Content/Shaders/GI/DDGI.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0984d39f5226f8eac725b307c88a663d54836a820ae4d4d3cffe73b8fe6d0a2 -size 38199 +oid sha256:71211a37b4e053fcc8e5bd9c126e4f3283c3b9e04a054c111db3e171272c5e33 +size 39553 diff --git a/Source/Shaders/GI/DDGI.hlsl b/Source/Shaders/GI/DDGI.hlsl index b88b846a6..bf11aaa7f 100644 --- a/Source/Shaders/GI/DDGI.hlsl +++ b/Source/Shaders/GI/DDGI.hlsl @@ -29,6 +29,7 @@ #define DDGI_FALLBACK_COORDS_ENCODE(coord) ((float3)(coord + 1) / 128.0f) #define DDGI_FALLBACK_COORDS_DECODE(data) (uint3)(data.xyz * 128.0f - 1) #define DDGI_FALLBACK_COORDS_VALID(data) (length(data.xyz) > 0) +#define DDGI_FALLBACK_OUTER_DEDICATED_PROBE 1 // Enables using a special probe at (0, 0, 0) of the last cascade to be used for ambient GI on far pixels outside the DDGI range //#define DDGI_DEBUG_CASCADE 0 // Forces a specific cascade to be only in use (for debugging) // DDGI data for a constant buffer @@ -166,6 +167,21 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes { bool invalidCascade = cascadeIndex >= data.CascadesCount; cascadeIndex = min(cascadeIndex, data.CascadesCount - 1); +#if DDGI_FALLBACK_OUTER_DEDICATED_PROBE + if (invalidCascade) + { + // Sample a special probe as a fallback for ambient GI outside the last cascade + float2 octahedralCoords = GetOctahedralCoords(worldNormal); + float2 uv = GetDDGIProbeUV(data, cascadeIndex, 0, octahedralCoords, DDGI_PROBE_RESOLUTION_IRRADIANCE); + float3 probeIrradiance = probesIrradiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; +#if DDGI_SRGB_BLENDING + probeIrradiance = pow(probeIrradiance, data.IrradianceGamma * 0.5f); + probeIrradiance *= probeIrradiance; +#endif + probeIrradiance *= 2.0f * PI; + return probeIrradiance; + } +#endif uint3 probeCoordsEnd = data.ProbesCounts - uint3(1, 1, 1); uint3 baseProbeCoords = clamp(uint3((worldPosition - probesOrigin + probesExtent) / probesSpacing), uint3(0, 0, 0), probeCoordsEnd); @@ -258,8 +274,10 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes irradiance = float4(0, 1, 0, 1); else if (cascadeIndex == 2) irradiance = float4(0, 0, 1, 1); - else + else if (invalidCascade) // Area outside the last cascade that clamps to it irradiance = float4(1, 0, 1, 1); + else + irradiance = float4(0, 1, 1, 1); #endif if (irradiance.a > 0.0f) @@ -267,7 +285,11 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes // Normalize irradiance //irradiance.rgb /= irradiance.a; //irradiance.rgb /= lerp(1, irradiance.a, saturate(irradiance.a * irradiance.a + 0.9f)); +#if DDGI_FALLBACK_OUTER_DEDICATED_PROBE + irradiance.rgb /= lerp(1, irradiance.a, saturate(irradiance.a * irradiance.a + 0.9f)); +#else irradiance.rgb /= invalidCascade ? irradiance.a : lerp(1, irradiance.a, saturate(irradiance.a * irradiance.a + 0.9f)); +#endif #if DDGI_SRGB_BLENDING irradiance.rgb *= irradiance.rgb; #endif @@ -331,13 +353,15 @@ float3 SampleDDGIIrradiance(DDGIData data, Texture2D probesData, T // Blend with the next cascade (or fallback irradiance outside the volume) #if DDGI_CASCADE_BLEND_SMOOTH && !defined(DDGI_DEBUG_CASCADE) cascadeIndex++; - if (cascadeIndex < data.CascadesCount && cascadeWeight < 0.99f) + if (cascadeIndex <= data.CascadesCount && cascadeWeight < 0.99f) { + uint cascadeIndexTmp = cascadeIndex; + cascadeIndex = min(cascadeIndex, data.CascadesCount - 1); 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); + float3 resultNext = SampleDDGIIrradianceCascade(data, probesData, probesDistance, probesIrradiance, worldPosition, worldNormal, cascadeIndexTmp, probesOrigin, probesExtent, probesSpacing, biasedWorldPosition); result *= cascadeWeight; result += resultNext * (1 - cascadeWeight); } diff --git a/Source/Shaders/GI/DDGI.shader b/Source/Shaders/GI/DDGI.shader index bade984fc..a90d3c4c6 100644 --- a/Source/Shaders/GI/DDGI.shader +++ b/Source/Shaders/GI/DDGI.shader @@ -30,6 +30,7 @@ #define DDGI_PROBE_EMPTY_AREA_DENSITY 8 // Spacing (in probe grid) between fallback probes placed into empty areas to provide valid GI for nearby dynamic objects or transparency #define DDGI_DEBUG_STATS 0 // Enables additional GPU-driven stats for probe/rays count #define DDGI_DEBUG_INSTABILITY 0 // Enables additional probe irradiance instability debugging +#define DDGI_SKY_DISTANCE 1e27f // Sky is the limit META_CB_BEGIN(0, Data0) DDGIData DDGI; @@ -191,21 +192,51 @@ void CS_Classify(uint3 DispatchThreadId : SV_DispatchThreadID) float relocateLimit = probesSpacing * ProbesRelocateLimits[CascadeIndex]; #ifdef DDGI_PROBE_EMPTY_AREA_DENSITY uint3 probeCoordsStable = GetDDGIProbeCoords(DDGI, probeIndex); +#endif + + // Classify probe based on previous state and neighborhood +#if DDGI_FALLBACK_OUTER_DEDICATED_PROBE + if (CascadeIndex == DDGI.CascadesCount - 1 && probeIndex == 0) + { + // Special probe as a fallback for ambient GI outside the last cascade + probeOffset = float3(0, 0, 0); + if (probeStateOld == DDGI_PROBE_STATE_INACTIVE) + { + probeState = DDGI_PROBE_STATE_ACTIVATED; + probeAttention = 1.0f; + } + else + { + probeState = DDGI_PROBE_STATE_ACTIVE; + probeAttention = 0.5f; + } + } + else +#endif +#ifdef DDGI_PROBE_EMPTY_AREA_DENSITY if (sdf > probesSpacing * DDGI.ProbesCounts.x * 0.3f #if DDGI_PROBE_EMPTY_AREA_DENSITY > 1 && ( // Low-density grid grid (probeCoordsStable.x % DDGI_PROBE_EMPTY_AREA_DENSITY == 0 && probeCoordsStable.y % DDGI_PROBE_EMPTY_AREA_DENSITY == 0 && probeCoordsStable.z % DDGI_PROBE_EMPTY_AREA_DENSITY == 0) - // Edge probes at the last cascade (for good fallback irradiance outside the GI distance) + // Edge probes at the last cascade (for good fallback irradiance outside the GI distance) - not needed anymore as DDGI_FALLBACK_OUTER_DEDICATED_PROBE does a far better job //|| (CascadeIndex + 1 == DDGI.CascadesCount && IsProbeAtBorder(probeCoords)) ) #endif ) { - // Addd some fallback probes in empty areas to provide valid GI for nearby dynamic objects or transparency + // Add some fallback probes in empty areas to provide valid GI for nearby dynamic objects or transparency probeOffset = float3(0, 0, 0); - probeState = wasScrolled || probeStateOld == DDGI_PROBE_STATE_INACTIVE ? DDGI_PROBE_STATE_ACTIVATED : DDGI_PROBE_STATE_ACTIVE; - probeAttention = DDGI_PROBE_ATTENTION_MIN; + if (wasScrolled || probeStateOld == DDGI_PROBE_STATE_INACTIVE) + { + probeState = DDGI_PROBE_STATE_ACTIVATED; + probeAttention = 1.0f; + } + else + { + probeState = DDGI_PROBE_STATE_ACTIVE; + probeAttention = DDGI_PROBE_ATTENTION_MIN; + } } else #endif @@ -479,6 +510,15 @@ void CS_TraceRays(uint3 DispatchThreadId : SV_DispatchThreadID) float3 probeRayDirection = GetProbeRayDirection(DDGI, rayIndex, probeRaysCount, probeIndex, probeCoords); // TODO: implement ray-guiding based on the probe irradiance (prioritize directions with high luminance) +#if DDGI_FALLBACK_OUTER_DEDICATED_PROBE + if (CascadeIndex == DDGI.CascadesCount - 1 && probeIndex == 0) + { + // Special probe as a fallback for ambient GI outside the last cascade + RWProbesTrace[uint2(rayIndex, DispatchThreadId.x)] = float4(Skybox.SampleLevel(SamplerLinearClamp, probeRayDirection, 0).rgb * SkyboxIntensity, DDGI_SKY_DISTANCE); + return; + } +#endif + // Trace ray with Global SDF GlobalSDFTrace trace; trace.Init(probePosition, probeRayDirection, 0.0f, DDGI.RayMaxDistance); @@ -513,7 +553,7 @@ void CS_TraceRays(uint3 DispatchThreadId : SV_DispatchThreadID) { // Ray hits sky radiance.rgb = Skybox.SampleLevel(SamplerLinearClamp, probeRayDirection, 0).rgb * SkyboxIntensity; - radiance.a = 1e27f; // Sky is the limit + radiance.a = DDGI_SKY_DISTANCE; } // Write into probes trace results