Editor/SuperShotCapture.cs

Editor utility that captures high-resolution screenshots from the editor scene. It prepares the scene (hiding UI or tagged objects), resolves or builds a camera (including a temporary freecam), applies post-processes, renders to a Bitmap with optional supersampling and resizing, and restores scene state.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Editor.SuperShot;

public static class SuperShotCapture
{
	public static Bitmap Capture( CaptureSettings settings )
	{
		var scene = SuperShotContext.ActiveScene;
		if ( scene is null )
		{
			Log.Warning( "[Supershot] No active editor scene to capture." );
			return null;
		}

		var (outW, outH) = SuperShotContext.ResolveSize( settings );
		var ss = MathX.Clamp( settings.SuperSampling, 1f, 4f );
		int renderW = Math.Clamp( (int)(outW * ss), 1, 16384 );
		int renderH = Math.Clamp( (int)(outH * ss), 1, 16384 );

		IDisposable restore = null;
		try
		{
			restore = PrepareScene( scene, settings );
			var bitmap = RenderToBitmap( scene, settings, renderW, renderH );
			if ( bitmap is null )
				return null;

			if ( renderW != outW || renderH != outH )
			{
				var resized = bitmap.Resize( outW, outH );
				bitmap.Dispose();
				return resized;
			}

			return bitmap;
		}
		catch ( Exception e )
		{
			Log.Error( $"[Supershot] Capture failed: {e.Message}" );
			return null;
		}
		finally
		{
			restore?.Dispose();
		}
	}

	public static bool RenderPreview( Bitmap target, CaptureSettings settings )
	{
		var scene = SuperShotContext.ActiveScene;
		if ( scene is null || target is null )
			return false;

		try
		{
			RenderInto( scene, settings, target );
			return true;
		}
		catch
		{
			return false;
		}
	}

	static Bitmap RenderToBitmap( Scene scene, CaptureSettings settings, int width, int height )
	{
		var bitmap = new Bitmap( width, height );
		if ( !RenderInto( scene, settings, bitmap ) )
		{
			bitmap.Dispose();
			return null;
		}
		return bitmap;
	}

	static bool RenderInto( Scene scene, CaptureSettings settings, Bitmap bitmap )
	{
		using ( scene.Push() )
		{
			GameObject tempGo = null;
			try
			{
				var source = SuperShotContext.EffectiveSource( settings );

				var cam = ResolveCamera( scene, settings, source, out tempGo );
				if ( cam is null )
				{
					Log.Warning( "[Supershot] No camera available for capture." );
					return false;
				}

				// FOV override only applies to an explicitly-chosen scene main camera; freecam and auto-attached
				// game-camera shots mirror exactly what's on screen.
				float? prevFov = null;
				if ( settings.OverrideFov && settings.Source == CaptureSource.SceneMainCamera && !SuperShotContext.IsAttachedGameView )
				{
					prevFov = cam.FieldOfView;
					cam.FieldOfView = settings.Fov;
				}

				var prevBg = cam.BackgroundColor;
				if ( settings.TransparentBackground )
					cam.BackgroundColor = Color.Transparent;

				using ( SuperShotPostFx.Apply( cam.GameObject, settings.PostFx ) )
				{
					cam.RenderToBitmap( bitmap );
				}

				if ( prevFov.HasValue )
					cam.FieldOfView = prevFov.Value;
				if ( settings.TransparentBackground )
					cam.BackgroundColor = prevBg;

				return true;
			}
			finally
			{
				tempGo?.Destroy();
			}
		}
	}

	static CameraComponent ResolveCamera( Scene scene, CaptureSettings settings, CaptureSource source, out GameObject tempGo )
	{
		tempGo = null;

		if ( source == CaptureSource.SceneMainCamera )
		{
			if ( scene.Camera.IsValid() )
				return scene.Camera;

			return scene.GetAllComponents<CameraComponent>().FirstOrDefault( c => c.IsMainCamera )
				?? scene.GetAllComponents<CameraComponent>().FirstOrDefault();
		}

		if ( !SuperShotContext.TryResolveFreecam( out var freecam, out var freecamFov, out var freecamZNear, out var freecamZFar ) )
		{
			Log.Warning( "[Supershot] Couldn't read the editor 3D viewport camera. Move the scene view (or activate the Supershot tool) and try again." );
			return null;
		}

		return BuildTempCamera( ref tempGo, settings, freecam, freecamFov, freecamZNear, freecamZFar );
	}

	static CameraComponent BuildTempCamera( ref GameObject tempGo, CaptureSettings settings, Transform view, float defaultFov, float zNear, float zFar )
	{
		tempGo = new GameObject( true, "SuperShot_Freecam" );
		tempGo.Flags = GameObjectFlags.NotSaved | GameObjectFlags.Hidden;
		tempGo.WorldPosition = view.Position;
		tempGo.WorldRotation = view.Rotation;

		var temp = tempGo.Components.Create<CameraComponent>();
		temp.IsMainCamera = false;
		temp.FovAxis = CameraComponent.Axis.Horizontal;
		temp.FieldOfView = defaultFov;
		temp.ZNear = MathX.Clamp( zNear, 1f, 1000f );
		temp.ZFar = zFar;
		temp.BackgroundColor = settings.TransparentBackground ? Color.Transparent : Color.Black;

		// A bare CameraComponent renders raw linear HDR. Without a Tonemapping post-process the image
		// blows out to near-white (the symptom that looks like "wrong source": a washed-out frame).
		// The editor scene-view tonemaps with the Source 2 filmic curve, so attach the same default
		// tonemapper here unless the user already enabled their own under Engine Post FX.
		if ( !(settings.PostFx?.TonemapEnabled ?? false) )
		{
			var tonemap = tempGo.Components.Create<Tonemapping>();
			tonemap.Mode = Tonemapping.TonemappingMode.HableFilmic;
			tonemap.AutoExposureEnabled = true;
		}

		return temp;
	}

	static IDisposable PrepareScene( Scene scene, CaptureSettings settings )
	{
		var hiddenComponents = new List<Component>();
		var hiddenObjects = new List<GameObject>();

		if ( settings.HideUI )
		{
			foreach ( var panel in scene.GetAllComponents<PanelComponent>() )
			{
				if ( panel.IsValid() && panel.Enabled )
				{
					panel.Enabled = false;
					hiddenComponents.Add( panel );
				}
			}
		}

		if ( !string.IsNullOrWhiteSpace( settings.HideTags ) )
		{
			var tags = settings.HideTags.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries );
			foreach ( var tag in tags )
			{
				foreach ( var obj in scene.GetAllObjects( true ).Where( o => o.Tags.Has( tag ) ) )
				{
					if ( obj.IsValid() && obj.Enabled )
					{
						obj.Enabled = false;
						hiddenObjects.Add( obj );
					}
				}
			}
		}

		return new RestoreScope( hiddenComponents, hiddenObjects );
	}

	sealed class RestoreScope : IDisposable
	{
		readonly List<Component> _components;
		readonly List<GameObject> _objects;

		public RestoreScope( List<Component> components, List<GameObject> objects )
		{
			_components = components;
			_objects = objects;
		}

		public void Dispose()
		{
			foreach ( var c in _components )
			{
				if ( c.IsValid() ) c.Enabled = true;
			}
			foreach ( var o in _objects )
			{
				if ( o.IsValid() ) o.Enabled = true;
			}
		}
	}
}