Code/Waves/WaterWaves.cs

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.

Native Interop
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;
	}
}