namespace Sandbox; /// /// Models a unit mass attached to a spring and a damper, with a particular /// and . /// internal readonly struct SpringDamper { /// /// Create a model of a damped spring from a and per oscillation. /// /// How many times the spring oscillates per second. /// How much damping to apply each oscillation, as with the legacy . public static SpringDamper FromDamping( float frequency = 2f, float damping = 0.5f ) => new( frequency, damping * frequency * MathF.PI * 2f ); /// /// Create a critically damped model with a given , for movement that doesn't oscillate but smoothly /// settles to the target value. /// /// Time until the spring has settled. public static SpringDamper FromSmoothingTime( float smoothingTime ) => smoothingTime <= 0f ? FromDamping( 1f, float.PositiveInfinity ) : FromDamping( 1f / smoothingTime, 1f ); /// /// How many times the spring oscillates per second. /// public float Frequency { get; } /// /// Exponential decay constant λ, higher values decay faster. /// public float DecayRate { get; } /// /// Angular frequency (radians per second), with decay accounted for. /// private readonly float _omega; /// /// Models a unit mass attached to a spring and a damper, with a particular /// and . /// public SpringDamper( float frequency, float decayRate ) { Frequency = frequency; DecayRate = decayRate; // Natural angular frequency, without damping var omega0 = Frequency * MathF.PI * 2f; // Damped angular frequency _omega = MathF.Sqrt( Math.Max( 0f, omega0 * omega0 - DecayRate * DecayRate ) ); } #region Simulate /// /// Simulate the evolution of a unit mass with given and /// being affected by this system, seconds into the future. /// /// Current displacement of the mass from the spring rest position. /// Current velocity of the mass. /// How far to simulate into the future. public (float Position, float Velocity) Simulate( float position, float velocity, float deltaTime ) { if ( deltaTime <= 0.0f ) return (position, velocity); if ( float.IsPositiveInfinity( DecayRate ) ) return (0f, 0f); // Correct for what our velocity would be without damping, so we can extrapolate the oscillation var velocityWithoutDecay = velocity + DecayRate * position; // Simulate spring without decay (position, velocityWithoutDecay) = SimulateOscillator( position, velocityWithoutDecay, deltaTime ); // Apply exponential decay (position, velocity) = SimulateDecay( position, velocityWithoutDecay, deltaTime ); return (position, velocity); } /// public (Vector2 Position, Vector2 Velocity) Simulate( Vector2 position, Vector2 velocity, float deltaTime ) { (position.x, velocity.x) = Simulate( position.x, velocity.x, deltaTime ); (position.y, velocity.y) = Simulate( position.y, velocity.y, deltaTime ); return (position, velocity); } /// public (Vector3 Position, Vector3 Velocity) Simulate( Vector3 position, Vector3 velocity, float deltaTime ) { (position.x, velocity.x) = Simulate( position.x, velocity.x, deltaTime ); (position.y, velocity.y) = Simulate( position.y, velocity.y, deltaTime ); (position.z, velocity.z) = Simulate( position.z, velocity.z, deltaTime ); return (position, velocity); } #endregion #region Private private (float MaxPosition, float MaxVelocity, float Phase) FindOscillationParameters( float position, float velocity ) { // Total energy (kinetic + potential) x 2, assuming unit mass var energy2 = velocity * velocity + _omega * _omega * position * position; // Snap to 0 if energy is super low, so we don't wobble forever if ( energy2 <= 0.001f ) return (0f, 0f, 0f); // Find maximum velocity by turning all energy into kinetic var vMax = MathF.Sqrt( energy2 ); // Find maximum amplitude by turning all energy into potential var amplitude = vMax / _omega; // Where are we in the oscillation var phase = MathF.Atan2( -velocity, position * _omega ); return (amplitude, vMax, phase); } private (float Position, float Velocity) SimulateOscillator( float position, float velocity, float deltaTime ) { if ( _omega <= 0.0001f ) { // We're not oscillating, just moving at constant velocity return (position + velocity * deltaTime, velocity); } // Work out where we are in the oscillation (the phase), and what its amplitude / max velocity is var (xMax, vMax, phase) = FindOscillationParameters( position, velocity ); if ( xMax <= 0f ) { // Fast path if we're at equilibrium return (0f, 0f); } // Project it into the future return (MathF.Cos( deltaTime * _omega + phase ) * xMax, -MathF.Sin( deltaTime * _omega + phase ) * vMax); } private (float Position, float Velocity) SimulateDecay( float position, float velocity, float deltaTime ) { var scale = float.IsPositiveInfinity( DecayRate ) ? 0f : MathF.Exp( -deltaTime * DecayRate ); // Apply exponential decay position *= scale; velocity *= scale; // Apply gradient of exponential decay velocity -= DecayRate * position; return (position, velocity); } #endregion }