Defines a WaterSample struct holding water state at a point (rest height, wave displacement, flow, waves, foam, depth, sea blend) and adds sampling/query methods on WaterNetwork to find the nearest water piece at a world position, compute displacement/height/flow and helper static accessors (TrySample, GetWaterHeightAt, GetFlatWaterHeightAt, GetFlowVelocityAt, GetWaveDisplacementAt, GetWaveVelocityAt, IsPositionInsideAny).
using System;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// The water at a point, as gameplay sees it: where its surface is (at rest, and with its waves and ripples), how it
/// flows, and how deep it is.
/// </summary>
public readonly struct WaterSample
{
/// <summary>
/// The surface's height without its waves.
/// </summary>
public readonly float RestHeight;
/// <summary>
/// The surface's height with its waves and ripples: what floats sits on this.
/// </summary>
public readonly float Height;
/// <summary>
/// How the waves and ripples move the surface at this point: XY the waves' orbital motion, Z their height.
/// </summary>
public readonly Vector3 Displacement;
/// <summary>
/// The current, level, in units per second.
/// </summary>
public readonly Vector3 Flow;
/// <summary>
/// The wave strength here, relative to the network's wave profile.
/// </summary>
public readonly float Waves;
public readonly float Foam;
/// <summary>
/// How deep the water is under its surface at rest.
/// </summary>
public readonly float Depth;
/// <summary>
/// How far the water here has handed over to the sea: 0 a river's or a lake's own, 1 the sea's.
/// </summary>
public readonly float SeaBlend;
/// <summary>
/// Where the water stops at the bottom.
/// </summary>
public float Bottom => RestHeight - Depth;
public WaterSample(float _RestHeight, Vector3 _Displacement, Vector3 _Flow, float _Waves, float _Foam, float _Depth, float _SeaBlend = 0.0f)
{
RestHeight = _RestHeight;
Height = _RestHeight + _Displacement.z;
Displacement = _Displacement;
Flow = _Flow;
Waves = _Waves;
Foam = _Foam;
Depth = _Depth;
SeaBlend = _SeaBlend;
}
}
public sealed partial class WaterNetwork
{
/// <summary>
/// The water at a world position: the piece over or under it whose water column (surface down to bed) is nearest,
/// else the open sea. False when there is no water there, or the position is below the bottom of the water.
/// </summary>
public bool Sample(Vector3 _Position, out WaterSample _Sample)
{
_Sample = default;
var point = new Vector2(_Position.x, _Position.y);
WaterPiece best = null;
SurfacePoint surface = default;
float bestDistance = float.MaxValue;
foreach (var piece in m_Pieces.Values)
{
if (!piece.Mesh.TrySample(point, out SurfacePoint candidate))
continue;
// Water has a bottom: below it this is not the water we are in.
if (_Position.z < candidate.Height - piece.Depth)
continue;
float distance = MathF.Abs(_Position.z - candidate.Height);
if (distance >= bestDistance)
continue;
best = piece;
surface = candidate;
bestDistance = distance;
}
if (best is null)
return SampleSea(_Position, out _Sample);
Vector3 displacement = WaterWaves.FlowingDisplacement(point, surface.Velocity, ResolvedProfile, Time.Now, FlowCycle) * surface.Waves;
// Where a river hands over to the sea, its waves give way to the sea's.
if (surface.SeaBlend > 0.0f)
displacement = Vector3.Lerp(displacement, SeaDisplacementAt(point), surface.SeaBlend);
displacement.z += ComputeRippleHeight(point);
_Sample = new WaterSample(surface.Height, displacement, new Vector3(surface.Velocity.x, surface.Velocity.y, 0.0f), surface.Waves, surface.Foam, best.Depth, surface.SeaBlend);
return true;
}
/// <summary>
/// The water at a world position, in the current network. False when there is none.
/// </summary>
public static bool TrySample(Vector3 _Position, out WaterSample _Sample)
{
_Sample = default;
return Current.IsValid() && Current.Sample(_Position, out _Sample);
}
/// <summary>
/// The height of the water's surface (waves and ripples included) over or under a position, or float.MinValue
/// where there is no water.
/// </summary>
public static float GetWaterHeightAt(Vector3 _Position)
{
return TrySample(_Position, out var sample) ? sample.Height : float.MinValue;
}
/// <summary>
/// The height of the water's surface at rest, without waves, or float.MinValue where there is no water.
/// </summary>
public static float GetFlatWaterHeightAt(Vector3 _Position)
{
return TrySample(_Position, out var sample) ? sample.RestHeight : float.MinValue;
}
/// <summary>
/// The current at a position (level, in units per second). Zero where there is no water, or the water is still.
/// </summary>
public static Vector3 GetFlowVelocityAt(Vector3 _Position)
{
return TrySample(_Position, out var sample) ? sample.Flow : Vector3.Zero;
}
/// <summary>
/// How the waves and ripples move the surface at a position: XY the waves' orbital motion, Z their height.
/// </summary>
public static Vector3 GetWaveDisplacementAt(Vector3 _Position)
{
return TrySample(_Position, out var sample) ? sample.Displacement : Vector3.Zero;
}
/// <summary>
/// How fast the surface itself moves at a position: the waves' orbital motion (the current comes from
/// <see cref="GetFlowVelocityAt"/>).
/// </summary>
public static Vector3 GetWaveVelocityAt(Vector3 _Position)
{
if (!Current.IsValid() || !Current.Sample(_Position, out var sample))
return Vector3.Zero;
var point = new Vector2(_Position.x, _Position.y);
var network = Current;
Vector3 own = sample.SeaBlend < 1.0f
? WaterWaves.SurfaceVelocity(point, new Vector2(sample.Flow.x, sample.Flow.y), sample.Waves, network.ResolvedProfile, Time.Now, network.FlowCycle)
: Vector3.Zero;
if (sample.SeaBlend <= 0.0f)
return own;
Vector3 sea = WaterWaves.SurfaceVelocity(point, Vector2.Zero, network.SeaWaves, network.ResolvedSeaProfile, Time.Now, network.FlowCycle);
return Vector3.Lerp(own, sea, sample.SeaBlend);
}
/// <summary>
/// Whether a position is in the water: under its surface, waves included, and above its bottom.
/// </summary>
public static bool IsPositionInsideAny(Vector3 _Position)
{
return TrySample(_Position, out var sample) && _Position.z <= sample.Height;
}
}