OceanSpectrum.cs
using System;

namespace OceanLib;

/// <summary>
/// The initial wave spectrum h0(k) of one ocean cascade, after Tessendorf: a Phillips spectrum for a wind of a given
/// speed and direction, with each wave's amplitude and phase drawn from a Gaussian. A cascade only keeps the waves in
/// its own band of wave numbers, so several cascades of different tile sizes add up to one sea without counting any
/// wave twice; amplitudes carry the cascade's frequency spacing (dk), so the bands join smoothly whatever their tile.
///
/// Everything comes from integer hashes of the seed and the wave's index - never from a shared random generator - so
/// every machine, the GPU's surface and any CPU-side replica of it for physics build exactly the same sea.
/// </summary>
public sealed class OceanSpectrum
{
	public const float Gravity = 9.81f;

	/// <summary>FFT resolution: waves per side of the tile.</summary>
	public int Size { get; }

	/// <summary>Metres covered by one tile.</summary>
	public float TileLength { get; }

	/// <summary>h0(k) in .xy and conj(h0(-k)) in .zw, row-major, wave vectors centred on index Size / 2.</summary>
	public Vector4[] Texels { get; }

	/// <summary>Sum of |h0|^2 before any scaling: this cascade's share of the sea's height variance, halved.</summary>
	public double RawVariance { get; }

	/// <summary>The settings shared by every cascade of one sea.</summary>
	public readonly record struct Settings( float WindSpeed, float WindAngle, float Directionality, float SmallWaveCutoff, int Seed );

	/// <summary>
	/// Builds the waves with wave numbers in [minK, maxK) - radians per metre - at their raw Phillips amplitude. Call
	/// <see cref="Scale"/> afterwards with the factor that brings all cascades together to the height wanted.
	/// </summary>
	public OceanSpectrum( int size, float tileLength, float minK, float maxK, int cascade, Settings settings )
	{
		Size = size;
		TileLength = tileLength;

		var h0 = new Vector2[size * size];
		// The leading term of h(k, t) - h0(k) e^{i w t} - runs against its wave vector, so the waves that should travel
		// downwind are the ones whose k points upwind: the spectrum is laid out against the wind.
		var wind = -new Vector2( MathF.Cos( settings.WindAngle.DegreeToRadian() ), MathF.Sin( settings.WindAngle.DegreeToRadian() ) );

		// The longest wave a wind of this speed raises; the spectrum falls away below it.
		float longest = settings.WindSpeed * settings.WindSpeed / Gravity;
		float shortest = settings.SmallWaveCutoff;
		float dk = 2.0f * MathF.PI / tileLength;
		double variance = 0.0;

		for ( int y = 0; y < size; y++ )
		{
			for ( int x = 0; x < size; x++ )
			{
				var k = WaveVector( x, y );
				float kLength = k.Length;

				// The first row and column hold the Nyquist frequency, which is its own mirror image: a displacement
				// along it can't be represented by a real field, and would leak into the height packed beside it.
				if ( kLength < 1e-6f || x == 0 || y == 0 || kLength < minK || kLength >= maxK )
					continue;

				var direction = k / kLength;
				float alignment = Vector2.Dot( direction, wind );

				// Phillips: P(k) = exp(-1/(kL)^2) / k^4 * |k.w|^p, small waves damped by exp(-k^2 l^2). Waves running
				// against the wind keep only a little of their energy.
				float kl = kLength * longest;
				float phillips = MathF.Exp( -1.0f / (kl * kl) ) / (kLength * kLength * kLength * kLength);
				phillips *= MathF.Pow( MathF.Abs( alignment ), settings.Directionality );
				phillips *= MathF.Exp( -kLength * kLength * shortest * shortest );

				if ( alignment < 0.0f )
					phillips *= 0.07f;

				var gauss = Gaussian( settings.Seed, cascade, x, y );
				h0[y * size + x] = gauss * (MathF.Sqrt( phillips * 0.5f ) * dk);
				variance += h0[y * size + x].LengthSquared;
			}
		}

		RawVariance = variance;
		Texels = new Vector4[size * size];

		for ( int y = 0; y < size; y++ )
		{
			for ( int x = 0; x < size; x++ )
			{
				var a = h0[y * size + x];
				var b = h0[((size - y) % size) * size + (size - x) % size];
				Texels[y * size + x] = new Vector4( a.x, a.y, b.x, -b.y );
			}
		}
	}

	private OceanSpectrum( int size, float tileLength, Vector4[] texels )
	{
		Size = size;
		TileLength = tileLength;
		Texels = texels;
	}

	/// <summary>
	/// The spectrum part way between two of the same tile: the sea is linear in its amplitudes, so this is exactly the
	/// sea a blend between them shows.
	/// </summary>
	public static OceanSpectrum Lerp( OceanSpectrum a, OceanSpectrum b, float t )
	{
		var texels = new Vector4[a.Texels.Length];

		for ( int i = 0; i < texels.Length; i++ )
			texels[i] = a.Texels[i] + (b.Texels[i] - a.Texels[i]) * t;

		return new OceanSpectrum( a.Size, a.TileLength, texels );
	}

	/// <summary>Multiplies every amplitude.</summary>
	public void Scale( float factor )
	{
		for ( int i = 0; i < Texels.Length; i++ )
			Texels[i] *= factor;
	}

	/// <summary>
	/// The factor that brings a set of cascades to a significant wave height (four standard deviations of the height,
	/// in metres). h(x) sums h0(k) and conj(h0(-k)) for every k, so its variance is twice the sum of |h0|^2.
	/// </summary>
	public static float ScaleFor( float significantHeight, double totalRawVariance )
	{
		return totalRawVariance > 0.0 ? significantHeight / (4.0f * MathF.Sqrt( (float)(2.0 * totalRawVariance) )) : 0.0f;
	}

	/// <summary>The wave vector at a texel, in radians per metre.</summary>
	public Vector2 WaveVector( int x, int y ) => new Vector2( x - Size / 2, y - Size / 2 ) * (2.0f * MathF.PI / TileLength);

	/// <summary>A pair of independent standard Gaussians for one wave, by Box-Muller over two hashed uniforms.</summary>
	private static Vector2 Gaussian( int seed, int cascade, int x, int y )
	{
		float u1 = MathF.Max( Uniform( seed, cascade, x, y, 0 ), 1e-7f );
		float u2 = Uniform( seed, cascade, x, y, 1 );
		float r = MathF.Sqrt( -2.0f * MathF.Log( u1 ) );
		return new Vector2( r * MathF.Cos( 2.0f * MathF.PI * u2 ), r * MathF.Sin( 2.0f * MathF.PI * u2 ) );
	}

	/// <summary>A uniform in [0, 1) from a PCG-style integer hash: the same on every machine.</summary>
	private static float Uniform( int seed, int cascade, int x, int y, int channel )
	{
		uint h = (uint)seed * 747796405u + 2891336453u + (uint)cascade * 0x27D4EB2Fu;
		h ^= (uint)x * 0x9E3779B1u;
		h = Pcg( h ) ^ ((uint)y * 0x85EBCA77u);
		h = Pcg( h ) ^ ((uint)channel * 0xC2B2AE3Du);
		h = Pcg( h );
		return (h >> 8) * (1.0f / 16777216.0f);
	}

	private static uint Pcg( uint v )
	{
		uint state = v * 747796405u + 2891336453u;
		uint word = ((state >> (int)((state >> 28) + 4u)) ^ state) * 277803737u;
		return (word >> 22) ^ word;
	}
}