Editor utility that renders preview pixmaps for architectural surface slots. It composes simple geometry into a Scene, sets up lights and camera, renders to a Pixmap, and caches results per palette and surface binding.
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchSlotPreview
{
const float Metre = 39.37f;
const float WallHeight = 3f * Metre;
const float WallThickness = 9f;
const float Span = 4f * Metre;
const float Overhang = 30f;
static readonly Dictionary<string, Pixmap> cache = new();
static readonly HashSet<string> failed = new();
public static void Invalidate()
{
cache.Clear();
failed.Clear();
}
public static Pixmap For( string palette, ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, int size = 256 )
{
if ( bindings is null || !bindings.TryGetValue( slot, out var subject ) )
{
return null;
}
var key = $"{palette}|{slot}|{subject.Name}";
if ( cache.TryGetValue( key, out var hit ) )
{
return hit;
}
if ( failed.Contains( key ) )
{
return null;
}
var pixmap = Render( slot, bindings, size );
if ( pixmap is null )
{
failed.Add( key );
return null;
}
cache[key] = pixmap;
return pixmap;
}
static Pixmap Render( ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, int size )
{
Scene scene = null;
try
{
scene = new Scene();
using ( scene.Push() )
{
var subject = Compose( scene, slot, bindings );
if ( subject is null )
{
return null;
}
var camera = Frame( scene, slot, subject );
var pixmap = new Pixmap( size, size );
return camera.RenderToPixmap( pixmap ) ? pixmap : null;
}
}
catch ( Exception exception )
{
Log.Warning( $"Architecture: could not preview {slot}: {exception.Message}" );
return null;
}
finally
{
scene?.Destroy();
}
}
static ModelRenderer Compose( Scene scene, ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings )
{
Ground( scene, Brush( bindings, ArchSurface.Floor ) );
// Stair shots look along the flight; a shell wall would block the camera.
var stairs = slot is ArchSurface.StairTread or ArchSurface.StairRiser;
var wallBrush = Brush( bindings, ArchSurface.WallExterior );
var wall = stairs ? null : Piece( scene, "Wall", canvas => canvas.Box(
new Vector3( -WallThickness, -Span, 0 ),
new Vector3( 0, Span, WallHeight ), wallBrush ) );
return slot switch
{
ArchSurface.WallExterior => wall,
ArchSurface.WallInterior or ArchSurface.Reveal => Piece( scene, "Interior", canvas => canvas.Box(
new Vector3( 0, -Span, 0 ),
new Vector3( WallThickness, Span, WallHeight ), Brush( bindings, slot ) ) ),
ArchSurface.Floor => Ground( scene, Brush( bindings, ArchSurface.Floor ) ),
ArchSurface.Ceiling => Piece( scene, "Ceiling", canvas => canvas.Box(
new Vector3( 0, -Span, WallHeight - 5 ),
new Vector3( Span, Span, WallHeight ), Brush( bindings, slot ) ) ),
// Wound so the pitch faces UP; reversed, it culls to black.
ArchSurface.Roof => Piece( scene, "Roof", canvas => canvas.Quad(
new Vector3( -Overhang, -Span, WallHeight ),
new Vector3( Span * 0.7f, -Span, WallHeight + Span * 0.5f ),
new Vector3( Span * 0.7f, Span, WallHeight + Span * 0.5f ),
new Vector3( -Overhang, Span, WallHeight ), Brush( bindings, slot ) ) ),
ArchSurface.RoofEdge => Piece( scene, "Fascia", canvas => canvas.Box(
new Vector3( -Overhang - 4, -Span, WallHeight - 10 ),
new Vector3( -Overhang, Span, WallHeight + 4 ), Brush( bindings, slot ) ) ),
ArchSurface.Soffit => Piece( scene, "Soffit", canvas => canvas.Box(
new Vector3( -Overhang, -Span, WallHeight - 6 ),
new Vector3( 0, Span, WallHeight - 2 ), Brush( bindings, slot ) ) ),
ArchSurface.Gutter => Gutter( scene, Brush( bindings, slot ) ),
ArchSurface.Trim => Piece( scene, "Trim", canvas => canvas.Box(
new Vector3( -WallThickness - 3, -Span, WallHeight * 0.55f ),
new Vector3( -WallThickness, Span, WallHeight * 0.55f + 14 ), Brush( bindings, slot ) ) ),
ArchSurface.Baseboard => Piece( scene, "Baseboard", canvas => canvas.Box(
new Vector3( 0, -Span, 0 ),
new Vector3( 3, Span, 11 ), Brush( bindings, slot ) ) ),
ArchSurface.Pillar => Piece( scene, "Pillar", canvas => canvas.Box(
new Vector3( -34, -14, 0 ),
new Vector3( -20, 14, WallHeight ), Brush( bindings, slot ) ) ),
ArchSurface.StairTread => Steps( scene, bindings, true ),
ArchSurface.StairRiser => Steps( scene, bindings, false ),
_ => wall
};
}
static ModelRenderer Gutter( Scene scene, ArchBrush brush )
{
Piece( scene, "Fascia", canvas => canvas.Box(
new Vector3( -Overhang - 4, -Span, WallHeight - 10 ),
new Vector3( -Overhang, Span, WallHeight + 4 ), brush ) );
return Piece( scene, "Gutter", canvas =>
{
var front = -Overhang - 14f;
var back = -Overhang - 4f;
var top = WallHeight - 2f;
var floor = WallHeight - 9f;
canvas.Box( new Vector3( front, -Span, floor ), new Vector3( front + 2, Span, top ), brush );
canvas.Box( new Vector3( front, -Span, floor ), new Vector3( back, Span, floor + 2 ), brush );
canvas.Box( new Vector3( back - 2, -Span, floor ), new Vector3( back, Span, top ), brush );
} );
}
static ModelRenderer Steps( Scene scene, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, bool outlineTread )
{
var treadBrush = Brush( bindings, ArchSurface.StairTread );
var riserBrush = Brush( bindings, ArchSurface.StairRiser );
const float rise = 7f;
const float going = 11f;
var treads = Piece( scene, "Treads", canvas =>
{
for ( var step = 0; step < 5; step++ )
{
var z = (step + 1) * rise;
var x = step * going;
canvas.Box( new Vector3( x, -Span * 0.4f, z - 2 ), new Vector3( x + going + 2, Span * 0.4f, z ), treadBrush );
}
} );
var risers = Piece( scene, "Risers", canvas =>
{
for ( var step = 0; step < 5; step++ )
{
var z = (step + 1) * rise;
var x = step * going;
canvas.Box( new Vector3( x, -Span * 0.4f, z - rise ), new Vector3( x + 2, Span * 0.4f, z - 2 ), riserBrush );
}
} );
return outlineTread ? treads : risers;
}
static ModelRenderer Ground( Scene scene, ArchBrush brush )
{
return Piece( scene, "Ground", canvas => canvas.Box(
new Vector3( -Span, -Span, -4 ),
new Vector3( Span, Span, 0 ), brush ) );
}
static ModelRenderer Piece( Scene scene, string name, Action<ArchMesh> build )
{
var canvas = new ArchMesh( Transform.Zero );
build( canvas );
if ( canvas.IsEmpty )
{
return null;
}
var node = new GameObject( true, name );
node.SetParent( scene );
var renderer = node.Components.GetOrCreate<ModelRenderer>();
renderer.Model = canvas.Finish().Rebuild();
renderer.Tint = Color.White;
return renderer;
}
static ArchBrush Brush( Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, ArchSurface slot )
{
var material = ArchBrush.Missing;
if ( bindings.TryGetValue( slot, out var surface ) )
{
material = Material.Load( surface.MaterialPath ) ?? ArchBrush.Missing;
}
return new ArchBrush { Material = material, TexelScale = ArchMesh.TexelScale };
}
static CameraComponent Frame( Scene scene, ArchSurface slot, Renderer subject )
{
var sun = new GameObject( true, "Sun" );
sun.SetParent( scene );
sun.WorldRotation = Rotation.From( -52, 135, 0 );
var light = sun.Components.GetOrCreate<DirectionalLight>();
light.LightColor = Color.White * 4.2f;
light.SkyColor = new Color( 0.42f, 0.47f, 0.58f ) * 3.2f;
light.Shadows = false;
var fill = new GameObject( true, "Fill" );
fill.SetParent( scene );
fill.WorldRotation = Rotation.From( -14, -40, 0 );
var fillLight = fill.Components.GetOrCreate<DirectionalLight>();
fillLight.LightColor = Color.White * 1.5f;
fillLight.SkyColor = Color.Black;
fillLight.Shadows = false;
var node = new GameObject( true, "Camera" );
node.SetParent( scene );
var camera = node.Components.GetOrCreate<CameraComponent>();
camera.BackgroundColor = new Color( 0.07f, 0.075f, 0.085f );
camera.FieldOfView = 42;
camera.ZNear = 1;
camera.ZFar = 5000;
var (target, offset) = Shot( slot );
node.WorldPosition = target + offset;
node.WorldRotation = Rotation.LookAt( (target - node.WorldPosition).Normal );
node.Components.GetOrCreate<Highlight>();
var outline = subject.Components.GetOrCreate<HighlightOutline>();
outline.OverrideTargets = true;
outline.Targets = new List<Renderer> { subject };
outline.Color = Color.White;
outline.ObscuredColor = Color.White.WithAlpha( 0.4f );
outline.InsideColor = Color.Transparent;
outline.InsideObscuredColor = Color.Transparent;
outline.Width = 6f;
return camera;
}
static (Vector3 Target, Vector3 Offset) Shot( ArchSurface slot )
{
var eave = new Vector3( -Overhang, 0, WallHeight );
return slot switch
{
ArchSurface.Roof => (eave + new Vector3( 20, 0, 20 ), new Vector3( -150, -120, 90 )),
ArchSurface.RoofEdge or ArchSurface.Soffit => (eave, new Vector3( -78, -70, -26 )),
ArchSurface.Gutter => (eave + new Vector3( -9, 0, -4 ), new Vector3( -62, -58, -18 )),
ArchSurface.Trim => (new Vector3( -WallThickness, 0, WallHeight * 0.58f ), new Vector3( -74, -62, 26 )),
ArchSurface.Baseboard => (new Vector3( 2, 0, 8 ), new Vector3( 66, -54, 34 )),
ArchSurface.Pillar => (new Vector3( -27, 0, WallHeight * 0.5f ), new Vector3( -104, -86, 44 )),
ArchSurface.Floor => (Vector3.Zero, new Vector3( 96, -96, 96 )),
ArchSurface.Ceiling => (new Vector3( Span * 0.4f, 0, WallHeight - 4 ), new Vector3( 92, -78, -44 )),
ArchSurface.WallInterior or ArchSurface.Reveal => (new Vector3( WallThickness, 0, WallHeight * 0.5f ), new Vector3( 128, -96, 34 )),
ArchSurface.StairTread or ArchSurface.StairRiser => (new Vector3( 28, 0, 20 ), new Vector3( -66, -84, 58 )),
_ => (new Vector3( -WallThickness, 0, WallHeight * 0.5f ), new Vector3( -132, -104, 40 ))
};
}
}