Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Mathfs/Runtime/Curves/Catenary2D.cs /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
286 lines (244 sloc)
10.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) | |
| using System; | |
| using UnityEngine; | |
| namespace Freya { | |
| /// <summary>A catenary curve passing through two points with a given an arc length</summary> | |
| public struct Catenary2D { | |
| #region Standard catenary equations | |
| /// <summary>Returns the y coordinate of a catenary at the given x value</summary> | |
| /// <param name="x">The x coordinate to evaluate at</param> | |
| /// <param name="a">The a-parameter of the catenary</param> | |
| public static float Eval( float x, float a ) => a * Mathfs.Cosh( x / a ); | |
| /// <summary>Evaluates the arc length from the apex of the catenary, to the given x coordinate. | |
| /// Note that this is negative when x is less than 0</summary> | |
| /// <param name="x">The x coordinate to get the length to</param> | |
| /// <param name="a">The a-parameter of the catenary</param> | |
| public static float EvalArcLen( float x, float a ) => a * Mathfs.Sinh( x / a ); | |
| /// <summary>Evaluates the x coordinate at the given arc length relative to the apex of the catenary. | |
| /// Note that the input arc length can be negative, to get the negative x coordinates</summary> | |
| /// <param name="s">The arc length to get the x coordinate of</param> | |
| /// <param name="a">The a-parameter of the catenary</param> | |
| public static float EvalXByArcLength( float s, float a ) => a * Mathfs.Asinh( s / a ); | |
| /// <summary>Evaluates the n:th 2D derivative at the given arc length relative to the apex of the catenary. | |
| /// Note that the input arc length can be negative, to get the tangents on the negative x side</summary> | |
| /// <param name="s">The arc length coordinate to get the tangent of</param> | |
| /// <param name="a">The a-parameter of the catenary</param> | |
| public static Vector2 EvalDerivByArcLength( float s, float a, int n = 1 ) { | |
| if( n == 0 ) { // position | |
| float x = EvalXByArcLength( s, a ); | |
| float y = Eval( x, a ); | |
| return new Vector2( x, y ); | |
| } | |
| float xNum = default; | |
| float yNum = default; | |
| float aSq = a * a; | |
| float sSq = s * s; | |
| if( n == 1 ) { // velocity | |
| xNum = a; | |
| yNum = s; | |
| } else if( n == 2 ) { // acceleration | |
| xNum = -a * s; | |
| yNum = aSq; | |
| } else if( n == 3 ) { // jerk/jolt | |
| xNum = a * ( -aSq + 2 * sSq ); | |
| yNum = 3 * aSq * s; | |
| } else if( n == 4 ) { // 4th derivative | |
| xNum = 3 * s * a * ( -3 * aSq + 2 * sSq ); | |
| yNum = 3 * aSq * ( -aSq + 4 * sSq ); | |
| } else { | |
| throw new NotImplementedException( $"Derivative ({n}) of Catenaries are not implemented" ); | |
| } | |
| float den = MathF.Pow( aSq + sSq, ( n * 2 - 1 ) / 2f ); | |
| return new Vector2( xNum / den, yNum / den ); | |
| } | |
| #endregion | |
| enum Evaluability { | |
| Unknown = 0, | |
| Catenary, | |
| LinearVertical, | |
| LineSegment | |
| } | |
| const int INTERVAL_SEARCH_ITERATIONS = 12; | |
| const int BISECT_REFINE_COUNT = 14; | |
| // data | |
| Vector2 p0, p1; | |
| float s; | |
| // cached state | |
| float a; | |
| Vector2 p; | |
| Vector2 delta; | |
| float arcLenSampleOffset; | |
| Evaluability evaluability; | |
| public float Length { | |
| get => s; | |
| set => ( s, evaluability ) = ( value, Evaluability.Unknown ); | |
| } | |
| public Vector2 P0 { | |
| get => p0; | |
| set => ( p0, evaluability ) = ( value, Evaluability.Unknown ); | |
| } | |
| public Vector2 P1 { | |
| get => p1; | |
| set => ( p1, evaluability ) = ( value, Evaluability.Unknown ); | |
| } | |
| public bool IsVertical => MathF.Abs( p.x ) < 0.001f; | |
| public bool IsStraightLine => s <= Vector2.Distance( p0, p1 ) * 1.00005f; | |
| /// <summary>Creates a catenary curve between two points, given an arc length <c>s</c></summary> | |
| /// <param name="p0">The start of the curve</param> | |
| /// <param name="p1">The end of the curve</param> | |
| /// <param name="s">The length of the curve. note: has to be equal or longer than the distance between the points</param> | |
| public Catenary2D( Vector2 p0, Vector2 p1, float s ) { | |
| ( this.p0, this.p1, this.s ) = ( p0, p1, s ); | |
| a = 0; | |
| p = default; | |
| delta = default; | |
| arcLenSampleOffset = default; | |
| evaluability = Evaluability.Unknown; | |
| } | |
| /// <summary>Evaluates a position on this catenary curve, given a <c>t</c>-value from 0 to 1</summary> | |
| /// <param name="t">A value from 0 to 1 along the whole curve</param> | |
| public Vector2 Eval( float t ) => EvalByArcLength( t * s ); | |
| /// <summary>Evaluates a position on this catenary curve at the given arc length of <c>sEval</c></summary> | |
| /// <param name="sEval">The arc length along the curve to sample, relative to the first point</param> | |
| public Vector2 EvalByArcLength( float sEval ) { | |
| ReadyForEvaluation(); | |
| return evaluability switch { | |
| Evaluability.Catenary => EvalCatPosByArcLength( sEval ), | |
| Evaluability.LineSegment => EvalStraightLineByArcLength( sEval ), | |
| Evaluability.LinearVertical => EvalVerticalLinearApproxByArcLength( sEval ), | |
| Evaluability.Unknown or _ => throw new Exception( "Failed to evaluate catenary, couldn't calculate evaluability" ) | |
| }; | |
| } | |
| /// <summary>Evaluates the tangent on this catenary curve at the given arc length of <c>sEval</c></summary> | |
| /// <param name="sEval">The arc length along the curve to sample, relative to the first point</param> | |
| /// <param name="n">The derivative to sample. 1 = first derivative, 2 = second derivative</param> | |
| public Vector2 EvalDerivativeByArcLength( float sEval, int n = 1 ) { | |
| if( n == 0 ) // position | |
| return EvalByArcLength( sEval ); | |
| ReadyForEvaluation(); | |
| return evaluability switch { | |
| Evaluability.Catenary => EvalCatDerivByArcLength( sEval ), | |
| Evaluability.LineSegment => n == 1 ? ( p1 - p0 ).normalized : Vector2.zero, | |
| Evaluability.LinearVertical => new Vector2( 0, n == 1 ? ( sEval < -( p.y - s ) / 2 ? -1 : 1 ) : 0 ), | |
| Evaluability.Unknown or _ => throw new Exception( "Failed to evaluate catenary, couldn't calculate evaluability" ) | |
| }; | |
| } | |
| // straight line from p0 to p1 | |
| Vector2 EvalStraightLineByArcLength( float sEval ) => Vector3.LerpUnclamped( p0, p1, sEval / s ); | |
| // almost completely vertical line when p0.x is approx. equal to p1.x | |
| Vector2 EvalVerticalLinearApproxByArcLength( float sEval ) { | |
| float x = Mathfs.Lerp( 0, p.x, sEval / s ); // just to make it not snap to x=0 | |
| float b = ( p.y - s ) / 2; // bottom | |
| float seg0 = -b; | |
| float y = ( sEval < seg0 ) ? -sEval : -2 * seg0 + sEval; | |
| return new Vector2( x, y ) + p0; | |
| } | |
| // evaluates the position of the catenary at the given arc length, relative to the first point | |
| Vector2 EvalCatPosByArcLength( float sEval ) { | |
| sEval *= p.x.Sign(); // since we go backwards when p0.x < p1.x | |
| float x = Catenary2D.EvalXByArcLength( sEval + arcLenSampleOffset, a ) + delta.x; | |
| float y = EvalPassingThrough0( x ); | |
| return new Vector2( x, y ) + p0; | |
| } | |
| /// <summary>/// Evaluates the n-th derivative of the catenary at the given arc length</summary> | |
| /// <param name="sEval">The arc length, relative to the first point</param> | |
| /// <param name="n">The derivative to evaluate</param> | |
| public Vector2 EvalCatDerivByArcLength( float sEval, int n = 1 ) { | |
| if( n == 0 ) | |
| return EvalCatPosByArcLength( sEval ); | |
| sEval *= p.x.Sign(); // since we go backwards when p0.x < p1.x | |
| return Catenary2D.EvalDerivByArcLength( sEval + arcLenSampleOffset, a, n ); | |
| } | |
| // Evaluate passing through the origin and p | |
| float EvalPassingThrough0( float x ) => Catenary2D.Eval( x - delta.x, a ) + delta.y; | |
| // calculates p, a, delta, arcLenSampleOffset, and which evaluation method to use | |
| void ReadyForEvaluation() { | |
| if( evaluability != Evaluability.Unknown ) | |
| return; | |
| // cache p, ie: p1 relative to p0 | |
| p = p1 - p0; | |
| // CASE 1: | |
| // first, test if it's a line segment | |
| if( IsStraightLine ) { | |
| evaluability = Evaluability.LineSegment; | |
| return; | |
| } | |
| // CASE 2: | |
| // check if it's basically a fully vertical hanging chain | |
| if( IsVertical ) { | |
| evaluability = Evaluability.LinearVertical; | |
| return; | |
| } | |
| // CASE 3: | |
| // Now we've got a catenary on our hands unless something explodes. | |
| float c = MathF.Sqrt( s * s - p.y * p.y ); | |
| float pAbsX = p.x.Abs(); // solve only in x > 0 | |
| float R( float a ) => 2 * a * Mathfs.Sinh( pAbsX / ( 2 * a ) ) - c; // set up root solve function | |
| // find bounds of the root | |
| float xRoot = ( p.x * p.x ) / ( 2 * s ); // intial guess based on freya's flawless heuristics | |
| if( TryFindRootBounds( R, xRoot, out FloatRange xRange ) ) { | |
| // refine range, if necessary (which is very likely) | |
| if( Mathfs.Approximately( xRange.Length, 0 ) == false ) | |
| RootFindBisections( R, ref xRange, BISECT_REFINE_COUNT ); // Catenary seems valid, with roots inside, refine the range | |
| a = xRange.Center; // set a to the middle of the latest range | |
| delta = CalcCatenaryDelta( a, p ); // find delta to pass through both points | |
| arcLenSampleOffset = CalcArcLenSampleOffset( delta.x, a ); | |
| evaluability = Evaluability.Catenary; | |
| } else { | |
| // CASE 4: | |
| // something exploded, couldn't find a range, so let's use a straight line as a fallback | |
| evaluability = Evaluability.LineSegment; | |
| } | |
| } | |
| // Calculates the arc length offset so that it's relative to the start of the chain when evaluating by arc length | |
| static float CalcArcLenSampleOffset( float deltaX, float a ) => Catenary2D.EvalArcLen( -deltaX, a ); | |
| // Calculates the required offset to make a catenary pass through the origin and a point p | |
| static Vector2 CalcCatenaryDelta( float a, Vector2 p ) { | |
| Vector2 d; | |
| d.x = p.x / 2 - a * Mathfs.Asinh( p.y / ( 2 * a * Mathfs.Sinh( p.x / ( 2 * a ) ) ) ); | |
| d.y = -Catenary2D.Eval( d.x, a ); // technically -d.x but because of symmetry d.x works too | |
| return d; | |
| } | |
| // presumes a decreasing function with one root in x > 0 | |
| // g = initial guess | |
| static bool TryFindRootBounds( Func<float, float> R, float g, out FloatRange xRange ) { | |
| float y = R( g ); | |
| xRange = new FloatRange( g, g ); | |
| if( Mathfs.Approximately( y, 0 ) ) // somehow landed *on* our root in our initial guess | |
| return true; | |
| bool findingUpper = y > 0; | |
| for( int n = 1; n <= INTERVAL_SEARCH_ITERATIONS; n++ ) { | |
| if( findingUpper ) { | |
| // It's positive - we found our lower bound | |
| // exponentially search for upper bound | |
| xRange.a = xRange.b; | |
| xRange.b = g * MathF.Pow( 2, n ); | |
| y = R( xRange.b ); | |
| if( y < 0 ) | |
| return true; // upper bound found! | |
| } else { | |
| // It's negative - we found our upper bound | |
| // exponentially search for lower bound | |
| xRange.b = xRange.a; | |
| xRange.a = g * MathF.Pow( 2, -n ); | |
| y = R( xRange.a ); | |
| if( y > 0 ) | |
| return true; // lower bound found! | |
| } | |
| } | |
| return false; // no root found | |
| } | |
| static void RootFindBisections( Func<float, float> F, ref FloatRange xRange, int iterationCount ) { | |
| for( int i = 0; i < iterationCount; i++ ) | |
| RootFindBisection( F, ref xRange ); | |
| } | |
| static void RootFindBisection( Func<float, float> F, ref FloatRange xRange ) { | |
| float xInter = xRange.Center; // bisection | |
| float yInter = F( xInter ); | |
| if( yInter > 0 ) | |
| xRange.a = xInter; // adjust left bound | |
| else | |
| xRange.b = xInter; // adjust right bound | |
| } | |
| } | |
| } |