Game/MenuDichotomyRig.cs
using System;

namespace Causal;

public sealed class MenuDichotomyRig : Component
{
	[Property] public string DichotomyMaterialPath { get; set; } = "materials/menu_dichotomy.vmat";
	[Property] public float DichotomyOffset { get; set; } = 0f;
	[Property] public float DichotomySeamWidth { get; set; } = 0.006f;
	[Property] public float SplitVerticalAngle { get; set; } = -1.5708f;
	[Property] public float SplitSweepOffset { get; set; } = -0.8f;
	[Property] public bool WaveEnabled { get; set; } = true;
	[Property] public float WaveAmplitude { get; set; } = 0.008f;
	[Property] public float WaveCount { get; set; } = 2.5f;
	[Property] public float WaveSpeed { get; set; } = 1.5f;
	[Property] public bool VoronoiEnabled { get; set; } = true;
	[Property] public float VoronoiCellSize { get; set; } = 120f;
	[Property, Range( 0f, 1f )] public float VoronoiPatchCoverage { get; set; } = 0.15f;
	[Property, Range( 0f, 1f )] public float VoronoiEmptyFraction { get; set; } = 0f;
	[Property, Range( 0f, 1f )] public float VoronoiShadowStrength { get; set; } = 0.45f;
	[Property] public float VoronoiShadowWidth { get; set; } = 0.25f;
	[Property] public float VoronoiPatchPulse { get; set; } = 0.08f;
	[Property] public float VoronoiRotationSpeed { get; set; } = 0.05f;
	[Property] public float VoronoiDriftSpeed { get; set; } = 0.05f;

	private GameObject _causeCamGo;
	private GameObject _effectCamGo;
	private CameraComponent _menuCam;
	private DichotomyComposite _composite;
	private Material _material;
	private Texture _causeFeed;
	private Vector2 _feedSize = new( -1f, -1f );
	private bool _menuActive;
	private float _pushedAngle = float.MaxValue;
	private float _pushedOffset = float.MaxValue;
	private float _pushedSeam = float.MaxValue;
	private float _pushedVoronoiCell = float.MaxValue;
	private float _pushedVoronoiCover = float.MaxValue;
	private float _pushedVoronoiEmpty = float.MaxValue;
	private float _pushedVoronoiPulse = float.MaxValue;
	private float _pushedVoronoiShadow = float.MaxValue;
	private float _pushedVoronoiShadowWidth = float.MaxValue;
	private float _pushedSplitMix = float.MaxValue;
	private float _pushedIntroWave = float.MaxValue;
	private float _baseAngle;
	private float _splitAngle;
	private float _splitOffset;
	private float _splitMix;
	private float _introSweepT;
	private bool _introDrive;
	private readonly List<ScreenPanel> _retargetedPanels = new();
	private readonly List<CameraComponent> _panelTargets = new();

	protected override void OnUpdate()
	{
		var manager = CausalGameManager.Instance;
		var state = manager.IsValid() ? manager.State : CausalGameManager.GameState.Menu;
		bool wantMenu = state == CausalGameManager.GameState.Menu
			|| state == CausalGameManager.GameState.PreVideo
			|| state == CausalGameManager.GameState.Intro;

		if ( wantMenu != _menuActive )
		{
			SetMenuActive( wantMenu );
		}

		if ( _menuActive )
		{
			TickFeed();
		}
	}

	protected override void OnDisabled()
	{
		Deactivate();
	}

	protected override void OnDestroy()
	{
		Deactivate();
	}

	public void SetMenuActive( bool menu )
	{
		if ( menu == _menuActive && (!menu || _causeCamGo.IsValid()) )
		{
			return;
		}

		_menuActive = menu;
		_introDrive = false;
		_introSweepT = 0f;
		_splitMix = 0f;
		if ( menu )
		{
			Activate();
		}
		else
		{
			Deactivate();
		}
	}

	public bool IsSplitActive => _menuActive && _causeCamGo.IsValid() && _composite.IsValid();

	public CameraComponent MenuCam => _menuCam;

	public void DriveIntroSplit( float rotateT, float sweepT )
	{
		if ( !IsSplitActive )
		{
			return;
		}

		_introDrive = true;
		_introSweepT = sweepT;
		float a = rotateT * rotateT * (3f - 2f * rotateT);
		_splitAngle = _baseAngle + (SplitVerticalAngle - _baseAngle) * a;
		_splitOffset = SplitSweepOffset * sweepT;
		_splitMix = Math.Clamp( a, 0f, 1f );
	}

