OMRVisualFoundation.EdgeCues.cs
using Sandbox;
using System;
/// <summary>
/// Optional screen-space geometry separation for the gameplay world.
///
/// Phase 8F3E1 Hotfix 2 uses S&box's native BasePostProcess pipeline rather
/// than a raw camera CommandList. OMRVisualFoundation remains the authored
/// source of truth; a runtime-only OMREdgeCuePostProcess mirrors these values
/// onto the gameplay/world camera while the scene is playing.
/// </summary>
public sealed partial class OMRVisualFoundation
{
public enum EdgeCueDebugMode
{
Off = 0,
Depth = 1,
Normals = 2,
Combined = 3,
PassCheck = 4,
RawDepth = 5,
RawNormals = 6
}
[Property, Group( "World Geometry/Edge Cues" ), Title( "Enable Edge Cues" )]
[Description( "Adds subtle world-only separation from scene depth and G-buffer normals. Viewmodel, physical scope surfaces and HUD are excluded by render order." )]
public bool EnableEdgeCues { get; set; } = false;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 1f ), Title( "Overall Strength" )]
public float EdgeCueStrength { get; set; } = 1f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 2f ), Title( "Depth Strength" )]
public float EdgeCueDepthStrength { get; set; } = 2f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 2f ), Title( "Normal Strength" )]
public float EdgeCueNormalStrength { get; set; } = 1f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0.001f, 0.25f ), Title( "Depth Threshold" )]
[Description( "Relative view-depth discontinuity required before an edge contributes. Lower values detect smaller depth changes." )]
public float EdgeCueDepthThreshold { get; set; } = 0.15f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0.001f, 1f ), Title( "Normal Threshold" )]
[Description( "World-normal discontinuity required before an edge contributes. Lower values react to shallower corners." )]
public float EdgeCueNormalThreshold { get; set; } = 0.11f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 1f, 3f ), Title( "Sample Radius (px)" )]
[Description( "Distance in screen pixels used by the four-neighbour edge detector. Start at 1; larger values produce broader, less subtle separation." )]
public float EdgeCueSampleRadius { get; set; } = 1.4f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 0f, 4096f ), Title( "Distance Fade Start" )]
public float EdgeCueFadeStart { get; set; } = 1000f;
[Property, Group( "World Geometry/Edge Cues" ), Range( 1f, 4096f ), Title( "Distance Fade End" )]
[Description( "Edge cues fade away with view distance so haze/fog can still hide distant geometry instead of being defeated by a screen-space outline." )]
public float EdgeCueFadeEnd { get; set; } = 1500f;
[Property, Group( "World Geometry/Edge Cues" ), Title( "Debug View" )]
[Description( "Pass Check must turn the world magenta. Raw Depth and Raw Normals expose the renderer inputs directly. Depth/Normals/Combined show the detected edges." )]
public EdgeCueDebugMode EdgeCueDebug { get; set; } = EdgeCueDebugMode.Off;
[Button, Group( "World Geometry/Edge Cues" ), Title( "Reset Edge Cues" )]
public void ResetEdgeCues()
{
EnableEdgeCues = false;
EdgeCueStrength = 1f;
EdgeCueDepthStrength = 2f;
EdgeCueNormalStrength = 1f;
EdgeCueDepthThreshold = 0.15f;
EdgeCueNormalThreshold = 0.11f;
EdgeCueSampleRadius = 1.4f;
EdgeCueFadeStart = 1000f;
EdgeCueFadeEnd = 1500f;
EdgeCueDebug = EdgeCueDebugMode.Off;
RebuildEdgeCueCommands();
}
private OMREdgeCuePostProcess _edgeCuePostProcess;
private bool _edgeCuePostProcessCreatedByFoundation;
private bool _edgeCueReadyLogged;
/// <summary>
/// Kept under the original method name so OMRVisualFoundation.cs does not
/// need another lifecycle refactor. It now synchronizes a native
/// BasePostProcess component instead of rebuilding a raw CommandList.
/// </summary>
private void RebuildEdgeCueCommands()
{
// OMRVisualFoundation executes in editor. Do not mutate the authored
// scene by creating an internal helper component outside Play mode.
if ( !Game.IsPlaying )
{
DisableRuntimeEdgeCuePostProcess();
return;
}
CameraComponent camera = GetComponent<CameraComponent>( true );
if ( camera is null || !camera.IsValid )
return;
if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
{
_edgeCuePostProcess = GetComponent<OMREdgeCuePostProcess>( true );
if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
{
_edgeCuePostProcess = GameObject.AddComponent<OMREdgeCuePostProcess>( false );
_edgeCuePostProcessCreatedByFoundation = true;
}
}
if ( _edgeCuePostProcess is null || !_edgeCuePostProcess.IsValid )
return;
float fadeStart = MathF.Max( 0f, EdgeCueFadeStart );
float fadeEnd = MathF.Max( fadeStart + 1f, EdgeCueFadeEnd );
_edgeCuePostProcess.Strength = MathX.Clamp( EdgeCueStrength, 0f, 1f );
_edgeCuePostProcess.DepthStrength = MathX.Clamp( EdgeCueDepthStrength, 0f, 2f );
_edgeCuePostProcess.NormalStrength = MathX.Clamp( EdgeCueNormalStrength, 0f, 2f );
_edgeCuePostProcess.DepthThreshold = MathX.Clamp( EdgeCueDepthThreshold, 0.001f, 0.25f );
_edgeCuePostProcess.NormalThreshold = MathX.Clamp( EdgeCueNormalThreshold, 0.001f, 1f );
_edgeCuePostProcess.SampleRadius = MathX.Clamp( EdgeCueSampleRadius, 1f, 3f );
_edgeCuePostProcess.FadeStart = fadeStart;
_edgeCuePostProcess.FadeEnd = fadeEnd;
_edgeCuePostProcess.DebugMode = (int)EdgeCueDebug;
bool debugActive = EdgeCueDebug != EdgeCueDebugMode.Off;
bool shouldRun = EnableEdgeCues &&
(_edgeCuePostProcess.Strength > 0.0001f || debugActive);
_edgeCuePostProcess.Enabled = shouldRun;
if ( shouldRun && LogSetup && !_edgeCueReadyLogged )
{
_edgeCueReadyLogged = true;
Log.Info(
$"[OMR VISUAL] Edge cues using native post-process on '{GameObject.Name}' | " +
$"CameraPostProcessing:{camera.EnablePostProcessing} | Debug:{EdgeCueDebug}"
);
}
if ( !shouldRun )
_edgeCueReadyLogged = false;
}
private void DisableRuntimeEdgeCuePostProcess()
{
if ( _edgeCuePostProcess is not null && _edgeCuePostProcess.IsValid )
_edgeCuePostProcess.Enabled = false;
_edgeCueReadyLogged = false;
}
private void ReleaseEdgeCuePass()
{
if ( _edgeCuePostProcess is not null && _edgeCuePostProcess.IsValid )
{
_edgeCuePostProcess.Enabled = false;
// Only destroy helpers that this foundation created at runtime.
// An authored component, should one ever be added manually, is left alone.
if ( _edgeCuePostProcessCreatedByFoundation )
_edgeCuePostProcess.Destroy();
}
_edgeCuePostProcess = null;
_edgeCuePostProcessCreatedByFoundation = false;
_edgeCueReadyLogged = false;
}
}