OMRVisualFoundation.Tonemapping.cs
using Sandbox;
/// <summary>
/// Phase 8F3F4: native S&box tonemapping A/B controller for the gameplay world.
///
/// Tonemapping is deliberately kept as the engine-native post-process. It runs
/// at Stage.Tonemapping, after OMR's current world Bloom / Adaptive Pixelation /
/// Edge Cues chain. Auto exposure is OFF by default so comparisons between
/// curves are stable and reproducible.
///
/// This first pass affects the gameplay/world camera only. OMR's independent
/// viewmodel capture/composite remains untouched so we can evaluate the world
/// curve in isolation before deciding whether presentation layers should share
/// a matching final transform.
/// </summary>
public sealed partial class OMRVisualFoundation
{
[Property, Group( "World Look/Tonemapping" ), Title( "Enable Tonemapping" )]
[Description( "Native HDR-to-display tonemapping for the gameplay world. Keep Auto Exposure off while comparing operators." )]
public bool EnableTonemapping { get; set; } = false;
[Property, Group( "World Look/Tonemapping" ), Title( "Mode" )]
public Sandbox.Tonemapping.TonemappingMode TonemappingMode { get; set; } =
Sandbox.Tonemapping.TonemappingMode.HableFilmic;
[Property, Group( "World Look/Tonemapping" ), Title( "Exposure Method" )]
[Description( "Primarily relevant to Hable Filmic. RGB is S&box's default." )]
public Sandbox.Tonemapping.ExposureColorSpaceEnum TonemappingExposureMethod { get; set; } =
Sandbox.Tonemapping.ExposureColorSpaceEnum.RGB;
[Property, Group( "World Look/Tonemapping/Auto Exposure" ), Title( "Enable Auto Exposure" )]
[Description( "OFF is recommended for visual A/B testing. When enabled, the camera adapts exposure over time and can obscure differences between tonemapping curves." )]
public bool TonemappingAutoExposure { get; set; } = false;
[Property, Group( "World Look/Tonemapping/Auto Exposure" ), Range( 0f, 3f ), Title( "Minimum Exposure" )]
public float TonemappingMinimumExposure { get; set; } = 1f;
[Property, Group( "World Look/Tonemapping/Auto Exposure" ), Range( 0f, 5f ), Title( "Maximum Exposure" )]
public float TonemappingMaximumExposure { get; set; } = 3f;
[Property, Group( "World Look/Tonemapping/Auto Exposure" ), Range( -5f, 5f ), Title( "Exposure Compensation" )]
public float TonemappingExposureCompensation { get; set; } = 0f;
[Property, Group( "World Look/Tonemapping/Auto Exposure" ), Range( 1f, 10f ), Title( "Adaptation Rate" )]
public float TonemappingRate { get; set; } = 1f;
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "Hable Filmic" )]
public void UseHableTonemapping()
{
EnableTonemapping = true;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.HableFilmic;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "AgX" )]
public void UseAgXTonemapping()
{
EnableTonemapping = true;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.AgX;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "Reinhard-Jodie" )]
public void UseReinhardJodieTonemapping()
{
EnableTonemapping = true;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.ReinhardJodie;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "ACES" )]
public void UseACESTonemapping()
{
EnableTonemapping = true;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.ACES;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "Linear Reference" )]
public void UseLinearTonemapping()
{
EnableTonemapping = true;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.Linear;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping/A-B" ), Title( "Disable Tonemapping" )]
public void DisableTonemapping()
{
EnableTonemapping = false;
RebuildTonemapping();
}
[Button, Group( "World Look/Tonemapping" ), Title( "Setup Tonemapping Preview" )]
[Description( "Adds/reuses Sandbox.Tonemapping on the gameplay camera for editor preview and runtime world tonemapping." )]
public void SetupTonemappingPreview()
{
CameraComponent camera = GetComponent<CameraComponent>( true );
if ( camera is null || !camera.IsValid )
{
Log.Warning(
"[OMR VISUAL] Tonemapping preview setup failed: OMRVisualFoundation must share a GameObject with the gameplay CameraComponent."
);
return;
}
ResolveTonemappingComponent();
if ( _tonemapping is null || !_tonemapping.IsValid )
{
_tonemapping = GameObject.AddComponent<Sandbox.Tonemapping>( false );
_tonemappingCreatedByFoundation = false;
}
ApplyTonemappingSettings();
Log.Info(
$"[OMR VISUAL] Native Tonemapping ready on '{GameObject.Name}' | " +
$"Enabled:{_tonemapping.Enabled} | Mode:{_tonemapping.Mode} | " +
$"AutoExposure:{_tonemapping.AutoExposureEnabled}"
);
}
private Sandbox.Tonemapping _tonemapping;
private bool _tonemappingCreatedByFoundation;
private bool _tonemappingReadyLogged;
private void ResetTonemappingSettings()
{
EnableTonemapping = false;
TonemappingMode = Sandbox.Tonemapping.TonemappingMode.HableFilmic;
TonemappingExposureMethod = Sandbox.Tonemapping.ExposureColorSpaceEnum.RGB;
TonemappingMinimumExposure = 1f;
TonemappingMaximumExposure = 3f;
TonemappingExposureCompensation = 0f;
TonemappingRate = 1f;
}
private void ResolveTonemappingComponent()
{
if ( _tonemapping is not null && _tonemapping.IsValid )
return;
_tonemapping = GetComponent<Sandbox.Tonemapping>( true );
_tonemappingCreatedByFoundation = false;
}
private void RebuildTonemapping()
{
CameraComponent camera = GetComponent<CameraComponent>( true );
if ( camera is null || !camera.IsValid )
return;
ResolveTonemappingComponent();
// Do not silently author a component in edit mode. The phase patch adds
// one to main.scene and the explicit Setup button remains available for
// other scenes/cameras.
if ( !Game.IsPlaying )
{
if ( _tonemapping is not null && _tonemapping.IsValid )
ApplyTonemappingSettings();
return;
}
if (
EnableTonemapping &&
(_tonemapping is null || !_tonemapping.IsValid)
)
{
_tonemapping = GameObject.AddComponent<Sandbox.Tonemapping>( false );
_tonemappingCreatedByFoundation = true;
}
if ( _tonemapping is null || !_tonemapping.IsValid )
return;
ApplyTonemappingSettings();
if ( _tonemapping.Enabled && LogSetup && !_tonemappingReadyLogged )
{
_tonemappingReadyLogged = true;
Log.Info(
$"[OMR VISUAL] Native world Tonemapping active on '{GameObject.Name}' | " +
$"Mode:{_tonemapping.Mode} | AutoExposure:{_tonemapping.AutoExposureEnabled} | " +
$"CameraPostProcessing:{camera.EnablePostProcessing}"
);
}
if ( !_tonemapping.Enabled )
_tonemappingReadyLogged = false;
}
private void ApplyTonemappingSettings()
{
if ( _tonemapping is null || !_tonemapping.IsValid )
return;
_tonemapping.Mode = TonemappingMode;
_tonemapping.ExposureMethod = TonemappingExposureMethod;
_tonemapping.AutoExposureEnabled = TonemappingAutoExposure;
float minimum = MathX.Clamp( TonemappingMinimumExposure, 0f, 3f );
float maximum = MathX.Clamp( TonemappingMaximumExposure, 0f, 5f );
if ( maximum < minimum )
maximum = minimum;
_tonemapping.MinimumExposure = minimum;
_tonemapping.MaximumExposure = maximum;
_tonemapping.ExposureCompensation = MathX.Clamp( TonemappingExposureCompensation, -5f, 5f );
_tonemapping.Rate = MathX.Clamp( TonemappingRate, 1f, 10f );
_tonemapping.Enabled = EnableWorldLook && EnableTonemapping;
}
private void ReleaseTonemapping()
{
if ( _tonemapping is not null && _tonemapping.IsValid )
{
_tonemapping.Enabled = false;
if ( _tonemappingCreatedByFoundation )
_tonemapping.Destroy();
}
_tonemapping = null;
_tonemappingCreatedByFoundation = false;
_tonemappingReadyLogged = false;
}
}