Editor utility that generates and applies blockout materials for architectural roles. It defines color mappings for ArchSurface roles, produces .vmat material files with a grid texture, ensures files exist in the project's assets, provides brushes and palette application helpers, and caches a grid pixmap.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchBlockout
{
public const string Folder = "materials/blockout";
public const string GridTexture = "materials/blockout/blockout_grid.png";
static Pixmap grid;
static bool written;
// Never scale per role - 512px at 128 px/m is one 4m tile.
public static readonly IReadOnlyList<(ArchSurface Role, string Hex)> Roles = new[]
{
(ArchSurface.WallExterior, "e8e4dc"),
(ArchSurface.WallInterior, "b9c6d6"),
(ArchSurface.WallCap, "6c7a89"),
(ArchSurface.Reveal, "7fd1c4"),
(ArchSurface.Trim, "ffb347"),
(ArchSurface.Sill, "ff8c42"),
(ArchSurface.Threshold, "b4ff39"),
(ArchSurface.Baseboard, "e03c31"),
(ArchSurface.Floor, "8c7b6b"),
(ArchSurface.Ceiling, "d9cbb3"),
(ArchSurface.Roof, "4a6fa5"),
(ArchSurface.RoofEdge, "7ba7d9"),
(ArchSurface.Soffit, "a8c8e8"),
(ArchSurface.Gutter, "2e8b57"),
(ArchSurface.StairTread, "c8a02c"),
(ArchSurface.StairRiser, "8f6b1f"),
(ArchSurface.StairStringer, "5e4b22"),
(ArchSurface.Pillar, "9b59b6"),
(ArchSurface.PillarCap, "c39bd3"),
(ArchSurface.DoorLeaf, "d2691e"),
(ArchSurface.Siding, "7fb069"),
(ArchSurface.Wainscot, "4f7942"),
(ArchSurface.WindowFrame, "ff69b4"),
(ArchSurface.WindowSash, "ffb6d5"),
(ArchSurface.Deck, "b5651d"),
(ArchSurface.Railing, "00a6a6"),
(ArchSurface.Foundation, "555555"),
(ArchSurface.Road, "3b3b46"),
(ArchSurface.Pavement, "8e9aa8"),
(ArchSurface.Kerb, "f2f2f2"),
(ArchSurface.RoadLine, "ffe14d"),
(ArchSurface.Panel, "a3b0a0"),
(ArchSurface.Post, "6b705c"),
(ArchSurface.Wire, "ff2d55"),
(ArchSurface.RoofFrame, "8a5a2b"),
(ArchSurface.Bridge, "6e7f8d"),
(ArchSurface.Pier, "3f4c57"),
(ArchSurface.Lining, "8d8578"),
(ArchSurface.Portal, "5c5346"),
// Must read as clearly different from WallExterior, or a band can't be judged.
(ArchSurface.WallBase, "9c5a44"),
(ArchSurface.Pipework, "c2453d"),
(ArchSurface.Cabling, "3d7fc2"),
(ArchSurface.Bracket, "d8d2c4")
};
public static string PathFor( ArchSurface role ) => $"{Folder}/blockout_{Slug( role )}.vmat";
public static bool Paints( ArchSurface role ) => Roles.Any( pair => pair.Role == role );
// What a role falls back to while nothing has been painted, so the first building placed after installing the
// library reads as a blockout rather than as missing-material grey. Writes the vmats on the first ask.
public static ArchBrush Brush( ArchSurface role )
{
if ( !written )
{
Ensure();
}
return new ArchBrush
{
Material = ArchStyle.Load( PathFor( role ) ),
TexelScale = ArchMesh.TexelScale
};
}
public static string Describe() => $"Every role in its own hue over one seamless grid, so a defect can be named by colour"
+ $" rather than pointed at. The grid is a ruler too - 512px at 0.3076 units/texel is exactly 4m, heavy lines every metre"
+ $" and light every quarter. Glass is deliberately left out: painted opaque it hides every fitting behind it.";
// Cached for the session - the file only changes when redrawn by hand.
public static Pixmap Grid()
{
if ( grid is not null )
{
return grid;
}
// This library ships the grid, so it is on the content path and never in the host game's own Assets folder.
var file = Editor.FileSystem.Content.GetFullPath( GridTexture );
return grid = string.IsNullOrWhiteSpace( file ) ? null : Pixmap.FromFile( file );
}
public static Color ColourOf( ArchSurface role )
{
var entry = Roles.FirstOrDefault( pair => pair.Role == role );
return entry.Hex is null ? Color.White : Color.Parse( $"#{entry.Hex}" ) ?? Color.White;
}
// Glass is absent on purpose - opaque it hides every fitting behind it.
public static int Apply( ArchPalette target )
{
return Apply( target, Roles.Select( pair => pair.Role ) );
}
public static int Apply( ArchPalette target, IEnumerable<ArchSurface> roles )
{
if ( target is null )
{
return 0;
}
Ensure();
var assigned = 0;
foreach ( var role in roles.Distinct() )
{
target.Set( role, PathFor( role ), ArchMesh.TexelScale );
assigned++;
}
ArchStyle.InvalidateCache();
Log.Info( $"Architecture: applied the blockout grid to {assigned} roles." );
return assigned;
}
public static void Clear( ArchPalette target )
{
if ( target is null )
{
return;
}
foreach ( var pair in Roles )
{
target.Set( pair.Role, null );
}
ArchStyle.InvalidateCache();
}
public static void Ensure()
{
var assets = Project.Current?.GetAssetsPath();
if ( string.IsNullOrWhiteSpace( assets ) )
{
return;
}
written = true;
var folder = Path.Combine( assets, Folder.Replace( '/', Path.DirectorySeparatorChar ) );
Directory.CreateDirectory( folder );
var shader = ArchHost.Load().BlockoutShader;
foreach ( var pair in Roles )
{
var file = Path.Combine( folder, $"blockout_{Slug( pair.Role )}.vmat" );
if ( File.Exists( file ) )
{
continue;
}
File.WriteAllText( file, Definition( pair.Role, pair.Hex, shader ) );
AssetSystem.RegisterFile( file )?.Compile( false );
}
}
static string Definition( ArchSurface role, string hex, string shader )
{
var colour = Color.Parse( $"#{hex}" ) ?? Color.White;
var texture = role is ArchSurface.WindowFrame or ArchSurface.WindowSash or ArchSurface.Sill
? "[1.000000 1.000000 1.000000 0.000000]"
: GridTexture;
return $$"""
// THIS FILE IS AUTO-GENERATED by ArchBlockout - edit the colour table, not this file.
// BLOCKOUT {{role}}
Layer0
{
shader "{{shader}}"
//---- Fog ----
g_bFogEnabled "1"
//---- Material ----
g_flTintColor "[{{colour.r:0.000000}} {{colour.g:0.000000}} {{colour.b:0.000000}} 0.000000]"
TextureColor "{{texture}}"
TextureNormal "[0.500000 0.500000 1.000000 0.000000]"
TextureRoughness "[0.850000 0.850000 0.850000 0.000000]"
TextureMetalness "[0.000000 0.000000 0.000000 0.000000]"
TextureAmbientOcclusion "[1.000000 1.000000 1.000000 0.000000]"
}
""";
}
static string Slug( ArchSurface role )
{
var name = role.ToString();
var text = new System.Text.StringBuilder();
foreach ( var letter in name )
{
if ( char.IsUpper( letter ) && text.Length > 0 )
{
text.Append( '_' );
}
text.Append( char.ToLowerInvariant( letter ) );
}
return text.ToString();
}
}