OMRVisualFoundation.AdaptivePixelation.cs
using Sandbox;
using System;
/// <summary>
/// Phase 8F3F2 depth-adaptive + edge-aware world pixelation controls.
///
/// Adaptive pixel sizes are authored as multipliers of the existing world
/// PixelateScale. This keeps Native vs Adaptive comparisons meaningful:
/// a multiplier of 1.0 matches the legacy/native-style world pixel block,
/// values above 1.0 are chunkier, and values below 1.0 are finer.
///
/// The profile supports 2-6 distance anchors. Only the first Point Count
/// anchors are evaluated; the remaining authored values are preserved so the
/// profile can be expanded/collapsed without losing tuning.
/// </summary>
public sealed partial class OMRVisualFoundation
{
public enum AdaptivePixelationDebugMode
{
Off = 0,
Depth = 1,
PixelBands = 2,
EdgeProtection = 3,
Unprotected = 4,
PassCheck = 5
}
public enum AdaptivePixelationInterpolationMode
{
Step = 0,
Linear = 1,
Smooth = 2
}
[Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Enable Adaptive Pixelation" )]
[Description( "Depth-adaptive world-only pixelation with depth/normal edge protection. Runs before Edge Cues and does not replace the existing Pixelate path." )]
public bool EnableAdaptivePixelation { get; set; } = false;
[Property, Group( "World Geometry/Adaptive Pixelation" ), Range( 0f, 1f ), Title( "Blend" )]
public float AdaptivePixelationBlend { get; set; } = 1f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile" ), Range( 2, 6 ), Title( "Point Count" )]
[Description( "How many distance/size anchors are active. The first 2-6 profile points below are used in order." )]
public int AdaptivePixelationPointCount { get; set; } = 3;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile" ), Title( "Interpolation" )]
[Description( "Step keeps hard distance zones. Linear blends directly. Smooth eases between anchors and is the recommended default." )]
public AdaptivePixelationInterpolationMode AdaptivePixelationInterpolation { get; set; } = AdaptivePixelationInterpolationMode.Smooth;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 1" ), Range( 0f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint1Distance { get; set; } = 0f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 1" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
[Description( "Multiplier relative to the existing World Pixelate Scale. 2.0 = twice as chunky as Native at this distance." )]
public float AdaptivePixelationPoint1Multiplier { get; set; } = 2f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 2" ), Range( 1f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint2Distance { get; set; } = 560f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 2" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
[Description( "1.0 matches the existing Native/legacy world pixel block at this distance." )]
public float AdaptivePixelationPoint2Multiplier { get; set; } = 1f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 3" ), Range( 2f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint3Distance { get; set; } = 1000f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 3" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
[Description( "0.5 targets half the Native block size. The final screen-space block cannot become smaller than one physical framebuffer pixel." )]
public float AdaptivePixelationPoint3Multiplier { get; set; } = 0.5f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 4" ), Range( 3f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint4Distance { get; set; } = 1400f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 4" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
public float AdaptivePixelationPoint4Multiplier { get; set; } = 0.4f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 5" ), Range( 4f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint5Distance { get; set; } = 2000f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 5" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
public float AdaptivePixelationPoint5Multiplier { get; set; } = 0.3f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 6" ), Range( 5f, 8192f ), Title( "Distance" )]
public float AdaptivePixelationPoint6Distance { get; set; } = 3000f;
[Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 6" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
public float AdaptivePixelationPoint6Multiplier { get; set; } = 0.25f;
[Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.001f, 0.5f ), Title( "Depth Threshold" )]
[Description( "Relative depth difference that starts protecting a pixel block from sampling across another depth layer." )]
public float AdaptivePixelationDepthThreshold { get; set; } = 0.035f;
[Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Title( "Use Normal Protection" )]
[Description( "Also rejects coarse samples that cross a strong G-buffer normal discontinuity." )]
public bool AdaptivePixelationUseNormalProtection { get; set; } = true;
[Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.001f, 1f ), Title( "Normal Threshold" )]
public float AdaptivePixelationNormalThreshold { get; set; } = 0.16f;
[Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0f, 1f ), Title( "Protection Strength" )]
[Description( "0 ignores detected boundaries. 1 fully prefers the safer fallback sample around protected edges." )]
public float AdaptivePixelationProtectionStrength { get; set; } = 1f;
[Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.1f, 1f ), Title( "Fallback Block Scale" )]
[Description( "When a coarse block crosses an edge, retry using this fraction of the current block size before falling back to the original pixel." )]
public float AdaptivePixelationFallbackScale { get; set; } = 0.5f;
[Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Pixelate Sky" )]
[Description( "When disabled, pixels without opaque scene depth keep the clean sky. Recommended for Standard." )]
public bool AdaptivePixelationPixelateSky { get; set; } = false;
[Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Debug View" )]
public AdaptivePixelationDebugMode AdaptivePixelationDebug { get; set; } = AdaptivePixelationDebugMode.Off;
[Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Native Pixelate" )]
public void UseNativePixelationForComparison()
{
EnablePixelate = true;
EnableAdaptivePixelation = false;
RefreshPixelationComparison();
}
[Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Adaptive Pixelate" )]
public void UseAdaptivePixelationForComparison()
{
EnablePixelate = false;
EnableAdaptivePixelation = true;
RefreshPixelationComparison();
}
[Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Both" )]
public void UseBothPixelationForComparison()
{
EnablePixelate = true;
EnableAdaptivePixelation = true;
RefreshPixelationComparison();
}
[Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Disable Both" )]
public void DisableBothPixelationForComparison()
{
EnablePixelate = false;
EnableAdaptivePixelation = false;
RefreshPixelationComparison();
}
[Button, Group( "World Geometry/Adaptive Pixelation" ), Title( "Reset Adaptive Pixelation" )]
public void ResetAdaptivePixelation()
{
EnableAdaptivePixelation = false;
AdaptivePixelationBlend = 1f;
AdaptivePixelationPointCount = 3;
AdaptivePixelationInterpolation = AdaptivePixelationInterpolationMode.Smooth;
AdaptivePixelationPoint1Distance = 0f;
AdaptivePixelationPoint1Multiplier = 2f;
AdaptivePixelationPoint2Distance = 560f;
AdaptivePixelationPoint2Multiplier = 1f;
AdaptivePixelationPoint3Distance = 1000f;
AdaptivePixelationPoint3Multiplier = 0.5f;
AdaptivePixelationPoint4Distance = 1400f;
AdaptivePixelationPoint4Multiplier = 0.4f;
AdaptivePixelationPoint5Distance = 2000f;
AdaptivePixelationPoint5Multiplier = 0.3f;
AdaptivePixelationPoint6Distance = 3000f;
AdaptivePixelationPoint6Multiplier = 0.25f;
AdaptivePixelationDepthThreshold = 0.035f;
AdaptivePixelationUseNormalProtection = true;
AdaptivePixelationNormalThreshold = 0.16f;
AdaptivePixelationProtectionStrength = 1f;
AdaptivePixelationFallbackScale = 0.5f;
AdaptivePixelationPixelateSky = false;
AdaptivePixelationDebug = AdaptivePixelationDebugMode.Off;
RebuildAdaptivePixelation();
}
private OMRAdaptivePixelationPostProcess _adaptivePixelationPostProcess;
private bool _adaptivePixelationPostProcessCreatedByFoundation;
private bool _adaptivePixelationReadyLogged;
private void RefreshPixelationComparison()
{
ResolveNativeEffects();
ApplySettings();
RebuildAdaptivePixelation();
RebuildRuntimeCommands();
}
private void RebuildAdaptivePixelation()
{
// Match Edge Cues: keep the authored scene clean in edit mode and create
// only the runtime helper while playing.
if ( !Game.IsPlaying )
{
DisableRuntimeAdaptivePixelation();
return;
}
CameraComponent camera = GetComponent<CameraComponent>( true );
if ( camera is null || !camera.IsValid )
return;
if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
{
_adaptivePixelationPostProcess = GetComponent<OMRAdaptivePixelationPostProcess>( true );
if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
{
_adaptivePixelationPostProcess = GameObject.AddComponent<OMRAdaptivePixelationPostProcess>( false );
_adaptivePixelationPostProcessCreatedByFoundation = true;
}
}
if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
return;
int pointCount = Math.Clamp( AdaptivePixelationPointCount, 2, 6 );
float d1 = MathF.Max( 0f, AdaptivePixelationPoint1Distance );
float d2 = MathF.Max( d1 + 1f, AdaptivePixelationPoint2Distance );
float d3 = MathF.Max( d2 + 1f, AdaptivePixelationPoint3Distance );
float d4 = MathF.Max( d3 + 1f, AdaptivePixelationPoint4Distance );
float d5 = MathF.Max( d4 + 1f, AdaptivePixelationPoint5Distance );
float d6 = MathF.Max( d5 + 1f, AdaptivePixelationPoint6Distance );
_adaptivePixelationPostProcess.Blend = MathX.Clamp( AdaptivePixelationBlend, 0f, 1f );
_adaptivePixelationPostProcess.BasePixelateScale = MathX.Clamp( PixelateScale, 0f, 1f );
_adaptivePixelationPostProcess.PointCount = pointCount;
_adaptivePixelationPostProcess.InterpolationMode = (int)AdaptivePixelationInterpolation;
_adaptivePixelationPostProcess.Point1Distance = d1;
_adaptivePixelationPostProcess.Point1Multiplier = MathX.Clamp( AdaptivePixelationPoint1Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.Point2Distance = d2;
_adaptivePixelationPostProcess.Point2Multiplier = MathX.Clamp( AdaptivePixelationPoint2Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.Point3Distance = d3;
_adaptivePixelationPostProcess.Point3Multiplier = MathX.Clamp( AdaptivePixelationPoint3Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.Point4Distance = d4;
_adaptivePixelationPostProcess.Point4Multiplier = MathX.Clamp( AdaptivePixelationPoint4Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.Point5Distance = d5;
_adaptivePixelationPostProcess.Point5Multiplier = MathX.Clamp( AdaptivePixelationPoint5Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.Point6Distance = d6;
_adaptivePixelationPostProcess.Point6Multiplier = MathX.Clamp( AdaptivePixelationPoint6Multiplier, 0.1f, 4f );
_adaptivePixelationPostProcess.DepthThreshold = MathX.Clamp( AdaptivePixelationDepthThreshold, 0.001f, 0.5f );
_adaptivePixelationPostProcess.UseNormalProtection = AdaptivePixelationUseNormalProtection;
_adaptivePixelationPostProcess.NormalThreshold = MathX.Clamp( AdaptivePixelationNormalThreshold, 0.001f, 1f );
_adaptivePixelationPostProcess.ProtectionStrength = MathX.Clamp( AdaptivePixelationProtectionStrength, 0f, 1f );
_adaptivePixelationPostProcess.FallbackScale = MathX.Clamp( AdaptivePixelationFallbackScale, 0.1f, 1f );
_adaptivePixelationPostProcess.PixelateSky = AdaptivePixelationPixelateSky;
_adaptivePixelationPostProcess.DebugMode = (int)AdaptivePixelationDebug;
bool debugActive = AdaptivePixelationDebug != AdaptivePixelationDebugMode.Off;
bool shouldRun = EnableAdaptivePixelation &&
(_adaptivePixelationPostProcess.Blend > 0.0001f || debugActive);
_adaptivePixelationPostProcess.Enabled = shouldRun;
if ( shouldRun && LogSetup && !_adaptivePixelationReadyLogged )
{
_adaptivePixelationReadyLogged = true;
float nativeBlock = 1f + 23f * _adaptivePixelationPostProcess.BasePixelateScale;
Log.Info(
$"[OMR VISUAL] Adaptive pixelation ready on '{GameObject.Name}' | " +
$"CameraPostProcessing:{camera.EnablePostProcessing} | Debug:{AdaptivePixelationDebug} | " +
$"Points:{pointCount} | NativeBase:{nativeBlock:0.##}px | " +
$"Multipliers:{_adaptivePixelationPostProcess.Point1Multiplier:0.##}/" +
$"{_adaptivePixelationPostProcess.Point2Multiplier:0.##}/" +
$"{_adaptivePixelationPostProcess.Point3Multiplier:0.##}"
);
}
if ( !shouldRun )
_adaptivePixelationReadyLogged = false;
}
private void DisableRuntimeAdaptivePixelation()
{
if ( _adaptivePixelationPostProcess is not null && _adaptivePixelationPostProcess.IsValid )
_adaptivePixelationPostProcess.Enabled = false;
_adaptivePixelationReadyLogged = false;
}
private void ReleaseAdaptivePixelation()
{
if ( _adaptivePixelationPostProcess is not null && _adaptivePixelationPostProcess.IsValid )
{
_adaptivePixelationPostProcess.Enabled = false;
if ( _adaptivePixelationPostProcessCreatedByFoundation )
_adaptivePixelationPostProcess.Destroy();
}
_adaptivePixelationPostProcess = null;
_adaptivePixelationPostProcessCreatedByFoundation = false;
_adaptivePixelationReadyLogged = false;
}
}