CPU-side water wave utilities. Implements Gerstner wave sums, phase noise, flowing-wave blending, displacement and surface velocity functions that mirror shader implementations for water motion.
using System;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// The CPU side of the water's motion. Every function here mirrors one in advancedwater.shader term for term, so what
/// floats and swims rides the very surface the player sees.
/// </summary>
public static class WaterWaves
{
/// <summary>
/// Each Gerstner octave turns this many radians from the one before, so the octaves cross instead of stacking.
/// </summary>
public const float OctaveAngleStep = 1.2f;
/// <summary>
/// Size, in world units, of the slow noise that offsets the flow cycle across the surface, so the two-phase cross
/// fade of flowing water never pulses everywhere at once.
/// </summary>
public const float PhaseNoiseScale = 750.0f;
/// <summary>
/// The still water's waves (detail and swell) at a world point: XY is the orbital motion, Z the height.
/// Mirrors WaveDisplacement() in the shader.
/// </summary>
public static Vector3 Displacement(Vector2 _Point, WaterDefinition _Profile, float _Time)
{
Vector3 detail = Gerstner(_Point, _Profile.WavesScale, _Profile.WavesSpeed, _Profile.WavesDirection, _Profile.WavesOctaves, _Profile.WavesLacunarity, _Profile.WavesPersistence, _Profile.WavesSteepness, _Time) * _Profile.WavesIntensity;
Vector3 swell = Gerstner(_Point, _Profile.SwellScale, _Profile.SwellSpeed, _Profile.SwellDirection, _Profile.SwellOctaves, _Profile.SwellLacunarity, _Profile.SwellPersistence, _Profile.SwellSteepness, _Time) * _Profile.SwellIntensity;
return detail + swell;
}
/// <summary>
/// The waves carried along by flowing water: the still waves sampled twice, each copy sliding downstream with the
/// flow for one cycle before it starts over, the two half a cycle apart and cross-faded so neither restart is ever
/// seen. Every point uses its own flow, and the result only depends on the point and its flow, so two pieces that
/// share a vertex always move it the same. Mirrors FlowingWaves() in the shader.
/// </summary>
public static Vector3 FlowingDisplacement(Vector2 _Point, Vector2 _Flow, WaterDefinition _Profile, float _Time, float _Cycle)
{
if (_Flow.LengthSquared < 0.0001f || _Cycle <= 0.0f)
return Displacement(_Point, _Profile, _Time);
float phase = _Time / _Cycle + PhaseNoise(_Point);
float phase0 = Frac(phase);
float phase1 = Frac(phase + 0.5f);
float weight0 = 1.0f - MathF.Abs(2.0f * phase0 - 1.0f);
Vector3 first = Displacement(_Point - _Flow * ((phase0 - 0.5f) * _Cycle), _Profile, _Time);
Vector3 second = Displacement(_Point - _Flow * ((phase1 - 0.5f) * _Cycle), _Profile, _Time);
return first * weight0 + second * (1.0f - weight0);
}
/// <summary>
/// How fast the surface itself moves at a point (the waves' orbital motion), by stepping the displacement a little
/// either side in time.
/// </summary>
public static Vector3 SurfaceVelocity(Vector2 _Point, Vector2 _Flow, float _WaveScale, WaterDefinition _Profile, float _Time, float _Cycle)
{
const float step = 0.05f;
Vector3 before = FlowingDisplacement(_Point, _Flow, _Profile, _Time - step, _Cycle);
Vector3 after = FlowingDisplacement(_Point, _Flow, _Profile, _Time + step, _Cycle);
return (after - before) * (_WaveScale / (2.0f * step));
}
/// <summary>
/// Smooth value noise in [0, 1), one cell every <see cref="PhaseNoiseScale"/> units. Mirrors FlowPhaseNoise() in the
/// shader.
/// </summary>
public static float PhaseNoise(Vector2 _Point)
{
float x = _Point.x / PhaseNoiseScale;
float y = _Point.y / PhaseNoiseScale;
float ix = MathF.Floor(x);
float iy = MathF.Floor(y);
float fx = x - ix;
float fy = y - iy;
float ux = fx * fx * (3.0f - 2.0f * fx);
float uy = fy * fy * (3.0f - 2.0f * fy);
float a = Hash(ix, iy);
float b = Hash(ix + 1.0f, iy);
float c = Hash(ix, iy + 1.0f);
float d = Hash(ix + 1.0f, iy + 1.0f);
float bottom = a + (b - a) * ux;
float top = c + (d - c) * ux;
return bottom + (top - bottom) * uy;
}
private static float Hash(float _X, float _Y)
{
float a = Frac(_X * 0.1031f);
float b = Frac(_Y * 0.1031f);
float c = Frac(_X * 0.1031f);
float s = a * (b + 33.33f) + b * (c + 33.33f) + c * (a + 33.33f);
a += s;
b += s;
c += s;
return Frac((a + b) * c);
}
private static float Frac(float _Value)
{
return _Value - MathF.Floor(_Value);
}
/// <summary>
/// A Gerstner wave sum, each octave smaller, faster and turned from the last. Mirrors ComputeGerstner() in the
/// shader.
/// </summary>
private static Vector3 Gerstner(Vector2 _Point, float _Scale, float _Speed, Vector2 _Direction, int _Octaves, float _Lacunarity, float _Persistence, float _Steepness, float _Time)
{
if (_Octaves <= 0)
return Vector3.Zero;
Vector2 direction = _Direction.LengthSquared > 0.000001f ? _Direction.Normal : new Vector2(1.0f, 0.0f);
float time = _Time * _Speed;
Vector3 displacement = Vector3.Zero;
float amplitude = 1.0f;
float frequency = _Scale;
float total = 0.0f;
for (int octave = 0; octave < _Octaves; octave++)
{
float angle = octave * OctaveAngleStep;
float cos = MathF.Cos(angle);
float sin = MathF.Sin(angle);
var octaveDirection = new Vector2(direction.x * cos - direction.y * sin, direction.x * sin + direction.y * cos);
float phase = frequency * (octaveDirection.x * _Point.x + octaveDirection.y * _Point.y) + time * frequency * 0.5f;
displacement.x += _Steepness * amplitude * octaveDirection.x * MathF.Cos(phase);
displacement.y += _Steepness * amplitude * octaveDirection.y * MathF.Cos(phase);
displacement.z += amplitude * MathF.Sin(phase);
total += amplitude;
amplitude *= _Persistence;
frequency *= _Lacunarity;
}
return total > 0.0f ? displacement / total : Vector3.Zero;
}
}