From 8a5ff58b9e03788d12300ed59d9894fa88b48c70 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 21 Jul 2026 11:29:24 +0200 Subject: [PATCH] Add roughness-based filtering when sampling DDGI probes --- Content/Shaders/GI/DDGI.flax | 4 +- .../GI/DynamicDiffuseGlobalIllumination.cpp | 23 ++++++---- Source/Shaders/GI/DDGI.hlsl | 46 +++++++++++++------ Source/Shaders/GI/DDGI.shader | 7 ++- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/Content/Shaders/GI/DDGI.flax b/Content/Shaders/GI/DDGI.flax index eecc8f066..30badee3f 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:70c90b5ea9ecfa243ff57caa8c3101ce2ff08758a0677910b07b610d5ff2e67a -size 42473 +oid sha256:6797373cfc9ab175141ac4cf74b9369cf5e923485c3ffb3ddd7d8b8ae6d12ba8 +size 42840 diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp index ebf205208..082a20861 100644 --- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp +++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp @@ -75,9 +75,8 @@ GPU_CB_STRUCT(Data0 { float TestValue; Float3 QuantizationError; int32 FrameIndexMod8; + Float2 ResolveDitherScale; Float2 Padding0; - float ResolveDitherScaleIndirect; - float ResolveDitherScaleSpecular; }); GPU_CB_STRUCT(Data1 { @@ -185,16 +184,18 @@ public: } }; -void InitData0Shared(const RenderContext& renderContext, const DDGICustomBuffer& ddgiData, Data0& data) +void InitData0(const RenderContext& renderContext, const DDGICustomBuffer& ddgiData, Data0& data) { + Platform::MemoryClear((byte*)&data + sizeof(data.DDGI), sizeof(Data0) - sizeof(data.DDGI)); data.DDGI = ddgiData.Result.Constants; + data.ProbesCount = data.DDGI.ProbesCounts[0] * data.DDGI.ProbesCounts[1] * data.DDGI.ProbesCounts[2]; data.TestValue = Graphics::TestValue; data.TemporalTime = renderContext.List->Setup.UseTemporalAAJitter ? RenderTools::ComputeTemporalTime() : 0.0f; data.FrameIndexMod8 = (int32)(Engine::FrameCount % 8); auto& settings = renderContext.List->Settings.GlobalIllumination; constexpr float DitherScaleHalfRes = 0.1f; // Hardcoded to reduce noise at half-res due to TAA not being able to filter this out (in future BilateralUpscale could try smooth it) - data.ResolveDitherScaleIndirect = settings.IndirectResolution == ResolutionMode::Full ? 1.0f : DitherScaleHalfRes; - data.ResolveDitherScaleSpecular = settings.ReflectionsResolution == ResolutionMode::Full ? 1.0f : DitherScaleHalfRes; + data.ResolveDitherScale.X = settings.IndirectResolution == ResolutionMode::Full ? 1.0f : DitherScaleHalfRes; + data.ResolveDitherScale.Y = settings.ReflectionsResolution == ResolutionMode::Full ? 1.0f : DitherScaleHalfRes; GBufferPass::SetInputs(renderContext.View, data.GBuffer); } @@ -301,6 +302,8 @@ bool DynamicDiffuseGlobalIlluminationPass::setupResources() // Initialize resources const auto shader = _shader->GPU; + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data0); + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 1, Data1); _cb0 = shader->GetCB(0); _cb1 = shader->GetCB(1); if (!_cb0 || !_cb1) @@ -658,6 +661,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont ddgiData.Result.ProbesRadiance = ddgiData.ProbesRadiance ? ddgiData.ProbesRadiance->View() : nullptr; Data0 data; + InitData0(renderContext, ddgiData, data); // Compute random rotation matrix for probe rays orientation (randomized every frame) Matrix3x3 raysRotationMatrix; @@ -668,7 +672,6 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont data.GlobalSDF = bindingDataSDF.Constants; data.GlobalSurfaceAtlas = bindingDataSurfaceAtlas.Constants; - data.ProbesCount = data.DDGI.ProbesCounts[0] * data.DDGI.ProbesCounts[1] * data.DDGI.ProbesCounts[2]; data.ResetBlend = clear ? 1.0f : 0.0f; for (int32 cascadeIndex = 0; cascadeIndex < cascadesCount; cascadeIndex++) { @@ -678,8 +681,6 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont data.ViewDir = renderContext.View.Direction; data.SkyboxIntensity = renderContext.List->Sky ? renderContext.List->Sky->GetIndirectLightingIntensity() : 1.0f; data.QuantizationError = RenderTools::GetColorQuantizationError(ddgiData.ProbesIrradiance->Format()); - InitData0Shared(renderContext, ddgiData, data); - GBufferPass::SetInputs(renderContext.View, data.GBuffer); context->UpdateCB(_cb0, &data); context->BindCB(0, _cb0); } @@ -911,8 +912,9 @@ bool DynamicDiffuseGlobalIlluminationPass::Render(RenderContext& renderContext, #endif if (!render) { + // Bind constants if not set to draw DDGI Data0 data; - InitData0Shared(renderContext, *ddgiData, data); + InitData0(renderContext, *ddgiData, data); context->UpdateCB(_cb0, &data); context->BindCB(0, _cb0); } @@ -1060,8 +1062,9 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderReflections(RenderContext& rend PROFILE_GPU_CPU_NAMED("Specular Lighting"); if (!render) { + // Bind constants if not set to draw DDGI Data0 data; - InitData0Shared(renderContext, *ddgiData, data); + InitData0(renderContext, *ddgiData, data); context->UpdateCB(_cb0, &data); context->BindCB(0, _cb0); } diff --git a/Source/Shaders/GI/DDGI.hlsl b/Source/Shaders/GI/DDGI.hlsl index 9bbe0db53..454b5e9cb 100644 --- a/Source/Shaders/GI/DDGI.hlsl +++ b/Source/Shaders/GI/DDGI.hlsl @@ -160,7 +160,7 @@ float2 GetDDGIProbeUV(DDGIData data, uint cascadeIndex, uint probeIndex, float2 float probeTexelSize = resolution + 2.0f; float2 textureSize = float2(data.ProbesCounts.x * data.ProbesCounts.y, data.ProbesCounts.z * data.CascadesCount) * probeTexelSize; float2 uv = float2(coords.x * probeTexelSize, coords.y * probeTexelSize) + (probeTexelSize * 0.5f); - uv += octahedralCoords.xy * (resolution * 0.5f); + uv += octahedralCoords * (resolution * 0.5f); uv /= textureSize; return uv; } @@ -275,11 +275,11 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes { bool invalidCascade = cascade.CascadeIndex >= data.CascadesCount; cascade.CascadeIndex = min(cascade.CascadeIndex, data.CascadesCount - 1); + float2 octahedralCoords = GetOctahedralCoords(worldNormal); #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, cascade.CascadeIndex, 0, octahedralCoords, DDGI_PROBE_RESOLUTION_IRRADIANCE); float3 probeIrradiance = probesIrradiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; #if DDGI_SRGB_BLENDING == 1 @@ -301,7 +301,6 @@ float3 SampleDDGIIrradianceCascade(DDGIData data, Texture2D probes DDGIProbeSample probe = SampleDDGIProbe(data, probesData, probesDistance, worldPosition, worldNormal, cascade, base, i, fallbacks); // Sample irradiance texture - float2 octahedralCoords = GetOctahedralCoords(worldNormal); float2 uv = GetDDGIProbeUV(data, cascade.CascadeIndex, probe.ProbeIndex, octahedralCoords, DDGI_PROBE_RESOLUTION_IRRADIANCE); float3 probeIrradiance = probesIrradiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; #if DDGI_SRGB_BLENDING == 1 @@ -357,7 +356,33 @@ float3 DDGIHash3D(float3 p) return frac(frac(p.xxy * p.yzz) * 2.0 - 1.0); } -float3 SampleDDGISpecularCascade(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesRadiance, float3 worldPosition, float3 worldNormal, float3 reflection, DDGICascadeSampling cascade) +float3 FilterProbeRadiance(DDGIData data, Texture2D probesRadiance, float roughness, uint cascadeIndex, uint probeIndex, float3 direction) +{ + uint2 coords = GetDDGIProbeTexelCoords(data, cascadeIndex, probeIndex); + const float probeTexelSize = DDGI_PROBE_RESOLUTION_RADIANCE + 2.0f; + float2 uv = float2(coords.x * probeTexelSize, coords.y * probeTexelSize) + (probeTexelSize * 0.5f); + uv += GetOctahedralCoords(direction) * (DDGI_PROBE_RESOLUTION_RADIANCE * 0.5f); + float2 textureSize = float2(data.ProbesCounts.x * data.ProbesCounts.y, data.ProbesCounts.z * data.CascadesCount) * probeTexelSize; + uv /= textureSize; + float3 radiance = probesRadiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; + + // Box-blur filter for roughness (not physically correct, but cheap) + float2 uvBase = float2(coords.x * probeTexelSize, coords.y * probeTexelSize) + 1; // The first texel of the probe (skip 1px padding) + float2 uvMin = uvBase / textureSize; + float2 uvMax = (uvBase + float2(DDGI_PROBE_RESOLUTION_RADIANCE, DDGI_PROBE_RESOLUTION_RADIANCE)) / textureSize; + float2 filterSize = (float2)1.0f / textureSize; + float3 radiance00 = probesRadiance.SampleLevel(SamplerLinearClamp, clamp(uv - filterSize, uvMin, uvMax), 0).rgb; + float3 radiance10 = probesRadiance.SampleLevel(SamplerLinearClamp, clamp(uv + float2(filterSize.x, -filterSize.y), uvMin, uvMax), 0).rgb; + float3 radiance01 = probesRadiance.SampleLevel(SamplerLinearClamp, clamp(uv + float2(-filterSize.x, filterSize.y), uvMin, uvMax), 0).rgb; + float3 radiance11 = probesRadiance.SampleLevel(SamplerLinearClamp, clamp(uv + filterSize, uvMin, uvMax), 0).rgb; + float3 radianceRough = (radiance00 + radiance10 + radiance01 + radiance11) * 0.25f; + float alpha = (roughness - 0.4f) / 0.6f; // Remap [0.4; 1] to [0; 1] + radiance = lerp(radiance, radianceRough, saturate(alpha)); + + return radiance; +} + +float3 SampleDDGISpecularCascade(DDGIData data, Texture2D probesData, Texture2D probesDistance, Texture2D probesRadiance, float3 worldPosition, float3 worldNormal, float roughness, float3 reflection, DDGICascadeSampling cascade) { bool invalidCascade = cascade.CascadeIndex >= data.CascadesCount; cascade.CascadeIndex = min(cascade.CascadeIndex, data.CascadesCount - 1); @@ -365,10 +390,7 @@ float3 SampleDDGISpecularCascade(DDGIData data, Texture2D probesDa if (invalidCascade) { // Sample a special probe as a fallback for ambient sky reflection outside the last cascade - float2 octahedralCoords = GetOctahedralCoords(reflection); - float2 uv = GetDDGIProbeUV(data, cascade.CascadeIndex, 0, octahedralCoords, DDGI_PROBE_RESOLUTION_RADIANCE); - float3 probeRadiance = probesRadiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; - return probeRadiance; + return FilterProbeRadiance(data, probesRadiance, roughness, cascade.CascadeIndex, 0, reflection); } #endif @@ -390,11 +412,7 @@ float3 SampleDDGISpecularCascade(DDGIData data, Texture2D probesDa sampleVector += worldNoise; // Sample radiance texture - float2 octahedralCoords = GetOctahedralCoords(sampleVector); - float2 uv = GetDDGIProbeUV(data, cascade.CascadeIndex, probe.ProbeIndex, octahedralCoords, DDGI_PROBE_RESOLUTION_RADIANCE); - float3 probeRadiance = probesRadiance.SampleLevel(SamplerLinearClamp, uv, 0).rgb; - // TODO: filter radiance based on roughness - // TODO: or maybe build mip-chain for updated probes radiance texture and sample it based on roughness like env probes? + float3 probeRadiance = FilterProbeRadiance(data, probesRadiance, roughness, cascade.CascadeIndex, probe.ProbeIndex, sampleVector); // Accumulate weighted radiance totalRadiance += float4(probeRadiance * probe.Weights.x, probe.Weights.x); @@ -516,7 +534,7 @@ float3 SampleDDGISpecular(DDGIData data, Texture2D probesData, Tex // Sample cascade float3 reflection = reflect(normalize(worldPosition - data.ViewPos), worldNormal); - float3 specular = SampleDDGISpecularCascade(data, probesData, probesDistance, probesRadiance, worldPosition, worldNormal, reflection, cascade); + float3 specular = SampleDDGISpecularCascade(data, probesData, probesDistance, probesRadiance, worldPosition, worldNormal, roughness, reflection, cascade); return specular; } diff --git a/Source/Shaders/GI/DDGI.shader b/Source/Shaders/GI/DDGI.shader index 032e0210d..76c1ee5b5 100644 --- a/Source/Shaders/GI/DDGI.shader +++ b/Source/Shaders/GI/DDGI.shader @@ -52,9 +52,8 @@ float3 ViewDir; float TestValue; float3 QuantizationError; uint FrameIndexMod8; +float2 ResolveDitherScale; float2 Padding0; -float ResolveDitherScaleIndirect; -float ResolveDitherScaleSpecular; META_CB_END META_CB_BEGIN(1, Data1) @@ -994,7 +993,7 @@ float4 PS_IndirectLighting(Quad_VS2PS input) : SV_Target0 return float4(0, 0, 0, 0); // Sample irradiance - float dither = DDGI_GET_DITHER * ResolveDitherScaleIndirect; + float dither = DDGI_GET_DITHER * ResolveDitherScale.x; float3 samplePos = DDGI_GET_SAMPLE_POS; float3 irradiance = SampleDDGIIrradiance(DDGI, ProbesData, ProbesDistance, ProbesIrradiance, samplePos, gBuffer.Normal, DDGI_DEFAULT_BIAS, dither); @@ -1021,7 +1020,7 @@ float4 PS_SpecularLighting(Quad_VS2PS input) : SV_Target0 return float4(0, 0, 0, 0); // Sample specular reflection - float dither = DDGI_GET_DITHER * ResolveDitherScaleSpecular; + float dither = DDGI_GET_DITHER * ResolveDitherScale.y; float3 samplePos = DDGI_GET_SAMPLE_POS; float3 specular = SampleDDGISpecular(DDGI, ProbesData, ProbesDistance, ProbesRadiance, samplePos, gBuffer.Normal, gBuffer.Roughness, DDGI_DEFAULT_BIAS, dither);