Code/Building/LakeBuilder.cs

Builds tiled water mesh data for a lake surface from a snapshot of networked input. Subdivides and triangulates an outline, evaluates jet influences per vertex, partitions triangles into square tiles, remaps vertex indices per tile and produces WaterMeshData tiles.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// Everything a lake's surface is built from, copied out of the network so the build can run on a worker thread while
/// the editor carries on.
/// </summary>
internal sealed class LakeBuildInput
{
	public int LakeId;
	public long Signature;
	public List<OutlineVertex> Outline;
	public LakeJet[] Jets;
	public float Height;
	public float Waves;
	public float CellSize;
	public float TileSize;
	public Transform Transform;
}



/// <summary>
/// Builds a lake's surface: its outline (with the rows of every river that meets it) triangulated with points all over
/// its inside, the water's state on every point (still water, stirred by the jets of the rivers running in and out),
/// cut into square tiles so the parts of a big lake out of view are not drawn.
/// </summary>
internal static class LakeBuilder
{
	public static List<WaterMeshData> Build(LakeBuildInput _Input)
	{
		var tiles = new List<WaterMeshData>();
		var outline = SurfaceMesher.Subdivide(_Input.Outline, _Input.CellSize);

		if (outline.Count < 3)
			return tiles;

		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 positions = new Vector3[count];
		var velocities = new Vector2[count];
		var waves = new float[count];
		var foams = new float[count];

		for (int i = 0; i < count; i++)
		{
			if (i < outline.Count && outline[i].IsFixed)
			{
				var vertex = outline[i];

				positions[i] = vertex.Position;
				velocities[i] = vertex.Velocity;
				waves[i] = vertex.Waves;
				foams[i] = vertex.Foam;
				continue;
			}

			positions[i] = i < outline.Count ? outline[i].Position : new Vector3(points[i].x, points[i].y, _Input.Height);
			waves[i] = _Input.Waves;

			Vector2 velocity = Vector2.Zero;
			float foam = 0.0f;

			foreach (var jet in _Input.Jets)
			{
				velocity += jet.Evaluate(points[i], out float jetFoam);
				foam += jetFoam;
			}

			velocities[i] = velocity;
			foams[i] = Math.Clamp(foam, 0.0f, 1.0f);
		}

		// Into tiles, by where each triangle's middle is. A vertex on a tile's edge is copied into every tile that uses
		// it, with the same data, so the tiles meet exactly.
		float tileSize = MathF.Max(_Input.CellSize * 4.0f, _Input.TileSize);
		var byTile = new Dictionary<(int X, int Y), List<int>>();

		for (int t = 0; t + 2 < triangles.Count; t += 3)
		{
			Vector2 middle = (points[triangles[t]] + points[triangles[t + 1]] + points[triangles[t + 2]]) / 3.0f;
			var key = ((int)MathF.Floor(middle.x / tileSize), (int)MathF.Floor(middle.y / tileSize));

			if (!byTile.TryGetValue(key, out var list))
			{
				list = new List<int>();
				byTile[key] = list;
			}

			list.Add(t);
		}

		var remap = new Dictionary<int, int>();

		foreach (var (_, list) in byTile)
		{
			remap.Clear();

			var indices = new List<int>(list.Count * 3);

			foreach (int t in list)
			{
				for (int k = 0; k < 3; k++)
				{
					int source = triangles[t + k];

					if (!remap.TryGetValue(source, out int local))
					{
						local = remap.Count;
						remap[source] = local;
					}

					indices.Add(local);
				}
			}

			var mesh = new WaterMeshData(remap.Count);

			foreach (var (source, local) in remap)
			{
				mesh.Positions[local] = positions[source];
				mesh.Velocities[local] = velocities[source];
				mesh.Waves[local] = waves[source];
				mesh.Foams[local] = foams[source];
			}

			mesh.Finish(_Input.Transform, indices, true);
			tiles.Add(mesh);
		}

		return tiles;
	}
}