Utility that builds river surface geometry. It computes per-row positions, velocities, wave and foam values across a river width, zips adjacent rows into triangle indices, and produces a WaterMeshData with sea cutout polygons transformed into world space.
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++;
}
}
}
}