Water sampling utilities for the water system. Defines WaterSample struct holding heights, displacement, flow, waves, foam, depth and sea blend, and provides methods on WaterNetwork to sample water at world positions, query height, flat height, flow, wave displacement and wave velocity, and test if a position is inside water.
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;
}
}