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;
}
}