Geometry code for lake and river junctions. Defines LakeOpening (metadata about a river opening into a lake), LakeJet (a read-only struct modeling a widening/slowing jet from a river into a lake with evaluation and presence math), and LakeOutline (static methods building polygonal outlines for lakes and mouths, adding flares between shore and river rows, and helper math).
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Where a river opens into a lake (or out of it): where round the shore, how wide the river is and how far the shore
/// flares into it, and the river's own row where the lake hands over to it. The flares are part of the lake's outline,
/// so the lake's surface fills the corners between the shore and the river, and its outline runs across the river's
/// row point for point.
/// </summary>
internal sealed class LakeOpening
{
public int NodeId;
public WaterRiver River;
/// <summary>
/// The river runs into the lake (otherwise it runs out of it).
/// </summary>
public bool Inflow;
/// <summary>
/// The middle of the opening, as a distance round the shore.
/// </summary>
public float At;
public float HalfWidth;
public float Flare;
/// <summary>
/// The river's row where the lake hands over to it, in the order the shore runs.
/// </summary>
public RiverRow Row;
/// <summary>
/// The way the river heads away from the lake at its row, level.
/// </summary>
public Vector3 Away;
/// <summary>
/// How much shore the opening and its flares take either side of its middle.
/// </summary>
public float Reach => HalfWidth + Flare;
}
/// <summary>
/// How a river's water spreads into a lake: a jet that leaves the river's mouth, widens and slows as it goes, so a river
/// flowing in pushes a plume of current out into the lake and the water by a lake's outlet is drawn towards it.
/// </summary>
internal readonly struct LakeJet
{
/// <summary>
/// How fast the jet widens: this much either side for every unit it travels.
/// </summary>
private const float Spread = 0.3f;
public readonly Vector2 Mouth;
public readonly Vector2 Inward;
public readonly Vector2 Across;
public readonly float HalfWidth;
public readonly Vector2 Velocity;
public readonly float Foam;
public readonly float Length;
public LakeJet(Vector2 _Mouth, Vector2 _Inward, float _HalfWidth, Vector2 _Velocity, float _Foam, float _Length)
{
Mouth = _Mouth;
Inward = _Inward;
Across = new Vector2(-_Inward.y, _Inward.x);
HalfWidth = MathF.Max(1.0f, _HalfWidth);
Velocity = _Velocity;
Foam = _Foam;
Length = MathF.Max(1.0f, _Length);
}
public Vector2 Evaluate(Vector2 _Point, out float _Foam)
{
float strength = Strength(_Point, out float half);
_Foam = Foam * strength;
// As the jet widens the same water spreads over more of the lake, so it slows.
return Velocity * (strength * HalfWidth / half);
}
/// <summary>
/// How much of the river's own water is at a point: all of it in the jet's middle at its mouth, fading out across it
/// and along it.
/// </summary>
public float Presence(Vector2 _Point)
{
return Strength(_Point, out _);
}
private float Strength(Vector2 _Point, out float _Half)
{
Vector2 offset = _Point - Mouth;
float along = MathF.Max(0.0f, Vector2.Dot(offset, Inward));
float side = MathF.Abs(Vector2.Dot(offset, Across));
_Half = HalfWidth + along * Spread;
// Full strength across the middle of the jet, fading out past its edges.
float profile = 1.0f - SmoothStep(0.7f, 1.4f, side / _Half);
float decay = MathF.Exp(-along / Length);
return profile * decay;
}
public static float SmoothStep(float _From, float _To, float _Value)
{
float t = Math.Clamp((_Value - _From) / (_To - _From), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
}
/// <summary>
/// A lake's outline with its openings cut in: the shore, and at every river that meets it a flare out to the river's
/// row, the row itself point for point, and a flare back to the shore.
/// </summary>
internal static class LakeOutline
{
/// <summary>
/// Flares are rounded in about this many steps.
/// </summary>
private const int FlareSteps = 6;
public static List<OutlineVertex> Build(LakeShore _Shore, IReadOnlyList<LakeOpening> _Openings, int _FirstRowId)
{
var outline = new List<OutlineVertex>();
if (_Openings.Count == 0)
{
foreach (var point in _Shore.Points)
outline.Add(OutlineVertex.Free(point));
return SurfaceMesher.RemoveDuplicates(outline);
}
int count = _Openings.Count;
for (int i = 0; i < count; i++)
{
var opening = _Openings[i];
var previous = _Openings[(i + count - 1) % count];
float leaves = opening.At - opening.Reach;
float returns = opening.At + opening.Reach;
// The shore from where the previous opening came back to it, to where this one leaves it.
foreach (var point in _Shore.Between(previous.At + previous.Reach, leaves))
outline.Add(OutlineVertex.Free(point));
_Shore.At(leaves, out Vector3 leavesPoint, out _, out Vector3 leavesDirection);
_Shore.At(returns, out Vector3 returnsPoint, out _, out Vector3 returnsDirection);
var row = opening.Row;
// Round into the river, across its row, and back out to the shore on the other side.
AddFlare(outline, leavesPoint, leavesDirection, row.Points[0], opening.Away);
for (int k = 0; k < row.Count; k++)
outline.Add(OutlineVertex.OnRow(row, k, _FirstRowId + i));
AddFlare(outline, row.Points[^1], -opening.Away, returnsPoint, returnsDirection);
}
return SurfaceMesher.RemoveDuplicates(outline);
}
/// <summary>
/// The outline of a river's mouth into the sea: the coast either side of the opening, out to where it leaves the
/// block of sea the mouth is built in, with the opening cut in as a lake's (flares and the river's row); then the
/// block's edge out at sea (<paramref name="_Edge"/>, its points counter-clockwise, on the grid the sea is drawn on)
/// back to where the coast came in. The water is inside, as in a lake's outline. Null when the coast never leaves the
/// block, or the opening's flares don't fit inside it.
/// </summary>
public static List<OutlineVertex> BuildMouth(LakeShore _Shore, LakeOpening _Opening, Func<Vector3, bool> _Inside, IReadOnlyList<Vector3> _Edge)
{
float leaves = _Opening.At - _Opening.Reach;
float returns = _Opening.At + _Opening.Reach;
if (_Edge.Count < 4)
return null;
if (!_Shore.TryLeave(returns, true, _Inside, out float to) || !_Shore.TryLeave(leaves, false, _Inside, out float from))
return null;
// The two walks must not meet round the far side of a small coast.
if (to - from >= _Shore.Length - 1.0f)
return null;
var outline = new List<OutlineVertex>();
foreach (var point in _Shore.Between(from, leaves))
outline.Add(OutlineVertex.Free(point));
_Shore.At(leaves, out Vector3 leavesPoint, out _, out Vector3 leavesDirection);
_Shore.At(returns, out Vector3 returnsPoint, out _, out Vector3 returnsDirection);
var row = _Opening.Row;
AddFlare(outline, leavesPoint, leavesDirection, row.Points[0], _Opening.Away);
for (int k = 0; k < row.Count; k++)
outline.Add(OutlineVertex.OnRow(row, k, 0));
AddFlare(outline, row.Points[^1], -_Opening.Away, returnsPoint, returnsDirection);
foreach (var point in _Shore.Between(returns, to))
outline.Add(OutlineVertex.Free(point));
// Out at sea: along the block's edge (the water is on the left of the coast, so counter-clockwise) from where the
// coast left it back to where it came in.
_Shore.At(to, out Vector3 end, out _, out _);
_Shore.At(from, out Vector3 start, out _, out _);
var lengths = new float[_Edge.Count + 1];
for (int k = 0; k < _Edge.Count; k++)
lengths[k + 1] = lengths[k] + (_Edge[(k + 1) % _Edge.Count] - _Edge[k]).WithZ(0.0f).Length;
float perimeter = lengths[^1];
float leaveAt = EdgeParameter(_Edge, lengths, end);
float target = Wrap(EdgeParameter(_Edge, lengths, start) - leaveAt, perimeter);
// The edge's points in the order the walk meets them.
int first = 0;
float firstAhead = float.MaxValue;
for (int k = 0; k < _Edge.Count; k++)
{
float ahead = Wrap(lengths[k] - leaveAt, perimeter);
if (ahead > 0.01f && ahead < firstAhead)
{
first = k;
firstAhead = ahead;
}
}
for (int step = 0; step < _Edge.Count; step++)
{
int k = (first + step) % _Edge.Count;
float ahead = Wrap(lengths[k] - leaveAt, perimeter);
if (ahead <= 0.01f || ahead >= target - 0.01f)
break;
outline.Add(OutlineVertex.OnSeaEdge(_Edge[k]));
}
return SurfaceMesher.RemoveDuplicates(outline);
}
/// <summary>
/// How far round a closed edge (from its first point) the point of it nearest to another is.
/// </summary>
private static float EdgeParameter(IReadOnlyList<Vector3> _Edge, float[] _Lengths, Vector3 _Point)
{
float best = float.MaxValue;
float parameter = 0.0f;
var target = new Vector2(_Point.x, _Point.y);
for (int k = 0; k < _Edge.Count; k++)
{
var from = new Vector2(_Edge[k].x, _Edge[k].y);
var span = new Vector2(_Edge[(k + 1) % _Edge.Count].x, _Edge[(k + 1) % _Edge.Count].y) - from;
float lengthSquared = span.LengthSquared;
float t = lengthSquared > 0.0f ? Math.Clamp(Vector2.Dot(target - from, span) / lengthSquared, 0.0f, 1.0f) : 0.0f;
float distance = (from + span * t).Distance(target);
if (distance >= best)
continue;
best = distance;
parameter = _Lengths[k] + MathF.Sqrt(lengthSquared) * t;
}
return parameter;
}
private static float Wrap(float _Value, float _Length)
{
if (_Length <= 0.0f)
return 0.0f;
float value = _Value % _Length;
return value < 0.0f ? value + _Length : value;
}
/// <summary>
/// The shore turning out into the side of a river (or back from it): a curve that leaves along one direction and
/// arrives along another, its two ends left out (they are the shore's and the row's own points). The pulls are
/// kept level, so the curve's height runs smoothly from one end to the other.
/// </summary>
private static void AddFlare(List<OutlineVertex> _Outline, Vector3 _From, Vector3 _FromDirection, Vector3 _To, Vector3 _ToDirection)
{
float span = _From.Distance(_To);
if (span <= 0.01f)
return;
Vector3 fromDirection = Flat(_FromDirection);
Vector3 toDirection = Flat(_ToDirection);
// A quarter turn's worth of pull at each end, which is what makes the curve read as an arc.
float pull = span * 0.39f;
Vector3 controlFrom = _From + fromDirection * pull;
Vector3 controlTo = _To - toDirection * pull;
for (int i = 1; i < FlareSteps; i++)
{
float t = (float)i / FlareSteps;
float u = 1.0f - t;
Vector3 point = u * u * u * _From + 3.0f * u * u * t * controlFrom + 3.0f * u * t * t * controlTo + t * t * t * _To;
_Outline.Add(OutlineVertex.Free(point));
}
}
private static Vector3 Flat(Vector3 _Direction)
{
Vector3 flat = _Direction.WithZ(0.0f);
return flat.IsNearZeroLength ? _Direction.Normal : flat.Normal;
}
}