OMRAdaptivePixelationPostProcess.cs
using Sandbox;
using Sandbox.Rendering;
using System;

/// <summary>
/// Runtime world-only depth-adaptive, edge-aware pixelation pass.
///
/// The existing OMR PixelateScale is used as the reference block size so a
/// profile multiplier of 1.0 is directly comparable with the legacy/native-
/// style world pixelation. Up to six distance anchors can vary that block size
/// while depth/normal protection prevents coarse samples crossing silhouettes.
/// </summary>
[Title( "OMR Adaptive Pixelation Post Process" )]
[Category( "One More Round/Internal" )]
public sealed class OMRAdaptivePixelationPostProcess : BasePostProcess<OMRAdaptivePixelationPostProcess>
{
    private const string ShaderPath = "shaders/omr_adaptive_pixelation.shader";
    private const int RenderOrder = 800;

    public float Blend { get; set; } = 1f;
    public float BasePixelateScale { get; set; } = 0.1f;
    public int PointCount { get; set; } = 3;
    public int InterpolationMode { get; set; } = 2;

    public float Point1Distance { get; set; } = 0f;
    public float Point1Multiplier { get; set; } = 2f;
    public float Point2Distance { get; set; } = 560f;
    public float Point2Multiplier { get; set; } = 1f;
    public float Point3Distance { get; set; } = 1000f;
    public float Point3Multiplier { get; set; } = 0.5f;
    public float Point4Distance { get; set; } = 1400f;
    public float Point4Multiplier { get; set; } = 0.4f;
    public float Point5Distance { get; set; } = 2000f;
    public float Point5Multiplier { get; set; } = 0.3f;
    public float Point6Distance { get; set; } = 3000f;
    public float Point6Multiplier { get; set; } = 0.25f;

    public float DepthThreshold { get; set; } = 0.035f;
    public bool UseNormalProtection { get; set; } = true;
    public float NormalThreshold { get; set; } = 0.16f;
    public float ProtectionStrength { get; set; } = 1f;
    public float FallbackScale { get; set; } = 0.5f;
    public bool PixelateSky { get; set; } = false;

    public int DebugMode { get; set; } = 0;

    private Material _material;
    private bool _materialFailureLogged;

    public override void Render()
    {
        bool debugActive = DebugMode != 0;
        if ( Blend <= 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 adaptive-pixelation material from '{ShaderPath}'." );
                }

                return;
            }

            _materialFailureLogged = false;
        }

        int pointCount = Math.Clamp( PointCount, 2, 6 );
        int interpolationMode = Math.Clamp( InterpolationMode, 0, 2 );

        float d1 = MathF.Max( 0f, Point1Distance );
        float d2 = MathF.Max( d1 + 1f, Point2Distance );
        float d3 = MathF.Max( d2 + 1f, Point3Distance );
        float d4 = MathF.Max( d3 + 1f, Point4Distance );
        float d5 = MathF.Max( d4 + 1f, Point5Distance );
        float d6 = MathF.Max( d5 + 1f, Point6Distance );

        Attributes.Set( "OMRAdaptiveBlend", MathX.Clamp( Blend, 0f, 1f ) );
        Attributes.Set( "OMRAdaptiveBasePixelateScale", MathX.Clamp( BasePixelateScale, 0f, 1f ) );
        Attributes.Set( "OMRAdaptivePointCount", (float)pointCount );
        Attributes.Set( "OMRAdaptiveInterpolationMode", (float)interpolationMode );

        Attributes.Set( "OMRAdaptivePoint1Distance", d1 );
        Attributes.Set( "OMRAdaptivePoint1Multiplier", MathX.Clamp( Point1Multiplier, 0.1f, 4f ) );
        Attributes.Set( "OMRAdaptivePoint2Distance", d2 );
        Attributes.Set( "OMRAdaptivePoint2Multiplier", MathX.Clamp( Point2Multiplier, 0.1f, 4f ) );
        Attributes.Set( "OMRAdaptivePoint3Distance", d3 );
        Attributes.Set( "OMRAdaptivePoint3Multiplier", MathX.Clamp( Point3Multiplier, 0.1f, 4f ) );
        Attributes.Set( "OMRAdaptivePoint4Distance", d4 );
        Attributes.Set( "OMRAdaptivePoint4Multiplier", MathX.Clamp( Point4Multiplier, 0.1f, 4f ) );
        Attributes.Set( "OMRAdaptivePoint5Distance", d5 );
        Attributes.Set( "OMRAdaptivePoint5Multiplier", MathX.Clamp( Point5Multiplier, 0.1f, 4f ) );
        Attributes.Set( "OMRAdaptivePoint6Distance", d6 );
        Attributes.Set( "OMRAdaptivePoint6Multiplier", MathX.Clamp( Point6Multiplier, 0.1f, 4f ) );

        Attributes.Set( "OMRAdaptiveDepthThreshold", MathX.Clamp( DepthThreshold, 0.001f, 0.5f ) );
        Attributes.Set( "OMRAdaptiveUseNormals", UseNormalProtection ? 1f : 0f );
        Attributes.Set( "OMRAdaptiveNormalThreshold", MathX.Clamp( NormalThreshold, 0.001f, 1f ) );
        Attributes.Set( "OMRAdaptiveProtectionStrength", MathX.Clamp( ProtectionStrength, 0f, 1f ) );
        Attributes.Set( "OMRAdaptiveFallbackScale", MathX.Clamp( FallbackScale, 0.1f, 1f ) );
        Attributes.Set( "OMRAdaptivePixelateSky", PixelateSky ? 1f : 0f );
        Attributes.Set( "OMRAdaptiveDebugMode", (float)DebugMode );

        // Order 800 intentionally runs immediately before OMR Edge Cues (900).
        // Pixelation simplifies world color first while Edge Cues still reads
        // the untouched scene depth/G-buffer to reinforce spatial separation.
        var blit = BlitMode.WithBackbuffer(
            _material,
            Stage.BeforePostProcess,
            RenderOrder,
            false
        );

        Blit( blit, "OMR Adaptive Pixelation" );
    }
}