Builds a triangulated water mesh for a junction where multiple rivers meet. It subdivides and triangulates the confluence outline, initializes per-vertex water state (velocity, waves, foam, height), relaxes those interior values via successive over-relaxation, and falls back to a fan triangulation if triangulation fails.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Builds the water where rivers meet: the confluence's outline triangulated with points all over its inside, and the
/// water's state (flow, wave strength, foam and height) spread smoothly from the rows of the rivers into the middle.
/// The rows are held exactly as the rivers have them, so each river runs into the confluence without a seam, and in
/// between the currents meet and turn into each other.
/// </summary>
internal static class JunctionBuilder
{
public static WaterMeshData Build(List<OutlineVertex> _Outline, Vector3 _Centre, float _CellSize, in Transform _Transform)
{
var outline = SurfaceMesher.Subdivide(_Outline, _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, _CellSize, out var points, out var triangles))
return BuildFan(outline, _Centre, _Transform);
int count = points.Count;
var mesh = new WaterMeshData(count);
var fixedState = new bool[count];
var fixedHeight = new bool[count];
// What the inside starts from before it is relaxed: the average of the rows.
Vector2 velocitySum = Vector2.Zero;
float wavesSum = 0.0f;
float foamSum = 0.0f;
float heightSum = 0.0f;
int rowPoints = 0;
foreach (var vertex in outline)
{
if (!vertex.IsFixed)
continue;
velocitySum += vertex.Velocity;
wavesSum += vertex.Waves;
foamSum += vertex.Foam;
heightSum += vertex.Position.z;
rowPoints++;
}
float divisor = MathF.Max(1, rowPoints);
Vector2 velocityStart = velocitySum / divisor;
float wavesStart = wavesSum / divisor;
float foamStart = foamSum / divisor;
float heightStart = rowPoints > 0 ? heightSum / divisor : _Centre.z;
for (int i = 0; i < count; i++)
{
if (i < outline.Count)
{
var vertex = outline[i];
mesh.Positions[i] = vertex.Position;
fixedHeight[i] = true;
if (vertex.IsFixed)
{
mesh.Velocities[i] = vertex.Velocity;
mesh.Waves[i] = vertex.Waves;
mesh.Foams[i] = vertex.Foam;
fixedState[i] = true;
continue;
}
}
else
{
mesh.Positions[i] = new Vector3(points[i].x, points[i].y, heightStart);
}
mesh.Velocities[i] = velocityStart;
mesh.Waves[i] = wavesStart;
mesh.Foams[i] = foamStart;
}
Relax(mesh, triangles, fixedState, fixedHeight, false);
mesh.Finish(_Transform, triangles, true);
return mesh;
}
/// <summary>
/// Spreads the water's state inward from the rows: every point that is not on a row becomes the average of its
/// neighbours, again and again until it settles (successive over-relaxation, so a few dozen sweeps are enough).
/// The banks' heights are kept, the inside's eased between them. With <paramref name="_SeaBlend"/> the hand-over to
/// the sea is spread the same way.
/// </summary>
internal static void Relax(WaterMeshData _Mesh, List<int> _Triangles, bool[] _FixedState, bool[] _FixedHeight, bool _SeaBlend)
{
int count = _Mesh.VertexCount;
var neighbours = new List<int>[count];
for (int i = 0; i < count; i++)
neighbours[i] = new List<int>(6);
for (int t = 0; t + 2 < _Triangles.Count; t += 3)
{
for (int k = 0; k < 3; k++)
{
int a = _Triangles[t + k];
int b = _Triangles[t + (k + 1) % 3];
if (!neighbours[a].Contains(b))
neighbours[a].Add(b);
if (!neighbours[b].Contains(a))
neighbours[b].Add(a);
}
}
const float overRelaxation = 1.7f;
int sweeps = Math.Clamp((int)(MathF.Sqrt(count) * 3.0f), 30, 400);
for (int sweep = 0; sweep < sweeps; sweep++)
{
for (int i = 0; i < count; i++)
{
var around = neighbours[i];
if (around.Count == 0)
continue;
if (!_FixedState[i])
{
Vector2 velocity = Vector2.Zero;
float waves = 0.0f;
float foam = 0.0f;
foreach (int n in around)
{
velocity += _Mesh.Velocities[n];
waves += _Mesh.Waves[n];
foam += _Mesh.Foams[n];
}
float inverse = 1.0f / around.Count;
_Mesh.Velocities[i] += (velocity * inverse - _Mesh.Velocities[i]) * overRelaxation;
_Mesh.Waves[i] += (waves * inverse - _Mesh.Waves[i]) * overRelaxation;
_Mesh.Foams[i] += (foam * inverse - _Mesh.Foams[i]) * overRelaxation;
if (_SeaBlend)
{
float blend = 0.0f;
foreach (int n in around)
blend += _Mesh.SeaBlend[n];
_Mesh.SeaBlend[i] += (blend * inverse - _Mesh.SeaBlend[i]) * overRelaxation;
}
}
if (!_FixedHeight[i])
{
float height = 0.0f;
foreach (int n in around)
height += _Mesh.Positions[n].z;
var position = _Mesh.Positions[i];
position.z += (height / around.Count - position.z) * overRelaxation;
_Mesh.Positions[i] = position;
}
}
}
// Over-relaxation can overshoot a hair past the rows' range; nothing here can be negative.
for (int i = 0; i < count; i++)
{
_Mesh.Waves[i] = MathF.Max(0.0f, _Mesh.Waves[i]);
_Mesh.Foams[i] = Math.Clamp(_Mesh.Foams[i], 0.0f, 1.0f);
_Mesh.SeaBlend[i] = Math.Clamp(_Mesh.SeaBlend[i], 0.0f, 1.0f);
}
}
/// <summary>
/// The fallback for an outline that could not be triangulated properly (it folds over itself somewhere): a fan from
/// the node, so the confluence is at least covered.
/// </summary>
private static WaterMeshData BuildFan(List<OutlineVertex> _Outline, Vector3 _Centre, in Transform _Transform)
{
int count = _Outline.Count;
var mesh = new WaterMeshData(count + 1);
var indices = new List<int>(count * 3);
Vector2 velocity = Vector2.Zero;
float waves = 0.0f;
float foam = 0.0f;
int rowPoints = 0;
for (int i = 0; i < count; i++)
{
var vertex = _Outline[i];
mesh.Positions[i] = vertex.Position;
mesh.Velocities[i] = vertex.Velocity;
mesh.Waves[i] = vertex.Waves;
mesh.Foams[i] = vertex.Foam;
if (vertex.IsFixed)
{
velocity += vertex.Velocity;
waves += vertex.Waves;
foam += vertex.Foam;
rowPoints++;
}
}
float divisor = MathF.Max(1, rowPoints);
mesh.Positions[count] = _Centre;
mesh.Velocities[count] = velocity / divisor;
mesh.Waves[count] = waves / divisor;
mesh.Foams[count] = foam / divisor;
for (int i = 0; i < count; i++)
{
if (_Outline[i].IsFixed)
continue;
mesh.Velocities[i] = mesh.Velocities[count];
mesh.Waves[i] = mesh.Waves[count];
mesh.Foams[i] = mesh.Foams[count];
}
for (int i = 0; i < count; i++)
{
indices.Add(count);
indices.Add(i);
indices.Add((i + 1) % count);
}
mesh.Finish(_Transform, indices, true);
return mesh;
}
}