Editor utility that renders framed shots of architecture parts. It sets up a camera, optionally isolates target objects, renders to a bitmap or a grid sheet of bitmaps, and annotates results.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Along/Across use the part's OWN yaw: off-axis parts have no useful world-axis side view.
public enum ArchAngle
{
Iso,
IsoBack,
IsoLocal,
Front,
Back,
Left,
Right,
Top,
Bottom,
Along,
AlongBack,
Across,
AcrossBack
}
public sealed class ArchShotOptions
{
public int Width { get; set; } = 1280;
public int Height { get; set; } = 800;
public bool Orthographic { get; set; } = true;
public float Padding { get; set; } = 0.08f;
public bool Isolate { get; set; }
public bool Annotate { get; set; } = true;
public bool Frame { get; set; }
// Ortho keeps the near plane: pushing it past the near face is a section cut.
public float Cutaway { get; set; }
public float Zoom { get; set; } = 1f;
}
// Framed orthos with a grid - the editor's own screenshot is a gizmo'd perspective grab.
public static partial class ArchShot
{
const string IsolationTag = "arch_shot_subject_v2";
const float MetresToInches = 39.37f;
public static Bitmap Render( ArchTool tool, ArchTarget target, ArchAngle angle, ArchShotOptions options )
{
var bounds = target.Frame;
var rotation = Look( angle, target.Yaw );
var bitmap = new Bitmap( options.Width, options.Height );
var stand = Stand( bounds, rotation, options.Width / (float)options.Height, options );
var camera = Rig( tool.Scene );
try
{
camera.WorldPosition = stand.Eye;
camera.WorldRotation = stand.Rotation;
camera.Orthographic = stand.Orthographic;
camera.OrthographicHeight = stand.OrthoHeight;
camera.FieldOfView = stand.FieldOfView;
camera.CustomSize = new Vector2( options.Width, options.Height );
camera.ZNear = stand.Near;
camera.ZFar = stand.Far;
using var isolation = new Isolation( tool, target, camera, options.Isolate );
camera.RenderToBitmap( bitmap, false );
}
finally
{
camera.GameObject.DestroyImmediate();
}
if ( options.Annotate )
{
Annotate( bitmap, target, angle, stand.Eye, stand.Rotation, stand.OrthoHeight, options );
}
return bitmap;
}
public static Bitmap Sheet( ArchTool tool, ArchTarget target, IReadOnlyList<ArchAngle> angles, ArchShotOptions options )
{
var columns = angles.Count <= 1 ? 1 : 2;
var rows = (angles.Count + columns - 1) / columns;
var cell = new ArchShotOptions
{
Width = options.Width / columns,
Height = options.Height / rows,
Orthographic = options.Orthographic,
Padding = options.Padding,
Isolate = options.Isolate,
Annotate = options.Annotate,
Frame = options.Frame,
Cutaway = options.Cutaway,
Zoom = options.Zoom
};
var sheet = new Bitmap( cell.Width * columns, cell.Height * rows );
sheet.Clear( new Color( 0.06f, 0.065f, 0.075f ) );
for ( var index = 0; index < angles.Count; index++ )
{
using var pane = Render( tool, target, angles[index], cell );
var column = index % columns;
var row = index / columns;
var at = new Rect( column * cell.Width, row * cell.Height, cell.Width, cell.Height );
sheet.DrawBitmap( pane, at );
sheet.SetPen( new Color( 0.28f, 0.3f, 0.34f ), 1f );
sheet.DrawRect( at );
}
return sheet;
}
// Its own camera: no user viewport state, gizmos, or tool overlay.
static CameraComponent Rig( Scene scene )
{
var node = scene.CreateObject();
node.Name = "Architecture Shot";
node.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden;
var camera = node.Components.GetOrCreate<CameraComponent>();
camera.BackgroundColor = new Color( 0.09f, 0.1f, 0.115f );
return camera;
}
// Tag filter, not disabling renderers: a failed render can't leave the scene off.
readonly struct Isolation : IDisposable
{
readonly List<GameObject> tagged;
readonly List<(MeshComponent Mesh, bool Enabled)> hidden;
public Isolation( ArchTool tool, ArchTarget target, CameraComponent camera, bool isolate )
{
tagged = new List<GameObject>();
hidden = new List<(MeshComponent Mesh, bool Enabled)>();
if ( !isolate )
{
return;
}
foreach ( var node in target.RenderObjects() )
{
node.Tags.Add( IsolationTag );
tagged.Add( node );
}
if ( tagged.Count == 0 )
{
return;
}
camera.RenderTags.Add( IsolationTag );
var allowed = target.Meshes().ToHashSet();
foreach ( var root in ArchScene.FindRoots( tool.Scene ) )
{
foreach ( var node in ArchScene.Descendants( root ) )
{
foreach ( var mesh in node.Components.GetAll<MeshComponent>( FindMode.EverythingInSelf ) )
{
if ( allowed.Contains( mesh ) || !mesh.Enabled )
{
continue;
}
hidden.Add( (mesh, mesh.Enabled) );
mesh.Enabled = false;
}
}
}
}
public void Dispose()
{
foreach ( var (mesh, enabled) in hidden.Where( entry => entry.Mesh.IsValid() ) )
{
mesh.Enabled = enabled;
}
foreach ( var node in tagged.Where( node => node.IsValid() ) )
{
node.Tags.Remove( IsolationTag );
}
}
}
}