Code/OceanFoamSource.cs
using System;

namespace OceanLib;

/// <summary>
/// Makes foam where this object cuts the sea: a band round its outline at the waterline, stronger the faster it moves,
/// which <see cref="OceanFoam"/> carries on after it - a ship's bow wave and wake, the white water round a rock. The
/// outline is an ellipse in the object's own space; left at zero it is fitted to the model's bounds.
/// </summary>
[Title( "Ocean Foam Source" ), Category( "Ocean" ), Icon( "bubble_chart" )]
public sealed class OceanFoamSource : Component
{
	/// <summary>Half the length and half the width of the outline, in metres. Zero fits it to the model.</summary>
	[Property] public Vector2 HalfSize { get; set; }

	/// <summary>Foam added per second at a standstill, where the band is thickest.</summary>
	[Property, Range( 0, 10 )] public float Strength { get; set; } = 0.6f;

	/// <summary>Extra foam per second for each metre per second the object moves.</summary>
	[Property, Range( 0, 5 )] public float SpeedStrength { get; set; } = 0.6f;

	/// <summary>How far out from the outline the foam reaches, in metres.</summary>
	[Property, Range( 0.1f, 10 )] public float BandWidth { get; set; } = 1.5f;

	/// <summary>The outline in world units: half length and half width.</summary>
	public Vector2 WorldHalfSize { get; private set; }

	/// <summary>The lowest and highest point of the object, in world z: it only foams where the sea lies between them.</summary>
	public Vector2 HeightRange { get; private set; }

	/// <summary>How fast it moves across the sea, in metres per second.</summary>
	public float Speed { get; private set; }

	/// <summary>
	/// Where the object's upright axis crosses the mean sea level: the middle of its waterline. A leaning rock whose
	/// centre lies deep under water cuts the surface metres away from that centre.
	/// </summary>
	public Vector3 WaterlineCenter { get; private set; }

	private Vector3 m_LastPosition;
	private bool m_HasLast;

	protected override void OnUpdate()
	{
		var bounds = Components.Get<ModelRenderer>()?.Model?.Bounds ?? BBox.FromPositionAndSize( Vector3.Zero, 32 );
		var scale = WorldScale;
		var half = HalfSize.Length > 0.0f ? HalfSize * Ocean.UnitsPerMetre : new Vector2( bounds.Extents.x, bounds.Extents.y );
		WorldHalfSize = new Vector2( half.x * scale.x, half.y * scale.y );

		var world = bounds.Transform( WorldTransform );
		HeightRange = new Vector2( world.Mins.z, world.Maxs.z );

		var centre = WorldTransform.PointToWorld( bounds.Center );
		var up = WorldRotation.Up;
		WaterlineCenter = MathF.Abs( up.z ) > 0.2f ? centre - up * (centre.z / up.z) : centre;

		if ( m_HasLast && Time.Delta > 0.0f )
			Speed = MathX.Lerp( Speed, (WorldPosition - m_LastPosition).WithZ( 0 ).Length / Time.Delta / Ocean.UnitsPerMetre, 0.2f );

		m_LastPosition = WorldPosition;
		m_HasLast = true;
	}

	/// <summary>Foam per second this frame: the steady band, more for speed, and any splash still settling.</summary>
	public float Rate => Strength + SpeedStrength * Speed + m_Splash;

	private float m_Splash;

	/// <summary>A burst of white water round the hull - it slammed into a wave - fading over half a second.</summary>
	public void Splash( float amount )
	{
		m_Splash = MathF.Max( m_Splash, amount );
	}

	protected override void OnFixedUpdate()
	{
		m_Splash *= MathF.Exp( -Time.Delta / 0.5f );
	}

	protected override void DrawGizmos()
	{
		// The outline the foam forms round, at the waterline.
		var bounds = Components.Get<ModelRenderer>()?.Model?.Bounds ?? BBox.FromPositionAndSize( Vector3.Zero, 32 );
		var half = HalfSize.Length > 0.0f ? HalfSize * Ocean.UnitsPerMetre : new Vector2( bounds.Extents.x, bounds.Extents.y );
		var centre = bounds.Center.WithZ( 0 );

		Gizmo.Draw.Color = Color.White.WithAlpha( 0.6f );

		for ( int i = 0; i < 48; i++ )
		{
			float a = i / 48.0f * MathF.PI * 2.0f, b = (i + 1) / 48.0f * MathF.PI * 2.0f;
			Gizmo.Draw.Line( centre + new Vector3( MathF.Cos( a ) * half.x, MathF.Sin( a ) * half.y, 0 ),
				centre + new Vector3( MathF.Cos( b ) * half.x, MathF.Sin( b ) * half.y, 0 ) );
		}
	}
}