	private void Activate()
	{
		if ( _causeCamGo.IsValid() )
		{
			return;
		}

		_menuCam = Scene.Camera;
		if ( !_menuCam.IsValid() )
		{
			Log.Warning( "MenuDichotomyRig found no menu camera." );
			_menuActive = false;
			return;
		}

		_material = Material.Load( DichotomyMaterialPath );
		if ( _material is null )
		{
			Log.Warning( $"MenuDichotomyRig found no material at '{DichotomyMaterialPath}'." );
			_menuActive = false;
			return;
		}

		_menuCam.Enabled = false;

		_causeCamGo = new GameObject( "Menu Cause Cam" );
		var causeCam = _causeCamGo.AddComponent<CameraComponent>();
		CopyView( causeCam );
		causeCam.IsMainCamera = false;
		causeCam.Priority = 1;
		causeCam.RenderExcludeTags.Add( TimeShiftManager.EffectTag );
		CopyTonemapping( _causeCamGo.AddComponent<Tonemapping>() );

		_effectCamGo = new GameObject( "Menu Effect Cam" );
		var effectCam = _effectCamGo.AddComponent<CameraComponent>();
		CopyView( effectCam );
		effectCam.IsMainCamera = false;
		effectCam.Priority = 2;
		effectCam.RenderExcludeTags.Add( TimeShiftManager.CauseTag );
		CopyTonemapping( _effectCamGo.AddComponent<Tonemapping>() );

		var overlay = _effectCamGo.AddComponent<DichotomyComposite>();
		overlay.Material = _material;
		// Composite last so the cause feed is tonemapped exactly once.
		overlay.Order = 1000;
		_composite = overlay;

		RetargetInterface( effectCam );

		_baseAngle = DiagonalAngle( Screen.Size );
		_splitAngle = _baseAngle;
		_splitOffset = 0f;
		_splitMix = 0f;
		_introSweepT = 0f;
		_introDrive = false;
		_pushedAngle = float.MaxValue;
		_pushedOffset = float.MaxValue;
		_pushedSeam = float.MaxValue;
		_pushedVoronoiCell = float.MaxValue;
		_pushedVoronoiCover = float.MaxValue;
		_pushedVoronoiEmpty = float.MaxValue;
		_pushedVoronoiPulse = float.MaxValue;
		_pushedVoronoiShadow = float.MaxValue;
		_pushedVoronoiShadowWidth = float.MaxValue;
		_pushedSplitMix = float.MaxValue;
		_pushedIntroWave = float.MaxValue;
		_feedSize = new Vector2( -1f, -1f );
		TickFeed();
	}

	private void Deactivate()
	{
		_menuActive = false;
		RestoreInterface();

		if ( _causeCamGo.IsValid() )
		{
			_causeCamGo.Destroy();
			_causeCamGo = null;
		}

		if ( _effectCamGo.IsValid() )
		{
			_effectCamGo.Destroy();
			_effectCamGo = null;
		}

		_causeFeed = null;
		_material = null;
		_composite = null;

		if ( _menuCam.IsValid() && !_menuCam.Enabled )
		{
			_menuCam.Enabled = true;
		}

		_menuCam = null;
	}

	private void TickEdge( Vector2 size )
	{
		float amplitude = 0f;
		float frequency = 0f;
		float phase = 0f;

		float diagonal = MathF.Sqrt( size.x * size.x + size.y * size.y );
		float damp = _introDrive ? Math.Clamp( 1f - _introSweepT, 0f, 1f ) : 1f;
		if ( WaveEnabled && diagonal > 0f && WaveCount > 0f )
		{
			amplitude = WaveAmplitude * size.x * damp;
			frequency = WaveCount * (MathF.PI * 2f) / diagonal;
			phase = Time.Now * WaveSpeed;
		}

		_composite.WaveAmplitude = amplitude;
		_composite.WaveFrequency = frequency;
		_composite.WavePhase = phase;

		float cellSize = VoronoiCellSize <= 1f ? 1f : VoronoiCellSize;
		float cover = VoronoiEnabled ? Math.Clamp( VoronoiPatchCoverage, 0f, 1f ) : 0f;
		float wave = _introDrive ? Math.Clamp( _introSweepT, 0f, 1f ) : -1f;

		if ( cellSize != _pushedVoronoiCell )
		{
			_pushedVoronoiCell = cellSize;
			_composite.VoronoiCellSize = cellSize;
		}

		if ( cover != _pushedVoronoiCover )
		{
			_pushedVoronoiCover = cover;
			_composite.VoronoiCoverage = cover;
		}

		if ( VoronoiEmptyFraction != _pushedVoronoiEmpty )
		{
			_pushedVoronoiEmpty = VoronoiEmptyFraction;
			_composite.VoronoiEmpty = VoronoiEmptyFraction;
		}

		if ( VoronoiShadowStrength != _pushedVoronoiShadow )
		{
			_pushedVoronoiShadow = VoronoiShadowStrength;
			_composite.VoronoiShadow = VoronoiShadowStrength;
		}

		if ( VoronoiShadowWidth != _pushedVoronoiShadowWidth )
		{
			_pushedVoronoiShadowWidth = VoronoiShadowWidth;
			_composite.VoronoiShadowWidth = VoronoiShadowWidth;
		}

		if ( VoronoiPatchPulse != _pushedVoronoiPulse )
		{
			_pushedVoronoiPulse = VoronoiPatchPulse;
			_composite.VoronoiPulse = VoronoiPatchPulse;
		}

		if ( wave != _pushedIntroWave )
		{
			_pushedIntroWave = wave;
			_composite.IntroWave = wave;
		}

		float splitMix = _introDrive ? Math.Clamp( _splitMix, 0f, 1f ) : 0f;
		if ( splitMix != _pushedSplitMix )
		{
			_pushedSplitMix = splitMix;
			_composite.SplitMix = splitMix;
		}

		_composite.VoronoiAngle = Time.Now * VoronoiRotationSpeed;
		_composite.VoronoiDrift = Time.Now * VoronoiDriftSpeed;
	}

