Editor/Road/ArchRoadTerrainContribution.cs
namespace Sunless.Architecture;
public sealed class ArchRoadTerrainContribution : IArchTerrainContribution {
public string Ident => "sunless.arch_roads";
public int Carve( ArchTerrainRequest request ) {
var padding = MathF.Max( 8f, request.Kit.RoadPadding );
var inset = MathF.Max( 0f, request.Kit.RoadInset );
var ground = new ArchGround( request.Terrain.Scene );
var stamped = 0;
var shallow = 0;
foreach ( var road in request.Plan.Roads().Where( entry => entry.Stamp ) ) {
var curve = road.Curve();
if ( !curve.IsUsable ) {
continue;
}
var frames = ArchRoadGen.Frames( road, curve );
if ( frames.Count < 2 ) {
continue;
}
var bores = ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan )
.Where( shape => shape.Part.Bored )
.ToList();
var verge = road.Reach();
var reach = verge + padding;
var min = new Vector2( frames.Min( frame => frame.Position.x ), frames.Min( frame => frame.Position.y ) );
var max = new Vector2( frames.Max( frame => frame.Position.x ), frames.Max( frame => frame.Position.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 );
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 );
if ( !Closest( frames, world, out var surface, out var gap, out var right, out var along ) || gap > reach ) {
continue;
}
if ( ArchBridge.Carried( road, along ) ) {
continue;
}
if ( bores.Any( bore => bore.Carries( along ) ) ) {
continue;
}
var index = y * request.Resolution + x;
var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
var shoulder = road.Reach( right );
var target = Shoulder( road, request.Kit, surface, gap, shoulder, inset );
if ( gap > shoulder ) {
target = MathX.Lerp( target, current, Ease( (gap - shoulder) / padding ) );
}
// A road is cut into the ground, never stood on a bank raised to meet it: filling here is what
// walled a street in on both sides and left the kerb reading as a plinth.
if ( target >= current ) {
shallow++;
continue;
}
request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
stamped++;
}
}
}
// Silence here reads as a broken stamp; it is nearly always a road laid over the ground rather than into it.
if ( stamped == 0 && shallow > 0 ) {
Log.Info( $"Architecture: every one of {shallow} samples under the roads already sits below them - nothing to cut. Lower the run onto the ground, or give the ground headroom first." );
}
return stamped;
}
public int Cut( ArchTerrainRequest request ) {
var ground = new ArchGround( request.Terrain.Scene );
var cut = 0;
foreach ( var road in request.Plan.Roads() ) {
var curve = road.Curve();
if ( !curve.IsUsable ) {
continue;
}
var frames = ArchRoadGen.Frames( road, curve );
foreach ( var tunnel in ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan ) ) {
foreach ( var mouth in new[] { tunnel.Near, tunnel.Far } ) {
cut += Cutting( tunnel, mouth, request );
}
}
}
return cut;
}
static int Cutting( ArchTunnelShape shape, ArchTunnelMouth mouth, ArchTerrainRequest request ) {
var part = shape.Part;
var frame = mouth.Frame;
var reach = ArchTunnel.Reach( part, shape.Section );
var padding = MathF.Max( 8f, request.Kit.RoadPadding );
var inset = MathF.Max( 0f, request.Kit.RoadInset );
var run = MathF.Max( MathF.Max( 96f, part.WingWall ), MathF.Max( 12f, part.PortalDepth ) * 2f );
var splay = MathF.Tan( Math.Clamp( part.WingSplay, 0f, 75f ).DegreeToRadian() );
var outward = mouth.Near ? -frame.Along : frame.Along;
var ahead = new Vector2( outward.x, outward.y ).Normal;
var sideways = new Vector2( -ahead.y, ahead.x );
var floor = frame.Position.z + shape.Section.Floor - inset;
var widest = reach + run * splay + padding;
var lo = Cell( frame.Flat - new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
var hi = Cell( frame.Flat + new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
var cut = 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 offset = world - frame.Flat;
var along = Vector2.Dot( offset, ahead );
if ( along < 0f || along > run + padding ) {
continue;
}
var across = MathF.Abs( Vector2.Dot( offset, sideways ) );
var half = reach + along * splay;
var outside = MathF.Max( across - half, along - run );
if ( outside >= padding ) {
continue;
}
var index = y * request.Resolution + x;
var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
var target = outside > 0f ? MathX.Lerp( current, floor, Ease( 1f - outside / padding ) ) : floor;
if ( target >= current ) {
continue;
}
request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
cut++;
}
}
return cut;
}
// The ground meets the section it runs beside, not the crown. Carve the whole verge to the carriageway and the
// pavement is left standing on a plinth its own kerb height - which is what the drive's landing already knew.
public static float Shoulder( ArchRoadPart road, ArchKit kit, float surface, float gap, float shoulder, float inset ) {
var carriage = road.HalfWidth;
var grade = surface - inset;
if ( gap <= carriage || shoulder <= carriage + 0.01f ) {
return grade;
}
return MathX.Lerp( grade, surface + ArchCrossover.Back( road, kit ),
Math.Clamp( (gap - carriage) / (shoulder - carriage), 0f, 1f ) );
}
static bool Closest( IReadOnlyList<ArchFrame> frames, Vector2 point, out float height, out float gap, out bool right, out float station ) {
height = 0f;
gap = float.MaxValue;
right = false;
station = 0f;
for ( var index = 0; index < frames.Count - 1; index++ ) {
var from = frames[index];
var to = frames[index + 1];
var span = to.Flat - from.Flat;
var length = span.Length;
if ( length < 0.01f ) {
continue;
}
var direction = span / length;
var along = Math.Clamp( Vector2.Dot( point - from.Flat, direction ), 0f, length );
var offset = point - (from.Flat + direction * along);
var reach = offset.Length;
if ( reach >= gap ) {
continue;
}
gap = reach;
// Which pavement this sample stands beside: only that side's verge decides how high the ground comes up.
right = Vector2.Dot( offset, new Vector2( direction.y, -direction.x ) ) > 0f;
height = MathX.Lerp( from.Position.z, to.Position.z, along / length );
station = MathX.Lerp( from.Distance, to.Distance, along / length );
}
return gap < float.MaxValue;
}
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);
}
}