Files
TheriapolisV3/Theriapolis.Game/Rendering/LineFeatureRenderer.cs
T

203 lines
7.4 KiB
C#
Raw Normal View History

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Theriapolis.Core.World.Polylines;
using Theriapolis.Core.Util;
namespace Theriapolis.Game.Rendering;
/// <summary>
/// Renders rivers, roads, and rail lines as thick world-space polylines.
/// Uses simplified LOD geometry at low zoom levels.
/// </summary>
public sealed class LineFeatureRenderer : IDisposable
{
private readonly GraphicsDevice _gd;
private readonly WorldGenContext _ctx;
private Texture2D _pixel = null!;
private bool _disposed;
// Zoom threshold below which SimplifiedPoints are used
private const float LodSwitchZoom = 0.15f;
// Line widths in world pixels at full zoom (scaled by camera zoom)
private const float RiverMajorWidth = 6f;
private const float RiverWidth = 3.5f;
private const float StreamWidth = 1.5f;
private const float RailWidth = 3f;
private const float HighwayWidth = 4f;
private const float PostRoadWidth = 2.5f;
private const float DirtRoadWidth = 1.5f;
// Line colors
private static readonly Color RiverColor = new(60, 120, 200);
private static readonly Color RailColor = new(120, 100, 80);
private static readonly Color RailTieColor = new(80, 70, 60);
private static readonly Color HighwayColor = new(210, 180, 80);
private static readonly Color PostRoadColor = new(180, 155, 70);
private static readonly Color DirtRoadColor = new(150, 130, 90);
private static readonly Color BridgeDeckColor = new(160, 140, 100);
private static readonly Color BridgeRailColor = new(100, 85, 60);
private const float BridgeDeckWidth = 6f; // wider than HighwayWidth so the deck fully covers the road underneath
public LineFeatureRenderer(GraphicsDevice gd, WorldGenContext ctx)
{
_gd = gd;
_ctx = ctx;
BuildPixel();
}
private void BuildPixel()
{
_pixel = new Texture2D(_gd, 1, 1);
_pixel.SetData(new[] { Color.White });
}
public void Draw(SpriteBatch sb, Camera2D camera)
{
bool useLod = camera.Zoom < LodSwitchZoom;
sb.Begin(
transformMatrix: camera.TransformMatrix,
samplerState: SamplerState.PointClamp,
sortMode: SpriteSortMode.Deferred,
blendState: BlendState.AlphaBlend);
// Draw order: roads → rivers → bridges → rail (rail on top)
DrawRoads(sb, useLod, camera.Zoom);
DrawRivers(sb, useLod, camera.Zoom);
DrawBridges(sb, camera.Zoom);
DrawRail(sb, useLod, camera.Zoom);
sb.End();
}
private void DrawRoads(SpriteBatch sb, bool useLod, float zoom)
{
// Smallest first, biggest last — so when a smaller road has been merged
// onto a larger road by PolylineCleanupStage, the larger road's wider
// stroke covers the overdraw and the junction looks clean.
foreach (var road in _ctx.World.Roads.OrderBy(RoadDrawRank))
{
var (color, width) = road.RoadClassification switch
{
RoadType.Highway => (HighwayColor, HighwayWidth),
RoadType.PostRoad => (PostRoadColor, PostRoadWidth),
_ => (DirtRoadColor, DirtRoadWidth),
};
DrawPolyline(sb, road, color, width, useLod);
}
}
private static int RoadDrawRank(Polyline r) => r.RoadClassification switch
{
RoadType.Footpath => 0,
RoadType.DirtRoad => 1,
RoadType.PostRoad => 2,
RoadType.Highway => 3,
_ => 1,
};
private void DrawRivers(SpriteBatch sb, bool useLod, float zoom)
{
foreach (var river in _ctx.World.Rivers)
{
var (color, width) = river.RiverClassification switch
{
RiverClass.MajorRiver => (RiverColor, RiverMajorWidth),
RiverClass.River => (RiverColor, RiverWidth),
_ => (RiverColor, StreamWidth),
};
// Scale river width slightly by flow for a natural look
float flowScale = 1f + (river.FlowAccumulation / (float)C.RIVER_MAJOR_THRESHOLD) * 0.3f;
DrawPolyline(sb, river, color, Math.Min(width * flowScale, RiverMajorWidth * 1.5f), useLod);
}
}
private void DrawRail(SpriteBatch sb, bool useLod, float zoom)
{
foreach (var rail in _ctx.World.Rails)
{
// Draw tie marks underneath, then the rail line on top
DrawPolyline(sb, rail, RailTieColor, RailWidth + 2f, useLod);
DrawPolyline(sb, rail, RailColor, RailWidth * 0.5f, useLod);
}
}
private void DrawBridges(SpriteBatch sb, float zoom)
{
foreach (var bridge in _ctx.World.Bridges)
{
Vector2 start = new(bridge.Start.X, bridge.Start.Y);
Vector2 end = new(bridge.End.X, bridge.End.Y);
Vector2 span = end - start;
float len = span.Length();
if (len < 0.5f) continue;
Vector2 roadDir = span / len;
Vector2 perpDir = new(-roadDir.Y, roadDir.X);
// Bridge deck: follows the actual road polyline at the crossing.
DrawSegment(sb, start, end, BridgeDeckColor, BridgeDeckWidth);
// Bridge abutments: short perpendicular bars at each end.
float halfBar = BridgeDeckWidth * 1.5f;
DrawSegment(sb, start - perpDir * halfBar, start + perpDir * halfBar, BridgeRailColor, 1f);
DrawSegment(sb, end - perpDir * halfBar, end + perpDir * halfBar, BridgeRailColor, 1f);
}
}
private void DrawPolyline(SpriteBatch sb, Polyline polyline, Color color, float worldWidth, bool useLod)
{
var pts = (useLod && polyline.SimplifiedPoints is { Count: >= 2 })
? polyline.SimplifiedPoints
: polyline.Points;
if (pts.Count < 2) return;
for (int i = 0; i < pts.Count - 1; i++)
{
DrawSegment(sb,
new Vector2(pts[i].X, pts[i].Y),
new Vector2(pts[i + 1].X, pts[i + 1].Y),
color, worldWidth);
}
}
private void DrawSegment(SpriteBatch sb, Vector2 from, Vector2 to, Color color, float width)
{
Vector2 diff = to - from;
float len = diff.Length();
if (len < 0.5f) return;
// Extend segment by half-width at both ends so consecutive segments
// overlap at joints, filling the triangular gap that appears when
// two thick rotated rectangles meet at an angle.
float extend = width * 0.5f;
Vector2 dir = diff / len;
Vector2 start = from - dir * extend;
float extLen = len + 2f * extend;
float angle = MathF.Atan2(diff.Y, diff.X);
sb.Draw(
texture: _pixel,
position: start,
sourceRectangle: null,
color: color,
rotation: angle,
origin: new Vector2(0f, 0.5f),
scale: new Vector2(extLen, width),
effects: SpriteEffects.None,
layerDepth: 0f);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_pixel?.Dispose();
}
}