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;