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.
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;
}
}