Editor component that contributes stair/approach terrain to an architectural terrain map. It iterates plan buildings, rooms and approaches, computes ramp or spline runs, and stamps height values into a heightmap (request.Map) to shape roads/ramps with padding, inset and easing.
namespace Sunless.Architecture;
public sealed class ArchApproachTerrainContribution : IArchTerrainContribution
{
public string Ident => "sunless.arch.stair";
public int Raise( ArchTerrainRequest request )
{
var stamped = 0;
foreach ( var building in request.Plan.Buildings )
{
var lift = ArchAsks.Lift( request.Plan, building, request.Kit );
foreach ( var room in building.Rooms )
{
foreach ( var approach in room.Approaches )
{
if ( approach.IsSpline )
{
foreach ( var run in ArchApproachGen.SplineRuns( approach, room, building, request.Kit, request.Plan ) )
{
stamped += Along( run, lift, request );
}
continue;
}
if ( ArchApproachGen.Resolved( approach, room, building, request.Kit, request.Plan, out var ramp ) )
{
stamped += Along( ramp, lift, request );
}
}
}
}
return stamped;
}
public int Carve( ArchTerrainRequest request ) => 0;
public int Cut( ArchTerrainRequest request ) => 0;
static int Along( ArchApproachGen.ArchRampRun ramp, float lift, ArchTerrainRequest request )
{
var span = ramp.Foot - ramp.Wall;
var length = span.Length;
if ( length < 1f )
{
return 0;
}
var padding = MathF.Max( 8f, request.Kit.RoadPadding );
var inset = MathF.Max( 0f, request.Kit.RoadInset );
var direction = span / length;
var sideways = new Vector2( -direction.y, direction.x );
var reach = ramp.HalfWidth + padding;
var min = new Vector2( MathF.Min( ramp.Wall.x, ramp.Foot.x ), MathF.Min( ramp.Wall.y, ramp.Foot.y ) );
var max = new Vector2( MathF.Max( ramp.Wall.x, ramp.Foot.x ), MathF.Max( ramp.Wall.y, ramp.Foot.y ) );
var lo = Cell( min - new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );
var hi = Cell( max + new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );
var stamped = 0;
for ( var y = lo.y; y <= hi.y; y++ )
{
for ( var x = lo.x; x <= hi.x; x++ )
{
var world = new Vector2( request.Origin.x + x * request.Step, request.Origin.y + y * request.Step );
var along = Vector2.Dot( world - ramp.Wall, direction );
var gap = MathF.Abs( Vector2.Dot( world - ramp.Wall, sideways ) );
var outside = MathF.Max( gap - ramp.HalfWidth, MathF.Max( -along, along - length ) );
if ( outside >= padding )
{
continue;
}
var index = y * request.Resolution + x;
var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
var deck = MathX.Lerp( ramp.WallTop, ramp.FootTop, Math.Clamp( along / length, 0f, 1f ) ) + lift;
var target = deck - inset;
if ( outside > 0f )
{
target = MathX.Lerp( current, target, Ease( 1f - outside / padding ) );
}
request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
stamped++;
}
}
return stamped;
}
static (int x, int y) Cell( Vector2 world, Vector3 origin, float step, int resolution )
{
var x = (int)MathF.Round( (world.x - origin.x) / step );
var y = (int)MathF.Round( (world.y - origin.y) / step );
return (Math.Clamp( x, 0, resolution - 1 ), Math.Clamp( y, 0, resolution - 1 ));
}
static float Ease( float t )
{
var clamped = Math.Clamp( t, 0f, 1f );
return clamped * clamped * (3f - 2f * clamped);
}
}