Builds a water mesh for a river mouth (estuary) inside a coast block. It takes a serialized MouthBuildInput, subdivides and triangulates the outline, evaluates the river jet presence, velocity and foam per vertex, blends with sea values near the block edge, sets positions/velocities/waves/sea blend and produces a WaterMeshData.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Everything a river mouth is built from, copied out of the network so the build can run on a worker thread.
/// </summary>
internal sealed class MouthBuildInput
{
/// <summary>
/// The outline, in network space: the coast with the river's opening cut in, closed along the block's edge.
/// </summary>
public List<OutlineVertex> Outline;
/// <summary>
/// The river's water carrying out into the sea, in network space.
/// </summary>
public LakeJet Jet;
/// <summary>
/// The river's own wave strength and water level on its last row.
/// </summary>
public float Waves;
public float Height;
/// <summary>
/// The block the mouth is built in, in the world (in plan): its water is all sea by its edge.
/// </summary>
public Vector2 Min;
public Vector2 Max;
public float SeaLevel;
public float CellSize;
public Transform Transform;
}
/// <summary>
/// Builds the water where a river opens into the sea: a block of the coast zone round the opening, bounded by the
/// coast. Its outline takes in the river's last row point for point (the river runs into it as into a lake), and its
/// edge out at sea is the sea itself, on the very points the rest of the coast zone has there. In between, the river's
/// water carries out as a jet, the way it carries into a lake, handing over to the sea's as it spreads and slows, and
/// all of it gone before the block's edge.
/// </summary>
internal static class EstuaryBuilder
{
/// <summary>
/// How far in from the block's edge the river's water has all given way to the sea, as a share of the block's size.
/// </summary>
private const float EdgeFade = 0.2f;
/// <summary>
/// Below these the river's water is left out of the mouth altogether: too little of it to see, and the sea alone
/// costs half as much to draw as the two blended, still water less than flowing.
/// </summary>
private const float MinPresence = 0.002f;
private const float MinSpeed = 1.0f;
private const float MinFoam = 0.01f;
public static WaterMeshData Build(MouthBuildInput _Input)
{
var outline = SurfaceMesher.Subdivide(_Input.Outline, _Input.CellSize);
var flat = new List<Vector2>(outline.Count);
foreach (var vertex in outline)
flat.Add(new Vector2(vertex.Position.x, vertex.Position.y));
if (!SurfaceMesher.Triangulate(flat, _Input.CellSize, out var points, out var triangles))
return null;
int count = points.Count;
var mesh = new WaterMeshData(count);
var transform = _Input.Transform;
float fadeWidth = MathF.Max(1.0f, MathF.Min(_Input.Max.x - _Input.Min.x, _Input.Max.y - _Input.Min.y) * EdgeFade);
for (int i = 0; i < count; i++)
{
bool onOutline = i < outline.Count;
var vertex = onOutline ? outline[i] : default;
if (onOutline && vertex.IsFixed)
{
// The river's own row, exactly as the river has it.
mesh.Positions[i] = vertex.Position;
mesh.Velocities[i] = vertex.Velocity;
mesh.Waves[i] = vertex.Waves;
mesh.Foams[i] = vertex.Foam;
mesh.SeaBlend[i] = 0.0f;
continue;
}
if (onOutline && vertex.SeaEdge)
{
// The sea: still, unfoamed, and at its level.
mesh.Positions[i] = vertex.Position.WithZ(_Input.SeaLevel);
mesh.Waves[i] = _Input.Waves;
mesh.SeaBlend[i] = 1.0f;
continue;
}
var point = new Vector2(points[i].x, points[i].y);
// How far from the block's edge, where it must all be sea.
Vector3 world = transform.PointToWorld(new Vector3(point.x, point.y, _Input.SeaLevel));
float edge = MathF.Min(MathF.Min(world.x - _Input.Min.x, _Input.Max.x - world.x), MathF.Min(world.y - _Input.Min.y, _Input.Max.y - world.y));
float fade = LakeJet.SmoothStep(0.0f, fadeWidth, edge);
float presence = _Input.Jet.Presence(point) * fade;
Vector2 velocity = _Input.Jet.Evaluate(point, out float foam) * fade;
foam = Math.Clamp(foam * fade, 0.0f, 1.0f);
if (presence < MinPresence)
presence = 0.0f;
else if (presence > 1.0f - MinPresence)
presence = 1.0f;
if (velocity.LengthSquared < MinSpeed * MinSpeed)
velocity = Vector2.Zero;
if (foam < MinFoam)
foam = 0.0f;
mesh.Velocities[i] = velocity;
mesh.Foams[i] = foam;
mesh.Waves[i] = _Input.Waves;
mesh.SeaBlend[i] = 1.0f - presence;
// The shore and the flares keep the heights the outline gives them; inside, the river's level gives way to the
// sea's with its water.
mesh.Positions[i] = onOutline
? vertex.Position
: new Vector3(point.x, point.y, _Input.SeaLevel + (_Input.Height - _Input.SeaLevel) * presence);
}
mesh.Finish(transform, triangles);
return mesh;
}
}