Editor utility for architectural mesh culling. It scans generated scene meshes under an architecture root, samples face points, traces rays against the scene to remove hidden or enclosed faces (roof/overhang culling), builds roof void regions, and updates meshes with removed faces.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
// Reach clears a wall's thickness, no more: longer culls every far wall.
public static partial class ArchCull
{
const float Skin = 0.15f;
const float Reach = 12f;
const float Upward = 1024f;
const float Fifth = 0.2f;
const float Nudge = 0.25f;
public static int Clean( Scene scene, ArchPlan plan, ArchKit kit )
{
var root = ArchScene.FindRoot( scene );
if ( !root.IsValid() )
{
Log.Warning( "Architecture: nothing to clean - no generated root in this scene." );
return 0;
}
var meshes = root.Components
.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants )
.Where( mesh => mesh.Mesh is not null )
.ToList();
if ( meshes.Count == 0 )
{
return 0;
}
var voids = Voids( plan, kit );
var removed = 0;
using ( SceneEditorSession.Active.UndoScope( "Clean Hidden Faces" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() )
{
foreach ( var mesh in meshes )
{
removed += Strip( scene, mesh, voids );
}
Lined( root, plan, kit );
}
return removed;
}
static int Strip( Scene scene, MeshComponent renderer, List<ArchRoofVoid> voids )
{
var mesh = renderer.Mesh;
var transform = renderer.WorldTransform;
var dead = mesh.FaceHandles
.ToList()
.Where( face => Dead( scene, renderer, mesh, face, transform, voids ) )
.ToList();
if ( dead.Count == 0 )
{
return 0;
}
mesh.RemoveFaces( dead );
renderer.RebuildMesh();
return dead.Count;
}
static bool Dead( Scene scene, MeshComponent renderer, PolygonMesh mesh, FaceHandle face, Transform transform, List<ArchRoofVoid> voids )
{
mesh.ComputeFaceNormal( face, out var local );
var normal = transform.NormalToWorld( local ).Normal;
if ( normal.Length < 0.5f )
{
return false;
}
return Samples( mesh, face, transform, Fifth, 0f ).All( point => Hidden( scene, point, normal ) )
|| Samples( mesh, face, transform, 0f, Nudge ).All( point => Enclosed( scene, renderer, voids, point, normal ) );
}
// A fifth in keeps half-buried faces; enclosure can't - feet drag corners past the eave.
static IEnumerable<Vector3> Samples( PolygonMesh mesh, FaceHandle face, Transform transform, float fraction, float nudge )
{
var centre = transform.PointToWorld( mesh.GetFaceCenter( face ) );
yield return centre;
foreach ( var corner in mesh.GetFaceVertexPositions( face, transform ) )
{
var point = Vector3.Lerp( corner, centre, fraction );
var toward = centre - point;
yield return toward.Length > nudge ? point + toward.Normal * nudge : centre;
}
}
// Both rays: the inward one alone reads the far wall as cover - gutters die.
static bool Hidden( Scene scene, Vector3 point, Vector3 normal )
{
var outward = point + normal * Reach;
var surface = point + normal * Skin;
return scene.Trace.Ray( outward, surface ).Run().Hit && !scene.Trace.Ray( surface, outward ).Run().Hit;
}
// Trace up must hit the piece's OWN object - a taller overhang covers from above too.
static bool Enclosed( Scene scene, MeshComponent renderer, List<ArchRoofVoid> voids, Vector3 point, Vector3 normal )
{
if ( voids.Count == 0 )
{
return false;
}
var probe = point + normal * Skin;
var ground = new Vector2( probe.x, probe.y );
foreach ( var cavity in voids )
{
if ( probe.z < cavity.Floor )
{
continue;
}
if ( !cavity.Region.Any( loop => ArchFloorGen.Contains( loop, ground ) ) )
{
continue;
}
var above = scene.Trace.Ray( probe, probe + Vector3.Up * Upward ).Run();
if ( above.Hit && above.GameObject == renderer.GameObject )
{
return true;
}
}
return false;
}
static List<ArchRoofVoid> Voids( ArchPlan plan, ArchKit kit )
{
var voids = new List<ArchRoofVoid>();
foreach ( var building in plan.Buildings )
{
foreach ( var roof in building.Roofs )
{
if ( !Sealed( roof, building, plan, kit ) )
{
continue;
}
var boxed = roof.Soffit && roof.Fascia && roof.Overhang > 0.5f;
voids.Add( new ArchRoofVoid
{
Region = Region( roof, boxed ? roof.Overhang + ArchProfiles.FasciaDepth( kit ) + 2f : 0f ),
// A bite below the plate takes the ceiling's top face, not the room's ceiling.
Floor = roof.BaseHeight - 1f
} );
}
}
return voids;
}
// Never the raked end: out there the deck flies over the gable in open air.
static List<List<Vector2>> Region( ArchRoofPart roof, float reach )
{
var outline = new List<List<Vector2>> { ArchFootprint.Wind( roof.Outline() ) };
if ( reach < 0.5f )
{
return outline;
}
// Hip and flat shed over every edge, so the whole ring is boxed.
if ( roof.Style is RoofStyle.Hip or RoofStyle.Flat )
{
return ArchFootprint.Grow( outline, reach );
}
// Sheds and sawtooth bays want their own answer; until then, keep their overhangs.
if ( roof.Style != RoofStyle.Gable )
{
return outline;
}
var eaves = roof.RidgeAlongX ? new Vector2( 0f, reach ) : new Vector2( reach, 0f );
return new List<List<Vector2>> { ArchFootprint.Rect( roof.Min - eaves, roof.Max + eaves ) };
}
// No way in and nothing above: anything else is a room, or becoming one.
static bool Sealed( ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit )
{
if ( !roof.Ceiling || roof.LoftFloor || !roof.EndWalls )
{
return false;
}
if ( building.Cutouts.Any( cutout => cutout.Level > roof.Level && Overlaps( roof, cutout.Min, cutout.Max ) ) )
{
return false;
}
// A carve cut is derived, so nothing stored says open - its own storey counts,
// and one anchored below still opens the roof it climbs through.
if ( plan.AllCuts().Any( cut =>
cut.Outlines().Any( loop => Opens( roof, loop ) )
&& ( cut.Level >= roof.Level
|| ArchCut.Resolve( cut, kit ).Any( volume => ArchCut.Reaches( volume, roof.BaseHeight - kit.FloorThickness, roof.BaseHeight ) ) ) ) )
{
return false;
}
foreach ( var room in building.Rooms )
{
if ( room.Floor == roof.Level && room.Stairs.Any( stair => Overlaps( roof, stair.Origin, stair.Origin ) ) )
{
return false;
}
if ( room.Floor <= roof.Level )
{
continue;
}
var footprint = ArchFloorGen.Footprint( room );
if ( footprint.Count < 3 )
{
continue;
}
ArchFootprint.Bounds( footprint, out var lo, out var hi );
if ( Overlaps( roof, lo, hi ) )
{
return false;
}
}
return true;
}
static bool Overlaps( ArchRoofPart roof, Vector2 min, Vector2 max )
{
return min.x <= roof.Max.x && max.x >= roof.Min.x && min.y <= roof.Max.y && max.y >= roof.Min.y;
}
static bool Opens( ArchRoofPart roof, IReadOnlyList<Vector2> loop )
{
ArchFootprint.Bounds( loop, out var min, out var max );
return Overlaps( roof, min, max );
}
}
sealed class ArchRoofVoid
{
public List<List<Vector2>> Region { get; init; }
public float Floor { get; init; }
}