OMRVisualFoundation.AmbientOcclusion.cs
using Sandbox;
using System;

/// <summary>
/// Phase 8F3E2: native S&box ambient occlusion / contact-depth controls.
///
/// The effect intentionally uses Sandbox.AmbientOcclusion rather than a custom
/// OMR shader. S&box's implementation runs its GTAO pass after the depth
/// prepass and publishes the result through the renderer's AmbientOcclusion
/// pipeline texture, so Standard-shaded world materials consume it naturally.
///
/// At runtime OMR creates a helper AO component only when necessary. In the
/// editor, use Setup AO Preview once if WYSIWYG camera-preview tuning is useful;
/// the resulting native component is an ordinary authored scene component.
/// </summary>
public sealed partial class OMRVisualFoundation
{
    [Property, Group( "World Geometry/Ambient Occlusion" ), Title( "Enable Ambient Occlusion" )]
    [Description( "Native screen-space ambient occlusion used as contact depth for corners, floor contacts and nearby overlapping world geometry." )]
    public bool EnableAmbientOcclusion { get; set; } = false;

    [Property, Group( "World Geometry/Ambient Occlusion" ), Range( 0f, 1f ), Title( "Intensity" )]
    [Description( "Darkening strength. This does not control AO quality or sample count." )]
    public float AmbientOcclusionIntensity { get; set; } = 0.32f;

    [Property, Group( "World Geometry/Ambient Occlusion" ), Range( 1, 512 ), Title( "Radius (world units)" )]
    [Description( "Maximum world-space sample distance. Smaller values emphasize local contact depth; larger values produce broader occlusion." )]
    public int AmbientOcclusionRadius { get; set; } = 64;

    [Property, Group( "World Geometry/Ambient Occlusion" ), Range( 0.01f, 1f ), Title( "Falloff Range" )]
    [Description( "Softens sample influence toward the outer edge of the radius. Lower values concentrate the effect closer to contacts." )]
    public float AmbientOcclusionFalloffRange { get; set; } = 0.55f;

    [Property, Group( "World Geometry/Ambient Occlusion" ), Range( 0f, 5f ), Title( "Thin Compensation" )]
    [Description( "Reduces false occlusion from thin/incomplete screen-space geometry. The engine default is 5." )]
    public float AmbientOcclusionThinCompensation { get; set; } = 5f;

    [Button, Group( "World Geometry/Ambient Occlusion" ), Title( "Contact Preset" )]
    public void ApplyAmbientOcclusionContactPreset()
    {
        EnableAmbientOcclusion = true;
        AmbientOcclusionIntensity = 0.32f;
        AmbientOcclusionRadius = 64;
        AmbientOcclusionFalloffRange = 0.55f;
        AmbientOcclusionThinCompensation = 5f;

        RebuildAmbientOcclusion();
    }

    [Button, Group( "World Geometry/Ambient Occlusion" ), Title( "Reset Ambient Occlusion" )]
    public void ResetAmbientOcclusion()
    {
        EnableAmbientOcclusion = false;
        AmbientOcclusionIntensity = 0.32f;
        AmbientOcclusionRadius = 64;
        AmbientOcclusionFalloffRange = 0.55f;
        AmbientOcclusionThinCompensation = 5f;

        RebuildAmbientOcclusion();
    }

    [Button, Group( "World Geometry/Ambient Occlusion" ), Title( "Setup AO Preview" )]
    [Description( "Adds/reuses the native S&box AmbientOcclusion component on this camera so AO can also be previewed outside Play mode." )]
    public void SetupAmbientOcclusionPreview()
    {
        CameraComponent camera = GetComponent<CameraComponent>( true );
        if ( camera is null || !camera.IsValid )
        {
            Log.Warning(
                "[OMR VISUAL] AO preview setup failed: OMRVisualFoundation must share a GameObject with the gameplay CameraComponent."
            );
            return;
        }

        _ambientOcclusion = GetComponent<AmbientOcclusion>( true );

        if ( _ambientOcclusion is null || !_ambientOcclusion.IsValid )
            _ambientOcclusion = GameObject.AddComponent<AmbientOcclusion>( false );

        // This is an explicit editor action, so the component becomes authored
        // scene state rather than a runtime helper owned by OMRVisualFoundation.
        _ambientOcclusionCreatedByFoundation = false;
        ApplyAmbientOcclusionSettings();

        Log.Info(
            $"[OMR VISUAL] Native AO preview ready on '{GameObject.Name}' | " +
            $"Enabled:{_ambientOcclusion.Enabled} | Intensity:{_ambientOcclusion.Intensity:0.##} | " +
            $"Radius:{_ambientOcclusion.Radius}"
        );
    }

