Editor/Output/ArchCull.cs

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.

File AccessNetworking
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; }
}