	private static float DiagonalAngle( Vector2 size )
	{
		if ( size.x <= 0f || size.y <= 0f )
		{
			return -MathF.PI / 4f;
		}

		return -MathF.Atan2( size.y, size.x );
	}

	private void CopyView( CameraComponent cam )
	{
		cam.WorldPosition = _menuCam.WorldPosition;
		cam.WorldRotation = _menuCam.WorldRotation;
		cam.FieldOfView = _menuCam.FieldOfView;
		cam.ZNear = _menuCam.ZNear;
		cam.ZFar = _menuCam.ZFar;
		cam.BackgroundColor = _menuCam.BackgroundColor;
		cam.ClearFlags = _menuCam.ClearFlags;
		cam.EnablePostProcessing = _menuCam.EnablePostProcessing;
		cam.Viewport = new Vector4( 0f, 0f, 1f, 1f );
	}

	private void CopyTonemapping( Tonemapping tone )
	{
		var source = _menuCam.GetComponent<Tonemapping>();
		if ( !source.IsValid() )
		{
			return;
		}

		tone.Mode = source.Mode;
		tone.ExposureMethod = source.ExposureMethod;
		tone.AutoExposureEnabled = source.AutoExposureEnabled;
		tone.ExposureCompensation = source.ExposureCompensation;
		tone.MinimumExposure = source.MinimumExposure;
		tone.MaximumExposure = source.MaximumExposure;
		tone.Rate = source.Rate;
	}

	private void RetargetInterface( CameraComponent cam )
	{
		RestoreInterface();

		foreach ( var panel in Scene.GetAllComponents<ScreenPanel>() )
		{
			if ( !panel.IsValid() )
			{
				continue;
			}

			_retargetedPanels.Add( panel );
			_panelTargets.Add( panel.TargetCamera );
			panel.TargetCamera = cam;
		}
	}

	private void RestoreInterface()
	{
		for ( int i = 0; i < _retargetedPanels.Count; i++ )
		{
			var panel = _retargetedPanels[i];
			if ( panel.IsValid() )
			{
				panel.TargetCamera = _panelTargets[i];
			}
		}

		_retargetedPanels.Clear();
		_panelTargets.Clear();
	}

	private void TickFeed()
	{
		if ( !_composite.IsValid() )
		{
			_menuActive = false;
			return;
		}

		var causeCam = _causeCamGo?.GetComponent<CameraComponent>();
		if ( !causeCam.IsValid() )
		{
			_menuActive = false;
			return;
		}

		var effectCam = _effectCamGo?.GetComponent<CameraComponent>();
		if ( !effectCam.IsValid() )
		{
			_menuActive = false;
			return;
		}

		if ( _menuCam.IsValid() )
		{
			causeCam.WorldPosition = _menuCam.WorldPosition;
			causeCam.WorldRotation = _menuCam.WorldRotation;
			effectCam.WorldPosition = _menuCam.WorldPosition;
			effectCam.WorldRotation = _menuCam.WorldRotation;
		}

		Vector2 size = Screen.Size;
		if ( _causeFeed is null || _feedSize != size )
		{
			_causeFeed = Texture.CreateRenderTarget( "MenuDichotomyCause", ImageFormat.RGBA8888, size, _causeFeed );
			_feedSize = size;
			_baseAngle = DiagonalAngle( size );
		}

		causeCam.RenderTarget = _causeFeed;
		_composite.CauseFeed = _causeFeed;
		_composite.Bounds = size;
		TickEdge( size );

		float wantAngle = _introDrive ? _splitAngle : _baseAngle;
		float wantOffset = _introDrive ? _splitOffset : DichotomyOffset;

		if ( wantAngle != _pushedAngle )
		{
			_pushedAngle = wantAngle;
			_composite.Angle = _pushedAngle;
		}

		if ( wantOffset != _pushedOffset )
		{
			_pushedOffset = wantOffset;
			_composite.Offset = _pushedOffset;
		}

		if ( DichotomySeamWidth != _pushedSeam )
		{
			_pushedSeam = DichotomySeamWidth;
			_composite.SeamWidth = _pushedSeam;
		}
	}
}