    private AmbientOcclusion _ambientOcclusion;
    private bool _ambientOcclusionCreatedByFoundation;
    private bool _ambientOcclusionReadyLogged;

    private void RebuildAmbientOcclusion()
    {
        CameraComponent camera = GetComponent<CameraComponent>( true );
        if ( camera is null || !camera.IsValid )
            return;

        if ( _ambientOcclusion is null || !_ambientOcclusion.IsValid )
            _ambientOcclusion = GetComponent<AmbientOcclusion>( true );

        // Never mutate the authored scene just because ExecuteInEditor is
        // ticking. Outside Play mode an AO component is created only through
        // the explicit Setup AO Preview button above.
        if ( !Game.IsPlaying )
        {
            if ( _ambientOcclusion is not null && _ambientOcclusion.IsValid )
                ApplyAmbientOcclusionSettings();

            return;
        }

        if (
            EnableAmbientOcclusion &&
            (_ambientOcclusion is null || !_ambientOcclusion.IsValid)
        )
        {
            _ambientOcclusion = GameObject.AddComponent<AmbientOcclusion>( false );
            _ambientOcclusionCreatedByFoundation = true;
        }

        if ( _ambientOcclusion is null || !_ambientOcclusion.IsValid )
            return;

        ApplyAmbientOcclusionSettings();

        if ( EnableAmbientOcclusion && LogSetup && !_ambientOcclusionReadyLogged )
        {
            _ambientOcclusionReadyLogged = true;
            Log.Info(
                $"[OMR VISUAL] Native AO active on '{GameObject.Name}' | " +
                $"CameraPostProcessing:{camera.EnablePostProcessing} | " +
                $"Intensity:{_ambientOcclusion.Intensity:0.##} | " +
                $"Radius:{_ambientOcclusion.Radius} | " +
                $"Falloff:{_ambientOcclusion.FalloffRange:0.##} | " +
                $"ThinComp:{_ambientOcclusion.ThinCompensation:0.##}"
            );
        }

        if ( !EnableAmbientOcclusion )
            _ambientOcclusionReadyLogged = false;
    }

    private void ApplyAmbientOcclusionSettings()
    {
        if ( _ambientOcclusion is null || !_ambientOcclusion.IsValid )
            return;

        _ambientOcclusion.Intensity = Math.Clamp( AmbientOcclusionIntensity, 0f, 1f );
        _ambientOcclusion.Radius = Math.Clamp( AmbientOcclusionRadius, 1, 512 );
        _ambientOcclusion.FalloffRange = Math.Clamp( AmbientOcclusionFalloffRange, 0.01f, 1f );
        _ambientOcclusion.ThinCompensation = Math.Clamp( AmbientOcclusionThinCompensation, 0f, 5f );
        _ambientOcclusion.Enabled = EnableAmbientOcclusion;
    }

    private void ReleaseAmbientOcclusion()
    {
        if ( _ambientOcclusion is not null && _ambientOcclusion.IsValid )
        {
            if ( _ambientOcclusionCreatedByFoundation )
            {
                _ambientOcclusion.Enabled = false;
                _ambientOcclusion.Destroy();
            }
            else if ( Game.IsPlaying )
            {
                // An authored AO component belongs to the scene. Leave it in
                // place but return it to disabled when this controller stops.
                _ambientOcclusion.Enabled = false;
            }
        }

        _ambientOcclusion = null;
        _ambientOcclusionCreatedByFoundation = false;
        _ambientOcclusionReadyLogged = false;
    }
}