using System.Runtime.CompilerServices; namespace Sandbox; /// /// A class to add functionality to the math library that System.Math and System.MathF don't provide. /// A lot of these methods are also extensions, so you can use for example `int i = 1.0f.FloorToInt();` /// public static partial class MathX { internal const float toRadians = (float)Math.PI * 2F / 360F; internal const float toDegrees = 1.0f / toRadians; internal const float toGradiansDegrees = 0.9f; internal const float toGradiansRadians = 0.01570796326f; /// /// Convert degrees to radians. /// /// 180 degrees is (roughly 3.14) radians, etc. /// /// A value in degrees to convert. /// The given value converted to radians. public static float DegreeToRadian( this float deg ) => deg * toRadians; /// /// Convert radians to degrees. /// /// 180 degrees is (roughly 3.14) radians, etc. /// /// A value in radians to convert. /// The given value converted to degrees. public static float RadianToDegree( this float rad ) => rad * toDegrees; /// /// Convert gradians to degrees. /// /// 100 gradian is 90 degrees, 200 gradian is 180 degrees, etc. /// /// A value in gradians to convert. /// The given value converted to degrees. public static float GradiansToDegrees( this float grad ) => grad * toGradiansDegrees; /// /// Convert gradians to radians. /// /// 200 gradian is (roughly 3.14) radians, etc. /// /// A value in gradians to convert. /// The given value converted to radians. public static float GradiansToRadians( this float grad ) => grad * toGradiansRadians; internal const float toMeters = 0.0254f; internal const float toInches = 1.0f / toMeters; internal const float toMillimeters = 25.4f; /// /// Convert meters to inches. /// public static float MeterToInch( this float meters ) => meters * toInches; /// /// Convert inches to meters. /// public static float InchToMeter( this float inches ) => inches * toMeters; /// /// Convert inches to millimeters. /// public static float InchToMillimeter( this float inches ) => inches * toMillimeters; /// /// Convert millimeters to inches. /// public static float MillimeterToInch( this float millimeters ) => millimeters * (1.0f / toMillimeters); /// /// Snap number to grid /// public static float SnapToGrid( this float f, float gridSize ) { if ( gridSize.AlmostEqual( 0.0f ) ) return f; var inv = 1 / gridSize; return MathF.Round( f * inv ) / inv; } /// /// Snap number to grid /// public static int SnapToGrid( this int f, int gridSize ) { return (f / gridSize) * gridSize; } /// /// Remove the fractional part and return the float as an integer. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static int FloorToInt( this float f ) { return (int)MathF.Floor( f ); } /// /// Remove the fractional part of given floating point number /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float Floor( this float f ) { return MathF.Floor( f ); } /// /// Rounds up given float to next integer value. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static int CeilToInt( this float f ) { return (int)MathF.Ceiling( f ); } /// /// Orders the two given numbers so that a is less than b. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] static void Order( ref float a, ref float b ) { if ( a <= b ) return; (b, a) = (a, b); } /// /// Clamp a float between 2 given extremes. /// If given value is lower than the given minimum value, returns the minimum value, etc. /// /// The value to clamp. /// Minimum return value. /// Maximum return value. /// The clamped float. [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float Clamp( this float v, float min, float max ) { Order( ref min, ref max ); return v < min ? min : v < max ? v : max; } /// /// Performs linear interpolation on floating point numbers. /// /// The "starting value" of the interpolation. /// The "final value" of the interpolation. /// The fraction in range of 0 (will return value of ) to 1 (will return value of ). /// Whether to clamp the fraction between 0 and 1, and therefore the output value between and . /// The result of linear interpolation. [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float Lerp( float from, float to, float frac, bool clamp = true ) { if ( clamp ) frac = frac.Clamp( 0, 1 ); return (from + frac * (to - from)); } /// /// Performs linear interpolation on floating point numbers. /// /// The "starting value" of the interpolation. /// The "final value" of the interpolation. /// The fraction in range of 0 (will return value of ) to 1 (will return value of ). /// Whether to clamp the fraction between 0 and 1, and therefore the output value between and . /// The result of linear interpolation. [MethodImpl( MethodImplOptions.AggressiveInlining )] public static double Lerp( double from, double to, double frac, bool clamp = true ) { if ( clamp ) frac = frac.Clamp( 0, 1 ); return (from + frac * (to - from)); } /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float LerpTo( this float from, float to, float frac, bool clamp = true ) { if ( clamp ) frac = frac.Clamp( 0, 1 ); return (from + frac * (to - from)); } /// /// Performs multiple linear interpolations at the same time. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float[] LerpTo( this float[] from, float[] to, float delta, bool clamp = true ) { // TODO: Throw on bogus input? if ( from == null ) return null; if ( to == null ) return from; float[] output = new float[Math.Min( from.Length, to.Length )]; for ( int i = 0; i < output.Length; i++ ) { output[i] = from[i].LerpTo( to[i], delta, clamp ); } return output; } /// /// Linearly interpolates between two angles in degrees, taking the shortest arc. /// public static float LerpDegrees( float from, float to, float frac, bool clamp = true ) { var delta = DeltaDegrees( from, to ); var lerped = from.LerpTo( from + delta, frac, clamp ).UnsignedMod( 360f ); return lerped >= 180f ? lerped - 360f : lerped; } /// public static float LerpDegreesTo( this float from, float to, float frac, bool clamp = true ) { return LerpDegrees( from, to, frac, clamp ); } /// /// Linearly interpolates between two angles in radians, taking the shortest arc. /// public static float LerpRadians( float from, float to, float frac, bool clamp = true ) { var delta = DeltaRadians( from, to ); var lerped = from.LerpTo( from + delta, frac, clamp ).UnsignedMod( MathF.Tau ); return lerped >= MathF.PI ? lerped - MathF.Tau : lerped; } /// public static float LerpRadiansTo( this float from, float to, float frac, bool clamp = true ) { return LerpRadians( from, to, frac, clamp ); } /// /// Performs inverse of a linear interpolation, that is, the return value is the fraction of a linear interpolation. /// /// The value relative to and . /// The "starting value" of the interpolation. If is at this value or less, the function will return 0 or less. /// The "final value" of the interpolation. If is at this value or greater, the function will return 1 or greater. /// Whether the return value is allowed to exceed range of 0 - 1. /// The resulting fraction. [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float LerpInverse( this float value, float from, float to, bool clamp = true ) { if ( clamp ) value = value.Clamp( from, to ); value -= from; to -= from; if ( to == 0 ) return 0; return value / to; } /// /// Adds or subtracts given amount based on whether the input is smaller of bigger than the target. /// public static float Approach( this float f, float target, float delta ) { if ( f > target ) { f -= delta; if ( f < target ) return target; } else { f += delta; if ( f > target ) return target; } return f; } /// /// Returns true if given value is close to given value within given tolerance. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static bool AlmostEqual( this float value, float b, float within = 0.0001f ) { return MathF.Abs( value - b ) <= within; } /// /// Does what you expected to happen when you did "a % b" /// public static float UnsignedMod( this float a, float b ) { return a - b * (a / b).Floor(); } /// /// Convert angle to between 0 - 360 /// public static float NormalizeDegrees( this float degree ) { degree = degree % 360; if ( degree < 0 ) degree += 360; return degree; } /// /// Difference between two angles in degrees. Will always be between -180 and +180. /// public static float DeltaDegrees( float from, float to ) { var delta = (to - from).UnsignedMod( 360f ); return delta >= 180f ? delta - 360f : delta; } /// /// Difference between two angles in radians. Will always be between -PI and +PI. /// public static float DeltaRadians( float from, float to ) { var delta = (to - from).UnsignedMod( MathF.Tau ); return delta >= MathF.PI ? delta - MathF.Tau : delta; } /// /// Remap a float value from a one range to another. Clamps value between newLow and newHigh. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float Remap( this float value, float oldLow, float oldHigh, float newLow = 0, float newHigh = 1 ) { return Remap( value, oldLow, oldHigh, newLow, newHigh, true ); } /// /// Remap a float value from a one range to another /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static float Remap( this float value, float oldLow, float oldHigh, float newLow, float newHigh, bool clamp ) { if ( MathF.Abs( oldHigh - oldLow ) < 0.0001f ) return clamp ? newLow : value; var v = newLow + (value - oldLow) * (newHigh - newLow) / (oldHigh - oldLow); if ( clamp ) v = v.Clamp( newLow, newHigh ); return v; } /// /// Remap a double value from a one range to another /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static double Remap( this double value, double oldLow, double oldHigh, double newLow, double newHigh, bool clamp ) { if ( Math.Abs( oldHigh - oldLow ) < 0.0001 ) return clamp ? newLow : value; var v = newLow + (value - oldLow) * (newHigh - newLow) / (oldHigh - oldLow); if ( clamp ) v = v.Clamp( newLow, newHigh ); return v; } /// /// Remap an integer value from a one range to another /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public static int Remap( this int value, int oldLow, int oldHigh, int newLow, int newHigh ) { return (int)Remap( (float)value, (float)oldLow, (float)oldHigh, (float)newLow, (float)newHigh, true ); } /// /// Given a sphere and a field of view, how far from the camera should we be to fully see the sphere? /// /// The radius of the sphere /// The field of view in degrees /// The optimal distance from the center of the sphere public static float SphereCameraDistance( float radius, float fieldOfView ) { if ( radius < 0.001f ) return 0.01f; if ( fieldOfView <= 0.01f ) return 0.01f; return radius / MathF.Abs( MathF.Sin( fieldOfView.DegreeToRadian() * 0.5f ) ); } /// /// Smoothly move towards the target /// public static float SmoothDamp( float current, float target, ref float velocity, float smoothTime, float deltaTime ) { var displacement = current - target; (displacement, velocity) = SpringDamper.FromSmoothingTime( smoothTime ) .Simulate( displacement, velocity, deltaTime ); return displacement + target; } /// /// Smoothly move towards the target using a spring-like motion /// public static float SpringDamp( float current, float target, ref float velocity, float deltaTime, float frequency = 2.0f, float damping = 0.5f ) { var displacement = current - target; (displacement, velocity) = SpringDamper.FromDamping( frequency, damping ) .Simulate( displacement, velocity, deltaTime ); return displacement + target; } /// /// Finds the real solutions to a quadratic equation of the form /// Ax² + Bx + C = 0. /// Useful for determining where a parabolic curve intersects the x-axis. /// /// A list of real roots (solutions). The list may contain zero, one, or two real numbers. internal static List SolveQuadratic( float a, float b, float c ) { if ( MathF.Abs( a ).AlmostEqual( 0.0f ) ) { // First coefficient is zero, so this is at most linear if ( MathF.Abs( b ).AlmostEqual( 0.0f ) ) { // Second coefficient is also zero return new List(); } // Linear Bx + C = 0 and B != 0. return new List { -c / b }; } // normal form: Ax^2 + Bx + C = 0 return QuadraticRoots( b / a, c / a ); } /// /// Finds the real solutions to a cubic equation of the form /// Ax³ + Bx² + Cx + D = 0. /// Useful for finding where a cubic curve crosses the x-axis. /// /// A list of real roots (solutions). The list may contain one, two, or three real numbers. internal static List SolveCubic( float a, float b, float c, float d ) { if ( MathF.Abs( a ).AlmostEqual( 0.0f ) ) { // Leading coefficient is zero, so this is at most quadratic var quadraticRoots = SolveQuadratic( b, c, d ); return quadraticRoots; } // normal form: x^3 + Ax^2 + Bx + C = 0 return CubicRoots( b / a, c / a, d / a ); } /// /// Calculates the real roots of a simplified quadratic equation /// in its normal form: x² + Ax + B = 0. /// This is a helper method used internally by . /// /// A list of real roots. May contain zero, one, or two real numbers. private static List QuadraticRoots( float a, float b ) { float discriminant = 0.25f * a * a - b; if ( discriminant >= 0.0f ) { var sqrtDiscriminant = MathF.Sqrt( discriminant ); var r0 = -0.5f * a - sqrtDiscriminant; var r1 = -0.5f * a + sqrtDiscriminant; if ( r0.AlmostEqual( r1 ) ) { return new List { r0 }; } return new List { r0, r1 }; } return new List(); } /// /// Calculates the real roots of a simplified cubic equation /// in its normal form: x³ + Ax² + Bx + C = 0. /// This is a helper method used internally by . /// /// A list of real roots. May contain one, two, or three real numbers. private static List CubicRoots( float a, float b, float c ) { /* substitute x = y - A/3 to eliminate quadric term: x^3 +px + q = 0 */ float squareA = a * a; float p = (1.0f / 3.0f) * (-1.0f / 3.0f * squareA + b); float q = (1.0f / 2.0f) * (2.0f / 27.0f * a * squareA - (1.0f / 3.0f) * a * b + c); float cubicP = p * p * p; float squareQ = q * q; float discriminant = squareQ + cubicP; float sub = 1.0f / 3 * a; if ( MathF.Abs( discriminant ).AlmostEqual( 0.0f ) ) { if ( MathF.Abs( q ).AlmostEqual( 0.0f ) ) { // One real root. return new List { 0.0f - sub }; } else { // One single and one double root. float U = MathF.Cbrt( -q ); return new List { 2.0f * U - sub, -U - sub }; } } else if ( discriminant < 0 ) { // Casus irreducibilis: three real solutions float phi = 1.0f / 3 * MathF.Acos( -q / MathF.Sqrt( -cubicP ) ); float t = 2.0f * MathF.Sqrt( -p ); return new List { t * MathF.Cos( phi ) - sub, -t * MathF.Cos( phi + MathF.PI / 3 ) - sub, -t * MathF.Cos( phi - MathF.PI / 3 ) - sub }; } else { // One real solution float sqrtDicriminant = MathF.Sqrt( discriminant ); float s = MathF.Cbrt( sqrtDicriminant - q ); float t = -MathF.Cbrt( sqrtDicriminant + q ); return new List { s + t - sub }; } } }