core/player/PlayerPacCamera.cs
using Sandbox.Rendering;
using Sandbox.Utility;

public class PlayerPacCamera : Component {
	[Property, ReadOnly] public BasePlayer Owner {get; set;}

	[Property, ReadOnly] public Vector2Int RenderSize {get; set;} = new(640,480);
	[Property, ReadOnly] public CameraComponent RenderCamera {get; set;}
	[Property, ReadOnly] public CameraComponent DrawCamera {get; set;}
	[Property] public bool AlwaysRerender {get; set;}
	[Property, Change(nameof(UpdateVideoMask))] public bool FullScreenFMV {get; set;}
	[Property, Range(-1,1)] public float Fade {get; set;}
	public enum TransitionType {
		None,
		SlideLeft,
		SlideRight,
		Fade,
	}
	public TransitionType DoTransition {get; set;}
	private TransitionType CurrentTransition {get; set;}
	public TimeUntil TransitionTime {get; set;}
	[Property, ReadOnly] public Texture StaticCurrent {get; set;}
	[Property, ReadOnly] public Texture StaticLast {get; set;}
	[Property, ReadOnly] public Texture VideoMask {get; set;}
	[Property, ReadOnly] public Texture VideoCurrent {get; set;}
	[Property, ReadOnly] public Texture VideoLast {get; set;}
	[Property, ReadOnly] public Texture Composite {get; set;}
	[Property, ReadOnly] public Action DelayMoveAction {get; set;}
	[Property, ReadOnly, Range(-1,1)] public int Turn {get; set;}
	[Property] public Texture OverlayTexture {get; set;}
	[Property, Range(0,1)] public float OverlayStrength {get; set;}

	protected override void OnAwake() {
		Composite = Texture.CreateRenderTarget().WithUAVBinding().WithSize(RenderSize).Create();

		Spawn.SetOwner(this);
		Spawn.Component(nameof(RenderCamera));
		RenderCamera.BackgroundColor = Color.Black;
		Spawn.Component(nameof(DrawCamera));
		DrawCamera.RenderTags.Add("null");
		DrawCamera.BackgroundColor = Color.Black;
		DrawCamera.Priority = 1000;
		DrawCamera.ClearCommandLists();
		var cmds = new CommandList();
		cmds.Attributes.Set("Frame", Composite);
		cmds.Blit(Material.FromShader(Shader.Load("shaders/post_draw.shader")));
		DrawCamera.AddCommandList(cmds, Stage.AfterPostProcess);
		base.OnAwake();
	}

	public Vector2 PointToRenderPixels(Vector3 point) {
		RenderCamera.CustomSize = RenderSize;
		var uv = RenderCamera.PointToScreenNormal(point);
		if (uv.x == 0) uv.x = -1; else if (uv.x == 1) uv.x = 2;
		if (uv.y == 0) uv.y = -1; else if (uv.y == 1) uv.y = 2;
		uv -= 0.5f;
		uv.y *= ((float)RenderSize.x/RenderSize.y) / Screen.Aspect;
		uv += 0.5f;
		uv *= RenderSize;
		uv.x = uv.x.Floor();
		uv.y = uv.y.Floor();
		return uv;
	}

	public void Update() {
		LocalTransform = global::Transform.Zero;
		Mouse.Visibility = MouseVisibility.Visible;
		Mouse.CursorType = "iso2_pointer";
		if (Turn == -1)
			Mouse.CursorType = "iso2_left";
		if (Turn == 1)
			Mouse.CursorType = "iso2_right";
		if (FullScreenFMV)
			Mouse.CursorType = "None";
		if (TransitionTime)
			CurrentTransition = TransitionType.None;
		else foreach (var animator in Scene.GetAllComponents<NpcAnimator>())
			animator.ResetToIdle();
		TerrainSuperBlendController.Set(Scene, Scene.RenderAttributes);
		RenderStatic();
		RenderVideo();
		Scene.RenderAttributes.Clear();
		DoComposite();
	}

	private int ViewHash = 0;
	private void RenderStatic() {
		var newtransition = DoTransition;
		DoTransition = TransitionType.None;
		if (FullScreenFMV)
			return;
		var angle = HashCode.Combine(WorldTransform, RenderCamera.FieldOfView);
		if (angle == ViewHash && !AlwaysRerender)
			return;
		ViewHash = angle;
		if (!AlwaysRerender) {
			foreach (var animator in Scene.GetAllComponents<NpcAnimator>())
				animator.ResetToIdle();
		}

		StaticLast?.Dispose();
		StaticLast = StaticCurrent;
		StaticCurrent = Texture.CreateRenderTarget().WithSize(RenderSize).Create();
		//RenderCamera.RenderExcludeTags.Add("dynamic");
		RenderCamera.RenderExcludeTags.Add("iso_waypoint");
		RenderCamera.RenderExcludeTags.Add("iso_entity");
		RenderCamera.RenderToTexture(StaticCurrent);
		RenderCamera.RenderExcludeTags.RemoveAll();
		UpdateVideoMask();
		LastVideoRender = 20f;
		if (newtransition != TransitionType.None) {
			CurrentTransition = newtransition;
			TransitionTime = 0.3f;
		}
	}

