OMRViewModelCamera.cs
using Sandbox;
using System;
using System.Linq;
/// <summary>
/// Local-only first-person camera stack for One More Round.
///
/// Phase 8F2A keeps the proven low-near-plane projection but captures ordinary
/// arms/weapon into a transparent render target. A later screen camera preserves
/// the world color, receives OMR's world-look + viewmodel composite command
/// lists, renders physical scope surfaces directly, then owns the HUD.
/// </summary>
[Title( "OMR ViewModel Camera" )]
public sealed class OMRViewModelCamera : Component, ICameraModifier
{
private const string DefaultViewModelTag = "viewmodel";
private const string DefaultScopeSurfaceTag = "viewmodel_scope_surface";
private const string DefaultLightTag = "light";
[Property, Group( "References" )] public CameraComponent SourceCamera { get; set; }
[Property, Group( "Projection" ), Range( 0.25f, 5f )] public float ViewModelZNear { get; set; } = 1f;
[Property, Group( "Projection" ), Range( 1, 10 )] public int PriorityOffset { get; set; } = 1;
[Property, Group( "Projection" )] public bool MatchSourceFieldOfView { get; set; } = true;
[Property, Group( "Projection" ), Range( 30f, 120f )] public float ViewModelFieldOfView { get; set; } = 80f;
public float CurrentViewModelFieldOfView { get; private set; }
public float CurrentViewModelAimFovScale { get; private set; } = 1f;
[Property, Group( "Filtering" )] public string ViewModelTag { get; set; } = DefaultViewModelTag;
[Property, Group( "Filtering" )] public string ScopeSurfaceTag { get; set; } = DefaultScopeSurfaceTag;
[Property, Group( "Filtering" )] public string LightTag { get; set; } = DefaultLightTag;
[Property, Group( "Filtering" )] public bool PropagateTagToViewModelChildren { get; set; } = true;
[Property, Group( "Presentation" )]
[Description( "Legacy compatibility toggle. The 8F2A offscreen capture intentionally keeps native camera post-processing disabled; OMR applies its own viewmodel treatment during composite." )]
public bool EnableViewModelPostProcessing { get; set; } = false;
[Property, Group( "Diagnostics" )] public bool LogSetup { get; set; } = false;
[Property, Group( "Diagnostics" )] public bool LogViewSync { get; set; } = false;
private GameObject _viewModelCameraObject;
private CameraComponent _viewModelCamera;
private GameObject _compositeCameraObject;
private CameraComponent _compositeCamera;
private Texture _viewModelRenderTarget;
private int _viewModelRenderWidth;
private int _viewModelRenderHeight;
private CameraView _sourceView;
private bool _hasSourceView;
private bool _addedWorldCameraExclude;
private float _nextDiagnosticAt;
private PlayerState _localPlayer;
private BaseInventoryComponent _localInventory;
private OMRVisualFoundation _visualFoundation;
int ICameraModifier.CameraOrder => 1_000_000;
protected override void OnStart()
{
ResolveSourceCamera();
if ( SourceCamera is null || !SourceCamera.IsValid )
{
Log.Warning( "[OMR VM CAM] No valid source CameraComponent was found." );
Enabled = false;
return;
}
CreatePresentationCameras();
ConfigureWorldCameraFilter();
if ( PropagateTagToViewModelChildren ) TagLocalViewModelHierarchies();
EnsureViewModelRenderTarget();
BindVisualFoundationRuntimePass();
if ( LogSetup )
{
Log.Info( $"[OMR VM CAM] Ready | Independent viewmodel buffer | World ZNear:{SourceCamera.ZNear:0.##} | Viewmodel ZNear:{ViewModelZNear:0.##} | World Priority:{SourceCamera.Priority} | Capture Priority:{_viewModelCamera?.Priority} | Composite Priority:{_compositeCamera?.Priority} | RT:{_viewModelRenderWidth}x{_viewModelRenderHeight}" );
}
}
protected override void OnUpdate()
{
ResolveSourceCamera();
if ( SourceCamera is null || !SourceCamera.IsValid ) return;
if ( _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid )
{
CreatePresentationCameras();
ConfigureWorldCameraFilter();
}
if ( _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;
if ( PropagateTagToViewModelChildren ) TagLocalViewModelHierarchies();
EnsureViewModelRenderTarget();
ConfigurePresentationCameras();
BindVisualFoundationRuntimePass();
}
protected override void OnDisabled()
{
RestoreWorldCameraFilter();
UnbindVisualFoundationRuntimePass();
DestroyPresentationCameras();
ReleaseViewModelRenderTarget();
_hasSourceView = false;
CurrentViewModelFieldOfView = 0f;
CurrentViewModelAimFovScale = 1f;
base.OnDisabled();
}
protected override void OnDestroy()
{
RestoreWorldCameraFilter();
UnbindVisualFoundationRuntimePass();
DestroyPresentationCameras();
ReleaseViewModelRenderTarget();
_hasSourceView = false;
CurrentViewModelFieldOfView = 0f;
CurrentViewModelAimFovScale = 1f;
base.OnDestroy();
}
void ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )
{
if ( SourceCamera is null || !SourceCamera.IsValid || _viewModelCamera is null || !_viewModelCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;
if ( camera == SourceCamera )
{
_sourceView = view;
_hasSourceView = true;
return;
}
if ( camera != _viewModelCamera && camera != _compositeCamera ) return;
CameraView source = _hasSourceView ? _sourceView : SourceCamera.View;
view.Position = source.Position;
view.Rotation = source.Rotation;
IWeaponOpticState opticState = ResolveLocalOpticState();
view.FieldOfView = ResolveViewModelFieldOfView( source.FieldOfView, opticState );
CurrentViewModelFieldOfView = view.FieldOfView;
view.ZNear = ViewModelZNear;
view.ZFar = source.ZFar;
if ( LogViewSync && camera == _compositeCamera && RealTime.Now >= _nextDiagnosticAt )
{
_nextDiagnosticAt = RealTime.Now + 1f;
Log.Info( $"[OMR VM CAM] Sync | FOV:{view.FieldOfView:0.##} | AimFovScale:{CurrentViewModelAimFovScale:0.###} | ZNear:{view.ZNear:0.##} | RT:{_viewModelRenderWidth}x{_viewModelRenderHeight} | Pos:{view.Position}" );
}
}
public float ResolveViewModelFieldOfView( float composedSourceFieldOfView, IWeaponOpticState opticState = null )
{
float baseFieldOfView = MatchSourceFieldOfView ? composedSourceFieldOfView : ViewModelFieldOfView;
float aimScale = MathX.Clamp( opticState?.CurrentViewModelAimFovScale ?? 1f, 0.5f, 1.5f );
CurrentViewModelAimFovScale = aimScale;
return MathX.Clamp( baseFieldOfView * aimScale, 1f, 179f );
}
private IWeaponOpticState ResolveLocalOpticState() => ResolveLocalInventory()?.ActiveItem as IWeaponOpticState;
private BaseInventoryComponent ResolveLocalInventory()
{
if ( _localPlayer is not null && _localPlayer.IsValid && !_localPlayer.IsProxy && !_localPlayer.IsBot && _localInventory is not null && _localInventory.IsValid ) return _localInventory;
_localPlayer = Scene?.GetAllComponents<PlayerState>().FirstOrDefault( x => x is not null && x.IsValid && !x.IsProxy && !x.IsBot );
_localInventory = _localPlayer?.Components.Get<BaseInventoryComponent>();
return _localInventory;
}
private void ResolveSourceCamera()
{
if ( SourceCamera is not null && SourceCamera.IsValid ) return;
SourceCamera = Components.Get<CameraComponent>();
SourceCamera ??= Scene?.Camera;
}
private void CreatePresentationCameras()
{
if ( Scene is null || SourceCamera is null || !SourceCamera.IsValid ) return;
if ( _viewModelCamera is null || !_viewModelCamera.IsValid )
{
_viewModelCameraObject = Scene.CreateObject( true );
_viewModelCameraObject.Name = "OMR ViewModel Capture Camera";
_viewModelCameraObject.NetworkMode = NetworkMode.Never;
_viewModelCameraObject.Flags |= GameObjectFlags.NotSaved;
_viewModelCamera = _viewModelCameraObject.Components.Create<CameraComponent>();
_viewModelCamera.IsMainCamera = false;
// The editor/game viewport can report 0x0 for the first frame after
// entering Play. Keep the capture camera disabled until a real
// render target exists so it never falls back to drawing on-screen
// or asks the renderer to interpolate a zero-sized viewport.
_viewModelCamera.Enabled = false;
}
if ( _compositeCamera is null || !_compositeCamera.IsValid )
{
_compositeCameraObject = Scene.CreateObject( true );
_compositeCameraObject.Name = "OMR ViewModel Composite Camera";
_compositeCameraObject.NetworkMode = NetworkMode.Never;
_compositeCameraObject.Flags |= GameObjectFlags.NotSaved;
_compositeCamera = _compositeCameraObject.Components.Create<CameraComponent>();
_compositeCamera.IsMainCamera = true;
}
ConfigurePresentationCameras();
}
private void ConfigurePresentationCameras()
{
if ( SourceCamera is null || !SourceCamera.IsValid ) return;
if ( _viewModelCamera is not null && _viewModelCamera.IsValid )
{
_viewModelCamera.ClearFlags = ClearFlags.All;
_viewModelCamera.BackgroundColor = Color.Transparent;
_viewModelCamera.EnablePostProcessing = false;
_viewModelCamera.IsMainCamera = false;
_viewModelCamera.FovAxis = SourceCamera.FovAxis;
_viewModelCamera.Viewport = SourceCamera.Viewport;
_viewModelCamera.TargetEye = SourceCamera.TargetEye;
_viewModelCamera.ZNear = ViewModelZNear;
_viewModelCamera.ZFar = SourceCamera.ZFar;
_viewModelCamera.Priority = SourceCamera.Priority + PriorityOffset;
if ( !string.IsNullOrWhiteSpace( ViewModelTag ) && !_viewModelCamera.RenderTags.Has( ViewModelTag ) ) _viewModelCamera.RenderTags.Add( ViewModelTag );
if ( !string.IsNullOrWhiteSpace( LightTag ) && !_viewModelCamera.RenderTags.Has( LightTag ) ) _viewModelCamera.RenderTags.Add( LightTag );
if ( !string.IsNullOrWhiteSpace( ScopeSurfaceTag ) && !_viewModelCamera.RenderExcludeTags.Has( ScopeSurfaceTag ) ) _viewModelCamera.RenderExcludeTags.Add( ScopeSurfaceTag );
}
if ( _compositeCamera is not null && _compositeCamera.IsValid )
{
_compositeCamera.ClearFlags = ClearFlags.Depth | ClearFlags.Stencil;
_compositeCamera.EnablePostProcessing = false;
_compositeCamera.IsMainCamera = true;
_compositeCamera.FovAxis = SourceCamera.FovAxis;
_compositeCamera.Viewport = SourceCamera.Viewport;
_compositeCamera.TargetEye = SourceCamera.TargetEye;
_compositeCamera.ZNear = ViewModelZNear;
_compositeCamera.ZFar = SourceCamera.ZFar;
_compositeCamera.Priority = SourceCamera.Priority + PriorityOffset + 1;
if ( !string.IsNullOrWhiteSpace( ScopeSurfaceTag ) && !_compositeCamera.RenderTags.Has( ScopeSurfaceTag ) ) _compositeCamera.RenderTags.Add( ScopeSurfaceTag );
if ( !string.IsNullOrWhiteSpace( LightTag ) && !_compositeCamera.RenderTags.Has( LightTag ) ) _compositeCamera.RenderTags.Add( LightTag );
}
}
private void EnsureViewModelRenderTarget()
{
if ( SourceCamera is null || !SourceCamera.IsValid || _viewModelCamera is null || !_viewModelCamera.IsValid ) return;
Vector2 size = SourceCamera.ScreenRect.Size;
// ScreenRect can legitimately be 0x0 during the first editor Play
// frame while the game viewport is being constructed. Do not fabricate
// a 2x2 target: simply wait until the renderer publishes a valid size.
// This avoids the transient "Interop width should be higher than 0"
// warning and prevents the capture camera from ever rendering to the
// screen while its offscreen target is unavailable.
if ( size.x < 2f || size.y < 2f )
{
_viewModelCamera.Enabled = false;
_viewModelCamera.RenderTarget = null;
_viewModelCamera.CustomSize = null;
return;
}
float scale = OMRClientPerformance.ViewModelResolutionScale;
int width = Math.Max( 2, (int)MathF.Round( size.x * scale ) );
int height = Math.Max( 2, (int)MathF.Round( size.y * scale ) );
if ( _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid && _viewModelRenderWidth == width && _viewModelRenderHeight == height )
{
_viewModelCamera.RenderTarget = _viewModelRenderTarget;
_viewModelCamera.CustomSize = new Vector2( width, height );
_viewModelCamera.Enabled = true;
return;
}
Texture oldTarget = _viewModelRenderTarget;
_visualFoundation?.UnbindRuntimeViewModelTexture( oldTarget );
_viewModelCamera.RenderTarget = null;
_viewModelRenderTarget = Texture.CreateRenderTarget()
.WithSize( width, height )
.WithFormat( ImageFormat.RGBA8888 )
.WithInitialColor( Color.Transparent )
.Create( "OMR ViewModel Capture", true );
_viewModelRenderWidth = width;
_viewModelRenderHeight = height;
_viewModelCamera.RenderTarget = _viewModelRenderTarget;
_viewModelCamera.CustomSize = new Vector2( width, height );
_viewModelCamera.Enabled = true;
if ( oldTarget is not null && oldTarget.IsValid ) oldTarget.Dispose();
if ( LogSetup && _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid ) Log.Info( $"[OMR VM CAM] Capture target ready | {width}x{height} | RGBA8888" );
}
private void ReleaseViewModelRenderTarget()
{
Texture target = _viewModelRenderTarget;
_visualFoundation?.UnbindRuntimeViewModelTexture( target );
if ( _viewModelCamera is not null && _viewModelCamera.IsValid )
{
_viewModelCamera.Enabled = false;
_viewModelCamera.RenderTarget = null;
_viewModelCamera.CustomSize = null;
}
_viewModelRenderTarget = null;
_viewModelRenderWidth = 0;
_viewModelRenderHeight = 0;
if ( target is not null && target.IsValid ) target.Dispose();
}
private void ConfigureWorldCameraFilter()
{
if ( SourceCamera is null || !SourceCamera.IsValid || string.IsNullOrWhiteSpace( ViewModelTag ) ) return;
if ( !SourceCamera.RenderExcludeTags.Has( ViewModelTag ) )
{
SourceCamera.RenderExcludeTags.Add( ViewModelTag );
_addedWorldCameraExclude = true;
}
}
private void RestoreWorldCameraFilter()
{
if ( _addedWorldCameraExclude && SourceCamera is not null && SourceCamera.IsValid && !string.IsNullOrWhiteSpace( ViewModelTag ) ) SourceCamera.RenderExcludeTags.Remove( ViewModelTag );
_addedWorldCameraExclude = false;
}
private void BindVisualFoundationRuntimePass()
{
if ( SourceCamera is null || !SourceCamera.IsValid || _compositeCamera is null || !_compositeCamera.IsValid ) return;
OMRVisualFoundation foundation = SourceCamera.GameObject.Components.Get<OMRVisualFoundation>();
if ( foundation != _visualFoundation )
{
if ( _visualFoundation is not null )
{
_visualFoundation.UnbindRuntimeViewModelTexture( _viewModelRenderTarget );
_visualFoundation.UnbindRuntimeOverlayCamera( _compositeCamera );
}
_visualFoundation = foundation;
}
_visualFoundation?.BindRuntimeOverlayCamera( _compositeCamera );
if ( _viewModelRenderTarget is not null && _viewModelRenderTarget.IsValid ) _visualFoundation?.BindRuntimeViewModelTexture( _viewModelRenderTarget );
}
private void UnbindVisualFoundationRuntimePass()
{
if ( _visualFoundation is not null )
{
_visualFoundation.UnbindRuntimeViewModelTexture( _viewModelRenderTarget );
_visualFoundation.UnbindRuntimeOverlayCamera( _compositeCamera );
}
_visualFoundation = null;
}
private void DestroyPresentationCameras()
{
if ( _viewModelCameraObject is not null && _viewModelCameraObject.IsValid ) _viewModelCameraObject.Destroy();
if ( _compositeCameraObject is not null && _compositeCameraObject.IsValid ) _compositeCameraObject.Destroy();
_viewModelCameraObject = null;
_viewModelCamera = null;
_compositeCameraObject = null;
_compositeCamera = null;
}
private GameObject _taggedViewModel;
private string _taggedViewModelTag;
private string _taggedScopeTag;
private float _nextHierarchyTagAt;
private void TagLocalViewModelHierarchies()
{
if ( Scene is null || string.IsNullOrWhiteSpace( ViewModelTag ) ) return;
OneMoreRoundWeapon weapon = ResolveLocalInventory()?.ActiveItem as OneMoreRoundWeapon;
GameObject viewModel = weapon?.IsValid == true && !weapon.IsBotControlled ? weapon.ViewModel : null;
if ( viewModel?.IsValid != true ) { _taggedViewModel = null; return; }
if ( viewModel == _taggedViewModel && ViewModelTag == _taggedViewModelTag &&
ScopeSurfaceTag == _taggedScopeTag && RealTime.Now < _nextHierarchyTagAt ) return;
_taggedViewModel = viewModel;
_taggedViewModelTag = ViewModelTag;
_taggedScopeTag = ScopeSurfaceTag;
_nextHierarchyTagAt = RealTime.Now + 0.50f;
TagHierarchy( viewModel );
TagScopeSurfaces( viewModel );
}
private void TagHierarchy( GameObject gameObject )
{
if ( gameObject is null || !gameObject.IsValid ) return;
if ( !gameObject.Tags.Has( ViewModelTag ) ) gameObject.Tags.Add( ViewModelTag );
foreach ( GameObject child in gameObject.Children ) TagHierarchy( child );
}
private void TagScopeSurfaces( GameObject viewModel )
{
if ( viewModel is null || !viewModel.IsValid || string.IsNullOrWhiteSpace( ScopeSurfaceTag ) ) return;
foreach ( OMRScopePhysicalLens lens in viewModel.Components.GetAll<OMRScopePhysicalLens>( FindMode.InSelf | FindMode.InDescendants ) )
{
if ( lens is null || !lens.IsValid ) continue;
AddScopeSurfaceTag( lens.ImageRenderer?.GameObject );
AddScopeSurfaceTag( lens.GlassRenderer?.GameObject );
}
TagScopeSurfaceNamesRecursive( viewModel );
}
private void TagScopeSurfaceNamesRecursive( GameObject gameObject )
{
if ( gameObject is null || !gameObject.IsValid ) return;
if ( string.Equals( gameObject.Name, "ScopeImagePlane", StringComparison.OrdinalIgnoreCase ) || string.Equals( gameObject.Name, "ScopeGlass", StringComparison.OrdinalIgnoreCase ) ) AddScopeSurfaceTag( gameObject );
foreach ( GameObject child in gameObject.Children ) TagScopeSurfaceNamesRecursive( child );
}
private void AddScopeSurfaceTag( GameObject gameObject )
{
if ( gameObject is null || !gameObject.IsValid || string.IsNullOrWhiteSpace( ScopeSurfaceTag ) ) return;
if ( !gameObject.Tags.Has( ScopeSurfaceTag ) ) gameObject.Tags.Add( ScopeSurfaceTag );
}
}