UI/OMRScopeLensPanel.razor
@using System;
@using System.Globalization;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
<root>
@if ( ShouldRenderLens )
{
<div class="lens-window">
@if ( UsesOrientationTransition )
{
<OMRScopeFeedPanel class="lens-feed lens-feed-hip" style="@HipFeedStyle" SourceTexture=@ScopeTexture />
<OMRScopeFeedPanel class="lens-feed lens-feed-ads" style="@AdsFeedStyle" SourceTexture=@ScopeTexture />
}
else
{
<OMRScopeFeedPanel class="lens-feed" style="@SingleFeedStyle" SourceTexture=@ScopeTexture />
}
<div class="lens-erect-transition" style="@LensErectTransitionStyle"></div>
<div class="lens-transmission" style="@LensTransmissionStyle"></div>
<div class="lens-tint" style="@LensTintStyle"></div>
<div class="lens-vignette" style="@LensVignetteStyle"></div>
<div class="lens-reflection" style="@LensReflectionStyle"></div>
@if ( ShowReticle )
{
<OMRScopeReticlePanel class="scope-reticle"
ReticleStyle=@(Profile?.SimpleReticleStyle ?? OMRScopeReticleStyle.FineCross)
ReticleColor=@(Profile?.SimpleReticleColor ?? Color.Black)
Thickness=@(Profile?.SimpleReticleThickness ?? 1.5f)
Gap=@(Profile?.SimpleReticleGap ?? 8f)
LineLength=@(Profile?.SimpleReticleLineLength ?? 72f)
DotSize=@(Profile?.SimpleReticleDotSize ?? 3f)
ReticleOpacity=@ReticleOpacity
OutlineWidth=@(Profile?.SimpleReticleOutlineWidth ?? 0f)
OutlineOpacity=@(Profile?.SimpleReticleOutlineOpacity ?? 0f) />
}
</div>
}
</root>
@code
{
private OMRScopePiPCamera _pipCamera;
private OMRScopePiPCamera PiPCamera
{
get
{
if ( _pipCamera is not null && _pipCamera.IsValid )
return _pipCamera;
_pipCamera = Scene?
.GetAllComponents<OMRScopePiPCamera>()
.FirstOrDefault( x => x is not null && x.IsValid );
return _pipCamera;
}
}
private IWeaponOpticState OpticState => PiPCamera?.ActiveOpticState;
private OMROpticProfile Profile => PiPCamera?.ActiveProfile;
private Texture ScopeTexture => PiPCamera?.OutputTexture;
private bool ShouldRenderLens =>
PiPCamera is not null &&
PiPCamera.IsRendering &&
OpticState is not null &&
Profile is not null &&
Profile.IsValid &&
Profile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture &&
ScopeTexture is not null &&
ScopeTexture.IsValid;
private bool ShowReticle =>
Profile?.ShowSimpleReticle ?? true;
private bool UsesOrientationTransition =>
Profile is not null &&
Profile.IsValid &&
Profile.HipPreviewOrientation != Profile.AdsPreviewOrientation;
private float LensErectBlend
{
get
{
if ( Profile is null || !Profile.IsValid || !UsesOrientationTransition )
return 1f;
float start = MathX.Clamp( Profile.LensErectStart, 0f, 1f );
float full = MathX.Clamp( Profile.LensErectFull, start, 1f );
float aim = OpticState?.AimFraction ?? 0f;
if ( full <= start + 0.0001f )
return aim >= full ? 1f : 0f;
return MathX.Clamp( (aim - start) / (full - start), 0f, 1f );
}
}
private float FeedZoomScale =>
MathX.Clamp( PiPCamera?.PresentationZoomScale ?? 1f, 1f, 20f );
private static string OrientationTransform(
OMRScopePreviewOrientation orientation,
float zoom
)
{
float z = MathX.Clamp( zoom, 1f, 20f );
string sx = Css( z );
string sy = sx;
return orientation switch
{
OMRScopePreviewOrientation.Rotate180 => $"scale(-{sx},-{sy})",
OMRScopePreviewOrientation.FlipHorizontal => $"scale(-{sx},{sy})",
OMRScopePreviewOrientation.FlipVertical => $"scale({sx},-{sy})",
_ => $"scale({sx},{sy})"
};
}
private string SingleFeedStyle =>
$"transform:{OrientationTransform( Profile?.AdsPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";
private string HipFeedStyle =>
$"opacity:{Css( 1f - LensErectBlend )};transform:{OrientationTransform( Profile?.HipPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";
private string AdsFeedStyle =>
$"opacity:{Css( LensErectBlend )};transform:{OrientationTransform( Profile?.AdsPreviewOrientation ?? OMRScopePreviewOrientation.Upright, FeedZoomScale )};";
private string LensErectTransitionStyle
{
get
{
if ( Profile is null || !Profile.IsValid || !UsesOrientationTransition )
return "opacity:0;";
float midpoint = 4f * LensErectBlend * (1f - LensErectBlend);
float opacity = midpoint * MathX.Clamp( Profile.LensErectTransitionDarkening, 0f, 0.75f );
return $"background-color:rgba(0,0,0,1);opacity:{Css( opacity )};";
}
}
private float ReticleOpacity =>
Profile is null || !Profile.IsValid
? 1f
: MathX.Lerp(
MathX.Clamp( Profile.ReticleHipOpacity, 0f, 1f ),
MathX.Clamp( Profile.ReticleAdsOpacity, 0f, 1f ),
LensClarityBlend
);
private float LensClarityBlend
{
get
{
if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
return 1f;
float start = MathX.Clamp( Profile.LensClarityStart, 0f, 1f );
float full = MathX.Clamp( Profile.LensClarityFull, start, 1f );
if ( full <= start + 0.0001f )
return OpticState?.AimFraction >= full ? 1f : 0f;
return MathX.Clamp(
((OpticState?.AimFraction ?? 0f) - start) / (full - start),
0f,
1f
);
}
}
private float OpticalLerp( float hip, float ads ) =>
MathX.Lerp( hip, ads, LensClarityBlend );
private string LensTransmissionStyle
{
get
{
if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
return "opacity:0;";
// Do not put a CSS filter directly on OMRScopeFeedPanel. The panel draws a
// live GPU render target in OnDraw; forcing that through an off-screen UI
// filter pass proved unreliable and could collapse the lens to a flat gray
// surface. Simulate light transmission with a simple black veil here, while
// blur/saturation/contrast remain authored for the eventual lens shader.
float brightness = MathX.Clamp(
OpticalLerp( Profile.LensHipBrightness, Profile.LensAdsBrightness ),
0f,
1f
);
float opacity = 1f - brightness;
return $"background-color:rgba(0,0,0,1);opacity:{Css( opacity )};";
}
}
private string LensTintStyle
{
get
{
if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
return "opacity:0;";
float opacity = OpticalLerp( Profile.LensHipTintOpacity, Profile.LensAdsTintOpacity );
return $"background-color:{Profile.LensTintColor.Rgba};opacity:{Css( opacity )};";
}
}
private string LensVignetteStyle
{
get
{
if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
return "opacity:0;";
float baseDarkness = MathX.Clamp(
OpticalLerp( Profile.LensHipEdgeDarkening, Profile.LensAdsEdgeDarkening ),
0f,
1f
);
float eyeBoxShadow = 0f;
float centerX = 50f;
float centerY = 50f;
if ( Profile.EnableEyeBoxSimulation && PiPCamera is not null )
{
float misalignment = 1f - MathX.Clamp( PiPCamera.CurrentEyeBoxAlignment, 0f, 1f );
eyeBoxShadow = MathX.Clamp( Profile.EyeBoxShadowStrength, 0f, 1f ) * misalignment;
Vector2 offset = PiPCamera.CurrentEyeBoxOffset;
float shift = MathX.Clamp( Profile.EyeBoxGradientShiftPercent, 0f, 35f );
centerX = MathX.Clamp( 50f + offset.x * shift, 15f, 85f );
centerY = MathX.Clamp( 50f - offset.y * shift, 15f, 85f );
}
// Combine the authored edge darkening with physical off-axis scope shadow
// without simply adding two opacities past 1. Positioned radial gradients are
// GPU-rendered by current S&box UI, so the clear exit-pupil region can drift
// toward the physical optic as the eye moves off-axis.
float darkness = 1f - (1f - baseDarkness) * (1f - eyeBoxShadow);
return
$"background-image:radial-gradient(circle at {Css( centerX )}% {Css( centerY )}%," +
$"rgba(0,0,0,0),rgba(0,0,0,{Css( darkness )}));";
}
}
private string LensReflectionStyle
{
get
{
if ( Profile is null || !Profile.IsValid || !Profile.EnableLensSimulation )
return "opacity:0;";
float opacity = OpticalLerp( Profile.LensHipReflectionOpacity, Profile.LensAdsReflectionOpacity );
return $"opacity:{Css( opacity )};";
}
}
private static string Css( float value ) =>
value.ToString( "0.###", CultureInfo.InvariantCulture );
protected override int BuildHash()
{
HashCode hash = new();
Texture texture = ScopeTexture;
hash.Add( ShouldRenderLens );
hash.Add( texture?.Index ?? -1 );
hash.Add( PiPCamera?.PresentationZoomScale ?? 1f );
hash.Add( PiPCamera?.CurrentEyeBoxAlignment ?? 1f );
hash.Add( PiPCamera?.CurrentEyeBoxOffset ?? Vector2.Zero );
hash.Add( Profile?.ResourceName );
hash.Add( OpticState?.AimFraction ?? 0f );
hash.Add( Profile?.EnableLensSimulation );
hash.Add( Profile?.LensClarityStart );
hash.Add( Profile?.LensClarityFull );
hash.Add( Profile?.LensHipBlur );
hash.Add( Profile?.LensAdsBlur );
hash.Add( Profile?.LensHipBrightness );
hash.Add( Profile?.LensAdsBrightness );
hash.Add( Profile?.LensHipSaturation );
hash.Add( Profile?.LensAdsSaturation );
hash.Add( Profile?.LensHipContrast );
hash.Add( Profile?.LensAdsContrast );
hash.Add( Profile?.LensTintColor );
hash.Add( Profile?.LensHipTintOpacity );
hash.Add( Profile?.LensAdsTintOpacity );
hash.Add( Profile?.LensHipEdgeDarkening );
hash.Add( Profile?.LensAdsEdgeDarkening );
hash.Add( Profile?.LensHipReflectionOpacity );
hash.Add( Profile?.LensAdsReflectionOpacity );
hash.Add( Profile?.ShowSimpleReticle );
hash.Add( Profile?.HipPreviewOrientation );
hash.Add( Profile?.AdsPreviewOrientation );
hash.Add( Profile?.LensErectStart );
hash.Add( Profile?.LensErectFull );
hash.Add( Profile?.LensErectTransitionDarkening );
hash.Add( Profile?.EnableEyeBoxSimulation );
hash.Add( Profile?.EyeBoxPerfectAngleDegrees );
hash.Add( Profile?.EyeBoxFullShadowAngleDegrees );
hash.Add( Profile?.EyeBoxShadowStrength );
hash.Add( Profile?.EyeBoxGradientShiftPercent );
hash.Add( Profile?.ReticleHipOpacity );
hash.Add( Profile?.ReticleAdsOpacity );
hash.Add( Profile?.SimpleReticleOutlineWidth );
hash.Add( Profile?.SimpleReticleOutlineOpacity );
hash.Add( Profile?.SimpleReticleStyle );
hash.Add( Profile?.SimpleReticleColor );
hash.Add( Profile?.SimpleReticleThickness );
hash.Add( Profile?.SimpleReticleGap );
hash.Add( Profile?.SimpleReticleLineLength );
hash.Add( Profile?.SimpleReticleDotSize );
return hash.ToHashCode();
}
}