Code/Building/JunctionBuilder.cs

Builder for a water junction mesh. It triangulates an outline, initializes per-vertex water state (velocity, waves, foam, height) from river rows, relaxes those values inward using successive over-relaxation, and falls back to a fan triangulation if triangulation fails.

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