OMREdgeCuePostProcess.cs
using Sandbox;
using Sandbox.Rendering;
using System;

/// <summary>
/// Runtime-only native S&box post-process bridge for OMR world geometry edge cues.
///
/// OMRVisualFoundation creates/configures this component on the authored gameplay
/// camera while the scene is playing. Because the later OMR viewmodel composite
/// camera has post-processing disabled and does not own this component, the pass
/// remains world-only.
/// </summary>
[Title( "OMR Edge Cue Post Process" )]
[Category( "One More Round/Internal" )]
public sealed class OMREdgeCuePostProcess : BasePostProcess<OMREdgeCuePostProcess>
{
    private const string ShaderPath = "shaders/omr_edge_cues.shader";
    private const int RenderOrder = 900;

    public float Strength { get; set; } = 0.16f;
    public float DepthStrength { get; set; } = 1f;
    public float NormalStrength { get; set; } = 0.7f;
    public float DepthThreshold { get; set; } = 0.018f;
    public float NormalThreshold { get; set; } = 0.08f;
    public float SampleRadius { get; set; } = 1f;
    public float FadeStart { get; set; } = 450f;
    public float FadeEnd { get; set; } = 1450f;
    public int DebugMode { get; set; } = 0;

    private Material _material;
    private bool _materialFailureLogged;

    public override void Render()
    {
        bool debugActive = DebugMode != 0;
        if ( Strength <= 0.0001f && !debugActive )
            return;

        if ( _material is null || !_material.IsValid )
        {
            _material = Material.FromShader( ShaderPath );
            if ( _material is null || !_material.IsValid )
            {
                if ( !_materialFailureLogged )
                {
                    _materialFailureLogged = true;
                    Log.Warning( $"[OMR VISUAL] Could not create edge-cue post-process material from '{ShaderPath}'." );
                }

                return;
            }

            _materialFailureLogged = false;
        }

        Attributes.Set( "OMREdgeStrength", MathX.Clamp( Strength, 0f, 1f ) );
        Attributes.Set( "OMREdgeDepthStrength", MathX.Clamp( DepthStrength, 0f, 2f ) );
        Attributes.Set( "OMREdgeNormalStrength", MathX.Clamp( NormalStrength, 0f, 2f ) );
        Attributes.Set( "OMREdgeDepthThreshold", MathX.Clamp( DepthThreshold, 0.001f, 0.25f ) );
        Attributes.Set( "OMREdgeNormalThreshold", MathX.Clamp( NormalThreshold, 0.001f, 1f ) );
        Attributes.Set( "OMREdgeSampleRadius", MathX.Clamp( SampleRadius, 1f, 3f ) );
        Attributes.Set( "OMREdgeFadeStart", MathF.Max( 0f, FadeStart ) );
        Attributes.Set( "OMREdgeFadeEnd", MathF.Max( FadeStart + 1f, FadeEnd ) );
        Attributes.Set( "OMREdgeDebugMode", (float)DebugMode );

        // Use S&box's supported post-process path. BeforePostProcess keeps the
        // world depth pyramid / G-buffer available and WithBackbuffer provides
        // the current world color as the shader's ColorBuffer input.
        var blit = BlitMode.WithBackbuffer(
            _material,
            Stage.BeforePostProcess,
            RenderOrder,
            false
        );

        Blit( blit, "OMR World Edge Cues" );
    }
}