Add option to render DDGI indirect lighting in half-res with bilateral upscaling

Cuts irradiance resolving time in half at cost of some aliasing. Ideal for low-quality settings or when rendering at high native res.
This commit is contained in:
2026-06-27 00:44:20 +02:00
parent 1a2d86826d
commit 7f403658df
8 changed files with 176 additions and 24 deletions
@@ -35,6 +35,7 @@ void GlobalIlluminationSettings::BlendWith(GlobalIlluminationSettings& other, fl
BLEND_FLOAT(Distance);
BLEND_FLOAT(IndirectShadowsStrength);
BLEND_COL(FallbackIrradiance);
BLEND_ENUM(IndirectResolution);
}
void BloomSettings::BlendWith(BloomSettings& other, float weight)
+13 -2
View File
@@ -339,10 +339,15 @@ API_ENUM(Attributes="Flags") enum class GlobalIlluminationSettingsOverride : int
/// </summary>
IndirectShadowsStrength = 1 << 6,
/// <summary>
/// Overrides <see cref="GlobalIlluminationSettings.IndirectResolution"/> property.
/// </summary>
IndirectResolution = 1 << 7,
/// <summary>
/// All properties.
/// </summary>
All = Mode | Intensity | TemporalResponse | Distance | FallbackIrradiance | BounceIntensity | IndirectShadowsStrength,
All = Mode | Intensity | TemporalResponse | Distance | FallbackIrradiance | BounceIntensity | IndirectShadowsStrength | IndirectResolution,
};
/// <summary>
@@ -393,7 +398,7 @@ API_STRUCT() struct FLAXENGINE_API GlobalIlluminationSettings : ISerializable
/// <summary>
/// Indirect lighting shadows intensity. Default is 1 for fully opaque shadowing, lower values bleed the lighting into shadowed areas. Can be sued for artistic control over GI.
/// </summary>
API_FIELD(Attributes = "EditorOrder(35), Limit(0.0f, 1.0f, 0.001f), PostProcessSetting((int)GlobalIlluminationSettingsOverride.IndirectShadowsStrength)")
API_FIELD(Attributes="EditorOrder(35), Limit(0.0f, 1.0f, 0.001f), PostProcessSetting((int)GlobalIlluminationSettingsOverride.IndirectShadowsStrength)")
float IndirectShadowsStrength = 1.0f;
/// <summary>
@@ -402,6 +407,12 @@ API_STRUCT() struct FLAXENGINE_API GlobalIlluminationSettings : ISerializable
API_FIELD(Attributes="EditorOrder(40), PostProcessSetting((int)GlobalIlluminationSettingsOverride.FallbackIrradiance)")
Color FallbackIrradiance = Color::Transparent;
/// <summary>
/// The indirect lighting render resolution. Full gives better quality, but half improves performance.
/// </summary>
API_FIELD(Attributes="EditorOrder(50), PostProcessSetting((int)GlobalIlluminationSettingsOverride.IndirectResolution)")
ResolutionMode IndirectResolution = ResolutionMode::Full;
public:
/// <summary>
/// Blends the settings using given weight.
@@ -26,6 +26,7 @@
#include "Engine/Graphics/Shaders/GPUShader.h"
#include "Engine/Level/Actors/BrushMode.h"
#include "Engine/Renderer/GBufferPass.h"
#include "Engine/Renderer/Utils/MultiScaler.h"
// Implementation based on:
// "Dynamic Diffuse Global Illumination with Ray-Traced Irradiance Probes", Journal of Computer Graphics Tools, April 2019
@@ -292,15 +293,20 @@ bool DynamicDiffuseGlobalIlluminationPass::setupResources()
auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
if (!_psIndirectLighting[0])
{
_psIndirectLighting[0] = device->CreatePipelineState();
_psIndirectLighting[1] = device->CreatePipelineState();
for (auto& pso : _psIndirectLighting)
pso = device->CreatePipelineState();
psDesc.PS = shader->GetPS("PS_IndirectLighting");
if (_psIndirectLighting[2]->Init(psDesc))
return true;
psDesc.BlendMode = BlendingMode::Add;
if (_psIndirectLighting[0]->Init(psDesc))
return true;
psDesc.PS = shader->GetPS("PS_IndirectLighting", 1);
if (_psIndirectLighting[1]->Init(psDesc))
return true;
psDesc.BlendMode = BlendingMode::Opaque;
if (_psIndirectLighting[3]->Init(psDesc))
return true;
}
return false;
@@ -320,8 +326,7 @@ void DynamicDiffuseGlobalIlluminationPass::OnShaderReloading(Asset* obj)
_csTraceRays[3] = nullptr;
_csUpdateProbesIrradiance = nullptr;
_csUpdateProbesDistance = nullptr;
SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[0]);
SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[1]);
SAFE_DELETE_GPU_RESOURCES(_psIndirectLighting)
invalidateResources();
}
@@ -335,8 +340,7 @@ void DynamicDiffuseGlobalIlluminationPass::Dispose()
_cb0 = nullptr;
_cb1 = nullptr;
_shader = nullptr;
SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[0]);
SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting[1]);
SAFE_DELETE_GPU_RESOURCES(_psIndirectLighting)
#if GPU_ENABLE_DEVELOPMENT
_debugModel = nullptr;
_debugMaterial = nullptr;
@@ -856,10 +860,30 @@ bool DynamicDiffuseGlobalIlluminationPass::Render(RenderContext& renderContext,
context->BindSR(4, ddgiData.Result.ProbesData);
context->BindSR(5, ddgiData.Result.ProbesDistance);
context->BindSR(6, ddgiData.Result.ProbesIrradiance);
context->SetViewportAndScissors(renderContext.View.ScreenSize.X, renderContext.View.ScreenSize.Y);
context->SetRenderTarget(lightBuffer);
context->SetState(_psIndirectLighting[Graphics::GICascadesBlending ? 1 : 0]);
context->DrawFullscreenTriangle();
auto& settings = renderContext.List->Settings.GlobalIllumination;
if (settings.IndirectResolution == ResolutionMode::Full || !MultiScaler::Instance()->IsReady())
{
// Full-res
context->SetViewportAndScissors(renderContext.View.ScreenSize.X, renderContext.View.ScreenSize.Y);
context->SetRenderTarget(lightBuffer);
context->SetState(_psIndirectLighting[Graphics::GICascadesBlending ? 1 : 0]);
context->DrawFullscreenTriangle();
}
else
{
// Upscale
auto width = RenderTools::GetResolution((int32)renderContext.View.ScreenSize.X, settings.IndirectResolution);
auto height = RenderTools::GetResolution((int32)renderContext.View.ScreenSize.Y, settings.IndirectResolution);
auto temp = RenderTargetPool::Get(GPUTextureDescription::New2D(width, height, lightBuffer->GetFormat(), GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget));
context->SetViewportAndScissors((float)width, (float)height);
context->SetRenderTarget(temp->View());
context->SetState(_psIndirectLighting[Graphics::GICascadesBlending ? 3 : 2]);
context->DrawFullscreenTriangle();
context->ResetRenderTarget();
MultiScaler::Instance()->BilateralUpscale(context, Viewport(Float2(renderContext.View.ScreenSize)), temp, lightBuffer, renderContext.Buffers->DepthBuffer, renderContext.Buffers->GBuffer1, BlendingMode::Add);
RenderTargetPool::Release(temp);
context->SetViewportAndScissors(renderContext.View.ScreenSize.X, renderContext.View.ScreenSize.Y);
}
}
#if GPU_ENABLE_DEVELOPMENT
@@ -48,7 +48,7 @@ private:
GPUShaderProgramCS* _csTraceRays[4];
GPUShaderProgramCS* _csUpdateProbesIrradiance;
GPUShaderProgramCS* _csUpdateProbesDistance;
GPUPipelineState* _psIndirectLighting[2] = {};
GPUPipelineState* _psIndirectLighting[4] = {};
#if GPU_ENABLE_DEVELOPMENT
AssetReference<Model> _debugModel;
AssetReference<MaterialBase> _debugMaterial;
+48 -1
View File
@@ -94,7 +94,9 @@ void MultiScaler::Dispose()
RendererPass::Dispose();
// Cleanup
SAFE_DELETE_GPU_RESOURCE(_psUpscale);
for (const auto& e : _psBilateralUpscale)
e.Value->ReleaseGPU();
_psBilateralUpscale.ClearDelete();
_psBlur5.Delete();
_psBlur9.Delete();
_psBlur13.Delete();
@@ -335,3 +337,48 @@ void MultiScaler::Upscale(GPUContext* context, const Viewport& viewport, GPUText
context->ResetRenderTarget();
}
void MultiScaler::BilateralUpscale(GPUContext* context, const Viewport& viewport, GPUTexture* src, GPUTextureView* dst, GPUTexture* depth, GPUTexture* normal, const BlendingMode& blendMode)
{
PROFILE_GPU_CPU("Bilateral Upscale");
auto rtAction = GPUDrawPassAction::Store;
GPUDrawPass drawPass(context, ToSpan(&dst, 1), ToSpan(&rtAction, 1));
context->SetViewportAndScissors(viewport);
if (checkIfSkipPass())
{
context->Draw(src);
}
else if (depth && normal)
{
GPUPipelineState* pso;
if (!_psBilateralUpscale.TryGet(blendMode, pso))
{
pso = GPUDevice::Instance->CreatePipelineState();
auto desc = GPUPipelineState::Description::DefaultFullscreenTriangle;
desc.PS = _shader->GPU->GetPS("PS_BilateralUpscale");
desc.BlendMode = blendMode;
pso->Init(desc);
_psBilateralUpscale.Add(blendMode, pso);
}
Data data;
data.TexelSize.X = 1.0f / (float)src->Width();
data.TexelSize.Y = 1.0f / (float)src->Height();
auto cb = _shader->GPU->GetCB(0);
context->UpdateCB(cb, &data);
context->BindCB(0, cb);
context->BindSR(0, src);
context->BindSR(1, depth);
context->BindSR(2, normal);
context->SetState(pso);
context->DrawFullscreenTriangle();
context->UnBindCB(0);
}
else
{
Upscale(context, viewport, src, dst);
}
context->ResetRenderTarget();
}
+18 -2
View File
@@ -3,7 +3,7 @@
#pragma once
#include "../RendererPass.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Graphics/GPUPipelineStatePermutations.h"
/// <summary>
@@ -18,6 +18,7 @@ private:
GPUPipelineStatePermutationsPs<2> _psBlur13;
GPUPipelineStatePermutationsPs<3> _psHalfDepth;
GPUPipelineState* _psUpscale = nullptr;
Dictionary<BlendingMode, GPUPipelineState*> _psBilateralUpscale;
public:
/// <summary>
@@ -83,7 +84,7 @@ public:
void BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ);
/// <summary>
/// Upscales the texture.
/// Upscales the texture using Catmull-Rom filtering with 9-taps.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="viewport">The viewport of the destination texture.</param>
@@ -91,6 +92,18 @@ public:
/// <param name="dst">The destination texture.</param>
void Upscale(GPUContext* context, const Viewport& viewport, GPUTexture* src, GPUTextureView* dst);
/// <summary>
/// Upscales the texture using bilateral filtering (depth+normal weighting).
/// </summary>
/// <param name="context">The context.</param>
/// <param name="viewport">The viewport of the destination texture.</param>
/// <param name="src">The source texture.</param>
/// <param name="dst">The destination texture.</param>
/// <param name="depth">The depth buffer (hardware depth).</param>
/// <param name="normal">The normal buffer (RGB encoded for GBuffer).</param>
/// <param name="blendMode">The blending mode to use when drawing the image. Can be used to composite upscaled image in additive mode (eg. into lighting/reflections buffer).</param>
void BilateralUpscale(GPUContext* context, const Viewport& viewport, GPUTexture* src, GPUTextureView* dst, GPUTexture* depth, GPUTexture* normal, const BlendingMode& blendMode = BlendingMode::Opaque);
public:
// [RendererPass]
String ToString() const override;
@@ -99,6 +112,9 @@ public:
#if COMPILE_WITH_DEV_ENV
void OnShaderReloading(Asset* obj)
{
for (const auto& e : _psBilateralUpscale)
e.Value->ReleaseGPU();
_psBilateralUpscale.ClearDelete();
_psUpscale->ReleaseGPU();
_psBlur5.Release();
_psBlur9.Release();
+3 -8
View File
@@ -969,20 +969,15 @@ Texture2D<float4> ProbesIrradiance : register(t6);
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)
float4 PS_IndirectLighting(Quad_VS2PS input) : SV_Target0
{
output = 0;
// Sample GBuffer
GBufferSample gBuffer = SampleGBuffer(GBuffer, input.TexCoord);
// Check if cannot shadow pixel
BRANCH
if (gBuffer.ShadingModel == SHADING_MODEL_UNLIT)
{
discard;
return;
}
return float4(0, 0, 0, 0);
// Sample irradiance
float dither = RandN2(input.TexCoord + TemporalTime).x;
@@ -992,7 +987,7 @@ void PS_IndirectLighting(Quad_VS2PS input, out float4 output : SV_Target0)
// Calculate lighting
float3 diffuseColor = GetDiffuseColor(gBuffer);
float3 diffuse = Diffuse_Lambert(diffuseColor);
output.rgb = diffuse * irradiance * gBuffer.AO;
return float4(diffuse * irradiance * gBuffer.AO, 1);
}
#endif
+58
View File
@@ -212,3 +212,61 @@ float4 PS_Upscale(Quad_VS2PS input) : SV_Target0
// Catmull-Rom filtering with 9-taps
return SampleTextureCatmullRom(Input, SamplerLinearClamp, input.TexCoord, TexelSize);
}
#ifdef _PS_BilateralUpscale
#include "./Flax/GBufferCommon.hlsl"
Texture2D Depths : register(t1);
Texture2D Normals : register(t2);
// Pixel Shader for upscaling image with bilateral filtering (depth and normal weighting)
META_PS(true, FEATURE_LEVEL_ES2)
float4 PS_BilateralUpscale(Quad_VS2PS input) : SV_Target0
{
const float epsilon = 0.0001f;
float2 inputSize = floor(1.0f / TexelSize);
float2 baseUV = floor(input.TexCoord * inputSize - 0.5f) / inputSize + 0.5f * TexelSize;
float2 bilinear = (input.TexCoord - baseUV) * inputSize;
float4 weights = float4((1 - bilinear.y) * (1 - bilinear.x), (1 - bilinear.y) * bilinear.x, bilinear.y * (1 - bilinear.x), bilinear.y * bilinear.x);
float4 values00 = SAMPLE_RT_LINEAR(Input, baseUV);
float4 values10 = SAMPLE_RT_LINEAR(Input, baseUV + float2(TexelSize.x, 0));
float4 values01 = SAMPLE_RT_LINEAR(Input, baseUV + float2(0, TexelSize.y));
float4 values11 = SAMPLE_RT_LINEAR(Input, baseUV + TexelSize);
float depth00 = SAMPLE_RT_DEPTH(Depths, baseUV);
float depth10 = SAMPLE_RT_DEPTH(Depths, baseUV + float2(TexelSize.x, 0));
float depth01 = SAMPLE_RT_DEPTH(Depths, baseUV + float2(0, TexelSize.y));
float depth11 = SAMPLE_RT_DEPTH(Depths, baseUV + TexelSize);
float minDepth = min(min(min(depth00, depth10), depth01), depth11);
float maxDepth = max(max(max(depth00, depth10), depth01), depth11);
if (maxDepth / minDepth > 1.021f)
{
float depth = SAMPLE_RT_DEPTH(Depths, input.TexCoord);
weights *= 1.0f / (abs(float4(depth00, depth10, depth01, depth11) - depth.xxxx) + epsilon);
}
float3 normal00 = DecodeNormal(SAMPLE_RT_LINEAR(Normals, baseUV).rgb);
float3 normal10 = DecodeNormal(SAMPLE_RT_LINEAR(Normals, baseUV + float2(TexelSize.x, 0)).rgb);
float3 normal01 = DecodeNormal(SAMPLE_RT_LINEAR(Normals, baseUV + float2(0, TexelSize.y)).rgb);
float3 normal11 = DecodeNormal(SAMPLE_RT_LINEAR(Normals, baseUV + TexelSize).rgb);
float3 normal = DecodeNormal(SAMPLE_RT(Normals, input.TexCoord).rgb);
float normalPower = 2;
weights.x *= min(pow(saturate(dot(normal, normal00)), normalPower), 1.0);
weights.y *= min(pow(saturate(dot(normal, normal10)), normalPower), 1.0);
weights.z *= min(pow(saturate(dot(normal, normal01)), normalPower), 1.0);
weights.w *= min(pow(saturate(dot(normal, normal11)), normalPower), 1.0);
values00 *= weights.x;
values10 *= weights.y;
values01 *= weights.z;
values11 *= weights.w;
return (values00 + values10 + values01 + values11) / (dot(weights, 1) + epsilon);
}
#endif