Editor utility that captures multiple camera views from the active scene and composites them into a single PNG. It resolves a scene, creates temporary camera objects per viewport preset, invokes an editor screenshot helper, draws sub-bitmaps onto a canvas, writes the PNG to a .sbox/captures folder and returns metadata about the capture.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sandbox;
namespace Editor.Mcp
{
internal static class MultiViewSnapshotDriver
{
private static Scene? ResolveScene()
{
if ( Game.ActiveScene != null ) return Game.ActiveScene;
var session = EditorRuntime.GetStaticProperty( "Editor.SceneEditorSession", "Active" );
if ( session != null )
{
return EditorRuntime.GetMember( session, "Scene" ) as Scene;
}
return null;
}
public struct ViewportSpec
{
public string Label;
public string TargetName;
public string Preset;
public Rect Rect;
}
public static ToolOutcome<object> CaptureMultiView(
string layout = "quad_4view",
string? primaryTarget = null,
string? v0Target = null,
string? v0Preset = null,
string? v1Target = null,
string? v1Preset = null,
string? v2Target = null,
string? v2Preset = null,
string? v3Target = null,
string? v3Preset = null,
int totalWidth = 1024,
int totalHeight = 1024,
string? fileName = null,
bool includeUi = false )
{
var scene = ResolveScene();
if ( scene == null )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "multiview.no_scene", "No active scene loaded." );
}
var normLayout = layout.ToLowerInvariant().Trim();
var w = Math.Clamp( totalWidth, 256, 4096 );
var h = Math.Clamp( totalHeight, 256, 4096 );
var specs = new List<ViewportSpec>();
if ( normLayout == "split_vertical" )
{
var halfW = w / 2f;
specs.Add( new ViewportSpec
{
Label = "LEFT [P1 / View 0]",
TargetName = v0Target ?? primaryTarget ?? "Camera",
Preset = v0Preset ?? "fps",
Rect = new Rect( 0, 0, halfW, h )
} );
specs.Add( new ViewportSpec
{
Label = "RIGHT [P2 / View 1]",
TargetName = v1Target ?? primaryTarget ?? "Camera",
Preset = v1Preset ?? "third_person",
Rect = new Rect( halfW, 0, halfW, h )
} );
}
else if ( normLayout == "split_horizontal" )
{
var halfH = h / 2f;
specs.Add( new ViewportSpec
{
Label = "TOP [P1 / View 0]",
TargetName = v0Target ?? primaryTarget ?? "Camera",
Preset = v0Preset ?? "fps",
Rect = new Rect( 0, 0, w, halfH )
} );
specs.Add( new ViewportSpec
{
Label = "BOTTOM [P2 / View 1]",
TargetName = v1Target ?? primaryTarget ?? "Camera",
Preset = v1Preset ?? "third_person",
Rect = new Rect( 0, halfH, w, halfH )
} );
}
else // quad_4view (default)
{
normLayout = "quad_4view";
var halfW = w / 2f;
var halfH = h / 2f;
specs.Add( new ViewportSpec
{
Label = "TOP-LEFT [FPS / Target POV]",
TargetName = v0Target ?? primaryTarget ?? "Camera",
Preset = v0Preset ?? "fps",
Rect = new Rect( 0, 0, halfW, halfH )
} );
specs.Add( new ViewportSpec
{
Label = "TOP-RIGHT [Third-Person Chase]",
TargetName = v1Target ?? primaryTarget ?? "Camera",
Preset = v1Preset ?? "third_person",
Rect = new Rect( halfW, 0, halfW, halfH )
} );
specs.Add( new ViewportSpec
{
Label = "BOTTOM-LEFT [Isometric ARPG]",
TargetName = v2Target ?? primaryTarget ?? "Camera",
Preset = v2Preset ?? "isometric",
Rect = new Rect( 0, halfH, halfW, halfH )
} );
specs.Add( new ViewportSpec
{
Label = "BOTTOM-RIGHT [Tactical RTS / Overhead]",
TargetName = v3Target ?? primaryTarget ?? "Camera",
Preset = v3Preset ?? "rts",
Rect = new Rect( halfW, halfH, halfW, halfH )
} );
}
// Composite Bitmap Canvas
var canvas = new Sandbox.Bitmap( w, h );
canvas.Clear( new Color( 0.08f, 0.08f, 0.10f, 1f ) );
var capturedViewports = new List<object>();
var temporaryCameras = new List<GameObject>();
try
{
for ( int i = 0; i < specs.Count; i++ )
{
var spec = specs[i];
var go = scene.Directory.FindByGuid( Guid.TryParse( spec.TargetName, out var guid ) ? guid : Guid.Empty )
?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, spec.TargetName, StringComparison.OrdinalIgnoreCase ) )
?? scene.GetAllObjects( false ).FirstOrDefault( o => o.GetComponent<CameraComponent>() != null );
var center = go?.WorldPosition ?? Vector3.Zero;
var forward = go?.WorldRotation.Forward ?? Vector3.Forward;
var right = go?.WorldRotation.Right ?? Vector3.Right;
var up = Vector3.Up;
Vector3 camPos;
Rotation camRot;
float camFov = 80f;
switch ( spec.Preset.ToLowerInvariant().Trim() )
{
case "fps":
camPos = center + up * 64f + forward * 6f;
camRot = go?.WorldRotation ?? Rotation.Identity;
camFov = 90f;
break;
case "third_person":
case "tps":
camPos = center + (forward * -85f) + (right * 22f) + (up * 55f);
camRot = Rotation.LookAt( (center + up * 42f) - camPos );
camFov = 75f;
break;
case "rts":
case "overhead":
camPos = center + new Vector3( 0f, -160f, 380f );
camRot = Rotation.LookAt( center - camPos );
camFov = 65f;
break;
case "isometric":
case "dimetric":
// Pitch ~35.264°, Yaw 45°
var isoRadPitch = (float)(35.264 * Math.PI / 180.0);
var isoRadYaw = (float)(45.0 * Math.PI / 180.0);
var isoDist = 320f;
var ox = isoDist * (float)Math.Cos( isoRadPitch ) * (float)Math.Sin( isoRadYaw );
var oy = isoDist * (float)Math.Cos( isoRadPitch ) * (float)Math.Cos( isoRadYaw );
var oz = isoDist * (float)Math.Sin( isoRadPitch );
camPos = center + new Vector3( ox, oy, oz );
camRot = Rotation.LookAt( center - camPos );
camFov = 45f;
break;
case "sidescroller":
case "2.5d":
camPos = center + new Vector3( 0f, -320f, 35f );
camRot = Rotation.LookAt( (center + up * 30f) - camPos );
camFov = 60f;
break;
default: // front / default
camPos = center + forward * 160f + up * 40f;
camRot = Rotation.LookAt( center - camPos );
camFov = 75f;
break;
}
var camGo = scene.CreateObject( true );
camGo.Name = $"__temp_multiview_cam_{i}__";
camGo.WorldPosition = camPos;
camGo.WorldRotation = camRot;
var cam = camGo.Components.Create<CameraComponent>();
cam.FieldOfView = camFov;
temporaryCameras.Add( camGo );
var subW = (int)spec.Rect.Width;
var subH = (int)spec.Rect.Height;
var screenshotResult = EditorRuntime.InvokeStatic( "Editor.Mcp.SceneTools", "CameraScreenshot", cam.Id.ToString(), subW, subH, includeUi );
if ( screenshotResult is Sandbox.Bitmap subBmp )
{
canvas.DrawBitmap( subBmp, spec.Rect );
subBmp.Dispose();
}
else if ( screenshotResult != null )
{
var json = JsonSerializer.Serialize( screenshotResult );
var node = JsonNode.Parse( json );
var base64Data = (node as JsonArray)?[0]?["data"]?.GetValue<string>() ?? (node as JsonObject)?["data"]?.GetValue<string>();
if ( !string.IsNullOrEmpty( base64Data ) )
{
var bytes = Convert.FromBase64String( base64Data );
var importedBmp = Sandbox.Bitmap.CreateFromBytes( bytes );
if ( importedBmp != null )
{
canvas.DrawBitmap( importedBmp, spec.Rect );
importedBmp.Dispose();
}
}
}
capturedViewports.Add( new
{
Index = i,
spec.Label,
Target = go?.Name ?? spec.TargetName,
spec.Preset,
CameraPosition = camPos,
CameraRotation = camRot.Angles(),
FOV = camFov,
Rect = new { spec.Rect.Left, spec.Rect.Top, spec.Rect.Width, spec.Rect.Height }
} );
}
// Draw dividing grid lines
canvas.SetPen( new Color( 0.25f, 0.45f, 0.75f, 0.9f ), 2f );
if ( normLayout == "split_vertical" )
{
canvas.DrawLine( new Vector2( w / 2f, 0 ), new Vector2( w / 2f, h ) );
}
else if ( normLayout == "split_horizontal" )
{
canvas.DrawLine( new Vector2( 0, h / 2f ), new Vector2( w, h / 2f ) );
}
else if ( normLayout == "quad_4view" )
{
canvas.DrawLine( new Vector2( w / 2f, 0 ), new Vector2( w / 2f, h ) );
canvas.DrawLine( new Vector2( 0, h / 2f ), new Vector2( w, h / 2f ) );
}
// Write artifact
var projectRoot = Project.Current?.GetRootPath() ?? Environment.CurrentDirectory;
var artifactDir = Path.Combine( projectRoot, ".sbox", "captures" );
Directory.CreateDirectory( artifactDir );
var outName = string.IsNullOrWhiteSpace( fileName )
? $"multiview_{normLayout}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png"
: fileName.Trim();
var fullPath = Path.Combine( artifactDir, outName );
var pngBytes = canvas.ToPng();
File.WriteAllBytes( fullPath, pngBytes );
return ToolOutcome<object>.Ready( new
{
Layout = normLayout,
TotalWidth = w,
TotalHeight = h,
ViewportCount = specs.Count,
ArtifactPath = fullPath,
FileSizeBytes = pngBytes.Length,
Viewports = capturedViewports
} );
}
finally
{
foreach ( var c in temporaryCameras )
{
c.Destroy();
}
canvas.Dispose();
}
}
}
}