using Sandbox.Rendering; namespace Sandbox; /// /// Adds an approximation of ambient occlusion using Screen Space Ambient Occlusion (SSAO). /// It darkens areas where ambient light is generally occluded from such as corners, crevices /// and surfaces that are close to each other. /// [Expose] [Title( "Ambient Occlusion (SSAO)" )] [Category( "Post Processing" )] [Icon( "contrast" )] public sealed partial class AmbientOcclusion : BasePostProcess { public override int ComponentVersion => 1; // This is too high level, the convars should be controlling num samples and that shit [ConVar( "r_ao_quality", Min = 0, Max = 3, Help = "Ambient occlusion quality (0: off, 1: low, 2: med, 3: high)" )] internal static int UserQuality { get; set; } = 3; /// /// The intensity of the darkening effect. Has no impact on performance. /// [Property, Range( 0, 1 ), Category( "Properties" )] public float Intensity { get; set; } = 1.0f; /// /// Maximum distance of samples from pixel when determining its occlusion, in world units. /// [Property, Range( 1, 512 ), Category( "Properties" )] public int Radius { get; set; } = 128; /// /// Gently reduce sample impact as it gets out of the effect's radius bounds /// [Property, Range( 0.01f, 1.0f ), Category( "Properties" )] public float FalloffRange { get; set; } = 1.0f; /// /// How we should denoise the effect /// [Property, Category( "Quality" )] public DenoiseModes DenoiseMode { get; set; } = DenoiseModes.Temporal; /// /// Slightly reduce impact of samples further back to counter the bias from depth-based (incomplete) input scene geometry data /// [Property, Category( "Quality" ), Range( 0.0f, 5.0f )] public float ThinCompensation { get; set; } = 5.0f; int Frame = 0; private struct GTAOConstants { public Vector2Int ViewportSize; // Unused with Command Lists public Vector2 ViewportPixelSize; // Unused with Command Lists public Vector2 DepthUnpackConsts; public Vector2 CameraTanHalfFOV; public Vector2 NDCToViewMul; public Vector2 NDCToViewAdd; public Vector2 NDCToViewMul_x_PixelSize; public float EffectRadius; // world (viewspace) maximum size of the shadow public float EffectFalloffRange; public float RadiusMultiplier = 1.457f; public float TAABlendAmount = 0; public float FinalValuePower = 2.2f; // modifies the final ambient occlusion value using power function - this allows some of the above heuristics to do different things public float DenoiseBlurBeta = 1.5f; public float SampleDistributionPower = 2.0f; // small crevices more important than big surfaces public float ThinOccluderCompensation = 0.0f; // the new 'thickness heuristic' approach public float DepthMIPSamplingOffset = 3.30f; // main trade-off between performance (memory bandwidth) and quality (temporal stability is the first affected, thin objects next) public int NoiseIndex = 0; // frameIndex % 64 if using TAA or 0 otherwise public GTAOConstants() { } }; enum GTAOPasses { ViewDepthChain, // XeGTAO depth filter does average depth, a bit similar to our depth chain MainPass, DenoiseSpatial, DenoiseTemporal } //------------------------------------------------------------------------- public enum DenoiseModes { /// /// Applies spatial denoising to reduce noise by averaging pixel values within a local neighborhood. /// This method smooths out noise by considering the spatial relationship between pixels in a single frame. /// [Icon( "filter_center_focus" )] Spatial, /// /// Applies temporal denoising to reduce noise by averaging pixel values over multiple frames. /// This method leverages the temporal coherence of consecutive frames to achieve a noise-free result. /// [Icon( "auto_awesome_motion" )] Temporal } GTAOConstants GetGTAOConstants() { var consts = new GTAOConstants(); // The above is calculated on shader now consts.ViewportSize = Vector2Int.Zero; consts.ViewportPixelSize = Vector2.Zero; consts.DepthUnpackConsts = Vector2.Zero; consts.CameraTanHalfFOV = Vector2.Zero; consts.NDCToViewMul = Vector2.Zero; consts.NDCToViewAdd = Vector2.Zero; consts.NDCToViewMul_x_PixelSize = Vector2.Zero; //------------------------------------------------------------------------- consts.EffectRadius = GetWeighted( x => x.Radius, 128.0f ); consts.EffectFalloffRange = GetWeighted( x => x.FalloffRange, 1.0f ); consts.DenoiseBlurBeta = 1.2f; // Used only on Spatial denoising consts.NoiseIndex = DenoiseMode == DenoiseModes.Temporal ? Frame % 64 : 0; consts.ThinOccluderCompensation = ThinCompensation; consts.FinalValuePower = GetWeighted( x => x.Intensity, 1.0f ) * 5.0f; switch ( UserQuality ) { case 1: consts.TAABlendAmount = 0.95f; break; case 2: consts.TAABlendAmount = 0.9f; break; case 3: consts.TAABlendAmount = 0.8f; break; } return consts; } CommandList commands = new CommandList( "Ambient Occlusion" ); public override void Render() { commands.Reset(); RenderTargetHandle ViewDepthChainTexture = commands.GetRenderTarget( "ViewDepthChainTexture", ImageFormat.R32F, numMips: 5 ); RenderTargetHandle WorkingEdgesTexture = commands.GetRenderTarget( "WorkingEdgesTexture", ImageFormat.R16F ); RenderTargetHandle WorkingAOTexture = commands.GetRenderTarget( "WorkingAOTexture", ImageFormat.A8 ); RenderTargetHandle AOTexture0 = commands.GetRenderTarget( "AOTexture0", ImageFormat.A8 ); RenderTargetHandle AOTexture1 = commands.GetRenderTarget( "AOTexture1", ImageFormat.A8 ); bool pingPong = (Frame++ % 2) == 0; var AOTextureCurrent = pingPong ? AOTexture0 : AOTexture1; var AOTexturePrev = pingPong ? AOTexture1 : AOTexture0; var csAO = new ComputeShader( "gtao_cs" ); commands.Attributes.SetData( "GTAOConstants", GetGTAOConstants() ); // // Bind textures to the compute shader commands.Attributes.Set( "WorkingDepthMIP0", ViewDepthChainTexture.ColorTexture, 0 ); commands.Attributes.Set( "WorkingDepthMIP1", ViewDepthChainTexture.ColorTexture, 1 ); commands.Attributes.Set( "WorkingDepthMIP2", ViewDepthChainTexture.ColorTexture, 2 ); commands.Attributes.Set( "WorkingDepthMIP3", ViewDepthChainTexture.ColorTexture, 3 ); commands.Attributes.Set( "WorkingDepthMIP4", ViewDepthChainTexture.ColorTexture, 4 ); commands.Attributes.Set( "WorkingDepth", ViewDepthChainTexture.ColorTexture ); commands.Attributes.Set( "WorkingAOTerm", WorkingAOTexture.ColorTexture ); commands.Attributes.Set( "WorkingEdges", WorkingEdgesTexture.ColorTexture ); commands.Attributes.Set( "FinalAOTerm", AOTextureCurrent.ColorTexture ); commands.Attributes.Set( "FinalAOTermPrev", AOTexturePrev.ColorTexture ); commands.Attributes.SetCombo( "D_QUALITY", (UserQuality - 1).Clamp( 0, 2 ) ); // View depth chain { commands.Attributes.SetCombo( "D_PASS", GTAOPasses.ViewDepthChain ); commands.DispatchCompute( csAO, AOTextureCurrent.Size ); } // Main pass { commands.Attributes.SetCombo( "D_PASS", GTAOPasses.MainPass ); commands.DispatchCompute( csAO, AOTextureCurrent.Size ); } // Denoise { commands.Attributes.SetCombo( "D_PASS", DenoiseMode == DenoiseModes.Temporal ? GTAOPasses.DenoiseTemporal : GTAOPasses.DenoiseSpatial ); commands.DispatchCompute( csAO, AOTextureCurrent.Size ); } commands.ResourceBarrierTransition( AOTextureCurrent, ResourceState.PixelShaderResource ); // // Finally pass the AO as a texture for the rest of the pipeline // Technically uses previous frame texture since it'll be applied next frame // We could try to parent rather than merging attributes but it's causing race conditions from managed size and more complex to manage // commands.GlobalAttributes.Set( "ScreenSpaceAmbientOcclusionTexture", AOTextureCurrent.ColorIndex ); InsertCommandList( commands, Stage.AfterDepthPrepass, 0, "Ambient Occlusion" ); } }