Code/Building/EstuaryBuilder.cs

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.

File Access
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;
	}
}