	public void InvalidateView() {
		ViewHash = 0;
	}

	public void UpdateVideoMask() {
		VideoMask?.Dispose();
		if (FullScreenFMV) {
			VideoMask = Texture.Create(1, 1).WithData(new byte[4] {255, 255, 255, 255}).Finish();
			return;
		}
		var tex = new Bitmap(RenderSize.x, RenderSize.y);
		tex.Clear(Color.Black);
		tex.SetFill(Color.White);
		foreach (var bound in Scene.GetAllComponents<FmvBoundsCollection>()) {
			if (!bound.Test(RenderCamera.GetFrustum(new(0,Screen.Size))))
				continue;
			var created = false;
			Rect rough = new();
			foreach (var corner in bound.GetCorners()) {
				var point = PointToRenderPixels(bound.WorldTransform.PointToWorld(corner));
				//tex.DrawCircle(point, 2);
				//DebugOverlay.Sphere(new(bound.WorldTransform.PointToWorld(corner), 1), Color.Red, 20f, default, true);
				if (!created)
					rough = new(point);
				else
					rough = rough.AddPoint(point);
				created = true;
			}
			if (rough.Width.AlmostEqual(0f) || rough.Height.AlmostEqual(0f))
				continue;
			tex.DrawRect(rough);
		}
		VideoMask = tex.ToTexture();
	}

	private TimeSince LastVideoRender;
	private void RenderVideo() {
		if (CurrentTransition != TransitionType.None)
			return;
		if (LastVideoRender < 1f/(FullScreenFMV ? 30f : 15f))
			return;
		LastVideoRender = 0f;
		VideoLast?.Dispose();
		VideoLast = VideoCurrent;
		VideoCurrent = Texture.CreateRenderTarget().WithUAVBinding().WithSize(RenderSize).Create();
		var videonew = Texture.CreateRenderTarget().WithUAVBinding().WithSize(RenderSize).Create();
		RenderCamera.RenderExcludeTags.Add("iso_waypoint");
		RenderCamera.RenderExcludeTags.Add("iso_entity");
		RenderCamera.RenderExcludeTags.Add("flame");
		RenderCamera.RenderToTexture(videonew);
		RenderCamera.RenderExcludeTags.RemoveAll();
		var shader = new ComputeShader("shaders/cs_pac_video_p_frames.shader");
		shader.Attributes.Set("VideoCurrent", videonew);
		shader.Attributes.Set("VideoLast", VideoLast);
		shader.Attributes.Set("RenderSize", VideoCurrent.Size);
		shader.Attributes.Set("Result", VideoCurrent);
		shader.Dispatch(VideoCurrent.Width, VideoCurrent.Height, 1);
		videonew.Dispose();
	}

	private void DoComposite() {
		var shader = new ComputeShader("shaders/cs_pac_composite.shader");
		shader.Attributes.Set("StaticLast", StaticLast);
		shader.Attributes.Set("StaticCurrent", StaticCurrent);
		shader.Attributes.Set("VideoMask", VideoMask);
		shader.Attributes.Set("VideoCurrent", VideoCurrent);
		if (CurrentTransition != TransitionType.None) {
			shader.Attributes.Set("RenderVideo", false);
			shader.Attributes.Set("TransitionFade", 1f);
			if (CurrentTransition == TransitionType.SlideLeft)
				shader.Attributes.Set("TransitionSlide",-Easing.SineEaseInOut(TransitionTime.Fraction));
			else if (CurrentTransition == TransitionType.SlideRight)
				shader.Attributes.Set("TransitionSlide", Easing.SineEaseInOut(TransitionTime.Fraction));
			else if (CurrentTransition == TransitionType.Fade)
				shader.Attributes.Set("TransitionFade", 1f - TransitionTime.Fraction);
		} else {
			shader.Attributes.Set("RenderVideo", true);
			shader.Attributes.Set("TransitionSlide", 0f);
			shader.Attributes.Set("TransitionFade", 0f);
		}
		shader.Attributes.Set("Overlay", OverlayTexture);
		shader.Attributes.Set("OverlayStrength", OverlayStrength);
		shader.Attributes.Set("Fade", Fade);
		shader.Attributes.Set("RenderSize", Composite.Size);
		shader.Attributes.Set("Result", Composite);
		shader.Dispatch(Composite.Width, Composite.Height, 1);
	}
}