Part of the WaterNetwork class that detects and builds the coastline (coast zone and river mouth handling). It scans scene geometry and terrain to find coastlines, creates coast lakes, computes coast zone tiles and river mouth blocks, builds GPU mask buffers, and provides editor helpers and mask application for rendering.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// What the coast is found from.
/// </summary>
public enum CoastDetection
{
/// <summary>
/// Collision: the terrain and every solid collider. Fast.
/// </summary>
Colliders,
/// <summary>
/// What is drawn: the terrain and every model, with collision or without. Slower.
/// </summary>
Meshes,
/// <summary>
/// Ground is wherever either finds it.
/// </summary>
Both
}
/// <summary>
/// The coast: where the sea meets the land, found by scanning the scene's ground (<see cref="BuildCoast"/>). The coast
/// zone, the sea's water round the land out to <see cref="CoastDistance"/>, is a surface of its own that rivers open
/// into as they open into a lake; the infinite sea is masked out over it and over the land, and takes over past it.
/// </summary>
public sealed partial class WaterNetwork
{
/// <summary>
/// The scan never takes more than this many samples along either side.
/// </summary>
private const int MaxScanPoints = 512;
/// <summary>
/// What the coast is found from: collision, what is drawn, or both.
/// </summary>
[Property(Title = "Detect From"), Group("Coast"), Order(6)] public CoastDetection CoastDetection { get; set; } = CoastDetection.Colliders;
/// <summary>
/// How far apart the scan samples the ground. Finer finds smaller coves and rocks, and takes longer.
/// </summary>
[Property(Title = "Scan Resolution"), Group("Coast"), Order(6), Range(16.0f, 512.0f)] public float CoastResolution { get; set { field = value.Clamp(4.0f, 16384.0f); } } = 64.0f;
/// <summary>
/// How far above sea level the coastline is taken: the water carries on under the beach up to there, so it is the
/// terrain, not the edge of the water, that draws the shoreline.
/// </summary>
[Property(Title = "Height Margin"), Group("Coast"), Order(6), Range(0.0f, 256.0f)] public float CoastHeightMargin { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 32.0f;
/// <summary>
/// How far out from the coast the coast zone reaches before the infinite sea takes over. The zone is a surface of
/// its own, as fine as the sea's finest ring, so the further it reaches the more it costs.
/// </summary>
[Property(Title = "Coast Distance"), Group("Coast"), Order(6), Range(0.0f, 16384.0f)]
public float CoastDistance
{
get;
set
{
field = value.Clamp(0.0f, 1000000.0f);
MarkDirty();
}
} = 2048.0f;
/// <summary>
/// How far round a river's mouth into the sea its water carries on before it is all sea, in river widths.
/// </summary>
[Property(Title = "River Mouth Size"), Group("Coast"), Order(6), Range(1.0f, 8.0f)]
public float EstuarySize
{
get;
set
{
field = value.Clamp(0.75f, 100.0f);
MarkDirty();
}
} = 2.5f;
/// <summary>
/// What the scan sees through: a bridge or a pier over the sea, say.
/// </summary>
[Property(Title = "Ignore Tags"), Group("Coast"), Order(6)] public TagSet CoastIgnoreTags { get; set; } = new();
/// <summary>
/// The sea level the coast was built at: once the level moves, the coast no longer follows the terrain.
/// </summary>
[Property, Hide] public float CoastSeaLevel { get; set; }
/// <summary>
/// What the last <see cref="BuildCoast"/> found, for the editor.
/// </summary>
public string CoastReport { get; private set; }
public bool HasCoast => Lakes.Any(lake => lake.IsCoast);
/// <summary>
/// The sea level has moved since the coast was built.
/// </summary>
public bool IsCoastStale => HasCoast && MathF.Abs(CoastSeaLevel - SeaLevel) > 0.01f;
private sealed class Mouth
{
public int NodeId;
public WaterRiver River;
public MouthBuildInput Input;
public Vector2 Min;
public Vector2 Max;
}
private readonly Dictionary<int, Mouth> m_Mouths = new();
private readonly HashSet<(int X, int Y)> m_MouthTiles = new();
private CoastZone m_CoastZone;
private long m_CoastZoneKey;
private long m_CoastGeometryKey;
private long m_MouthTilesKey;
/// <summary>
/// Bumped whenever what the infinite sea is masked out over changes: the zone, or the river mouths laid over it.
/// </summary>
private int m_CoastZoneRevision;
private GpuBuffer<float> m_CoastBuffer;
private int m_CoastBufferRevision = -1;
private int m_CoastMaskX;
private int m_CoastMaskY;
private int m_CoastMaskWidth;
private int m_CoastMaskHeight;
// ── Building the coast ──
/// <summary>
/// Scans the scene's ground and lays a coastline round every piece of land that rises out of the sea (the terrain, and
/// whatever <see cref="CoastDetection"/> takes in), replacing the coast there was. Ground below sea level that the
/// open sea doesn't reach (a basin inland) counts as land, and so do the network's own rivers and lakes, so the coast
/// crosses a river's mouth instead of running up its channel. Rivers that opened into the sea open into it again at
/// the new coast. Turns the sea on.
/// </summary>
[Button("Build Coast", "landscape")]
public void BuildCoast()
{
if (!TryGetGroundBounds(out BBox bounds))
{
CoastReport = "Nothing to build a coast round: no terrain or ground was found in the scene.";
return;
}
float seaLevel = WorldSeaLevel;
float coastHeight = seaLevel + CoastHeightMargin;
float extent = MathF.Max(bounds.Size.x, bounds.Size.y);
float step = MathF.Max(CoastResolution, extent / (MaxScanPoints - 5));
// On whole steps from the world's origin, so building again finds the same coast, with two steps of open sea past
// the ground on every side.
var origin = new Vector2(MathF.Floor(bounds.Mins.x / step) * step - 2.0f * step, MathF.Floor(bounds.Mins.y / step) * step - 2.0f * step);
int width = (int)MathF.Ceiling((bounds.Maxs.x + 2.0f * step - origin.x) / step) + 1;
int height = (int)MathF.Ceiling((bounds.Maxs.y + 2.0f * step - origin.y) / step) + 1;
float top = MathF.Max(bounds.Maxs.z, coastHeight) + 64.0f;
float bottom = MathF.Min(bounds.Mins.z, seaLevel) - 64.0f;
var terrains = ScanTerrains();
var field = new float[width * height];
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
field[j * width + i] = GroundHeight(origin.x + i * step, origin.y + j * step, top, bottom, terrains) - coastHeight;
}
MarkWaterAsLand(field, width, height, origin, step);
var loops = CoastScanner.Extract(field, width, height, origin, step, step * step * 9.0f);
ReplaceCoasts(loops, step);
CoastSeaLevel = SeaLevel;
SeaEnabled = true;
CoastReport = loops.Count == 0
? "No land rises out of the sea here: there is no coast."
: $"{loops.Count} coastline{(loops.Count == 1 ? "" : "s")}, the ground sampled every {step:0} units.";
}
/// <summary>
/// Takes the coast away: the infinite sea is everywhere again, and the rivers that opened into it are loose ends.
/// </summary>
[Button("Clear Coast", "delete")]
public void ClearCoast()
{
Lakes.RemoveAll(lake => lake.IsCoast);
CoastReport = null;
MarkDirty();
}
/// <summary>
/// Where the ground the coast is built from lies: the terrains, and the colliders or models the scan looks at.
/// </summary>
private bool TryGetGroundBounds(out BBox _Bounds)
{
var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
var max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
bool any = false;
void Add(BBox _Box)
{
Vector3 size = _Box.Size;
// A skybox, or something broken: no ground.
if (!float.IsFinite(size.x) || !float.IsFinite(size.y) || !float.IsFinite(size.z) || MathF.Max(size.x, size.y) > 1000000.0f)
return;
min = new Vector3(MathF.Min(min.x, _Box.Mins.x), MathF.Min(min.y, _Box.Mins.y), MathF.Min(min.z, _Box.Mins.z));
max = new Vector3(MathF.Max(max.x, _Box.Maxs.x), MathF.Max(max.y, _Box.Maxs.y), MathF.Max(max.z, _Box.Maxs.z));
any = true;
}
foreach (var terrain in ScanTerrains())
{
var storage = terrain.Storage;
var local = new BBox(Vector3.Zero, new Vector3(storage.TerrainSize, storage.TerrainSize, storage.TerrainHeight));
Add(local.Transform(terrain.WorldTransform));
}
if (CoastDetection != CoastDetection.Meshes)
{
foreach (var collider in Scene.GetAllComponents<Collider>())
{
if (!collider.IsValid() || !collider.Enabled || !collider.Active || collider.IsTrigger || collider is Terrain)
continue;
Add(collider.LocalBounds.Transform(collider.WorldTransform));
}
}
if (CoastDetection != CoastDetection.Colliders)
{
foreach (var renderer in Scene.GetAllComponents<ModelRenderer>())
{
if (!renderer.IsValid() || !renderer.Enabled || !renderer.Active)
continue;
Add(renderer.Bounds);
}
}
_Bounds = new BBox(min, max);
return any;
}
private List<Terrain> ScanTerrains()
{
return Scene.GetAllComponents<Terrain>()
.Where(terrain => terrain.IsValid() && terrain.Enabled && terrain.Active && terrain.Storage is not null)
.ToList();
}
/// <summary>
/// The top of the ground at a point in plan, as the scan sees it; below the bottom when there is none.
/// </summary>
private float GroundHeight(float _X, float _Y, float _Top, float _Bottom, List<Terrain> _Terrains)
{
float ground = _Bottom - 1.0f;
var from = new Vector3(_X, _Y, _Top);
var to = new Vector3(_X, _Y, _Bottom);
if (CoastDetection != CoastDetection.Meshes)
{
var trace = Scene.Trace.Ray(from, to).IgnoreGameObjectHierarchy(GameObject);
if (CoastIgnoreTags is not null)
trace = trace.WithoutTags(CoastIgnoreTags);
var hit = trace.Run();
if (hit.Hit)
ground = MathF.Max(ground, hit.EndPosition.z);
}
if (CoastDetection != CoastDetection.Colliders)
{
var trace = Scene.Trace.Ray(from, to).UseRenderMeshes(true).UsePhysicsWorld(false).IgnoreGameObjectHierarchy(GameObject);
if (CoastIgnoreTags is not null)
trace = trace.WithoutTags(CoastIgnoreTags);
var hit = trace.Run();
if (hit.Hit)
ground = MathF.Max(ground, hit.EndPosition.z);
// Terrain is no model: it is read off its heightmap.
foreach (var terrain in _Terrains)
{
if (TryTerrainHeight(terrain, _X, _Y, out float height))
ground = MathF.Max(ground, height);
}
}
return ground;
}
/// <summary>
/// A terrain's height at a point in plan, read off its heightmap. False off its edge, or over a hole.
/// </summary>
private static bool TryTerrainHeight(Terrain _Terrain, float _X, float _Y, out float _Height)
{
_Height = 0.0f;
var storage = _Terrain.Storage;
if (storage is null || storage.HeightMap is null || storage.Resolution <= 1 || storage.TerrainSize <= 0.0f)
return false;
var local = _Terrain.WorldTransform.PointToLocal(new Vector3(_X, _Y, 0.0f));
if (local.x < 0.0f || local.y < 0.0f || local.x > storage.TerrainSize || local.y > storage.TerrainSize)
return false;
int resolution = storage.Resolution;
float gridX = local.x / storage.TerrainSize * (resolution - 1);
float gridY = local.y / storage.TerrainSize * (resolution - 1);
int x0 = Math.Clamp((int)MathF.Floor(gridX), 0, resolution - 1);
int y0 = Math.Clamp((int)MathF.Floor(gridY), 0, resolution - 1);
int x1 = Math.Min(x0 + 1, resolution - 1);
int y1 = Math.Min(y0 + 1, resolution - 1);
if (storage.ControlMap is not null && new CompactTerrainMaterial(storage.ControlMap[x0 + y0 * resolution]).IsHole)
return false;
float tx = gridX - x0;
float ty = gridY - y0;
float h00 = storage.HeightMap[x0 + y0 * resolution];
float h10 = storage.HeightMap[x1 + y0 * resolution];
float h01 = storage.HeightMap[x0 + y1 * resolution];
float h11 = storage.HeightMap[x1 + y1 * resolution];
float near = h00 + (h10 - h00) * tx;
float far = h01 + (h11 - h01) * tx;
float height = (near + (far - near) * ty) * (storage.TerrainHeight / ushort.MaxValue);
_Height = _Terrain.WorldTransform.PointToWorld(new Vector3(local.x, local.y, height)).z;
return true;
}
/// <summary>
/// The network's own rivers and lakes are the land's water, not the sea's: their beds count as land however low, so
/// the coast runs across a river's mouth instead of up its channel.
/// </summary>
private void MarkWaterAsLand(float[] _Field, int _Width, int _Height, Vector2 _Origin, float _Step)
{
var transform = WorldTransform;
void Mark(int _X, int _Y)
{
int index = _Y * _Width + _X;
_Field[index] = MathF.Max(_Field[index], _Step);
}
foreach (var river in Rivers)
{
var table = GetCurveTable(river);
if (table is null)
continue;
float reach = WidthOf(river).MaxHalf + _Step;
int samples = Math.Max(2, (int)MathF.Ceiling(table.Length / (_Step * 0.5f)) + 1);
for (int k = 0; k < samples; k++)
{
Vector3 world = transform.PointToWorld(table.PositionAt(table.Length * k / (samples - 1)));
var centre = new Vector2(world.x, world.y);
int x0 = Math.Max(0, (int)MathF.Floor((centre.x - reach - _Origin.x) / _Step));
int x1 = Math.Min(_Width - 1, (int)MathF.Ceiling((centre.x + reach - _Origin.x) / _Step));
int y0 = Math.Max(0, (int)MathF.Floor((centre.y - reach - _Origin.y) / _Step));
int y1 = Math.Min(_Height - 1, (int)MathF.Ceiling((centre.y + reach - _Origin.y) / _Step));
for (int y = y0; y <= y1; y++)
{
for (int x = x0; x <= x1; x++)
{
if (new Vector2(_Origin.x + x * _Step, _Origin.y + y * _Step).Distance(centre) <= reach)
Mark(x, y);
}
}
}
}
foreach (var lake in Lakes)
{
if (lake.IsCoast || GetShore(lake) is not { } shore)
continue;
var min = new Vector2(float.MaxValue, float.MaxValue);
var max = new Vector2(float.MinValue, float.MinValue);
foreach (var point in shore.Points)
{
Vector3 world = transform.PointToWorld(point);
min = new Vector2(MathF.Min(min.x, world.x), MathF.Min(min.y, world.y));
max = new Vector2(MathF.Max(max.x, world.x), MathF.Max(max.y, world.y));
}
int x0 = Math.Max(0, (int)MathF.Floor((min.x - _Origin.x) / _Step));
int x1 = Math.Min(_Width - 1, (int)MathF.Ceiling((max.x - _Origin.x) / _Step));
int y0 = Math.Max(0, (int)MathF.Floor((min.y - _Origin.y) / _Step));
int y1 = Math.Min(_Height - 1, (int)MathF.Ceiling((max.y - _Origin.y) / _Step));
for (int y = y0; y <= y1; y++)
{
for (int x = x0; x <= x1; x++)
{
var world = new Vector3(_Origin.x + x * _Step, _Origin.y + y * _Step, 0.0f);
if (shore.Contains(transform.PointToLocal(world)))
Mark(x, y);
}
}
}
}
/// <summary>
/// Puts the coastlines found (world, in plan) in place of the old ones, and opens every river that opened into the
/// sea into it again at the nearest of them, if one is near.
/// </summary>
private void ReplaceCoasts(List<Vector2[]> _Loops, float _Step)
{
var transform = WorldTransform;
float level = WorldSeaLevel;
var mouths = Lakes.Where(lake => lake.IsCoast).SelectMany(lake => lake.Openings).ToList();
Lakes.RemoveAll(lake => lake.IsCoast);
var coasts = new List<WaterLake>();
foreach (var loop in _Loops)
{
var coast = new WaterLake
{
Id = NextId++,
Kind = WaterLakeKind.Coast,
Height = SeaLevel,
Smooth = false,
Depth = SeaDepth,
Points = loop.Select(point => transform.PointToLocal(new Vector3(point.x, point.y, level)).WithZ(SeaLevel)).ToList()
};
Lakes.Add(coast);
coasts.Add(coast);
}
foreach (int nodeId in mouths)
{
var node = FindNode(nodeId);
if (node is null || GetDegree(nodeId) != 1)
continue;
var river = GetRiversAt(nodeId).FirstOrDefault();
float bestOffset = (river?.WidthAt(nodeId) ?? 0.0f) + _Step * 4.0f;
WaterLake best = null;
foreach (var coast in coasts)
{
if (GetShore(coast) is not { } shore)
continue;
shore.NearestDistance(node.Position, out float offset);
if (offset >= bestOffset)
continue;
best = coast;
bestOffset = offset;
}
best?.Openings.Add(nodeId);
}
MarkDirty();
}
// ── The coast zone ──
/// <summary>
/// Every river opening into the sea gets a block of the coast zone of its own, built round the opening (its flares,
/// the river's row, and its water carrying out into the sea) instead of on the zone's grid. Blocks are whole tiles of
/// the zone; two never share one.
/// </summary>
private void ComputeMouths()
{
m_Mouths.Clear();
m_MouthTiles.Clear();
if (!SeaEnabled)
return;
float cell = SeaCellSize;
float tile = cell * CoastZone.TileCells;
float level = WorldSeaLevel;
foreach (var coast in Lakes.Where(lake => lake.IsCoast).OrderBy(lake => lake.Id))
{
var shore = GetShore(coast);
if (shore is null || !m_LakeOpenings.TryGetValue(coast.Id, out var openings))
continue;
foreach (var opening in openings.OrderBy(opening => opening.NodeId))
{
if (FindNode(opening.NodeId) is not { } node)
continue;
Vector3 world = m_BuiltTransform.PointToWorld(node.Position);
float smallest = opening.Reach * 1.25f;
Mouth mouth = null;
foreach (float half in new[] { MathF.Max(opening.HalfWidth * 2.0f * EstuarySize, smallest), smallest })
{
int x0 = (int)MathF.Floor((world.x - half) / tile);
int y0 = (int)MathF.Floor((world.y - half) / tile);
int x1 = (int)MathF.Floor((world.x + half) / tile) + 1;
int y1 = (int)MathF.Floor((world.y + half) / tile) + 1;
if (Overlaps(x0, y0, x1, y1))
continue;
var min = new Vector2(x0 * tile, y0 * tile);
var max = new Vector2(x1 * tile, y1 * tile);
var edge = BlockEdge(min, max, cell, level);
bool Inside(Vector3 _Local)
{
Vector3 point = m_BuiltTransform.PointToWorld(_Local);
return point.x >= min.x && point.x <= max.x && point.y >= min.y && point.y <= max.y;
}
var outline = LakeOutline.BuildMouth(shore, opening, Inside, edge);
if (outline is null || outline.Count < 3)
continue;
mouth = new Mouth
{
NodeId = opening.NodeId,
River = opening.River,
Input = MouthInput(opening, outline, min, max),
Min = min,
Max = max
};
for (int y = y0; y < y1; y++)
{
for (int x = x0; x < x1; x++)
m_MouthTiles.Add((x, y));
}
break;
}
if (mouth is not null)
m_Mouths[opening.NodeId] = mouth;
else if (m_ReportedFailures.Add(opening.NodeId * 7919L + coast.Id))
Log.Warning($"[WaterTool2] The river mouth at node #{opening.NodeId} can't be built: another mouth is too close, or the coast folds back on itself there.");
}
}
}
/// <summary>
/// Everything a river mouth is built from, copied out so it can be built on a worker thread: its outline, and the
/// river's water carrying out into the sea from its last row as a jet, gone by the block's edge.
/// </summary>
private MouthBuildInput MouthInput(LakeOpening _Opening, List<OutlineVertex> _Outline, Vector2 _Min, Vector2 _Max)
{
var row = _Opening.Row;
Vector3 inward = -_Opening.Away;
float waves = 0.0f;
for (int k = 0; k < row.Count; k++)
waves += row.Waves[k];
return new MouthBuildInput
{
Outline = _Outline,
Jet = new LakeJet(new Vector2(row.Centre.x, row.Centre.y), new Vector2(inward.x, inward.y).Normal, row.HalfWidth, row.MeanVelocity, row.MeanFoam, MathF.Min(_Max.x - _Min.x, _Max.y - _Min.y) * 0.25f),
Waves = row.Count > 0 ? waves / row.Count : 0.0f,
Height = row.Centre.z,
Min = _Min,
Max = _Max,
SeaLevel = SeaLevel,
CellSize = CellSize,
Transform = m_BuiltTransform
};
}
private bool Overlaps(int _X0, int _Y0, int _X1, int _Y1)
{
for (int y = _Y0; y < _Y1; y++)
{
for (int x = _X0; x < _X1; x++)
{
if (m_MouthTiles.Contains((x, y)))
return true;
}
}
return false;
}
/// <summary>
/// The edge of a block of the zone, counter-clockwise, on every grid point along it, in network space.
/// </summary>
private List<Vector3> BlockEdge(Vector2 _Min, Vector2 _Max, float _Cell, float _Level)
{
int across = Math.Max(1, (int)MathF.Round((_Max.x - _Min.x) / _Cell));
int up = Math.Max(1, (int)MathF.Round((_Max.y - _Min.y) / _Cell));
var points = new List<Vector3>((across + up) * 2);
for (int k = 0; k < across; k++)
points.Add(new Vector3(_Min.x + k * _Cell, _Min.y, _Level));
for (int k = 0; k < up; k++)
points.Add(new Vector3(_Max.x, _Min.y + k * _Cell, _Level));
for (int k = 0; k < across; k++)
points.Add(new Vector3(_Max.x - k * _Cell, _Max.y, _Level));
for (int k = 0; k < up; k++)
points.Add(new Vector3(_Min.x, _Max.y - k * _Cell, _Level));
for (int i = 0; i < points.Count; i++)
points[i] = m_BuiltTransform.PointToLocal(points[i]);
return points;
}
/// <summary>
/// The zone's tiles, rebuilt only when the coast or the sea change; the river mouths' blocks are laid over it, so
/// moving a mouth only changes the mask.
/// </summary>
private void UpdateCoastZone(long _TransformHash)
{
var mouths = StableHash.Create();
foreach (var (x, y) in m_MouthTiles.OrderBy(tile => tile.X).ThenBy(tile => tile.Y))
{
mouths.Add(x);
mouths.Add(y);
}
if (mouths.Value != m_MouthTilesKey)
{
m_MouthTilesKey = mouths.Value;
m_CoastZoneRevision++;
}
var geometry = StableHash.Create();
geometry.Add(_TransformHash);
geometry.Add(SeaCellSize);
geometry.Add(SeaLevel);
geometry.Add(CellSize);
bool any = false;
foreach (var lake in Lakes)
{
if (!lake.IsCoast)
continue;
lake.AddToHash(ref geometry);
any = true;
}
var zone = StableHash.Create();
zone.Add(geometry.Value);
zone.Add(SeaEnabled);
zone.Add(CoastDistance);
if (zone.Value == m_CoastZoneKey)
return;
m_CoastGeometryKey = geometry.Value;
m_CoastZoneKey = zone.Value;
m_CoastZoneRevision++;
m_CoastZone = null;
if (!SeaEnabled || !any)
return;
var loops = new List<Vector2[]>();
foreach (var lake in Lakes)
{
if (!lake.IsCoast || GetShore(lake) is not { } shore)
continue;
var loop = new Vector2[shore.Count];
for (int i = 0; i < shore.Count; i++)
{
Vector3 world = m_BuiltTransform.PointToWorld(shore.Points[i]);
loop[i] = new Vector2(world.x, world.y);
}
loops.Add(loop);
}
m_CoastZone = CoastZone.Build(loops, SeaCellSize, CoastDistance, WorldSeaLevel);
if (m_CoastZone is null && m_ReportedFailures.Add(zone.Value))
Log.Warning("[WaterTool2] The coast zone is far too big to build: lower the Coast Distance, or raise the sea's Cell Size.");
}
/// <summary>
/// Rebuilds every chunk of the zone whose tiles (or the coast through them, or the river mouths cut out of it)
/// changed. In the editor they build on worker threads.
/// </summary>
private void ReconcileCoast(HashSet<WaterPieceKey> _Wanted)
{
if (m_CoastZone is not { } zone)
return;
foreach (var (cx, cy) in zone.CoastChunks())
{
var key = new WaterPieceKey(WaterPieceKind.Coast, cx, cy);
_Wanted.Add(key);
var hash = StableHash.Create();
hash.Add(GeneratorVersion);
hash.Add((int)WaterPieceKind.Coast);
hash.Add(m_CoastGeometryKey);
hash.Add(cx);
hash.Add(cy);
zone.AddChunkToHash(ref hash, cx, cy, m_MouthTiles);
if (IsBuilt(key, hash.Value))
continue;
// The build gets its own copy of the river mouths in its chunk: the network's set changes under it.
var excluded = new HashSet<(int X, int Y)>();
for (int ty = 0; ty < CoastZone.ChunkTiles; ty++)
{
for (int tx = 0; tx < CoastZone.ChunkTiles; tx++)
{
var tile = (cx * CoastZone.ChunkTiles + tx, cy * CoastZone.ChunkTiles + ty);
if (m_MouthTiles.Contains(tile))
excluded.Add(tile);
}
}
int chunkX = cx;
int chunkY = cy;
BuildPiece(key, hash.Value, SeaDepth, () => zone.BuildChunk(chunkX, chunkY, excluded));
}
}
/// <summary>
/// Rebuilds every river mouth whose outline (the coast, the river's row, the block round it) changed, on a worker
/// thread in the editor. A mouth that can't be built hands its block back to the coast zone, so the sea there is never
/// left without water.
/// </summary>
private void ReconcileMouths(HashSet<WaterPieceKey> _Wanted, long _TransformHash)
{
var failed = new List<int>();
foreach (var (nodeId, mouth) in m_Mouths)
{
var key = new WaterPieceKey(WaterPieceKind.Estuary, nodeId, 0);
var hash = StableHash.Create();
hash.Add(GeneratorVersion);
hash.Add((int)WaterPieceKind.Estuary);
hash.Add(_TransformHash);
hash.Add(CellSize);
hash.Add(SeaLevel);
hash.Add(mouth.Min);
hash.Add(mouth.Max);
AddOutlineToHash(ref hash, mouth.Input.Outline);
long signature = hash.Value;
if (m_FailedMouths.Contains(signature))
{
failed.Add(nodeId);
continue;
}
_Wanted.Add(key);
if (IsBuilt(key, signature))
continue;
var input = mouth.Input;
BuildPiece(key, signature, MathF.Max(mouth.River.Depth, SeaDepth), () => EstuaryBuilder.Build(input));
}
foreach (int nodeId in failed)
{
var mouth = m_Mouths[nodeId];
float tile = SeaCellSize * CoastZone.TileCells;
for (int y = (int)MathF.Round(mouth.Min.y / tile); y < (int)MathF.Round(mouth.Max.y / tile); y++)
{
for (int x = (int)MathF.Round(mouth.Min.x / tile); x < (int)MathF.Round(mouth.Max.x / tile); x++)
m_MouthTiles.Remove((x, y));
}
m_Mouths.Remove(nodeId);
}
}
/// <summary>
/// Where the coast zone (and the land it is masked out with) gives way to the infinite sea, as lines in the world at
/// sea level, for the editor to draw. Nothing when there is no coast zone.
/// </summary>
public IEnumerable<(Vector3 A, Vector3 B)> CoastZoneOutline()
{
if (m_CoastZone is not { } zone)
yield break;
float level = zone.SeaLevel;
foreach (var (a, b) in zone.MaskOutline(m_MouthTiles))
yield return (new Vector3(a.x, a.y, level), new Vector3(b.x, b.y, level));
}
/// <summary>
/// The blocks of the coast zone river mouths are built in, in the world at sea level, for the editor to draw.
/// </summary>
public IEnumerable<BBox> RiverMouthBlocks()
{
float level = WorldSeaLevel;
foreach (var mouth in m_Mouths.Values)
yield return new BBox(new Vector3(mouth.Min.x, mouth.Min.y, level), new Vector3(mouth.Max.x, mouth.Max.y, level));
}
/// <summary>
/// Whether the infinite sea is masked out at a world position (in plan): the coast zone's water is there instead (or
/// a river mouth's), or land.
/// </summary>
public bool IsInCoastZone(Vector3 _Position)
{
return m_CoastZone is { } zone && zone.IsMasked(new Vector2(_Position.x, _Position.y), m_MouthTiles);
}
/// <summary>
/// The mask on the GPU (the coast zone, the land, and the river mouths laid over them), whenever it has changed.
/// </summary>
private void UploadCoastZone()
{
if (m_CoastBufferRevision == m_CoastZoneRevision)
return;
m_CoastBufferRevision = m_CoastZoneRevision;
m_CoastBuffer?.Dispose();
m_CoastBuffer = null;
if (m_CoastZone is null)
return;
var values = m_CoastZone.MaskValues(m_MouthTiles, out m_CoastMaskX, out m_CoastMaskY, out m_CoastMaskWidth, out m_CoastMaskHeight);
m_CoastBuffer = new GpuBuffer<float>(values.Length, GpuBuffer.UsageFlags.Structured);
m_CoastBuffer.SetData(values.AsSpan());
}
/// <summary>
/// What the shader needs to mask the infinite sea out over the coast zone and the land. No zone, no mask.
/// </summary>
private void ApplyCoastAttributes(RenderAttributes _Attributes)
{
var zone = m_CoastBuffer.IsValid() ? m_CoastZone : null;
_Attributes.Set("CoastZoneWidth", zone is null ? 0 : m_CoastMaskWidth);
_Attributes.Set("CoastZoneHeight", zone is null ? 0 : m_CoastMaskHeight);
_Attributes.Set("CoastZoneOrigin", zone is null ? Vector2.Zero : new Vector2(m_CoastMaskX * zone.Tile, m_CoastMaskY * zone.Tile));
_Attributes.Set("CoastZoneTile", zone?.Tile ?? 1.0f);
if (zone is not null)
_Attributes.Set("CoastZone", m_CoastBuffer);
}
private void ReleaseCoast()
{
m_CoastBuffer?.Dispose();
m_CoastBuffer = null;
m_CoastBufferRevision = -1;
}
}