Building/RiverBuilder.cs

Builds river surface geometry and per-vertex water data. It computes a row of points across a river with velocities, waves and foam, assembles multiple rows into a mesh with triangle indices, and generates sea-hole polygons where the river meets sea level.

Native Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// Builds the surface of a stretch of river: its rows, one after the other down the river, joined into a strip. Rows
/// may differ in how many points they have (a river widening, or the row it shares with a lake), so each pair of rows
/// is zipped together by where their points sit across the river rather than point for point.
/// </summary>
internal static class RiverBuilder
{
	/// <summary>
	/// How much the water slows towards the banks: 0 runs as fast at the bank as in the middle, 0.5 at half speed.
	/// </summary>
	public static float BankFactor(float _Across, float _BankDrag)
	{
		float offset = 2.0f * _Across - 1.0f;

		return 1.0f - _BankDrag * offset * offset;
	}



	/// <summary>
	/// A row across a river at a distance along it: its points from bank to bank, the water's velocity at each (along
	/// the river, slowed towards the banks), its wave strength and its foam.
	/// </summary>
	public static RiverRow BuildRow(WaterCurveTable _Table, in RiverWidth _Width, RiverFlow _Flow, float _Distance, float _CellSize, float _BankDrag, Func<float, float> _FoamFor)
	{
		_Table.Frame(_Distance, out Vector3 centre, out Vector3 tangent);

		Vector3 right = WaterCurve.RightOf(tangent);
		Vector3 flat = tangent.WithZ(0.0f);
		Vector2 direction = flat.IsNearZeroLength ? new Vector2(-right.y, right.x) : new Vector2(flat.x, flat.y).Normal;

		float half = _Width.HalfAt(_Distance);
		int count = RiverRow.ColumnCount(half * 2.0f, _CellSize);

		float speed = _Flow?.SpeedAt(_Distance) ?? 0.0f;
		float waves = _Flow?.WavesAt(_Distance) ?? 0.0f;
		float foam = _FoamFor?.Invoke(speed) ?? 0.0f;

		var row = new RiverRow(count) { Distance = _Distance };

		for (int i = 0; i < count; i++)
		{
			float across = RiverRow.Across(i, count);

			row.Points[i] = centre + right * (-half + 2.0f * half * across);
			row.Velocities[i] = direction * (speed * BankFactor(across, _BankDrag));
			row.Waves[i] = waves;
			row.Foams[i] = foam;
		}

		return row;
	}



	/// <summary>
	/// The strip through a list of rows, in network space, moved into the world.
	/// </summary>
	public static WaterMeshData Build(IReadOnlyList<RiverRow> _Rows, in Transform _Transform)
	{
		int total = 0;

		foreach (var row in _Rows)
			total += row.Count;

		var mesh = new WaterMeshData(total);
		var starts = new int[_Rows.Count];
		var indices = new List<int>(total * 6);
		int cursor = 0;

		for (int r = 0; r < _Rows.Count; r++)
		{
			var row = _Rows[r];

			starts[r] = cursor;

			for (int i = 0; i < row.Count; i++)
			{
				mesh.Positions[cursor] = row.Points[i];
				mesh.Velocities[cursor] = row.Velocities[i];
				mesh.Waves[cursor] = row.Waves[i];
				mesh.Foams[cursor] = row.Foams[i];
				cursor++;
			}
		}

		for (int r = 0; r + 1 < _Rows.Count; r++)
		{
			Zip(indices, starts[r], _Rows[r].Count, starts[r + 1], _Rows[r + 1].Count);

			// Where the river runs at sea level, the sea under it is cut away, one stretch between two rows at a time
			// (its banks are exactly the river's own edges there).
			var from = _Rows[r];
			var to = _Rows[r + 1];

			var quad = new[]
			{
				new Vector2(from.Points[0].x, from.Points[0].y),
				new Vector2(to.Points[0].x, to.Points[0].y),
				new Vector2(to.Points[^1].x, to.Points[^1].y),
				new Vector2(from.Points[^1].x, from.Points[^1].y)
			};

			float lowest = MathF.Min(MathF.Min(from.Points[0].z, from.Points[^1].z), MathF.Min(to.Points[0].z, to.Points[^1].z));

			mesh.SeaHoles.Add(SeaHole.Polygon(quad, lowest));
		}

		mesh.Finish(_Transform, indices);

		return mesh;
	}



	/// <summary>
	/// Joins two neighbouring rows with triangles, walking across both at once and always stepping along whichever row
	/// has its next point nearer the left bank. Rows run down the river, points from the left bank to the right, so the
	/// triangles come out counter-clockwise seen from above.
	/// </summary>
	private static void Zip(List<int> _Indices, int _StartA, int _CountA, int _StartB, int _CountB)
	{
		int a = 0;
		int b = 0;

		while (a < _CountA - 1 || b < _CountB - 1)
		{
			bool stepA;

			if (a >= _CountA - 1)
				stepA = false;
			else if (b >= _CountB - 1)
				stepA = true;
			else
				stepA = RiverRow.Across(a + 1, _CountA) <= RiverRow.Across(b + 1, _CountB);

			if (stepA)
			{
				_Indices.Add(_StartA + a);
				_Indices.Add(_StartA + a + 1);
				_Indices.Add(_StartB + b);
				a++;
			}
			else
			{
				_Indices.Add(_StartA + a);
				_Indices.Add(_StartB + b + 1);
				_Indices.Add(_StartB + b);
				b++;
			}
		}
	}
}