diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 91f9a20..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.meta diff --git a/Curves/BezierCubic2D.cs b/Curves/BezierCubic2D.cs deleted file mode 100644 index 0b259ec..0000000 --- a/Curves/BezierCubic2D.cs +++ /dev/null @@ -1,648 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; -using static Freya.Mathfs; - -namespace Freya { - - // Bezier math - // A lot of the following code is unrolled into floats and components for performance reasons. - // It's much faster than keeping the more readable function calls and vector types unfortunately - - /// An optimized 2D cubic bezier curve, with 4 control points - [Serializable] public struct BezierCubic2D : IParamCurve3Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// Creates a cubic bezier curve, from 4 control points - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The third control point of the curve, sometimes called the end tangent point - /// The end point of the curve - public BezierCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - validCoefficients = false; - c3 = c2 = c1 = default; - } - - #region Control Points - - [SerializeField] Vector2 p0, p1, p2, p3; // the points of the curve - - /// The starting point of the curve - public Vector2 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// The second control point of the curve, sometimes called the start tangent point - public Vector2 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// The third control point of the curve, sometimes called the end tangent point - public Vector2 P2 { - [MethodImpl( INLINE )] get => p2; - [MethodImpl( INLINE )] set => _ = ( p2 = value, validCoefficients = false ); - } - - /// The end point of the curve - public Vector2 P3 { - [MethodImpl( INLINE )] get => p3; - [MethodImpl( INLINE )] set => _ = ( p3 = value, validCoefficients = false ); - } - - /// Get or set a control point position by index. Valid indices: 0, 1, 2 or 3 - public Vector2 this[ int i ] { - get { - switch( i ) { - case 0: return P0; - case 1: return P1; - case 2: return P2; - case 3: return P3; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); - } - } - set { - switch( i ) { - case 0: - P0 = value; - break; - case 1: - P1 = value; - break; - case 2: - P2 = value; - break; - case 3: - P3 = value; - break; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); - } - } - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector2 c3, c2, c1; // cached coefficients for fast evaluation. c0 = p0 - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - c3.x = 3 * ( p1.x - p2.x ) + ( p3.x - p0.x ); - c2.x = 3 * ( p0.x - p1.x + p2.x - p1.x ); - c1.x = 3 * ( p1.x - p0.x ); - c3.y = 3 * ( p1.y - p2.y ) + ( p3.y - p0.y ); - c2.y = 3 * ( p0.y - p1.y + p2.y - p1.y ); - c1.y = 3 * ( p1.y - p0.y ); - } - - /// The constant coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C0 { - [MethodImpl( INLINE )] get => p0; - } - - /// The linear coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C1 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c1; - } - } - - /// The quadratic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// The cubic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C3 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c3; - } - } - - /// The polynomial coefficients in the form c3*t³ + c2*t² + c1*t + c0 - [MethodImpl( INLINE )] public (Vector2 c3, Vector2 c2, Vector2 c1, Vector2 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c3, c2, c1, p0 ); - } - - #endregion - - // Object comparison stuff - - #region Object Comparison & ToString - - public static bool operator ==( BezierCubic2D a, BezierCubic2D b ) => a.P0 == b.P0 && a.P1 == b.P1 && a.P2 == b.P2 && a.P3 == b.P3; - public static bool operator !=( BezierCubic2D a, BezierCubic2D b ) => !( a == b ); - public bool Equals( BezierCubic2D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); - public override bool Equals( object obj ) => obj is BezierCubic2D other && Equals( other ); - - public override int GetHashCode() { - unchecked { - int hashCode = P0.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P1.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P2.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P3.GetHashCode(); - return hashCode; - } - } - - public override string ToString() => $"{P0}, {P1}, {P2}, {P3}"; - - #endregion - - #region Type Casting - - /// Returns this bezier curve in 3D, where z = 0 - /// The 2D curve to cast - public static explicit operator BezierCubic3D( BezierCubic2D bezierCubic2D ) { - return new BezierCubic3D( bezierCubic2D.P0, bezierCubic2D.P1, bezierCubic2D.P2, bezierCubic2D.P3 ); - } - - #endregion - - // Base properties - Points, Derivatives & Tangents - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 3; - } - public int Count { - [MethodImpl( INLINE )] get => 4; - } - - [MethodImpl( INLINE )] public Vector2 GetStartPoint() => p0; - [MethodImpl( INLINE )] public Vector2 GetEndPoint() => p3; - - [MethodImpl( INLINE )] public Vector2 GetPoint( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return new Vector2( t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, t3 * c3.y + t2 * c2.y + t * c1.y + p0.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - return new Vector2( 3 * t2 * c3.x + 2 * t * c2.x + c1.x, 3 * t2 * c3.y + 2 * t * c2.y + c1.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return new Vector2( 6 * t * c3.x + 2 * c2.x, 6 * t * c3.y + 2 * c2.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetThirdDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector2( 6 * c3.x, 6 * c3.y ); - } - - #endregion - - #region Point Components - - /// Returns the X coordinate at the given t-value on the curve - /// The t-value along the curve to sample - [MethodImpl( INLINE )] public float GetPointX( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return t3 * c3.x + t2 * c2.x + t * c1.x + p0.x; - } - - /// Returns the Y coordinate at the given t-value on the curve - /// The t-value along the curve to sample - [MethodImpl( INLINE )] public float GetPointY( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return t3 * c3.y + t2 * c2.y + t * c1.y + p0.y; - } - - /// Returns a component of the coordinate at the given t-value on the curve - /// Which component of the coordinate to return. 0 is X, 1 is Y - /// The t-value along the curve to sample - public float GetPointComponent( int component, float t ) { - switch( component ) { - case 0: return GetPointX( t ); - case 1: return GetPointY( t ); - default: throw new ArgumentOutOfRangeException( nameof(component), "component has to be either 0 or 1" ); - } - } - - #endregion - - // Whole-curve properties & functions - - #region Interpolation - - /// Returns linear blend between two bézier curves - /// The first curve - /// The second curve - /// A value from 0 to 1 to blend between a and b - public static BezierCubic2D Lerp( BezierCubic2D a, BezierCubic2D b, float t ) { - return new BezierCubic2D( - Vector2.LerpUnclamped( a.p0, b.p0, t ), - Vector2.LerpUnclamped( a.p1, b.p1, t ), - Vector2.LerpUnclamped( a.p2, b.p2, t ), - Vector2.LerpUnclamped( a.p3, b.p3, t ) - ); - } - - /// Returns blend between two bézier curves, - /// where the endpoints are linearly interpolated, - /// while the tangents are spherically interpolated relative to their corresponding endpoint - /// The first curve - /// The second curve - /// A value from 0 to 1 to blend between a and b - public static BezierCubic2D Slerp( BezierCubic2D a, BezierCubic2D b, float t ) { - Vector2 p0 = Vector2.LerpUnclamped( a.p0, b.p0, t ); - Vector2 p3 = Vector2.LerpUnclamped( a.p3, b.p3, t ); - return new BezierCubic2D( - p0, - p0 + (Vector2)Vector3.SlerpUnclamped( a.p1 - a.p0, b.p1 - b.p0, t ), - p3 + (Vector2)Vector3.SlerpUnclamped( a.p2 - a.p3, b.p2 - b.p3, t ), - p3 - ); - } - - #endregion - - #region Splitting - - /// Splits this curve at the given t-value, into two curves of the exact same shape - /// The t-value along the curve to sample - public (BezierCubic2D pre, BezierCubic2D post) Split( float t ) { - Vector2 a = new Vector2( - P0.x + ( P1.x - P0.x ) * t, - P0.y + ( P1.y - P0.y ) * t ); - float bx = P1.x + ( P2.x - P1.x ) * t; - float by = P1.y + ( P2.y - P1.y ) * t; - Vector2 c = new Vector2( - P2.x + ( P3.x - P2.x ) * t, - P2.y + ( P3.y - P2.y ) * t ); - Vector2 d = new Vector2( - a.x + ( bx - a.x ) * t, - a.y + ( by - a.y ) * t ); - Vector2 e = new Vector2( - bx + ( c.x - bx ) * t, - by + ( c.y - by ) * t ); - Vector2 p = new Vector2( - d.x + ( e.x - d.x ) * t, - d.y + ( e.y - d.y ) * t ); - return ( new BezierCubic2D( P0, a, d, p ), new BezierCubic2D( p, e, c, P3 ) ); - } - - #endregion - - #region Bounds - - /// Returns the tight axis-aligned bounds of the curve - public Rect GetBounds() { - // first and last points are always included - Vector2 min = Vector2.Min( P0, P3 ); - Vector2 max = Vector2.Max( P0, P3 ); - - void Encapsulate( int axis, float value ) { - min[axis] = Min( min[axis], value ); - max[axis] = Max( max[axis], value ); - } - - for( int i = 0; i < 2; i++ ) { - ResultsMax2 extrema = GetLocalExtremaPoints( i ); - for( int j = 0; j < extrema.count; j++ ) - Encapsulate( i, extrema[j] ); - } - - return new Rect( min.x, min.y, max.x - min.x, max.y - min.y ); - } - - #endregion - - #region Project Point - - /// Returns the (approximate) point on the curve closest to the input point - /// The point to project against the curve - /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive. - /// This is how many subdivisions to split the curve into, to find candidates for the closest point. - /// If your curves are complex, you might need to use around 16 subdivisions. - /// If they are usually very simple, then around 8 subdivisions is likely fine - /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive. - /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary - public Vector2 ProjectPoint( Vector2 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations ); - - struct PointProjectSample { - public float t; - public float distDeltaSq; - public Vector2 f; - public Vector2 fp; - } - - static PointProjectSample[] pointProjectGuesses = { default, default, default }; - - /// Returns the (approximate) point on the curve closest to the input point - /// The point to project against the curve - /// The t-value at the projected point on the curve - /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive. - /// This is how many subdivisions to split the curve into, to find candidates for the closest point. - /// If your curves are complex, you might need to use around 16 subdivisions. - /// If they are usually very simple, then around 8 subdivisions is likely fine - /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive. - /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary - public Vector2 ProjectPoint( Vector2 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) { - // define a bezier relative to the test point - BezierCubic2D bez = new BezierCubic2D( P0 - point, P1 - point, P2 - point, P3 - point ); - - PointProjectSample SampleDistSqDelta( float tSmp ) { - PointProjectSample s = new PointProjectSample { t = tSmp }; - s.f = bez.GetPoint( tSmp ); - s.fp = bez.GetDerivative( tSmp ); - s.distDeltaSq = Vector2.Dot( s.f, s.fp ); - return s; - } - - // find initial candidates - int candidatesFound = 0; - PointProjectSample prevSmp = SampleDistSqDelta( 0 ); - - for( int i = 1; i < initialSubdivisions; i++ ) { - float ti = i / ( initialSubdivisions - 1f ); - PointProjectSample smp = SampleDistSqDelta( ti ); - if( SignAsInt( smp.distDeltaSq ) != SignAsInt( prevSmp.distDeltaSq ) ) { - pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 ); - if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree - } - - prevSmp = smp; - } - - // refine each guess w. Newton-Raphson iterations - void Refine( ref PointProjectSample smp ) { - Vector2 fpp = bez.GetSecondDerivative( smp.t ); - float tNew = smp.t - Vector2.Dot( smp.f, smp.fp ) / ( Vector2.Dot( smp.f, fpp ) + Vector2.Dot( smp.fp, smp.fp ) ); - smp = SampleDistSqDelta( tNew ); - } - - for( int p = 0; p < candidatesFound; p++ ) - for( int i = 0; i < refinementIterations; i++ ) - Refine( ref pointProjectGuesses[p] ); - - // Now find closest. First include the endpoints - float sqDist0 = bez.P0.sqrMagnitude; // include endpoints - float sqDist1 = bez.P3.sqrMagnitude; - bool firstClosest = sqDist0 < sqDist1; - float tClosest = firstClosest ? 0 : 1; - Vector2 ptClosest = firstClosest ? P0 : P3; - float distSqClosest = firstClosest ? sqDist0 : sqDist1; - - // then check internal roots - for( int i = 0; i < candidatesFound; i++ ) { - float pSqmag = pointProjectGuesses[i].f.sqrMagnitude; - if( pSqmag < distSqClosest ) { - distSqClosest = pSqmag; - tClosest = pointProjectGuesses[i].t; - ptClosest = pointProjectGuesses[i].f + point; - } - } - - t = tClosest; - return ptClosest; - } - - #endregion - - #region Intersection Tests - - // Internal - used by all other intersections - private ResultsMax3 Intersect( Vector2 origin, Vector2 direction, bool rangeLimited = false, float minRayT = float.NaN, float maxRayT = float.NaN ) { - Vector2 p0rel = this.P0 - origin; - Vector2 p1rel = this.P1 - origin; - Vector2 p2rel = this.P2 - origin; - Vector2 p3rel = this.P3 - origin; - float y0 = Determinant( p0rel, direction ); // transform bezier point components into the line space y components - float y1 = Determinant( p1rel, direction ); - float y2 = Determinant( p2rel, direction ); - float y3 = Determinant( p3rel, direction ); - Polynomial polynomY = SplineUtils.GetCubicPolynomial( y0, y1, y2, y3 ); - ResultsMax3 roots = polynomY.Roots; // t values of the function - - - Polynomial polynomX = default; - if( rangeLimited ) { - // if we're range limited, we need to verify position along the ray/line/lineSegment - // and if we do, we need to be able to go from t -> x coord - float x0 = Vector2.Dot( p0rel, direction ); // transform bezier point components into the line space x components - float x1 = Vector2.Dot( p1rel, direction ); - float x2 = Vector2.Dot( p2rel, direction ); - float x3 = Vector2.Dot( p3rel, direction ); - polynomX = SplineUtils.GetCubicPolynomial( x0, x1, x2, x3 ); - } - - float CurveTtoRayT( float t ) => polynomX.Sample( t ); - - ResultsMax3 returnVals = default; - - for( int i = 0; i < roots.count; i++ ) { - if( roots[i].Between( 0, 1 ) && ( rangeLimited == false || CurveTtoRayT( roots[i] ).Within( minRayT, maxRayT ) ) ) - returnVals = returnVals.Add( roots[i] ); - } - - return returnVals; - } - - // Internal - to unpack from curve t values to points - private ResultsMax3 TtoPoints( ResultsMax3 tVals ) { - ResultsMax3 pts = default; - for( int i = 0; i < tVals.count; i++ ) - pts = pts.Add( GetPoint( tVals[i] ) ); - return pts; - } - - /// Returns the t-values at which the given line intersects with the curve - /// The line to test intersection against - public ResultsMax3 Intersect( Line2D line ) => Intersect( line.origin, line.dir ); - - /// Returns the t-values at which the given ray intersects with the curve - /// The ray to test intersection against - public ResultsMax3 Intersect( Ray2D ray ) => Intersect( ray.origin, ray.dir, rangeLimited: true, 0, float.MaxValue ); - - /// Returns the t-values at which the given line segment intersects with the curve - /// The line segment to test intersection against - public ResultsMax3 Intersect( LineSegment2D lineSegment ) => Intersect( lineSegment.start, lineSegment.end - lineSegment.start, rangeLimited: true, 0, lineSegment.LengthSquared ); - - /// Returns the points at which the given line intersects with the curve - /// The line to test intersection against - public ResultsMax3 IntersectionPoints( Line2D line ) => TtoPoints( Intersect( line.origin, line.dir ) ); - - /// Returns the points at which the given ray intersects with the curve - /// The ray to test intersection against - public ResultsMax3 IntersectionPoints( Ray2D ray ) => TtoPoints( Intersect( ray.origin, ray.dir, rangeLimited: true, 0, float.MaxValue ) ); - - /// Returns the points at which the given line segment intersects with the curve - /// The line segment to test intersection against - public ResultsMax3 IntersectionPoints( LineSegment2D lineSegment ) => TtoPoints( Intersect( lineSegment.start, lineSegment.end - lineSegment.start, rangeLimited: true, 0, lineSegment.LengthSquared ) ); - - /// Raycasts and returns whether or not it hit, along with the closest hit point - /// The ray to use when raycasting - /// The closest point on the curve the ray hit - /// The maximum length of the ray - public bool Raycast( Ray2D ray, out Vector2 hitPoint, float maxDist = float.MaxValue ) => Raycast( ray, out hitPoint, out _, maxDist ); - - /// Raycasts and returns whether or not it hit, along with the closest hit point and the t-value on the curve - /// The ray to use when raycasting - /// The closest point on the curve the ray hit - /// The t-value of the curve at the point the ray hit - /// The maximum length of the ray - public bool Raycast( Ray2D ray, out Vector2 hitPoint, out float t, float maxDist = float.MaxValue ) { - float closestDist = float.MaxValue; - ResultsMax3 tPts = Intersect( ray ); - ResultsMax3 pts = TtoPoints( tPts ); - - // find closest point - bool didHit = false; - hitPoint = default; - t = default; - for( int i = 0; i < pts.count; i++ ) { - Vector2 pt = pts[i]; - float dist = Vector2.Dot( ray.dir, pt - ray.origin ); - if( dist < closestDist && dist <= maxDist ) { - closestDist = dist; - hitPoint = pt; - t = tPts[i]; - didHit = true; - } - } - - return didHit; - } - - #endregion - - // Esoteric math stuff - - #region Polynomial Factors - - /// Returns the factors of the derivative polynomials, per-component, in the form at²+bt+c - public (Vector2 a, Vector2 b, Vector2 c) GetDerivativeFactors() { - ReadyCoefficients(); - return ( new Vector2( 3 * c3.x, 3 * c3.y ), new Vector2( 2 * c2.x, 2 * c2.y ), c1 ); - } - - /// Returns the factors of the second derivative polynomials, per-component, in the form at+b - public (Vector2 a, Vector2 b) GetSecondDerivativeFactors() { - ReadyCoefficients(); - return ( new Vector2( 6 * c3.x, 6 * c3.y ), new Vector2( 2 * c2.x, 2 * c2.y ) ); - } - - #endregion - - #region Local Extrema - - /// Returns the t values of extrema (local minima/maxima) on a given axis in the 0 < t < 1 range - /// Either 0 (X) or 1 (Y) - public ResultsMax2 GetLocalExtrema( int axis ) { - if( axis < 0 || axis > 1 ) - throw new ArgumentOutOfRangeException( nameof(axis), "axis has to be either 0 or 1" ); - Polynomial polynom; - if( axis == 0 ) // a little silly but the vec[] indexers are kinda expensive - polynom = SplineUtils.GetCubicPolynomialDerivative( P0.x, P1.x, P2.x, P3.x ); - else - polynom = SplineUtils.GetCubicPolynomialDerivative( P0.y, P1.y, P2.y, P3.y ); - ResultsMax3 roots = polynom.Roots; - ResultsMax2 outPts = default; - for( int i = 0; i < roots.count; i++ ) { - float t = roots[i]; - if( t.Between( 0, 1 ) ) - outPts = outPts.Add( t ); - } - - return outPts; - } - - /// Returns the extrema points (local minima/maxima points) on a given axis in the 0 < t < 1 range - /// Either 0 (X) or 1 (Y) - public ResultsMax2 GetLocalExtremaPoints( int axis ) { - ResultsMax2 t = GetLocalExtrema( axis ); - ResultsMax2 pts = default; - for( int i = 0; i < t.count; i++ ) - pts = pts.Add( GetPointComponent( axis, t[i] ) ); - return pts; - } - - #endregion - - } - -} - -// code graveyard - kept just in case I want to bring any of it back: -/* -/// Returns the point and the derivative at the given t-value on the curve. This is more performant than calling GetPoint and GetDerivative separately -/// The t-value along the curve to sample -public (Vector2, Vector2) GetPointAndDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float tx2 = t * 2; - float t2x3 = t2 * 3; - float t3 = t2 * t; - return ( - new Vector2( t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, t3 * c3.y + t2 * c2.y + t * c1.y + p0.y ), - new Vector2( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y ) - ); -} - -/// Returns all three derivatives at the given t-value on the curve -/// The t-value along the curve to sample -public (Vector2, Vector2, Vector2) GetAllThreeDerivatives( float t ) { - ReadyCoefficients(); - float t2x3 = 3 * t * t; - float tx2 = 2 * t; - float tx6 = 6 * t; - return ( - new Vector2( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y ), - new Vector2( tx6 * c3.x + 2 * c2.x, tx6 * c3.y + 2 * c2.y ), - new Vector2( 6 * c3.x, 6 * c3.y ) - ); -} - -[MethodImpl( INLINE )] public (Vector2, Vector2) GetFirstTwoDerivatives( float t ) { - ReadyCoefficients(); - float t2x3 = 3 * t * t; - float tx2 = 2 * t; - float tx6 = 6 * t; - return ( - new Vector2( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y ), - new Vector2( tx6 * c3.x + 2 * c2.x, tx6 * c3.y + 2 * c2.y ) - ); -} - -public (Vector2, Vector2, Vector2) GetPointAndFirstTwoDerivatives( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float tx2 = t * 2; - float tx6 = t * 6; - float t2x3 = t2 * 3; - float t3 = t2 * t; - return ( - new Vector2( t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, t3 * c3.y + t2 * c2.y + t * c1.y + p0.y ), - new Vector2( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y ), - new Vector2( tx6 * c3.x + 2 * c2.x, tx6 * c3.y + 2 * c2.y ) - ); -} -*/ \ No newline at end of file diff --git a/Curves/BezierCubic3D.cs b/Curves/BezierCubic3D.cs deleted file mode 100644 index 4235bb8..0000000 --- a/Curves/BezierCubic3D.cs +++ /dev/null @@ -1,561 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; -using static Freya.Mathfs; - -namespace Freya { - - // Bezier math - // A lot of the following code is unrolled into floats and components for performance reasons. - // It's much faster than keeping the more readable function calls and vector types unfortunately - - /// An optimized 3D cubic bezier curve, with 4 control points - [Serializable] public struct BezierCubic3D : IParamCurve3Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// - public BezierCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - validCoefficients = false; - c3 = c2 = c1 = default; - } - - #region Control Points - - [SerializeField] Vector3 p0, p1, p2, p3; // the points of the curve - - /// - public Vector3 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// - public Vector3 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// - public Vector3 P2 { - [MethodImpl( INLINE )] get => p2; - [MethodImpl( INLINE )] set => _ = ( p2 = value, validCoefficients = false ); - } - - /// - public Vector3 P3 { - [MethodImpl( INLINE )] get => p3; - [MethodImpl( INLINE )] set => _ = ( p3 = value, validCoefficients = false ); - } - - /// - public Vector3 this[ int i ] { - get { - switch( i ) { - case 0: return P0; - case 1: return P1; - case 2: return P2; - case 3: return P3; - default: throw new IndexOutOfRangeException(); - } - } - set { - switch( i ) { - case 0: - P0 = value; - break; - case 1: - P1 = value; - break; - case 2: - P2 = value; - break; - case 3: - P3 = value; - break; - default: throw new IndexOutOfRangeException(); - } - } - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector3 c3, c2, c1; // cached coefficients for fast evaluation. c0 = p0 - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - c3.x = 3 * ( p1.x - p2.x ) + ( p3.x - p0.x ); - c2.x = 3 * ( p0.x - p1.x + p2.x - p1.x ); - c1.x = 3 * ( p1.x - p0.x ); - c3.y = 3 * ( p1.y - p2.y ) + ( p3.y - p0.y ); - c2.y = 3 * ( p0.y - p1.y + p2.y - p1.y ); - c1.y = 3 * ( p1.y - p0.y ); - c3.z = 3 * ( p1.z - p2.z ) + ( p3.z - p0.z ); - c2.z = 3 * ( p0.z - p1.z + p2.z - p1.z ); - c1.z = 3 * ( p1.z - p0.z ); - } - - /// - public Vector3 C0 { - [MethodImpl( INLINE )] get => p0; - } - - /// - public Vector3 C1 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c1; - } - } - - /// - public Vector3 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// - public Vector3 C3 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c3; - } - } - - /// - [MethodImpl( INLINE )] public (Vector3 c3, Vector3 c2, Vector3 c1, Vector3 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c3, c2, c1, p0 ); - } - - #endregion - - // Object comparison stuff - - #region Object Comparison & ToString - - public static bool operator ==( BezierCubic3D a, BezierCubic3D b ) => a.P0 == b.P0 && a.P1 == b.P1 && a.P2 == b.P2 && a.P3 == b.P3; - public static bool operator !=( BezierCubic3D a, BezierCubic3D b ) => !( a == b ); - public bool Equals( BezierCubic3D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); - public override bool Equals( object obj ) => obj is BezierCubic3D other && Equals( other ); - - public override int GetHashCode() { - unchecked { - int hashCode = P0.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P1.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P2.GetHashCode(); - hashCode = ( hashCode * 397 ) ^ P3.GetHashCode(); - return hashCode; - } - } - - public override string ToString() => $"{P0}, {P1}, {P2}, {P3}"; - - #endregion - - #region Type Casting - - /// Returns this bezier curve flattened to the Z plane, effectively setting z to 0 - /// The 3D curve to cast and flatten on the Z plane - public static explicit operator BezierCubic2D( BezierCubic3D bezierCubic3D ) { - return new BezierCubic2D( bezierCubic3D.P0, bezierCubic3D.P1, bezierCubic3D.P2, bezierCubic3D.P3 ); - } - - #endregion - - // Base properties - Points, Derivatives & Tangents - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 3; - } - public int Count { - [MethodImpl( INLINE )] get => 4; - } - - [MethodImpl( INLINE )] public Vector3 GetStartPoint() => p0; - [MethodImpl( INLINE )] public Vector3 GetEndPoint() => p3; - - [MethodImpl( INLINE )] public Vector3 GetPoint( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return new Vector3( t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, t3 * c3.y + t2 * c2.y + t * c1.y + p0.y, t3 * c3.z + t2 * c2.z + t * c1.z + p0.z ); - } - - [MethodImpl( INLINE )] public Vector3 GetDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - return new Vector3( 3 * t2 * c3.x + 2 * t * c2.x + c1.x, 3 * t2 * c3.y + 2 * t * c2.y + c1.y, 3 * t2 * c3.z + 2 * t * c2.z + c1.z ); - } - - [MethodImpl( INLINE )] public Vector3 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return new Vector3( 6 * t * c3.x + 2 * c2.x, 6 * t * c3.y + 2 * c2.y, 6 * t * c3.z + 2 * c2.z ); - } - - [MethodImpl( INLINE )] public Vector3 GetThirdDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector3( 6 * c3.x, 6 * c3.y, 6 * c3.z ); - } - - #endregion - - #region Point Components - - /// - [MethodImpl( INLINE )] public float GetPointX( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return t3 * c3.x + t2 * c2.x + t * c1.x + p0.x; - } - - /// - [MethodImpl( INLINE )] public float GetPointY( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return t3 * c3.y + t2 * c2.y + t * c1.y + p0.y; - } - - /// Returns the Z coordinate at the given t-value on the curve - /// The t-value along the curve to sample - [MethodImpl( INLINE )] public float GetPointZ( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return t3 * c3.z + t2 * c2.z + t * c1.z + p0.z; - } - - /// Returns a component of the coordinate at the given t-value on the curve - /// Which component of the coordinate to return. 0 is X, 1 is Y, 2 is Z - /// The t-value along the curve to sample - public float GetPointComponent( int component, float t ) { - switch( component ) { - case 0: return GetPointX( t ); - case 1: return GetPointY( t ); - case 2: return GetPointZ( t ); - default: throw new ArgumentOutOfRangeException( nameof(component), "component has to be either 0, 1 or 2" ); - } - } - - #endregion - - // Whole-curve properties & functions - - #region Interpolation - - /// - public static BezierCubic3D Lerp( BezierCubic3D a, BezierCubic3D b, float t ) { - return new BezierCubic3D( - Vector3.LerpUnclamped( a.p0, b.p0, t ), - Vector3.LerpUnclamped( a.p1, b.p1, t ), - Vector3.LerpUnclamped( a.p2, b.p2, t ), - Vector3.LerpUnclamped( a.p3, b.p3, t ) - ); - } - - /// - public static BezierCubic3D Slerp( BezierCubic3D a, BezierCubic3D b, float t ) { - Vector3 p0 = Vector3.LerpUnclamped( a.p0, b.p0, t ); - Vector3 p3 = Vector3.LerpUnclamped( a.p3, b.p3, t ); - return new BezierCubic3D( - p0, - p0 + Vector3.SlerpUnclamped( a.p1 - a.p0, b.p1 - b.p0, t ), - p3 + Vector3.SlerpUnclamped( a.p2 - a.p3, b.p2 - b.p3, t ), - p3 - ); - } - - #endregion - - #region Splitting - - /// - public (BezierCubic3D pre, BezierCubic3D post) Split( float t ) { - Vector3 a = new Vector3( - P0.x + ( P1.x - P0.x ) * t, - P0.y + ( P1.y - P0.y ) * t, - P0.z + ( P1.z - P0.z ) * t ); - float bx = P1.x + ( P2.x - P1.x ) * t; - float by = P1.y + ( P2.y - P1.y ) * t; - float bz = P1.z + ( P2.z - P1.z ) * t; - Vector3 c = new Vector3( - P2.x + ( P3.x - P2.x ) * t, - P2.y + ( P3.y - P2.y ) * t, - P2.z + ( P3.z - P2.z ) * t ); - Vector3 d = new Vector3( - a.x + ( bx - a.x ) * t, - a.y + ( by - a.y ) * t, - a.z + ( bz - a.z ) * t ); - Vector3 e = new Vector3( - bx + ( c.x - bx ) * t, - by + ( c.y - by ) * t, - bz + ( c.z - bz ) * t ); - Vector3 p = new Vector3( - d.x + ( e.x - d.x ) * t, - d.y + ( e.y - d.y ) * t, - d.z + ( e.z - d.z ) * t ); - return ( new BezierCubic3D( P0, a, d, p ), new BezierCubic3D( p, e, c, P3 ) ); - } - - #endregion - - #region Bounds - - /// - public Bounds GetBounds() { - // first and last points are always included - Vector3 min = Vector3.Min( P0, P3 ); - Vector3 max = Vector3.Max( P0, P3 ); - - void Encapsulate( int axis, float value ) { - min[axis] = Min( min[axis], value ); - max[axis] = Max( max[axis], value ); - } - - for( int i = 0; i < 3; i++ ) { - ResultsMax2 extrema = GetLocalExtremaPoints( i ); - for( int j = 0; j < extrema.count; j++ ) - Encapsulate( i, extrema[j] ); - } - - return new Bounds( ( max + min ) * 0.5f, max - min ); - } - - #endregion - - #region Project Point - - /// - public Vector3 ProjectPoint( Vector3 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations ); - - struct PointProjectSample { - public float t; - public float distDeltaSq; - public Vector3 f; - public Vector3 fp; - } - - static PointProjectSample[] pointProjectGuesses = { default, default, default }; - - /// - public Vector3 ProjectPoint( Vector3 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) { - // define a bezier relative to the test point - BezierCubic3D bez = new BezierCubic3D( P0 - point, P1 - point, P2 - point, P3 - point ); - - PointProjectSample SampleDistSqDelta( float tSmp ) { - PointProjectSample s = new PointProjectSample { t = tSmp }; - ( s.f, s.fp ) = ( bez.GetPoint( tSmp ), bez.GetDerivative( tSmp ) ); - s.distDeltaSq = Vector3.Dot( s.f, s.fp ); - return s; - } - - // find initial candidates - int candidatesFound = 0; - PointProjectSample prevSmp = SampleDistSqDelta( 0 ); - - for( int i = 1; i < initialSubdivisions; i++ ) { - float ti = i / ( initialSubdivisions - 1f ); - PointProjectSample smp = SampleDistSqDelta( ti ); - if( SignAsInt( smp.distDeltaSq ) != SignAsInt( prevSmp.distDeltaSq ) ) { - pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 ); - if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree - } - - prevSmp = smp; - } - - // refine each guess w. Newton-Raphson iterations - void Refine( ref PointProjectSample smp ) { - Vector3 fpp = bez.GetSecondDerivative( smp.t ); - float tNew = smp.t - Vector3.Dot( smp.f, smp.fp ) / ( Vector3.Dot( smp.f, fpp ) + Vector3.Dot( smp.fp, smp.fp ) ); - smp = SampleDistSqDelta( tNew ); - } - - for( int p = 0; p < candidatesFound; p++ ) - for( int i = 0; i < refinementIterations; i++ ) - Refine( ref pointProjectGuesses[p] ); - - // Now find closest. First include the endpoints - float sqDist0 = bez.P0.sqrMagnitude; // include endpoints - float sqDist1 = bez.P3.sqrMagnitude; - bool firstClosest = sqDist0 < sqDist1; - float tClosest = firstClosest ? 0 : 1; - Vector3 ptClosest = firstClosest ? P0 : P3; - float distSqClosest = firstClosest ? sqDist0 : sqDist1; - - // then check internal roots - for( int i = 0; i < candidatesFound; i++ ) { - float pSqmag = pointProjectGuesses[i].f.sqrMagnitude; - if( pSqmag < distSqClosest ) { - distSqClosest = pSqmag; - tClosest = pointProjectGuesses[i].t; - ptClosest = pointProjectGuesses[i].f + point; - } - } - - t = tClosest; - return ptClosest; - } - - #endregion - - // Esoteric math stuff - - #region Polynomial Factors - - /// - public (Vector3 a, Vector3 b, Vector3 c) GetDerivativeFactors() { - ReadyCoefficients(); - return ( new Vector3( 3 * c3.x, 3 * c3.y, 3 * c3.z ), new Vector3( 2 * c2.x, 2 * c2.y, 2 * c2.z ), c1 ); - } - - /// - public (Vector3 a, Vector3 b) GetSecondDerivativeFactors() { - ReadyCoefficients(); - return ( new Vector3( 6 * c3.x, 6 * c3.y, 6 * c3.z ), new Vector3( 2 * c2.x, 2 * c2.y, 2 * c2.z ) ); - } - - #endregion - - #region Local Extrema - - /// Returns the t values of extrema (local minima/maxima) on a given axis in the 0 < t < 1 range - /// Either 0 (X), 1 (Y) or 2 (Z) - public ResultsMax2 GetLocalExtrema( int axis ) { - if( axis < 0 || axis > 2 ) - throw new ArgumentOutOfRangeException( nameof(axis), "axis has to be either 0, 1 or 2" ); - Polynomial polynom; - if( axis == 0 ) // a little silly but the vec[] indexers are kinda expensive - polynom = SplineUtils.GetCubicPolynomialDerivative( P0.x, P1.x, P2.x, P3.x ); - else if( axis == 1 ) - polynom = SplineUtils.GetCubicPolynomialDerivative( P0.y, P1.y, P2.y, P3.y ); - else - polynom = SplineUtils.GetCubicPolynomialDerivative( P0.z, P1.z, P2.z, P3.z ); - ResultsMax3 roots = polynom.Roots; - ResultsMax2 outPts = default; - for( int i = 0; i < roots.count; i++ ) { - float t = roots[i]; - if( t.Between( 0, 1 ) ) - outPts = outPts.Add( t ); - } - - return outPts; - } - - /// Returns the extrema points (local minima/maxima points) on a given axis in the 0 < t < 1 range - /// Either 0 (X), 1 (Y) or 2 (Z) - public ResultsMax2 GetLocalExtremaPoints( int axis ) { - ResultsMax2 t = GetLocalExtrema( axis ); - ResultsMax2 pts = default; - for( int i = 0; i < t.count; i++ ) - pts = pts.Add( GetPointComponent( axis, t[i] ) ); - return pts; - } - - #endregion - - } - -} - -// Code graveyard below - saving just in case -/* -#region Point & Derivative combos - -/// -public (Vector3, Vector3) GetPointAndTangent( float t ) { - ( Vector3 p, Vector3 d ) = GetPointAndDerivative( t ); - return ( p, d.normalized ); -} - -/// -public (Vector3, Vector3) GetPointAndDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float tx2 = t * 2; - float t2x3 = t2 * 3; - float t3 = t2 * t; - return ( - new Vector3( - t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, - t3 * c3.y + t2 * c2.y + t * c1.y + p0.y, - t3 * c3.z + t2 * c2.z + t * c1.z + p0.z - ), - new Vector3( - t2x3 * c3.x + tx2 * c2.x + c1.x, - t2x3 * c3.y + tx2 * c2.y + c1.y, - t2x3 * c3.z + tx2 * c2.z + c1.z - ) - ); -} - -/// -public (Vector3, Vector3) GetFirstTwoDerivatives( float t ) { - ReadyCoefficients(); - float t2x3 = 3 * t * t; - float tx2 = 2 * t; - float tx6 = 6 * t; - return ( - new Vector3( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y, t2x3 * c3.z + tx2 * c2.z + c1.z ), - new Vector3( tx6 * c3.x + 2 * c2.x, tx6 * c3.y + 2 * c2.y, tx6 * c3.z + 2 * c2.z ) - ); -} - -/// -public (Vector3, Vector3, Vector3) GetAllThreeDerivatives( float t ) { - ReadyCoefficients(); - float t2x3 = 3 * t * t; - float tx2 = 2 * t; - float tx6 = 6 * t; - return ( - new Vector3( t2x3 * c3.x + tx2 * c2.x + c1.x, t2x3 * c3.y + tx2 * c2.y + c1.y, t2x3 * c3.z + tx2 * c2.z + c1.z ), - new Vector3( tx6 * c3.x + 2 * c2.x, tx6 * c3.y + 2 * c2.y, tx6 * c3.z + 2 * c2.z ), - new Vector3( 6 * c3.x, 6 * c3.y, 6 * c3.z ) - ); -} - -/// -public (Vector3, Vector3, Vector3) GetPointAndFirstTwoDerivatives( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float tx2 = t * 2; - float tx6 = t * 6; - float t2x3 = t2 * 3; - float t3 = t2 * t; - return ( - new Vector3( - t3 * c3.x + t2 * c2.x + t * c1.x + p0.x, - t3 * c3.y + t2 * c2.y + t * c1.y + p0.y, - t3 * c3.z + t2 * c2.z + t * c1.z + p0.z - ), - new Vector3( - t2x3 * c3.x + tx2 * c2.x + c1.x, - t2x3 * c3.y + tx2 * c2.y + c1.y, - t2x3 * c3.z + tx2 * c2.z + c1.z - ), - new Vector3( - tx6 * c3.x + 2 * c2.x, - tx6 * c3.y + 2 * c2.y, - tx6 * c3.z + 2 * c2.z - ) - ); -} - -#endregion -*/ \ No newline at end of file diff --git a/Curves/BezierQuad2D.cs b/Curves/BezierQuad2D.cs deleted file mode 100644 index baa98c0..0000000 --- a/Curves/BezierQuad2D.cs +++ /dev/null @@ -1,180 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// An optimized 2D quadratic bezier curve, with 3 control points - [Serializable] public struct BezierQuad2D : IParamCurve2Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// Creates a quadratic bezier curve, from 3 control points - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The end point of the curve, sometimes called the end tangent point - public BezierQuad2D( Vector2 p0, Vector2 p1, Vector2 p2 ) { - ( this.p0, this.p1, this.p2 ) = ( p0, p1, p2 ); - validCoefficients = false; - c2 = c1 = default; - } - - #region Control Points - - [SerializeField] Vector2 p0, p1, p2; // the points of the curve - - /// The starting point of the curve - public Vector2 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// The middle control point of the curve - public Vector2 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// The end point of the curve - public Vector2 P2 { - [MethodImpl( INLINE )] get => p2; - [MethodImpl( INLINE )] set => _ = ( p2 = value, validCoefficients = false ); - } - - /// Get or set a control point position by index. Valid indices: 0, 1, 2 or 3 - public Vector2 this[ int i ] { - get { - switch( i ) { - case 0: return P0; - case 1: return P1; - case 2: return P2; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); - } - } - set { - switch( i ) { - case 0: - P0 = value; - break; - case 1: - P1 = value; - break; - case 2: - P2 = value; - break; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); - } - } - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector2 c2, c1; // cached coefficients for fast evaluation. c0 = p0 - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - c2.x = p0.x - 2 * p1.x + p2.x; - c1.x = 2 * ( p1.x - p0.x ); - c2.y = p0.y - 2 * p1.y + p2.y; - c1.y = 2 * ( p1.y - p0.y ); - } - - /// The constant coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector2 C0 { - [MethodImpl( INLINE )] get => p0; - } - - /// The linear coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector2 C1 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c1; - } - } - - /// The quadratic coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector2 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// The polynomial coefficients in the form c2*t² + c1*t + c0 - [MethodImpl( INLINE )] public (Vector2 c2, Vector2 c1, Vector2 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c2, c1, p0 ); - } - - #endregion - - // todo: Object Comparison & ToString - - // todo: Type Casting - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 2; - } - public int Count { - [MethodImpl( INLINE )] get => 3; - } - - [MethodImpl( INLINE )] public Vector2 GetStartPoint() => p0; - [MethodImpl( INLINE )] public Vector2 GetEndPoint() => p2; - - public Vector2 GetPoint( float t ) { - ReadyCoefficients(); - float tt = t * t; - return new Vector2( c2.x * tt + c1.x * t + p0.x, c2.y * tt + c1.y * t + p0.y ); - } - - public Vector2 GetDerivative( float t ) { - ReadyCoefficients(); - float tx2 = 2 * t; - return new Vector2( tx2 * c2.x + c1.x, tx2 * c2.y + c1.y ); - } - - public Vector2 GetSecondDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector2( 2 * c2.x, 2 * c2.y ); - } - - #endregion - - // todo: Point Components - - /// - public BezierQuad2D Split( float t ) { - Vector2 mid = Vector2.LerpUnclamped( p0, p1, t ); - Vector2 b = Vector2.LerpUnclamped( p1, p2, t ); - Vector2 end = Vector2.LerpUnclamped( mid, b, t ); - return new BezierQuad2D( p0, mid, end ); - } - - // todo: Bounds - - // todo: exact length - - // todo: Project Point - - // todo: Intersection Tests - - // todo: Polynomial Factors - - // todo: Local Extrema - - - } - -} \ No newline at end of file diff --git a/Curves/BezierQuad3D.cs b/Curves/BezierQuad3D.cs deleted file mode 100644 index 6ad7f48..0000000 --- a/Curves/BezierQuad3D.cs +++ /dev/null @@ -1,182 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// An optimized 3D quadratic bezier curve, with 3 control points - [Serializable] public struct BezierQuad3D : IParamCurve2Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// Creates a quadratic bezier curve, from 3 control points - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The end point of the curve, sometimes called the end tangent point - public BezierQuad3D( Vector3 p0, Vector3 p1, Vector3 p2 ) { - ( this.p0, this.p1, this.p2 ) = ( p0, p1, p2 ); - validCoefficients = false; - c2 = c1 = default; - } - - #region Control Points - - [SerializeField] Vector3 p0, p1, p2; // the points of the curve - - /// The starting point of the curve - public Vector3 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// The middle control point of the curve - public Vector3 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// The end point of the curve - public Vector3 P2 { - [MethodImpl( INLINE )] get => p2; - [MethodImpl( INLINE )] set => _ = ( p2 = value, validCoefficients = false ); - } - - /// Get or set a control point position by index. Valid indices: 0, 1, 2 or 3 - public Vector3 this[ int i ] { - get { - switch( i ) { - case 0: return P0; - case 1: return P1; - case 2: return P2; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); - } - } - set { - switch( i ) { - case 0: - P0 = value; - break; - case 1: - P1 = value; - break; - case 2: - P2 = value; - break; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); - } - } - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector3 c2, c1; // cached coefficients for fast evaluation. c0 = p0 - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - c2.x = p0.x - 2 * p1.x + p2.x; - c2.y = p0.y - 2 * p1.y + p2.y; - c2.z = p0.z - 2 * p1.z + p2.z; - c1.x = 2 * ( p1.x - p0.x ); - c1.y = 2 * ( p1.y - p0.y ); - c1.z = 2 * ( p1.z - p0.z ); - } - - /// The constant coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector3 C0 { - [MethodImpl( INLINE )] get => p0; - } - - /// The linear coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector3 C1 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c1; - } - } - - /// The quadratic coefficient when evaluating this curve in the form C2*t² + C1*t + C0 - public Vector3 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// The polynomial coefficients in the form c2*t² + c1*t + c0 - [MethodImpl( INLINE )] public (Vector3 c2, Vector3 c1, Vector3 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c2, c1, p0 ); - } - - #endregion - - // todo: Object Comparison & ToString - - // todo: Type Casting - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 2; - } - public int Count { - [MethodImpl( INLINE )] get => 3; - } - - [MethodImpl( INLINE )] public Vector3 GetStartPoint() => p0; - [MethodImpl( INLINE )] public Vector3 GetEndPoint() => p2; - - public Vector3 GetPoint( float t ) { - ReadyCoefficients(); - float tt = t * t; - return new Vector3( c2.x * tt + c1.x * t + p0.x, c2.y * tt + c1.y * t + p0.y, c2.z * tt + c1.z * t + p0.z ); - } - - public Vector3 GetDerivative( float t ) { - ReadyCoefficients(); - float tx2 = 2 * t; - return new Vector3( tx2 * c2.x + c1.x, tx2 * c2.y + c1.y, tx2 * c2.z + c1.z ); - } - - public Vector3 GetSecondDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector3( 2 * c2.x, 2 * c2.y, 2 * c2.z ); - } - - #endregion - - // todo: Point Components - - /// - public BezierQuad3D Split( float t ) { - Vector3 mid = Vector3.LerpUnclamped( p0, p1, t ); - Vector3 b = Vector3.LerpUnclamped( p1, p2, t ); - Vector3 end = Vector3.LerpUnclamped( mid, b, t ); - return new BezierQuad3D( p0, mid, end ); - } - - // todo: Bounds - - // todo: exact length - - // todo: Project Point - - // todo: Intersection Tests - - // todo: Polynomial Factors - - // todo: Local Extrema - - - } - -} \ No newline at end of file diff --git a/Curves/BezierSampler.cs b/Curves/BezierSampler.cs deleted file mode 100644 index f415791..0000000 --- a/Curves/BezierSampler.cs +++ /dev/null @@ -1,112 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) - -using UnityEngine; - -namespace Freya { - - /// A helper class to let you sample a bezier curve by uniform T-values or by distance - public class BezierSampler { - - /// The number of distance samples used when calculating the cumulative distance table - public readonly int resolution; - - /// Cumulative distance samples, where the first element is 0 and the last element is the total length of the curve - public readonly float[] cumulativeDistances; - - /// Returns the approximate length of this curve. This property doesn't have to recalculate length, since it's already stored in the cumulative distances array - public float CurveLength => cumulativeDistances[resolution - 1]; - - // The way the point recalculation works right now is pretty naive, and doesn't handle extreme acceleration very well. - // Right now you need around 30 samples to properly sample, which, is a lot - - #region Constructors & Point recalc - - /// Creates a sampler that can be used to sample a bezier curve by distance or by uniform t-values. - /// You'll need to call sampler.Recalculate(bezier) to recalculate if the curve changes shape after this points. - /// Recommended resolution for animation: [8-16] - /// Recommended resolution for even point spacing: [16-50] - /// The curve to use when sampling - /// The accuracy of this sampler. - /// Higher values are more accurate, but are more costly to calculate for every new bezier shape - public BezierSampler( BezierCubic2D bezier, int resolution = 12 ) { - this.resolution = resolution; - cumulativeDistances = new float[resolution]; - Recalculate( bezier ); - } - - /// Creates a sampler that can be used to sample a bezier curve by distance or by uniform t-values. - /// You'll need to call sampler.Recalculate(bezier) to recalculate if the curve changes shape after this points. - /// Recommended resolution for animation: [8-16] - /// Recommended resolution for even point spacing: [16-50] - /// The curve to use when sampling - /// The accuracy of this sampler. - /// Higher values are more accurate, but are more costly to calculate for every new bezier shape - public BezierSampler( BezierCubic3D bezier, int resolution = 12 ) { - this.resolution = resolution; - cumulativeDistances = new float[resolution]; - Recalculate( bezier ); - } - - /// Recalculates the internal lookup table so that the bezier can be sampled by distance or by uniform t-values. - /// Only call this before sampling a different curve, or if the curve has changed shape since last time it was calculated - /// The curve to use when sampling - public void Recalculate( BezierCubic2D bezier ) { - float cumulativeLength = 0; - Vector2 prevPt = bezier.P0; - cumulativeDistances[0] = 0; - for( int i = 1; i < resolution; i++ ) { // todo: could optimize by moving all points so that p0 = (0,0) - Vector2 pt = bezier.GetPoint( i / ( resolution - 1f ) ); - cumulativeLength += Vector2.Distance( prevPt, pt ); - cumulativeDistances[i] = cumulativeLength; - prevPt = pt; - } - } - - /// Recalculates the internal lookup table so that the bezier can be sampled by distance or by uniform t-values. - /// Only call this before sampling a different curve, or if the curve has changed shape since last time it was calculated - /// The curve to use when sampling - public void Recalculate( BezierCubic3D bezier ) { - float cumulativeLength = 0; - Vector3 prevPt = bezier.P0; - cumulativeDistances[0] = 0; - for( int i = 1; i < resolution; i++ ) { // todo: could optimize by moving all points so that p0 = (0,0) - Vector3 pt = bezier.GetPoint( i / ( resolution - 1f ) ); - cumulativeLength += Vector3.Distance( prevPt, pt ); - cumulativeDistances[i] = cumulativeLength; - prevPt = pt; - } - } - - #endregion - - /// Converts a uniform t-value to a t-value. Useful to uniformly sample a bezier curve - /// A value from 0 to 1 representing uniform distance along the spline - public float UniformToT( float tUniform ) => DistanceToT( tUniform * CurveLength ); - - /// Converts a distance value to a t-value. Useful to sample a bezier curve by distance - /// The distance along the bezier curve at which you'd like to get the t-value for - public float DistanceToT( float distance ) { - // check if the value is within the length of the curve - if( distance.Between( 0, CurveLength ) ) { - // find which two distance values our input distance lies between - for( int i = 0; i < resolution - 1; i++ ) { - float distPrev = cumulativeDistances[i]; - float distNext = cumulativeDistances[i + 1]; - if( distance.Within( distPrev, distNext ) ) { // check if our input distance lies between the two distances - // get t-values at the samples - float tPrev = i / ( resolution - 1f ); - float tNext = ( i + 1 ) / ( resolution - 1f ); - // remap the distance range to the t-value range - return distance.Remap( distPrev, distNext, tPrev, tNext ); - } - } - } - - // distance is outside the length of the curve - extrapolate values outside - return distance / CurveLength; - } - - - } - -} \ No newline at end of file diff --git a/Curves/CatRom2D.cs b/Curves/CatRom2D.cs deleted file mode 100644 index e76b03b..0000000 --- a/Curves/CatRom2D.cs +++ /dev/null @@ -1,223 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// A 2D cubic catmull-rom curve - [Serializable] public struct CatRom2D { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - // serialized data - [SerializeField] Vector2 p0, p1, p2, p3; - [SerializeField] float k0, k1, k2, k3; // knots - [SerializeField] [Range( 0, 1 )] float alpha; - [SerializeField] [Range( 0, 1 )] float tension; - [SerializeField] bool manualKnots; - - // cached data to accelerate calculations - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector2 c3, c2, c1, c0; // cached coefficients for fast evaluation - - #region Properties - - /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - public Vector2 P0 { - [MethodImpl( INLINE )] get => p0; - set => _ = ( p0 = value, validCoefficients = false ); - } - /// The second control point, and the start of the catrom curve - public Vector2 P1 { - [MethodImpl( INLINE )] get => p1; - set => _ = ( p1 = value, validCoefficients = false ); - } - /// The third control point, and the end of the catrom curve - public Vector2 P2 { - [MethodImpl( INLINE )] get => p2; - set => _ = ( p2 = value, validCoefficients = false ); - } - /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - public Vector2 P3 { - [MethodImpl( INLINE )] get => p3; - set => _ = ( p3 = value, validCoefficients = false ); - } - - /// The alpha parameter, which controls how much the length of each segment should influence the shape of the curve. - /// A value of 0 is called a uniform catrom, and is fast to evaluate but has a tendency to overshoot. - /// A value of 0.5 is a centripetal catrom, which follows points very tightly, and prevents cusps and loops. - /// A value of 1 is a chordal catrom, which follows the points very smoothly with wide arcs - public float Alpha { - [MethodImpl( INLINE )] get => alpha; - set => _ = ( alpha = value, validCoefficients = false ); - } - - /// Controls tension of the curve, where a value of 0 is a standard smooth catrom curve, while a value of 1 flattens the curve to a straight line segment - public float Tension { - [MethodImpl( INLINE )] get => tension; - set => _ = ( tension = value, validCoefficients = false ); - } - - #endregion - - #region Constructors - - /// Creates a cubic catmull-rom curve, from 4 control points with explicit alpha parameter to define its type - /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// The second control point, and the start of the catrom curve - /// The third control point, and the end of the catrom curve - /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// The alpha parameter controls how much the length of each segment should influence the shape of the curve. - /// A value of 0 is called a uniform catrom, and is fast to evaluate but has a tendency to overshoot. - /// A value of 0.5 is a centripetal catrom, which follows points very tightly, and prevents cusps and loops. - /// A value of 1 is a chordal catrom, which follows the points very smoothly with wide arcs - /// Controls tension of the curve, where a value of 0 is a standard smooth catrom curve, while a value of 1 flattens the curve to a straight line segment - public CatRom2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float alpha = 0.5f, float tension = 0 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - this.alpha = alpha; - this.tension = tension; - this.manualKnots = false; - validCoefficients = false; - c0 = c1 = c2 = c3 = default; - k0 = k1 = k2 = k3 = default; - } - - /// Creates a cubic catmull-rom curve, from 4 control points with explicit alpha parameter to define its type - /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// The second control point, and the start of the catrom curve - /// The third control point, and the end of the catrom curve - /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// The type of catrom curve to use. This will internally determine the value of the alpha parameter - /// Controls tension of the curve, where a value of 0 is a standard smooth catrom curve, while a value of 1 flattens the curve to a straight line segment - public CatRom2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, CatRomType type, float tension = 0 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - this.alpha = type.AlphaValue(); - this.tension = tension; - this.manualKnots = false; - validCoefficients = false; - c0 = c1 = c2 = c3 = default; - k0 = k1 = k2 = k3 = default; - } - - /// Creates a catmull-rom curve with 4 control points and manually assigned knot values - /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// The second control point, and the start of the catrom curve - /// The third control point, and the end of the catrom curve - /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it - /// Controls tension of the curve, where a value of 0 is a standard smooth catrom curve, while a value of 1 flattens the curve to a straight line segment - /// The first knot value - /// The second knot value - /// The third knot value - /// The fourth knot value - public CatRom2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float k0, float k1, float k2, float k3, float tension = 0 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - ( this.k0, this.k1, this.k2, this.k3 ) = ( k0, k1, k2, k3 ); - this.alpha = 0; // unused when using manual vectors - this.tension = tension; - this.manualKnots = true; - validCoefficients = false; - c0 = c1 = c2 = c3 = default; - } - - #endregion - - #region Internal Functions - - /// Returns the internal knot vector of this curve - public (float, float, float, float) GetKnots() { - if( manualKnots ) - return ( k0, k1, k2, k3 ); - if( alpha == 0 ) // uniform catrom - return ( 0, 1, 2, 3 ); - const float ak0 = 0; - float ak1 = Vector2.SqrMagnitude( p0 - p1 ).Pow( 0.5f * alpha ) + ak0; - float ak2 = Vector2.SqrMagnitude( p1 - p2 ).Pow( 0.5f * alpha ) + ak1; - float ak3 = Vector2.SqrMagnitude( p2 - p3 ).Pow( 0.5f * alpha ) + ak2; - return ( ak0, ak1, ak2, ak3 ); - } - - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - if( Mathfs.Approximately( tension, 1f ) ) { // linear segment - c3 = default; - c2 = default; - c1 = p2 - p1; - c0 = p1; - } else { - ( float kn0, float kn1, float kn2, float kn3 ) = GetKnots(); - Vector2 m1 = ( 1 - tension ) * ( kn2 - kn1 ) * ( ( p1 - p0 ) / ( kn1 - kn0 ) - ( p2 - p0 ) / ( kn2 - kn0 ) + ( p2 - p1 ) / ( kn2 - kn1 ) ); - Vector2 m2 = ( 1 - tension ) * ( kn2 - kn1 ) * ( ( p2 - p1 ) / ( kn2 - kn1 ) - ( p3 - p1 ) / ( kn3 - kn1 ) + ( p3 - p2 ) / ( kn3 - kn2 ) ); - Vector2 p2p1 = p1 - p2; - c3 = 2 * p2p1 + m1 + m2; - c2 = -3 * p2p1 - 2 * m1 - m2; - c1 = m1; - c0 = p1; - } - } - - #endregion - - #region Points & Derivatives - - /// - [MethodImpl( INLINE )] public Vector2 GetPoint( float t ) { - ReadyCoefficients(); - return c3 * t * t * t + c2 * t * t + c1 * t + c0; - } - - /// - [MethodImpl( INLINE )] public Vector2 GetDerivative( float t ) { - ReadyCoefficients(); - return 3 * c3 * t * t + 2 * c2 * t + c1; - } - - /// - [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return 6 * c3 * t + 2 * c2; - } - - /// - [MethodImpl( INLINE )] public Vector2 GetThirdDerivative() { - ReadyCoefficients(); - return 6 * c3; - } - - /* Alternate method to calculate the point - this is slower but it's mathematically kinda pretty isn't it? - public Vector2 GetPoint( float t, float alpha ) { - ( float k0, float k1, float k2, float k3 ) = GetKnots( alpha ); - float v = Mathfs.Lerp( k1, k2, t ); // remap from 0-1 to k1-k2 - Vector2 A1 = Remap( v, k0, k1, p0, p1 ); - Vector2 A2 = Remap( v, k1, k2, p1, p2 ); - Vector2 A3 = Remap( v, k2, k3, p2, p3 ); - Vector2 B1 = Remap( v, k0, k2, A1, A2 ); - Vector2 B2 = Remap( v, k1, k3, A2, A3 ); - return C = Remap( v, k1, k2, B1, B2 ); - } - Vector2 Remap( float value, float t0, float t1, Vector2 a, Vector2 b ) { - float t = Mathfs.InverseLerp( t0, t1, value ); - return Vector2.LerpUnclamped( a, b, t ); - }*/ - - #endregion - - public (Vector2 m1, Vector2 m2) GetPointTangents() => ( GetDerivative( 0 ), GetDerivative( 1 ) ); - - public BezierCubic2D ToBezier() { - ( Vector2 m1, Vector2 m2 ) = GetPointTangents(); - return new BezierCubic2D( p1, p1 + m1 / 3, p2 - m2 / 3, p2 ); - } - - public Hermite2D ToHermite() { - ( Vector2 m1, Vector2 m2 ) = GetPointTangents(); - return new Hermite2D( p1, m1, p2, m2 ); - } - - } - -} \ No newline at end of file diff --git a/Curves/CatRom3D.cs b/Curves/CatRom3D.cs deleted file mode 100644 index 6f34c70..0000000 --- a/Curves/CatRom3D.cs +++ /dev/null @@ -1,165 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ - -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// A 3D cubic catmull-rom curve - [Serializable] public struct CatRom3D { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - // serialized data - [SerializeField] Vector3 p0, p1, p2, p3; - [SerializeField] [Range( 0, 1 )] float alpha; - [SerializeField] [Range( 0, 1 )] float tension; - - // cached data to accelerate calculations - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector3 c3, c2, c1, c0; // cached coefficients for fast evaluation - - #region Properties - - /// - public Vector3 P0 { - [MethodImpl( INLINE )] get => p0; - set => _ = ( p0 = value, validCoefficients = false ); - } - /// - public Vector3 P1 { - [MethodImpl( INLINE )] get => p1; - set => _ = ( p1 = value, validCoefficients = false ); - } - /// - public Vector3 P2 { - [MethodImpl( INLINE )] get => p2; - set => _ = ( p2 = value, validCoefficients = false ); - } - /// - public Vector3 P3 { - [MethodImpl( INLINE )] get => p3; - set => _ = ( p3 = value, validCoefficients = false ); - } - - /// - public float Alpha { - [MethodImpl( INLINE )] get => alpha; - set => _ = ( alpha = value, validCoefficients = false ); - } - - /// - public float Tension { - [MethodImpl( INLINE )] get => tension; - set => _ = ( tension = value, validCoefficients = false ); - } - - #endregion - - #region Constructors - - /// - public CatRom3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha = 0.5f, float tension = 0 ) { - _ = ( this.p0 = p0, this.p1 = p1, this.p2 = p2, this.p3 = p3 ); - this.alpha = alpha; - this.tension = tension; - validCoefficients = false; - c0 = c1 = c2 = c3 = default; - } - - /// - public CatRom3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, CatRomType type, float tension = 0 ) { - _ = ( this.p0 = p0, this.p1 = p1, this.p2 = p2, this.p3 = p3 ); - this.alpha = type.AlphaValue(); - this.tension = tension; - validCoefficients = false; - c0 = c1 = c2 = c3 = default; - } - - #endregion - - #region Internal Functions - - /// - public (float, float, float, float) GetKnots() { - if( alpha == 0 ) // uniform catrom - return ( 0, 1, 2, 3 ); - const float k0 = 0; - float k1 = Vector3.SqrMagnitude( p0 - p1 ).Pow( 0.5f * alpha ) + k0; - float k2 = Vector3.SqrMagnitude( p1 - p2 ).Pow( 0.5f * alpha ) + k1; - float k3 = Vector3.SqrMagnitude( p2 - p3 ).Pow( 0.5f * alpha ) + k2; - return ( k0, k1, k2, k3 ); - } - - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - if( Mathfs.Approximately( tension, 1f ) ) { // linear segment - c3 = default; - c2 = default; - c1 = p2 - p1; - c0 = p1; - } else { - ( float k0, float k1, float k2, float k3 ) = GetKnots(); - Vector3 m1 = ( 1 - tension ) * ( k2 - k1 ) * ( ( p1 - p0 ) / ( k1 - k0 ) - ( p2 - p0 ) / ( k2 - k0 ) + ( p2 - p1 ) / ( k2 - k1 ) ); - Vector3 m2 = ( 1 - tension ) * ( k2 - k1 ) * ( ( p2 - p1 ) / ( k2 - k1 ) - ( p3 - p1 ) / ( k3 - k1 ) + ( p3 - p2 ) / ( k3 - k2 ) ); - Vector3 p2p1 = p1 - p2; - c3 = 2 * p2p1 + m1 + m2; - c2 = -3 * p2p1 - 2 * m1 - m2; - c1 = m1; - c0 = p1; - } - } - - #endregion - - #region Points & Derivatives - - /// - [MethodImpl( INLINE )] public Vector3 GetPoint( float t ) { - ReadyCoefficients(); - return c3 * t * t * t + c2 * t * t + c1 * t + c0; - } - - /// - [MethodImpl( INLINE )] public Vector3 GetDerivative( float t ) { - ReadyCoefficients(); - return 3 * c3 * t * t + 2 * c2 * t + c1; - } - - /// - [MethodImpl( INLINE )] public Vector3 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return 6 * c3 * t + 2 * c2; - } - - /// - [MethodImpl( INLINE )] public Vector3 GetThirdDerivative() { - ReadyCoefficients(); - return 6 * c3; - } - - /* Alternate method to calculate the point - this is slower but it's mathematically kinda pretty isn't it? - public Vector3 GetPoint( float t, float alpha ) { - ( float k0, float k1, float k2, float k3 ) = GetKnots( alpha ); - float v = Mathfs.Lerp( k1, k2, t ); // remap from 0-1 to k1-k2 - Vector3 a = Remap( v, k0, k1, p0, p1 ); - Vector3 b = Remap( v, k1, k2, p1, p2 ); - Vector3 c = Remap( v, k2, k3, p2, p3 ); - Vector3 d = Remap( v, k0, k2, a, b ); - Vector3 e = Remap( v, k1, k3, b, c ); - return Remap( v, k1, k2, d, e ); - } - Vector3 Remap( float value, float t0, float t1, Vector3 a, Vector3 b ) { - float t = Mathfs.InverseLerp( t0, t1, value ); - return Vector3.LerpUnclamped( a, b, t ); - }*/ - - #endregion - - } - -} \ No newline at end of file diff --git a/Curves/Hermite2D.cs b/Curves/Hermite2D.cs deleted file mode 100644 index f04515c..0000000 --- a/Curves/Hermite2D.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// An optimized 2D cubic Hermite curve segment - [Serializable] public struct Hermite2D : IParamCurve3Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// Creates a cubic Hermite curve, from two control points and two tangents - /// The starting point of the curve - /// The relative tangent vector of the start point - /// The end point of the curve - /// The relative tangent vector of the end point - public Hermite2D( Vector2 p0, Vector2 m0, Vector2 p1, Vector2 m1 ) { - ( this.p0, this.m0, this.p1, this.m1 ) = ( p0, m0, p1, m1 ); - validCoefficients = false; - c3 = c2 = default; - } - - #region Control Points - - [SerializeField] Vector2 p0, m0, p1, m1; // the points & tangents of the curve - - /// The starting point of the curve - public Vector2 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// The second control point of the curve, sometimes called the start tangent point - public Vector2 M0 { - [MethodImpl( INLINE )] get => m0; - [MethodImpl( INLINE )] set => _ = ( m0 = value, validCoefficients = false ); - } - - /// The third control point of the curve, sometimes called the end tangent point - public Vector2 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// The end point of the curve - public Vector2 M1 { - [MethodImpl( INLINE )] get => m1; - [MethodImpl( INLINE )] set => _ = ( m1 = value, validCoefficients = false ); - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector2 c3, c2; // cached coefficients for fast evaluation. c0 = p0. c1 = m0 - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - c3.x = ( 2 * ( p0.x - p1.x ) + m0.x + m1.x ); - c2.x = ( 3 * ( p1.x - p0.x ) - 2 * m0.x - m1.x ); - c3.y = ( 2 * ( p0.y - p1.y ) + m0.y + m1.y ); - c2.y = ( 3 * ( p1.y - p0.y ) - 2 * m0.y - m1.y ); - // c1 = m0 - // c0 = p0 - } - - /// The constant coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C0 { - [MethodImpl( INLINE )] get => p0; - } - - /// The linear coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C1 { - [MethodImpl( INLINE )] get => m0; - } - - /// The quadratic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// The cubic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C3 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c3; - } - } - - /// The polynomial coefficients in the form c3*t³ + c2*t² + c1*t + c0 - [MethodImpl( INLINE )] public (Vector2 c3, Vector2 c2, Vector2 c1, Vector2 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c3, c2, m0, p0 ); - } - - #endregion - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 3; - } - public int Count { - [MethodImpl( INLINE )] get => 4; - } - - [MethodImpl( INLINE )] public Vector2 GetStartPoint() => p0; - [MethodImpl( INLINE )] public Vector2 GetEndPoint() => p1; - - [MethodImpl( INLINE )] public Vector2 GetPoint( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return new Vector2( t3 * c3.x + t2 * c2.x + t * m0.x + p0.x, t3 * c3.y + t2 * c2.y + t * m0.y + p0.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - return new Vector2( 3 * t2 * c3.x + 2 * t * c2.x + m0.x, 3 * t2 * c3.y + 2 * t * c2.y + m0.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return new Vector2( 6 * t * c3.x + 2 * c2.x, 6 * t * c3.y + 2 * c2.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetThirdDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector2( 6 * c3.x, 6 * c3.y ); - } - - #endregion - - public BezierCubic2D ToBezier() => new BezierCubic2D( p0, p0 + m0 / 3, p1 - m1 / 3, p1 ); - - } - -} \ No newline at end of file diff --git a/Curves/Polynomial.cs b/Curves/Polynomial.cs deleted file mode 100644 index 72d8635..0000000 --- a/Curves/Polynomial.cs +++ /dev/null @@ -1,212 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) - -using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// A polynomial in the form ax³+bx²+cx+d, up to a cubic, with functions like derivatives, root finding, and more - [Serializable] public struct Polynomial { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// The cubic factor. [fCubed]x³+bx²+cx+d - public float fCubic; - - /// The quadratic factor. [fQuadratic]x²+cx+d - public float fQuadratic; - - /// The linear factor. [fLinear]x+d - public float fLinear; - - /// The constant factor. ax+[fConstant] - public float fConstant; - - /// The type of polynomial - public PolynomialType Type => GetPolynomialType( fCubic, fQuadratic, fLinear, fConstant ); - - /// Creates a polynomial of the form ax+b - /// The linear factor a in ax+b - /// The constant factor b in ax+b - public Polynomial( float a, float b ) { - fCubic = fQuadratic = 0; - fLinear = a; - fConstant = b; - } - - /// Creates a polynomial of the form ax²+bx+c - /// The quadratic factor a in ax²+bx+c - /// The linear factor b in ax²+bx+c - /// The constant factor c in ax²+bx+c - public Polynomial( float a, float b, float c ) { - fCubic = 0; - fQuadratic = a; - fLinear = b; - fConstant = c; - } - - /// Creates a polynomial of the form ax³+bx²+cx+d - /// The cubic factor a in ax³+bx²+cx+d - /// The quadratic factor b in ax³+bx²+cx+d - /// The linear factor c in ax³+bx²+cx+d - /// The constant factor d in ax³+bx²+cx+d - public Polynomial( float a, float b, float c, float d ) { - fCubic = a; - fQuadratic = b; - fLinear = c; - fConstant = d; - } - - /// Calculates the derivative (rate of change) of this polynomial - public Polynomial Derivative => new Polynomial( 3 * fCubic, 2 * fQuadratic, fLinear ); - - /// Calculates the roots (values where this polynomial = 0) - public ResultsMax3 Roots => GetCubicRoots( fCubic, fQuadratic, fLinear, fConstant ); - - /// Samples the polynomial at a given x value - /// The value to sample at - public float Sample( float x ) => fCubic * ( x * x * x ) + fQuadratic * ( x * x ) + fLinear * x + fConstant; - - #region Statics - - static bool FactorAlmost0( float v ) => v.Abs() < 0.00001f; - - /// Given ax³+bx²+cx+d, returns the net polynomial type/degree, accounting for values very close to 0 - /// The cubic factor a in ax³+bx²+cx+d - /// The quadratic factor b in ax³+bx²+cx+d - /// The linear factor c in ax³+bx²+cx+d - /// The constant factor d in ax³+bx²+cx+d - [MethodImpl( INLINE )] public static PolynomialType GetPolynomialType( float a, float b, float c, float d ) => FactorAlmost0( a ) ? GetPolynomialType( b, c, d ) : PolynomialType.Cubic; - - /// Given ax²+bx+c, returns the net polynomial type/degree, accounting for values very close to 0 - /// The quadratic factor a in ax²+bx+c - /// The linear factor b in ax²+bx+c - /// The constant factor c in ax²+bx+c - [MethodImpl( INLINE )] public static PolynomialType GetPolynomialType( float a, float b, float c ) => FactorAlmost0( a ) ? GetPolynomialType( b, c ) : PolynomialType.Quadratic; - - /// Given ax+b, returns the net polynomial type/degree, accounting for values very close to 0 - /// The linear factor a in ax+b - /// The constant factor b in ax+b - [MethodImpl( INLINE )] public static PolynomialType GetPolynomialType( float a, float b ) => FactorAlmost0( a ) ? PolynomialType.Constant : PolynomialType.Linear; - - /// Returns the roots/solutions of ax³+bx²+cx+d = 0. There's either 0, 1, 2 or 3 roots, filled in left to right among the return values - /// The cubic factor a in ax³+bx²+cx+d - /// The quadratic factor b in ax³+bx²+cx+d - /// The linear factor c in ax³+bx²+cx+d - /// The constant factor d in ax³+bx²+cx+d - public static ResultsMax3 GetCubicRoots( float a, float b, float c, float d ) { - switch( GetPolynomialType( a, b, c, d ) ) { - case PolynomialType.Constant: return default; // either no roots or infinite roots if c == 0 - case PolynomialType.Linear: return new ResultsMax3( SolveLinearRoot( c, d ) ); - case PolynomialType.Quadratic: return SolveQuadraticRoots( b, c, d ); - case PolynomialType.Cubic: return SolveCubicRoots( a, b, c, d ); - default: throw new InvalidEnumArgumentException(); - } - } - - /// Returns the roots/solutions of ax²+bx+c = 0. There's either 0, 1 or 2 roots, filled in left to right among the return values - /// The quadratic factor a in ax²+bx+c - /// The linear factor b in ax²+bx+c - /// The constant factor c in ax²+bx+c - public static ResultsMax2 GetQuadraticRoots( float a, float b, float c ) { - switch( GetPolynomialType( a, b, c ) ) { - case PolynomialType.Constant: return default; // either no roots or infinite roots if c == 0 - case PolynomialType.Linear: return new ResultsMax2( SolveLinearRoot( b, c ) ); - case PolynomialType.Quadratic: return SolveQuadraticRoots( a, b, c ); - default: throw new InvalidEnumArgumentException(); - } - } - - /// Returns the root/solution of ax+b = 0. Returns null if there is no root - /// The linear factor a in ax+b - /// The constant factor b in ax+b - public static float? GetLinearRoots( float a, float b ) { - if( GetPolynomialType( a, b ) == PolynomialType.Constant ) - return null; - return -b / a; - } - - #region Internal root solvers - - // These functions lack safety checks (division by zero etc.) for lower degree equivalency - they presume "a" is always nonzero. - // These are private to avoid people mistaking them for the more stable/safe functions you are more likely to want to use - - [MethodImpl( INLINE )] static float SolveLinearRoot( float a, float b ) => -b / a; - - static ResultsMax2 SolveQuadraticRoots( float a, float b, float c ) { - float rootContent = b * b - 4 * a * c; - if( FactorAlmost0( rootContent ) ) - return new ResultsMax2( -b / ( 2 * a ) ); // two equivalent solutions at one point - - if( rootContent >= 0 ) { - float root = Mathf.Sqrt( rootContent ); - float r0 = ( -b - root ) / ( 2 * a ); // crosses at two points - float r1 = ( -b + root ) / ( 2 * a ); - return new ResultsMax2( Mathf.Min( r0, r1 ), Mathf.Max( r0, r1 ) ); - } - - return default; // no roots - } - - static ResultsMax3 SolveCubicRoots( float a, float b, float c, float d ) { - // first, depress the cubic to make it easier to solve - float p = ( 3 * a * c - b * b ) / ( 3 * a * a ); - float q = ( 2 * b * b * b - 9 * a * b * c + 27 * a * a * d ) / ( 27 * a * a * a ); - - ResultsMax3 dpr = SolveDepressedCubicRoots( p, q ); - - // we now have the roots of the depressed cubic, now convert back to the normal cubic - float UndepressRoot( float r ) => r - b / ( 3 * a ); - switch( dpr.count ) { - case 1: return new ResultsMax3( UndepressRoot( dpr.a ) ); - case 2: return new ResultsMax3( UndepressRoot( dpr.a ), UndepressRoot( dpr.b ) ); - case 3: return new ResultsMax3( UndepressRoot( dpr.a ), UndepressRoot( dpr.b ), UndepressRoot( dpr.c ) ); - default: return default; - } - } - - // t³+pt+q = 0 - static ResultsMax3 SolveDepressedCubicRoots( float p, float q ) { - if( FactorAlmost0( p ) ) // triple root - one solution. solve x³+q = 0 => x = cr(-q) - return new ResultsMax3( Mathfs.Cbrt( -q ) ); - float discriminant = 4 * p * p * p + 27 * q * q; - if( discriminant < 0.00001 ) { // two or three roots guaranteed, use trig solution - float pre = 2 * Mathf.Sqrt( -p / 3 ); - float acosInner = ( ( 3 * q ) / ( 2 * p ) ) * Mathf.Sqrt( -3 / p ); - - float GetRoot( int k ) => pre * Mathf.Cos( ( 1f / 3f ) * Mathfs.Acos( acosInner.ClampNeg1to1() ) - ( Mathfs.TAU / 3f ) * k ); - // if acos hits 0 or TAU/2, the offsets will have the same value, - // which means we have a double root plus one regular root on our hands - if( acosInner >= 0.9999f ) - return new ResultsMax3( GetRoot( 0 ), GetRoot( 2 ) ); // two roots - one single and one double root - if( acosInner <= -0.9999f ) - return new ResultsMax3( GetRoot( 1 ), GetRoot( 2 ) ); // two roots - one single and one double root - return new ResultsMax3( GetRoot( 0 ), GetRoot( 1 ), GetRoot( 2 ) ); // three roots - } - - if( discriminant > 0 && p < 0 ) { // one root - float coshInner = ( 1f / 3f ) * Mathfs.Acosh( ( -3 * q.Abs() / ( 2 * p ) ) * Mathf.Sqrt( -3 / p ) ); - float r = -2 * Mathfs.Sign( q ) * Mathf.Sqrt( -p / 3 ) * Mathfs.Cosh( coshInner ); - return new ResultsMax3( r ); - } - - if( p > 0 ) { // one root - float sinhInner = ( 1f / 3f ) * Mathfs.Asinh( ( ( 3 * q ) / ( 2 * p ) ) * Mathf.Sqrt( 3 / p ) ); - float r = ( -2 * Mathf.Sqrt( p / 3 ) ) * Mathfs.Sinh( sinhInner ); - return new ResultsMax3( r ); - } - - // no roots - return default; - } - - #endregion - - #endregion - - - } - -} \ No newline at end of file diff --git a/Curves/PolynomialType.cs b/Curves/PolynomialType.cs deleted file mode 100644 index c794b3b..0000000 --- a/Curves/PolynomialType.cs +++ /dev/null @@ -1,21 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) - -namespace Freya { - - /// The type/degree of a polynomial - public enum PolynomialType { - - /// A polynomial that is just a, straight up constant value - Constant, - - /// A polynomial of the form ax+b - Linear, - - /// A polynomial of the form ax²+bx+c - Quadratic, - - /// A polynomial of the form ax³+bx²+cx+d - Cubic - } - -} \ No newline at end of file diff --git a/Curves/SplineUtils.cs b/Curves/SplineUtils.cs deleted file mode 100644 index ffd4e15..0000000 --- a/Curves/SplineUtils.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using UnityEngine; - -namespace Freya { - - /// Various utility functions for splines - public static class SplineUtils { - - /// Returns the cubic factors of the polynomials, of a single component, in the form at³+bt²+ct+d - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The third control point of the curve, sometimes called the end tangent point - /// The end point of the curve - public static Polynomial GetCubicPolynomial( float p0, float p1, float p2, float p3 ) => - new Polynomial( - -p0 + 3 * ( p1 - p2 ) + p3, - 3 * ( p0 - 2 * p1 + p2 ), - 3 * ( -p0 + p1 ), - p0 ); - - - /// Returns the cubic factors of the derivative polynomials, of a single component, in the form at²+bt+c - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The third control point of the curve, sometimes called the end tangent point - /// The end point of the curve - public static Polynomial GetCubicPolynomialDerivative( float p0, float p1, float p2, float p3 ) => - new Polynomial( - 3 * ( -p0 + 3 * ( p1 - p2 ) + p3 ), - 6 * ( p0 - 2 * p1 + p2 ), - 3 * ( -p0 + p1 ) ); - - /// Returns the cubic factors of the second derivative polynomials, of a single component, in the form at+b - /// The starting point of the curve - /// The second control point of the curve, sometimes called the start tangent point - /// The third control point of the curve, sometimes called the end tangent point - /// The end point of the curve - public static Polynomial GetCubicPolynomialSecondDerivative( float p0, float p1, float p2, float p3 ) => - new Polynomial( - 6 * ( -p0 + 3 * ( p1 - p2 ) + p3 ), - 6 * ( p0 - 2 * p1 + p2 ) ); - - /// Returns the bernstein polynomial weights for positions in the curve at the given point t - /// The t-value along the curve to sample - public static Vector4 GetBernsteinPolynomialWeights( float t ) { - float omt = 1f - t; - float omt2 = omt * omt; - float t2 = t * t; - return new Vector4( - omt2 * omt, // (1-t)³ - 3f * omt2 * t, // 3(1-t)²t - 3f * omt * t2, // 3(1-t)t² - t2 * t // t³ - ); - } - - /// Returns the bernstein polynomial weights for the derivative of the curve at the given point t - /// The t-value along the curve to sample - public static Vector4 GetBernsteinPolynomialWeightsDerivative( float t ) { - float omt = 1f - t; - float omt2 = omt * omt; - float t2 = t * t; - return new Vector4( - -3 * omt2, // -3(1-t)² - 9 * t2 - 12 * t + 3, // 9t²-12t+3 - 6 * t - 9 * t2, // 6t-9t² - 3 * t2 // 3t² - ); - } - - /// Returns the bernstein polynomial weights for the second derivative of the curve at the given point t - /// The t-value along the curve to sample - public static Vector4 GetBernsteinPolynomialWeightsSecondDerivative( float t ) { - return new Vector4( 6 - 6 * t, 18 * t - 12, 6 - 18 * t, 6 * t ); - } - - /// Samples a bernstein polynomial bézier basis function - /// The degree of the bézier curve - /// The basis function index - /// The value to sample at - public static float SampleBasisFunction( int degree, int i, float t ) { - ulong bc = Mathfs.BinomialCoef( (uint)degree, (uint)i ); - double scale = Math.Pow( 1f - t, degree - i ) * Math.Pow( t, i ); - return (float)(bc * scale); - } - - public static float[] GenerateUniformKnots( int degree, int pCount, bool open ) { - int kCount = degree + pCount + 1; - float[] knots = new float[kCount]; - // open: 0 0[0 1 2 3 4]4 4 - // closed: [0 1 2 3 4 5 6 7 8] - for( int i = 0; i < kCount; i++ ) - knots[i] = open == false ? i : Mathf.Clamp( i - degree, 0, kCount - 2 * degree - 1 ); - return knots; - } - - internal static int BSplineKnotCount( int pointCount, int degree ) => degree + pointCount + 1; - - } - -} \ No newline at end of file diff --git a/Curves/UBSCubic2D.cs b/Curves/UBSCubic2D.cs deleted file mode 100644 index e2887d4..0000000 --- a/Curves/UBSCubic2D.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Freya { - - /// An optimized 2D uniform B-spline segment - [Serializable] public struct UBSCubic2D : IParamCurve3Diff { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - /// Creates a uniform cubic B-spline segment, given 4 control points - /// The first point of the B-spline hull - /// The second point of the B-spline hull - /// The third point of the B-spline hull - /// The fourth point of the B-spline hull - public UBSCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) { - ( this.p0, this.p1, this.p2, this.p3 ) = ( p0, p1, p2, p3 ); - validCoefficients = false; - c3 = c2 = c1 = c0 = default; - } - - #region Control Points - - [SerializeField] Vector2 p0, p1, p2, p3; // the points of the B-spline hull - - /// The first point of the B-spline hull - public Vector2 P0 { - [MethodImpl( INLINE )] get => p0; - [MethodImpl( INLINE )] set => _ = ( p0 = value, validCoefficients = false ); - } - - /// The second point of the B-spline hull - public Vector2 P1 { - [MethodImpl( INLINE )] get => p1; - [MethodImpl( INLINE )] set => _ = ( p1 = value, validCoefficients = false ); - } - - /// The third point of the B-spline hull - public Vector2 P2 { - [MethodImpl( INLINE )] get => p2; - [MethodImpl( INLINE )] set => _ = ( p2 = value, validCoefficients = false ); - } - - /// The fourth point of the B-spline hull - public Vector2 P3 { - [MethodImpl( INLINE )] get => p3; - [MethodImpl( INLINE )] set => _ = ( p3 = value, validCoefficients = false ); - } - - /// Get or set a control point position by index. Valid indices: 0, 1, 2 or 3 - public Vector2 this[ int i ] { - get { - switch( i ) { - case 0: return P0; - case 1: return P1; - case 2: return P2; - case 3: return P3; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); - } - } - set { - switch( i ) { - case 0: - P0 = value; - break; - case 1: - P1 = value; - break; - case 2: - P2 = value; - break; - case 3: - P3 = value; - break; - default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); - } - } - } - - #endregion - - #region Coefficients - - [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) - [NonSerialized] Vector2 c3, c2, c1, c0; // cached coefficients for fast evaluation - - // Coefficient Calculation - [MethodImpl( INLINE )] void ReadyCoefficients() { - if( validCoefficients ) - return; // no need to update - validCoefficients = true; - const float _6th = 1 / 6f; - c3.x = _6th * ( -p0.x + 3 * ( p1.x - p2.x ) + p3.x ); - c2.x = 0.5f * ( p0.x - 2 * p1.x + p2.x ); - c1.x = 0.5f * ( -p0.x + p2.x ); - c0.x = _6th * ( p0.x + 4 * p1.x + p2.x ); - - c3.y = _6th * ( -p0.y + 3 * ( p1.y - p2.y ) + p3.y ); - c2.y = 0.5f * ( p0.y - 2 * p1.y + p2.y ); - c1.y = 0.5f * ( -p0.y + p2.y ); - c0.y = _6th * ( p0.y + 4 * p1.y + p2.y ); - - } - - /// The constant coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C0 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c0; - } - } - - /// The linear coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C1 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c1; - } - } - - /// The quadratic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C2 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c2; - } - } - - /// The cubic coefficient when evaluating this curve in the form C3*t³ + C2*t² + C1*t + C0 - public Vector2 C3 { - [MethodImpl( INLINE )] get { - ReadyCoefficients(); - return c3; - } - } - - /// The polynomial coefficients in the form c3*t³ + c2*t² + c1*t + c0 - [MethodImpl( INLINE )] public (Vector2 c3, Vector2 c2, Vector2 c1, Vector2 c0) GetCoefficients() { - ReadyCoefficients(); - return ( c3, c2, c1, c0 ); - } - - #endregion - - #region Core IParamCurve Implementations - - public int Degree { - [MethodImpl( INLINE )] get => 3; - } - public int Count { - [MethodImpl( INLINE )] get => 4; - } - - [MethodImpl( INLINE )] public Vector2 GetStartPoint() => C0; - - [MethodImpl( INLINE )] public Vector2 GetEndPoint() { - ReadyCoefficients(); - float x = c0.x + c1.x + c2.x + c3.x; - float y = c0.y + c1.y + c2.y + c3.y; - return new Vector2( x, y ); - } - - [MethodImpl( INLINE )] public Vector2 GetPoint( float t ) { - ReadyCoefficients(); - float t2 = t * t; - float t3 = t2 * t; - return new Vector2( t3 * c3.x + t2 * c2.x + t * c1.x + c0.x, t3 * c3.y + t2 * c2.y + t * c1.y + c0.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetDerivative( float t ) { - ReadyCoefficients(); - float t2 = t * t; - return new Vector2( 3 * t2 * c3.x + 2 * t * c2.x + c1.x, 3 * t2 * c3.y + 2 * t * c2.y + c1.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( float t ) { - ReadyCoefficients(); - return new Vector2( 6 * t * c3.x + 2 * c2.x, 6 * t * c3.y + 2 * c2.y ); - } - - [MethodImpl( INLINE )] public Vector2 GetThirdDerivative( float t = 0 ) { - ReadyCoefficients(); - return new Vector2( 6 * c3.x, 6 * c3.y ); - } - - #endregion - - /// Returns the exact cubic bézier representation of this segment - public BezierCubic2D ToBezier() { - float ax = p0.x + ( 2f / 3f ) * ( p1.x - p0.x ); - float bx = p1.x + ( 1f / 3f ) * ( p2.x - p1.x ); - float cx = p1.x + ( 2f / 3f ) * ( p2.x - p1.x ); - float dx = p2.x + ( 1f / 3f ) * ( p3.x - p2.x ); - float ay = p0.y + ( 2f / 3f ) * ( p1.y - p0.y ); - float by = p1.y + ( 1f / 3f ) * ( p2.y - p1.y ); - float cy = p1.y + ( 2f / 3f ) * ( p2.y - p1.y ); - float dy = p2.y + ( 1f / 3f ) * ( p3.y - p2.y ); - return new BezierCubic2D( - new Vector2( 0.5f * ( ax + bx ), 0.5f * ( ay + by ) ), - new Vector2( bx, by ), - new Vector2( cx, cy ), - new Vector2( 0.5f * ( cx + dx ), 0.5f * ( cy + dy ) ) - ); - } - - } - -} \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..3c43245 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 481a01d96fbac42fd97dcbab9d904dd7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Codegen.meta b/Editor/Codegen.meta new file mode 100644 index 0000000..3cf4efc --- /dev/null +++ b/Editor/Codegen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 67ce1d58abb5eb743bc3fc5ca17109ca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Codegen/CodeGenerator.cs b/Editor/Codegen/CodeGenerator.cs new file mode 100644 index 0000000..a8c7b29 --- /dev/null +++ b/Editor/Codegen/CodeGenerator.cs @@ -0,0 +1,77 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; + +namespace Freya { + + public class CodeGenerator { + + int scope = 0; + public List content = new List(); + + public static string GetInheritdocString( string s ) => $"/// "; + + public void AppendLine( string s ) => content.Add( $"{new string( '\t', scope )}{s}" ); + public void Comment( string s ) => AppendLine( $"// {s}" ); + public void Using( string s ) => AppendLine( $"using {s};" ); + public void Summary( string s ) => AppendLine( $"/// {s}" ); + public void Inheritdoc( string s ) => AppendLine( GetInheritdocString( s ) ); + public void Param( string param, string desc ) => AppendLine( $"/// {desc}" ); + public void LineBreak() => content.Add( "" ); + + public void BeginScope( string s, bool includeBrackets = true ) { + AppendLine( includeBrackets ? $"{s} {{" : s ); + scope++; + } + + public void EndScope( bool includeBrackets = true ) { + scope--; + if( includeBrackets ) + AppendLine( "}" ); + } + + public void AppendHeader() { + Comment( "by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)" ); + Comment( $"Do not manually edit - this file is generated by {nameof(MathfsCodegen)}.cs" ); + LineBreak(); + } + + public CodeScope BracketScope( string s ) => new CodeScope( this, s, true ); + public CodeScope Scope( string s ) => new CodeScope( this, s, false ); + public RegionScope ScopeRegion( string s ) => new RegionScope( this, s ); + + public readonly struct CodeScope : IDisposable { + + readonly CodeGenerator gen; + readonly bool includeBrackets; + + public CodeScope( CodeGenerator gen, string s, bool includeBrackets = true ) { + this.gen = gen; + this.includeBrackets = includeBrackets; + gen.BeginScope( s, includeBrackets ); + } + + public void Dispose() { + gen.EndScope( includeBrackets ); + } + } + + public readonly struct RegionScope : IDisposable { + + readonly CodeGenerator gen; + + public RegionScope( CodeGenerator gen, string s ) { + this.gen = gen; + gen.AppendLine( $"#region {s}" ); + gen.LineBreak(); + } + + public void Dispose() { + gen.LineBreak(); + gen.AppendLine( "#endregion" ); + } + } + } + +} \ No newline at end of file diff --git a/Editor/Codegen/CodeGenerator.cs.meta b/Editor/Codegen/CodeGenerator.cs.meta new file mode 100644 index 0000000..b4c709c --- /dev/null +++ b/Editor/Codegen/CodeGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 809f3c22dc3524a428c8cea98678b5da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Codegen/ElemType.cs b/Editor/Codegen/ElemType.cs new file mode 100644 index 0000000..81b4dbe --- /dev/null +++ b/Editor/Codegen/ElemType.cs @@ -0,0 +1,11 @@ +namespace Freya { + + public enum ElemType { + _1D = 1, + _2D, + _3D, + _4D, + Quat + } + +} \ No newline at end of file diff --git a/Editor/Codegen/ElemType.cs.meta b/Editor/Codegen/ElemType.cs.meta new file mode 100644 index 0000000..5ca214d --- /dev/null +++ b/Editor/Codegen/ElemType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0af448fc320643498f0d301cc7ecdf14 +timeCreated: 1775666209 \ No newline at end of file diff --git a/Editor/Codegen/MathfsCodegen.cs b/Editor/Codegen/MathfsCodegen.cs new file mode 100644 index 0000000..4b97dc3 --- /dev/null +++ b/Editor/Codegen/MathfsCodegen.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Unity.Mathematics; +using UnityEditor; + +namespace Freya { + + public static class MathfsCodegen { + + static GUID guidRuntimeAsm = new("6071c9f2ce0a4407c93af459fa416e54"); // Mathfs.asmdef + static string PathRuntime => Path.GetDirectoryName( AssetDatabase.GUIDToAssetPath( guidRuntimeAsm ) ); + public static string PathSpline => $"{PathRuntime}/Splines"; + public static string PathNumerics => $"{PathRuntime}/Numerics"; + public static string PathStaticAccess => $"{PathRuntime}/Generated static functions"; + public static string PathMatrices => PathNumerics; + + static Dictionary iDefCodegens = new(); + + static StaticAccessInterfaceGenerator GetStaticCodegen( Type iType ) { + // Type iDef = iType.IsGenericType ? iType.GetGenericTypeDefinition() : iType; + string iName = iType.Name.Split( "`" )[0]; + if( iDefCodegens.TryGetValue( iName, out StaticAccessInterfaceGenerator code ) == false ) + iDefCodegens.Add( iName, code = new StaticAccessInterfaceGenerator( iType ) ); + return code; + } + + [MenuItem( "Assets/Run Mathfs Codegen" )] + public static void Regenerate() { + SplineCodegen.GenerateUniformSplines( PathSpline ); + MatrixCodegen.GenerateMatrices( PathMatrices ); + + GenerateStaticAccess( PathStaticAccess ); + + // Debug.Log( "Type: " + typeof(rat2).inter ); + } + + /// Interfaces ignored by the codegen + static Type[] interfaceBlacklist = new[] { + typeof(IEquatable<>), + typeof(IComparable<>), + }; + + static bool IsValidInterface( Type type ) { + if( type.IsGenericType ) + type = type.GetGenericTypeDefinition(); + return interfaceBlacklist.Contains( type ) == false; + } + + public static Type[] numericTypes = new[] { + typeof(rat), + typeof(rat2), + typeof(inth), + typeof(inth2), + }; + public static Type[] externalTypes = new[] { + typeof(int), + typeof(int2), + typeof(int3), + typeof(int4), + }; + + + static void GenerateStaticAccess( string pathStaticAccess ) { + iDefCodegens.Clear(); + + // collect all statics + foreach( Type type in numericTypes ) { + GenerateStaticAccessForInterface( type ); + } + + // extension interfaces/statics for external types + foreach( ( Type nType, NumericTypeInfo info ) in StaticAccessExtensions.numericInfo ) { + if( info.IsExternalType && ( info.numType is NumType.Float32 or NumType.Half16 or NumType.Double64 or NumType.Int32 ) ) { + foreach( Type iType in StaticAccessExtensions.InterfacesOfExternalType( nType ) ) { + GetStaticCodegen( iType ).GenerateSpecificsForType( nType, iType ); + } + } + } + + // finalize static codegen + foreach( StaticAccessInterfaceGenerator gen in iDefCodegens.Values ) { + gen.GenerateCode( pathStaticAccess ); + } + } + + static void GenerateStaticAccessForInterface( Type type ) { + foreach( Type iType in type.GetInterfaces().Where( IsValidInterface ) ) + GetStaticCodegen( iType ).GenerateSpecificsForType( type, iType ); + } + + + } + + +} \ No newline at end of file diff --git a/Editor/Codegen/MathfsCodegen.cs.meta b/Editor/Codegen/MathfsCodegen.cs.meta new file mode 100644 index 0000000..c437f9b --- /dev/null +++ b/Editor/Codegen/MathfsCodegen.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c152a4285a384e82b731c40d3751c36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Codegen/MatrixCodegen.cs b/Editor/Codegen/MatrixCodegen.cs new file mode 100644 index 0000000..f9f0ea4 --- /dev/null +++ b/Editor/Codegen/MatrixCodegen.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using System.Linq; + +namespace Freya { + + public static class MatrixCodegen { + public static void GenerateMatrices( string pathMatrices ) { + for( int dim = 1; dim < 5; dim++ ) { // 1D, 2D, 3D, 4D + GenerateMatrixNx1( pathMatrices, 3, GetVectorOfDim( dim ) ); + GenerateMatrixNx1( pathMatrices, 4, GetVectorOfDim( dim ) ); + } + GenerateMatrixNx1( pathMatrices, 4, ElemType.Quat ); + } + + static void GenerateMatrixNx1( string path, int count, ElemType dim ) { + const string vCompStr = "xyzw"; + const string vCompStrUp = "XYZW"; + int elemCompCount = ( (int)dim ).AtMost( 4 ); // quats also have 4 + int[] elemRange = Enumerable.Range( 0, count ).ToArray(); + int[] compRange = Enumerable.Range( 0, elemCompCount ).ToArray(); + string[] compRangeStr = compRange.Select( c => vCompStr[c].ToString() ).ToArray(); + string JoinRange( string separator, Func elem ) => string.Join( separator, elemRange.Select( elem ) ); + string elemType = dim switch { + ElemType._1D => "float", + ElemType.Quat => "Quaternion", + _ => $"Vector{elemCompCount}" + }; + string typePrefix = dim == ElemType._1D ? "" : elemType; + string lerpName = GetLerpName( dim ); + string typeName = $"{typePrefix}Matrix{count}x1"; + string csParams = JoinRange( ", ", i => $"m{i}" ); + string csParamsThis = JoinRange( ", ", i => $"this.m{i}" ); + string ctorParams = JoinRange( ", ", i => $"{elemType} m{i}" ); + string indexerException = $"throw new IndexOutOfRangeException( $\"Matrix row index has to be from 0 to {count - 1}, got: {{row}}\" )"; + string indexerGetterCases = JoinRange( ", ", i => $"{i} => m{i}" ) + $", _ => {indexerException}"; + string equalsCompare = JoinRange( " && ", i => $"m{i}.Equals( other.m{i} )" ); + string equalsOpCompare = JoinRange( " && ", i => $"a.m{i} == b.m{i}" ); + string lerpAtoB = JoinRange( ", ", i => $"{lerpName}( a.m{i}, b.m{i}, t )" ); + bool isMultiComponentVector = dim != ElemType._1D && dim != ElemType.Quat; + + // generate content + CodeGenerator code = new CodeGenerator(); + code.AppendHeader(); + code.AppendLine( "using System;" ); + if( dim != ElemType._1D ) // for Vector2/3 + code.AppendLine( "using UnityEngine;" ); + + using( code.BracketScope( "namespace Freya" ) ) { + code.Summary( $"A {count}x1 column matrix with {elemType} values" ); + using( code.BracketScope( $"[Serializable] public struct {typeName}" ) ) { + // fields + code.AppendLine( $"public {elemType} {csParams};" ); + + // constructors + code.AppendLine( $"public {typeName}({ctorParams}) => ({csParamsThis}) = ({csParams});" ); + if( isMultiComponentVector ) { // compose from float matrices + string s = $"public {typeName}({string.Join( ", ", compRangeStr.Select( c => $"Matrix{count}x1 {c}" ) )}) => "; + s += $"({csParams}) = ({JoinRange( ", ", i => $"new {elemType}({string.Join( ", ", compRangeStr.Select( c => $"{c}.m{i}" ) )})" )});"; + code.AppendLine( s ); + } + + // indexer + using( code.BracketScope( $"public {elemType} this[int row]" ) ) { + code.AppendLine( $"get => row switch{{{indexerGetterCases}}};" ); + using( code.BracketScope( "set" ) ) { + using( code.BracketScope( "switch(row)" ) ) { + code.AppendLine( JoinRange( " ", i => $"case {i}: m{i} = value; break;" ) ); + code.AppendLine( $"default: {indexerException};" ); + } + } + } + + // component extraction for vector-valued matrices + if( isMultiComponentVector ) { + for( int c = 0; c < elemCompCount; c++ ) { + int cc = c; + string parameters = JoinRange( ", ", i => $"m{i}.{vCompStr[cc]}" ); + code.AppendLine( $"public Matrix{count}x1 {vCompStrUp[c]} => new({parameters});" ); + } + } + + // interpolation + code.Summary( "Linearly interpolates between two matrices, based on a value t" ); + code.Param( "t", "The value to blend by" ); + string interpName = dim == ElemType.Quat ? "Slerp" : "Lerp"; + code.AppendLine( $"public static {typeName} {interpName}( {typeName} a, {typeName} b, float t ) => new {typeName}({lerpAtoB});" ); + + // comparison/operators + code.AppendLine( $"public static bool operator ==( {typeName} a, {typeName} b ) => {equalsOpCompare};" ); + code.AppendLine( $"public static bool operator !=( {typeName} a, {typeName} b ) => !( a == b );" ); + code.AppendLine( $"public bool Equals( {typeName} other ) => {equalsCompare};" ); + code.AppendLine( $"public override bool Equals( object obj ) => obj is {typeName} other && Equals( other );" ); + code.AppendLine( $"public override int GetHashCode() => HashCode.Combine( {csParams} );" ); + string stringPrint = JoinRange( "\\n", i => $"[{{m{i}}}]" ); + code.AppendLine( $"public override string ToString() => $\"{stringPrint}\";" ); + } + } + + // save/finalize + File.WriteAllLines( $"{path}/{typeName}.cs", code.content ); + } + + static string GetLerpName( ElemType dim ) { + return dim switch { + ElemType._1D => "Mathfs.Lerp", + ElemType._2D => "Vector2.LerpUnclamped", + ElemType._3D => "Vector3.LerpUnclamped", + ElemType._4D => "Vector4.LerpUnclamped", + ElemType.Quat => "Quaternion.SlerpUnclamped", + _ => throw new IndexOutOfRangeException() + }; + } + + public static ElemType GetVectorOfDim( int dim ) => (ElemType)dim; + + } + +} \ No newline at end of file diff --git a/Editor/Codegen/MatrixCodegen.cs.meta b/Editor/Codegen/MatrixCodegen.cs.meta new file mode 100644 index 0000000..30b9cf0 --- /dev/null +++ b/Editor/Codegen/MatrixCodegen.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5d76c176fdab4141980642aff15417ee +timeCreated: 1775667441 \ No newline at end of file diff --git a/Editor/Codegen/NumericTypeInfo.cs b/Editor/Codegen/NumericTypeInfo.cs new file mode 100644 index 0000000..f045d5c --- /dev/null +++ b/Editor/Codegen/NumericTypeInfo.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + public struct NumericTypeInfo { + public Type nType; + public int dims; + public TypeSource source; + public NumType numType; + + public NumericTypeInfo( Type nType, NumType numType, int dims, TypeSource source ) { + this.nType = nType; + this.numType = numType; + this.dims = dims; + this.source = source; + } + + public bool IsScalar => dims == 1; + public bool IsExternalType => source != TypeSource.Mathfs; + public IEnumerable Components() => Enumerable.Range( 0, dims ); + public IEnumerable Components( Func selector ) => Components().Select( i => selector( i, "xyzw"[i] ) ); + public IEnumerable<(int i, char c)> ComponentsTuples => Components().Select( i => ( i, "xyzw"[i] ) ); + public string JoinComponents( string separator, Func selector ) => string.Join( separator, Components( selector ) ); + public string CompSum( Func selector ) => string.Join( "+", Components( selector ) ); + public string NewFromComps( Func selector ) => $"new({string.Join( ", ", Components( selector ) )})"; + + public string CompAggrInstanceFuncs( string funcName ) { + if( dims == 1 ) + return "{0}"; + return ComponentsTuples.Skip( 1 ).Aggregate( "{0}.x", ( acc, item ) => $"{{0}}.{item.c}.{funcName}({acc})" ); + } + // .Aggregate( "", (agg,elem) => $"{prev}.{funcName}" ); + + + // x + // x.max(y) + // x.max(y.max(z)) + // x.max(y.max(z.max(w))) + + // max( x, y ) + // max( x, max( y, z ) ) + // max( x, max( y, max( z, w ) ) ) + + public bool IsAlwaysIntegerValue => + numType is + NumType.Bool2 or + NumType.Byte8 or NumType.SByte8 or + NumType.Short16 or NumType.UShort16 or + NumType.Int32 or NumType.UInt32 or + NumType.Long64 or NumType.ULong64; + + public bool IsFloatingPoint => numType is NumType.Float32 or NumType.Double64; + public bool IsHalfInteger => numType is NumType.IntHalf; + public bool IsRational => numType is NumType.Rational; + + public Type TypeAfterRounding => + dims switch { + 1 => typeof(int), + 2 => typeof(int2), + 3 => typeof(int3), + 4 => typeof(int4), + _ => throw new IndexOutOfRangeException() + }; + public Type ComponentType => + numType switch { + NumType.Bool2 => typeof(bool), + NumType.Byte8 => typeof(byte), + NumType.SByte8 => typeof(sbyte), + NumType.Short16 => typeof(short), + NumType.UShort16 => typeof(ushort), + NumType.Int32 => typeof(int), + NumType.UInt32 => typeof(uint), + NumType.Long64 => typeof(long), + NumType.ULong64 => typeof(ulong), + NumType.IntHalf => typeof(inth), + NumType.Rational => typeof(rat), + NumType.Half16 => typeof(half), + NumType.Float32 => typeof(float), + NumType.Double64 => typeof(double), + _ => throw new IndexOutOfRangeException() + }; + + public Type ScalarProjectionType { + get { + if( IsAlwaysIntegerValue || IsRational || IsHalfInteger ) + return typeof(rat); + if( IsFloatingPoint ) + return ComponentType; // double/float + throw new NotImplementedException(); + } + } + public Type VectorProjectionType { + get { + if( IsAlwaysIntegerValue || IsRational || IsHalfInteger ) + return GetTypeMatching( dims, typeof(rat) ); // integers and half-integers get type promoted to rational + return nType; + } + } + + public static Type GetTypeMatching( int dims, Type componentType ) { + if( componentType == typeof(bool) ) + switch( dims ) { + case 1: return typeof(bool); + case 2: return typeof(bool2); + case 3: return typeof(bool3); + case 4: return typeof(bool4); + } + if( componentType == typeof(int) ) + switch( dims ) { + case 1: return typeof(int); + case 2: return typeof(int2); + case 3: return typeof(int3); + case 4: return typeof(int4); + } + if( componentType == typeof(float) ) + switch( dims ) { + case 1: return typeof(float); + case 2: return typeof(float2); + case 3: return typeof(float3); + case 4: return typeof(float4); + } + if( componentType == typeof(double) ) + switch( dims ) { + case 1: return typeof(double); + case 2: return typeof(double2); + case 3: return typeof(double3); + case 4: return typeof(double4); + } + if( componentType == typeof(half) ) + switch( dims ) { + case 1: return typeof(half); + case 2: return typeof(half2); + case 3: return typeof(half3); + case 4: return typeof(half4); + } + if( componentType == typeof(byte) ) + switch( dims ) { + case 1: return typeof(byte); + // case 2: return typeof(byte2); + // case 3: return typeof(byte3); + // case 4: return typeof(byte4); + } + if( componentType == typeof(sbyte) ) + switch( dims ) { + case 1: return typeof(sbyte); + // case 2: return typeof(sbyte2); + // case 3: return typeof(sbyte3); + // case 4: return typeof(sbyte4); + } + if( componentType == typeof(short) ) + switch( dims ) { + case 1: return typeof(short); + // case 2: return typeof(short2); + // case 3: return typeof(short3); + // case 4: return typeof(short4); + } + if( componentType == typeof(ushort) ) + switch( dims ) { + case 1: return typeof(ushort); + // case 2: return typeof(ushort2); + // case 3: return typeof(ushort3); + // case 4: return typeof(ushort4); + } + if( componentType == typeof(uint) ) + switch( dims ) { + case 1: return typeof(uint); + case 2: return typeof(uint2); + case 3: return typeof(uint3); + case 4: return typeof(uint4); + } + if( componentType == typeof(long) ) + switch( dims ) { + case 1: return typeof(long); + // case 2: return typeof(long2); + // case 3: return typeof(long3); + // case 4: return typeof(long4); + } + if( componentType == typeof(ulong) ) + switch( dims ) { + case 1: return typeof(ulong); + // case 2: return typeof(ulong2); + // case 3: return typeof(ulong3); + // case 4: return typeof(ulong4); + } + if( componentType == typeof(inth) ) + switch( dims ) { + case 1: return typeof(inth); + case 2: return typeof(inth2); + // case 3: return typeof(inth3); + // case 4: return typeof(inth4); + } + if( componentType == typeof(rat) ) + switch( dims ) { + case 1: return typeof(rat); + case 2: return typeof(rat2); + // case 3: return typeof(rat3); + // case 4: return typeof(rat4); + } + throw new NotImplementedException( $"Missing implementation of a {dims}D {componentType.Name} vector" ); + } + + + } + +} \ No newline at end of file diff --git a/Editor/Codegen/NumericTypeInfo.cs.meta b/Editor/Codegen/NumericTypeInfo.cs.meta new file mode 100644 index 0000000..7936b7e --- /dev/null +++ b/Editor/Codegen/NumericTypeInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43eaff8786a54eb0bf53c4667a0379f0 +timeCreated: 1775947461 \ No newline at end of file diff --git a/Editor/Codegen/SplineCodegen.cs b/Editor/Codegen/SplineCodegen.cs new file mode 100644 index 0000000..f20992b --- /dev/null +++ b/Editor/Codegen/SplineCodegen.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Freya { + + public static class SplineCodegen { + + #region Type Definitions + + static SplineType typeBezier = new SplineType( 3, "Bezier", "Bézier", "cubicBezier", CharMatrix.cubicBezier, + new[] { "p0", "p1", "p2", "p3" }, + new[] { + "The starting point of the curve", + "The second control point of the curve, sometimes called the start tangent point", + "The third control point of the curve, sometimes called the end tangent point", + "The end point of the curve" + } + ); + + static SplineType typeBezierQuad = new SplineType( 2, "Bezier", "Bézier", "quadraticBezier", (RationalMatrix4x4)CharMatrix.quadraticBezier, + new[] { "p0", "p1", "p2" }, + new[] { + "The starting point of the curve", + "The middle control point of the curve, sometimes called a tangent point", + "The end point of the curve" + } + ); + + static SplineType typeHermite = new SplineType( 3, "Hermite", "Hermite", "cubicHermite", CharMatrix.cubicHermite, + new[] { "p0", "v0", "p1", "v1" }, + new[] { + "The starting point of the curve", + "The rate of change (velocity) at the start of the curve", + "The end point of the curve", + "The rate of change (velocity) at the end of the curve" + } + ); + + static SplineType typeBspline = new SplineType( 3, "UBS", "B-Spline", "cubicUniformBspline", CharMatrix.cubicUniformBspline, + new[] { "p0", "p1", "p2", "p3" }, + new[] { + "The first point of the B-spline hull", + "The second point of the B-spline hull", + "The third point of the B-spline hull", + "The fourth point of the B-spline hull" + } + ); + + static SplineType typeCatRom = new SplineType( 3, "CatRom", "Catmull-Rom", "cubicCatmullRom", CharMatrix.cubicCatmullRom, + new[] { "p0", "p1", "p2", "p3" }, + new[] { + "The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it", + "The second control point, and the start of the catmull-rom curve", + "The third control point, and the end of the catmull-rom curve", + "The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it" + } + ); + + static SplineType[] allSplineTypes = { typeBezier, typeBezierQuad, typeHermite, typeBspline, typeCatRom }; + + #endregion + + public static void GenerateUniformSplines( string splinesPath ) { + string pathUniformSplines = $"{splinesPath}/Uniform Spline Segments"; + for( int dim = 1; dim < 5; dim++ ) { // 1D, 2D, 3D, 4D + GenerateUniformSplineType( pathUniformSplines, typeBezier, dim ); + GenerateUniformSplineType( pathUniformSplines, typeBezierQuad, dim ); + GenerateUniformSplineType( pathUniformSplines, typeHermite, dim ); + GenerateUniformSplineType( pathUniformSplines, typeBspline, dim ); + GenerateUniformSplineType( pathUniformSplines, typeCatRom, dim ); + } + } + + // [MenuItem( "Assets/Port Spline Data" )] + public static void PortSplineData() { + string replacements = ""; + int replacementCount = 0; + GameObject[] gos = SceneManager.GetActiveScene().GetRootGameObjects(); + foreach( GameObject go in gos ) { + foreach( Component c in go.GetComponentsInChildren( true ) ) { + SerializedObject so = new SerializedObject( c ); // can actually find null components?? + so.Update(); + bool madeChanges = false; + SerializedProperty prop = so.GetIterator(); + while( prop.Next( true ) ) { + if( prop.isArray == false && IsSplineType( prop.type, out SplineType type, out int dim ) ) { + SerializedProperty ptMtx = prop.FindPropertyRelative( "pointMatrix" ); + try { + if( dim == 1 ) + for( int i = 0; i < type.paramNames.Length; i++ ) + ptMtx.FindPropertyRelative( $"m{i}" ).floatValue = prop.FindPropertyRelative( type.paramNames[i] ).floatValue; + else if( dim == 2 ) + for( int i = 0; i < type.paramNames.Length; i++ ) + ptMtx.FindPropertyRelative( $"m{i}" ).vector2Value = prop.FindPropertyRelative( type.paramNames[i] ).vector2Value; + else if( dim == 3 ) + for( int i = 0; i < type.paramNames.Length; i++ ) + ptMtx.FindPropertyRelative( $"m{i}" ).vector3Value = prop.FindPropertyRelative( type.paramNames[i] ).vector3Value; + else if( dim == 4 ) + for( int i = 0; i < type.paramNames.Length; i++ ) + ptMtx.FindPropertyRelative( $"m{i}" ).vector4Value = prop.FindPropertyRelative( type.paramNames[i] ).vector4Value; + } catch { + Debug.LogError( $"Null thing in {go.name}/{c.GetType().Name}/{prop.propertyPath} of type {type.className} mtx: {type.matrixName}" ); + } + + madeChanges = true; + replacements += $"Replaced: {go.name}/{c.GetType().Name}: {prop.displayName}\n"; + replacementCount++; + } + } + + if( madeChanges ) + so.ApplyModifiedProperties(); + } + } + + Debug.Log( $"{replacementCount} replacements:\n{replacements}" ); + } + + static bool IsSplineType( string name, out SplineType type, out int dim ) { + foreach( SplineType spline in allSplineTypes ) { + for( int d = 1; d <= 3; d++ ) { + if( name == $"{spline.className}{GetDegreeName( spline.degree, true )}{d}D" ) { + dim = d; + type = spline; + return true; + } + } + } + + ( type, dim ) = ( default, default ); + return false; + } + + + static void GenerateUniformSplineType( string uniformSplinePath, SplineType type, int dim ) { + int degree = type.degree; + string dataType = dim == 1 ? "float" : $"Vector{dim}"; + string polynomType = dim == 1 ? "Polynomial" : $"Polynomial{dim}D"; + int ptCount = degree + 1; + string degFullLower = GetDegreeName( degree, false ); + string degShortCapital = GetDegreeName( degree, true ); + string structName = $"{type.className}{degShortCapital}{dim}D"; + string[] points = type.paramNames; + int[] ptRange = Enumerable.Range( 0, ptCount ).ToArray(); + string[] pointDescs = type.paramDescs; + string lerpName = GetLerpName( (ElemType)dim ); + string pointMatrixType = $"{( dim == 1 ? "" : dataType )}Matrix{ptCount}x1"; + + string JoinRange( string separator, Func elem ) => string.Join( separator, ptRange.Select( elem ) ); + string JoinRangeStr( string separator, Func elem ) => string.Join( separator, points.Select( elem ) ); + + string ctorParams = JoinRange( ", ", i => $"{dataType} {type.paramNames[i]}" ); + string csPoints = JoinRange( ", ", i => $"{type.paramNames[i]}" ); + + CodeGenerator code = new CodeGenerator(); + code.AppendHeader(); + code.Using( "System" ); + code.Using( "System.Runtime.CompilerServices" ); + code.Using( "UnityEngine" ); + code.LineBreak(); + + using( code.BracketScope( "namespace Freya" ) ) { + code.LineBreak(); + + // type definition + code.Summary( $"An optimized uniform {dim}D {degFullLower} {type.prettyNameLower} segment, with {ptCount} control points" ); + using( code.BracketScope( $"[Serializable] public struct {structName} : IParamSplineSegment<{polynomType},{pointMatrixType}>" ) ) { // intentionally always Cubic right now + code.LineBreak(); + code.AppendLine( "const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;" ); + code.LineBreak(); + + // fields + code.AppendLine( $"[SerializeField] {pointMatrixType} pointMatrix;" ); + code.AppendLine( $"[NonSerialized] {polynomType} curve;" ); + code.AppendLine( "[NonSerialized] bool validCoefficients;" ); + code.LineBreak(); + + // constructors + string ctorSummary = $"Creates a uniform {dim}D {degFullLower} {type.prettyNameLower} segment, from {ptCount} control points"; + code.Summary( ctorSummary ); + for( int i = 0; i < ptCount; i++ ) + type.AppendParamStrings( code, degree, i ); + code.AppendLine( $"public {structName}( {ctorParams} ) : this(new {pointMatrixType}({csPoints})){{}}" ); + + code.Summary( ctorSummary ); + code.Param( "pointMatrix", "The matrix containing the control points of this spline" ); + code.AppendLine( $"public {structName}( {pointMatrixType} pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false);" ); + + code.LineBreak(); + + // properties + using( code.BracketScope( $"public {polynomType} Curve" ) ) { + using( code.BracketScope( $"get" ) ) { + using( code.Scope( "if( validCoefficients )" ) ) + code.AppendLine( "return curve; // no need to update" ); + code.AppendLine( "validCoefficients = true;" ); + using( code.Scope( $"return curve = new {polynomType}(" ) ) { + for( int icRow = 0; icRow < ptCount; icRow++ ) { + MathSum sum = new MathSum(); + for( int ip = 0; ip < ptCount; ip++ ) + sum.AddTerm( type.charMatrix[icRow, ip], $"{type.paramNames[ip].ToUpperInvariant()}" ); + code.AppendLine( $"{sum}{( icRow < ptCount - 1 ? "," : "" )}" ); + } + } + + code.AppendLine( ");" ); + } + // todo: set would be possible! setting the points based on a curve + } + + code.AppendLine( $"public {pointMatrixType} PointMatrix {{[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); }}" ); + for( int i = 0; i < ptCount; i++ ) { + code.Summary( pointDescs[i] ); + code.AppendLine( $"public {dataType} {points[i].ToUpperInvariant()}{{ [MethodImpl( INLINE )] get => pointMatrix.m{i}; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m{i} = value, validCoefficients = false ); }}" ); + } + + code.Summary( $"Get or set a control point position by index. Valid indices from 0 to {degree}" ); + using( code.BracketScope( $"public {dataType} this[ int i ]" ) ) { + string indexException = $"throw new ArgumentOutOfRangeException( nameof(i), $\"Index has to be in the 0 to {degree} range, and I think {{i}} is outside that range you know\" )"; + code.AppendLine( $"get => i switch {{ {JoinRange( ", ", i => $"{i} => {points[i].ToUpperInvariant()}" )}, _ => {indexException} }};" ); + code.AppendLine( $"set {{ switch( i ){{ {JoinRange( " ", i => $"case {i}: {points[i].ToUpperInvariant()} = value; break;" )} default: {indexException}; }}}}" ); + } + + // equality checks + string compEquals = JoinRangeStr( " && ", p => $"{p.ToUpperInvariant()}.Equals( other.{p.ToUpperInvariant()} )" ); + string toStringParams = JoinRange( ", ", i => $"{{pointMatrix.m{i}}}" ); + code.AppendLine( $"public static bool operator ==( {structName} a, {structName} b ) => a.pointMatrix == b.pointMatrix;" ); + code.AppendLine( $"public static bool operator !=( {structName} a, {structName} b ) => !( a == b );" ); + code.AppendLine( $"public bool Equals( {structName} other ) => {compEquals};" ); + code.AppendLine( $"public override bool Equals( object obj ) => obj is {structName} other && pointMatrix.Equals( other.pointMatrix );" ); + code.AppendLine( $"public override int GetHashCode() => pointMatrix.GetHashCode();" ); + code.AppendLine( $"public override string ToString() => $\"({toStringParams})\";" ); + code.LineBreak(); + + // typecasting + if( dim is 2 or 3 && degree is 3 ) { + if( dim == 2 ) { + // Typecast to 3D where z = 0 + string structName3D = $"{type.className}{degShortCapital}3D"; + code.Summary( "Returns this spline segment in 3D, where z = 0" ); + code.Param( "curve2D", "The 2D curve to cast to 3D" ); + string inParams = JoinRangeStr( ", ", p => $"curve2D.{p.ToUpperInvariant()}" ); + code.AppendLine( $"public static explicit operator {structName3D}( {structName} curve2D ) => new {structName3D}( {inParams} );" ); + } + + if( dim == 3 ) { + // typecast to 2D where z is omitted + string structName2D = $"{type.className}{degShortCapital}2D"; + code.Summary( "Returns this curve flattened to 2D. Effectively setting z = 0" ); + code.Param( "curve3D", "The 3D curve to flatten to the Z plane" ); + string inParams = JoinRangeStr( ", ", p => $"curve3D.{p.ToUpperInvariant()}" ); + code.AppendLine( $"public static explicit operator {structName2D}( {structName} curve3D ) => new {structName2D}( {inParams} );" ); + } + } + + // converting between spline types + if( degree == 3 ) { + string[] cubicSplineTypeNames = { + nameof(BezierCubic1D).Replace( "1D", $"{dim}D" ), + nameof(HermiteCubic1D).Replace( "1D", $"{dim}D" ), + nameof(CatRomCubic1D).Replace( "1D", $"{dim}D" ), + nameof(UBSCubic1D).Replace( "1D", $"{dim}D" ) + }; + RationalMatrix4x4[] typeMatrices = { + CharMatrix.cubicBezier, + CharMatrix.cubicHermite, + CharMatrix.cubicCatmullRom, + CharMatrix.cubicUniformBspline + }; + + // Conversion to other cubic splines + for( int i = 0; i < 4; i++ ) { + string targetType = cubicSplineTypeNames[i]; + if( targetType == structName ) + continue; // don't convert to self + RationalMatrix4x4 C = CharMatrix.GetConversionMatrix( type.charMatrix, typeMatrices[i] ); + + using( code.Scope( $"public static explicit operator {targetType}( {structName} s ) =>" ) ) { + using( code.Scope( $"new {targetType}(" ) ) { + for( int oPt = 0; oPt < 4; oPt++ ) { + MathSum sum = new(); + for( int iPt = 0; iPt < 4; iPt++ ) + sum.AddTerm( C[oPt, iPt], $"s.{type.paramNames[iPt].ToUpperInvariant()}" ); + code.AppendLine( $"{sum}{( oPt < 3 ? "," : "" )}" ); + } + } + + code.AppendLine( ");" ); + } + } + } + + // Interpolation + code.Summary( $"Returns a linear blend between two {type.prettyNameLower} curves" ); + code.Param( "a", "The first spline segment" ); + code.Param( "b", "The second spline segment" ); + code.Param( "t", "A value from 0 to 1 to blend between a and b" ); + using( code.Scope( $"public static {structName} Lerp( {structName} a, {structName} b, float t ) =>" ) ) { + using( code.Scope( "new(" ) ) { + for( int i = 0; i < ptCount; i++ ) { + code.AppendLine( $"{lerpName}( a.{points[i].ToUpperInvariant()}, b.{points[i].ToUpperInvariant()}, t )" + ( i == ptCount - 1 ? "" : "," ) ); + } + } + + code.AppendLine( ");" ); + } + + + // special case slerps for cubic beziers in 2D and 3D + if( dim is 2 or 3 && type == typeBezier ) { + // todo: hermite slerp + string slerpCast = dim == 2 ? "(Vector2)" : ""; + code.LineBreak(); + code.Summary( $"Returns a linear blend between two {type.prettyNameLower} curves, where the tangent directions are spherically interpolated" ); + code.Param( "a", "The first spline segment" ); + code.Param( "b", "The second spline segment" ); + code.Param( "t", "A value from 0 to 1 to blend between a and b" ); + using( code.BracketScope( $"public static {structName} Slerp( {structName} a, {structName} b, float t )" ) ) { + code.AppendLine( $"{dataType} P0 = {lerpName}( a.P0, b.P0, t );" ); + code.AppendLine( $"{dataType} P3 = {lerpName}( a.P3, b.P3, t );" ); + using( code.Scope( $"return new {structName}(" ) ) { + code.AppendLine( $"P0," ); + code.AppendLine( $"P0 + {slerpCast}Vector3.SlerpUnclamped( a.P1 - a.P0, b.P1 - b.P0, t )," ); + code.AppendLine( $"P3 + {slerpCast}Vector3.SlerpUnclamped( a.P2 - a.P3, b.P2 - b.P3, t )," ); + code.AppendLine( $"P3" ); + } + + code.AppendLine( ");" ); + } + } + + // special case splits + bool hasSplit = type == typeBezier || type == typeBezierQuad; + if( hasSplit ) { + code.Summary( "Splits this curve at the given t-value, into two curves that together form the exact same shape" ); + code.Param( "t", "The t-value to split at" ); + using( code.BracketScope( $"public ({structName} pre, {structName} post) Split( float t )" ) ) { + AppendBezierSplit( code, structName, dataType, degree, dim ); + } + } + } + } + + File.WriteAllLines( $"{uniformSplinePath}/{structName}.cs", code.content ); + } + + static string GetLerpName( ElemType dim ) { + return dim switch { + ElemType._1D => "Mathfs.Lerp", + ElemType._2D => "Vector2.LerpUnclamped", + ElemType._3D => "Vector3.LerpUnclamped", + ElemType._4D => "Vector4.LerpUnclamped", + ElemType.Quat => "Quaternion.SlerpUnclamped", + _ => throw new IndexOutOfRangeException() + }; + } + + class MathSum { + + rat globalScale = rat.one; + List<(rat coeff, string var)> terms = new List<(rat coeff, string var)>(); + + public void AddTerm( rat coeff, string var ) { + if( coeff != 0 ) + terms.Add( ( coeff, var ) ); + } + + void TryOptimize() { + if( terms.Count < 2 ) + return; // can't optimize 0 or 1 terms + + rat coeff0 = terms[0].coeff.abs; + if( terms.TrueForAll( t => t.coeff.abs == coeff0 ) ) { + globalScale = coeff0; + for( int i = 0; i < terms.Count; i++ ) + terms[i] = ( terms[i].coeff / coeff0, terms[i].var ); + } + } + + public override string ToString() { + if( terms.Count == 0 ) + return "0"; + + TryOptimize(); + + string line = ""; + for( int i = 0; i < terms.Count; i++ ) + line += FormatTerm( i ); + + if( globalScale != 1 ) { + if( globalScale.n == 1 ) { + line = $"({line})/{globalScale.d}"; + } else { + line = $"{FormatRational( globalScale )}*({line})"; + } + } + + return line; + } + + string FormatRational( rat v ) => v.isInteger ? $"{v.n}" : $"({v}f)"; + + string FormatTerm( int i ) { + rat value = terms[i].coeff; + string sign = i > 0 && value >= 0 ? "+" : ""; + string valueStr; + string op = ""; + if( value == rat.one ) + valueStr = ""; + else if( value == -rat.one ) + valueStr = "-"; + else if( value > 0 ) { + valueStr = FormatRational( value ); + op = "*"; + } else { // value < 0 + valueStr = FormatRational( -value ); + sign = "-"; + op = "*"; + } + + return $"{sign}{valueStr}{op}{terms[i].var}"; + } + + } + + static readonly string[] comp = { "x", "y", "z", "w" }; + + static void AppendBezierSplit( CodeGenerator code, string structName, string dataType, int degree, int dim ) { + string LerpStr( string A, string B, int c ) => $"{A}.{comp[c]} + ( {B}.{comp[c]} - {A}.{comp[c]} ) * t"; + + void AppendLerps( string varName, string A, string B ) { + if( dim > 1 ) { + using( code.Scope( $"{dataType} {varName} = new {dataType}(" ) ) { + for( int c = 0; c < dim; c++ ) { + string end = c == dim - 1 ? " );" : ","; + code.AppendLine( $"{LerpStr( A, B, c )}{end}" ); + } + } + } else { // floats + code.AppendLine( $"{dataType} {varName} = {A} + ( {B} - {A} ) * t;" ); + } + } + + AppendLerps( "a", "P0", "P1" ); + AppendLerps( "b", "P1", "P2" ); // this could be unrolled/optimized for the cubic case, as b is never used for the output + if( degree == 3 ) { + AppendLerps( "c", "P2", "P3" ); + AppendLerps( "d", "a", "b" ); + AppendLerps( "e", "b", "c" ); + AppendLerps( "p", "d", "e" ); + code.AppendLine( $"return ( new {structName}( P0, a, d, p ), new {structName}( p, e, c, P3 ) );" ); + } else if( degree == 2 ) { + AppendLerps( "p", "a", "b" ); + code.AppendLine( $"return ( new {structName}( P0, a, p ), new {structName}( p, b, P2 ) );" ); + } + } + + static string GetDegreeName( int d, bool shortName ) { + return d switch { + 1 => "Linear", + 2 => shortName ? "Quad" : "Quadratic", + 3 => "Cubic", + 4 => "Quartic", + 5 => "Quintic", + _ => throw new IndexOutOfRangeException() + }; + } + + + } + +} \ No newline at end of file diff --git a/Editor/Codegen/SplineCodegen.cs.meta b/Editor/Codegen/SplineCodegen.cs.meta new file mode 100644 index 0000000..6d1e7cc --- /dev/null +++ b/Editor/Codegen/SplineCodegen.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9a733093b6cf4cdfba45361185d1ba8e +timeCreated: 1775666119 \ No newline at end of file diff --git a/Editor/Codegen/SplineType.cs b/Editor/Codegen/SplineType.cs new file mode 100644 index 0000000..f5bdc1b --- /dev/null +++ b/Editor/Codegen/SplineType.cs @@ -0,0 +1,29 @@ +namespace Freya { + + public class SplineType { + public int degree; + public string className; + public string prettyName; + public string prettyNameLower; + public string[] paramNames; + public string[] paramDescs; + public string matrixName; + public RationalMatrix4x4 charMatrix; + + public SplineType( int degree, string className, string prettyName, string matrixName, RationalMatrix4x4 charMatrix, string[] paramNames, string[] paramDescs, string[] paramDescsQuad = null ) { + this.degree = degree; + this.className = className; + this.prettyName = prettyName; + this.prettyNameLower = prettyName.ToLowerInvariant(); + this.paramDescs = paramDescs; + this.matrixName = matrixName; + this.paramNames = paramNames; + this.charMatrix = charMatrix; + } + + public void AppendParamStrings( CodeGenerator gen, int degree, int i ) { + gen.Param( paramNames[i], paramDescs[i] ); + } + } + +} \ No newline at end of file diff --git a/Editor/Codegen/SplineType.cs.meta b/Editor/Codegen/SplineType.cs.meta new file mode 100644 index 0000000..65537ab --- /dev/null +++ b/Editor/Codegen/SplineType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 50863095183c481bb781e9263be4e35f +timeCreated: 1775665591 \ No newline at end of file diff --git a/Editor/Codegen/StaticAccessExtensions.cs b/Editor/Codegen/StaticAccessExtensions.cs new file mode 100644 index 0000000..cd5994b --- /dev/null +++ b/Editor/Codegen/StaticAccessExtensions.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + public enum TypeSource { + CsNative, + UnityMath, + Unity, + Mathfs + } + + public enum NumType { + Bool2, + Byte8, + SByte8, + Short16, + UShort16, + Int32, + UInt32, + Long64, + ULong64, + IntHalf, + Rational, + Half16, + Float32, + Double64, + } + + public static class StaticAccessExtensions { + + + public static readonly Dictionary numericInfo = new() { + { typeof(rat), new(typeof(rat), NumType.Rational, dims: 1, TypeSource.Mathfs) }, + { typeof(rat2), new(typeof(rat2), NumType.Rational, dims: 2, TypeSource.Mathfs) }, + // { typeof(rat3), new(typeof(rat3), NumType.Rational, dims: 3, TypeSource.Mathfs) }, // todo + // { typeof(rat4), new(typeof(rat4), NumType.Rational, dims: 4, TypeSource.Mathfs) }, + + { typeof(inth), new(typeof(inth), NumType.IntHalf, dims: 1, TypeSource.Mathfs) }, + { typeof(inth2), new(typeof(inth2), NumType.IntHalf, dims: 2, TypeSource.Mathfs) }, + // { typeof(inth3), new(typeof(inth3), NumType.IntHalf, dims: 3, TypeSource.Mathfs) }, // todo + // { typeof(inth4), new(typeof(inth4), NumType.IntHalf, dims: 4, TypeSource.Mathfs) }, + + { typeof(bool), new(typeof(bool), NumType.Bool2, dims: 1, TypeSource.CsNative) }, + { typeof(bool2), new(typeof(bool2), NumType.Bool2, dims: 2, TypeSource.UnityMath) }, + { typeof(bool3), new(typeof(bool3), NumType.Bool2, dims: 3, TypeSource.UnityMath) }, + { typeof(bool4), new(typeof(bool4), NumType.Bool2, dims: 4, TypeSource.UnityMath) }, + + { typeof(byte), new(typeof(byte), NumType.Byte8, dims: 1, TypeSource.CsNative) }, + { typeof(sbyte), new(typeof(sbyte), NumType.SByte8, dims: 1, TypeSource.CsNative) }, + { typeof(short), new(typeof(short), NumType.Short16, dims: 1, TypeSource.CsNative) }, + { typeof(ushort), new(typeof(ushort), NumType.UShort16, dims: 1, TypeSource.CsNative) }, + { typeof(long), new(typeof(long), NumType.Long64, dims: 1, TypeSource.CsNative) }, + { typeof(ulong), new(typeof(ulong), NumType.ULong64, dims: 1, TypeSource.CsNative) }, + + { typeof(uint), new(typeof(uint), NumType.UInt32, dims: 1, TypeSource.CsNative) }, + { typeof(uint2), new(typeof(uint2), NumType.UInt32, dims: 2, TypeSource.UnityMath) }, + { typeof(uint3), new(typeof(uint3), NumType.UInt32, dims: 3, TypeSource.UnityMath) }, + { typeof(uint4), new(typeof(uint4), NumType.UInt32, dims: 4, TypeSource.UnityMath) }, + + { typeof(int), new(typeof(int), NumType.Int32, dims: 1, TypeSource.CsNative) }, + { typeof(int2), new(typeof(int2), NumType.Int32, dims: 2, TypeSource.UnityMath) }, + { typeof(Vector2Int), new(typeof(Vector2Int), NumType.Int32, dims: 2, TypeSource.Unity) }, // legacy unity int2 + { typeof(int3), new(typeof(int3), NumType.Int32, dims: 3, TypeSource.UnityMath) }, + { typeof(Vector3Int), new(typeof(Vector3Int), NumType.Int32, dims: 3, TypeSource.Unity) }, // legacy unity int3 + { typeof(int4), new(typeof(int4), NumType.Int32, dims: 4, TypeSource.UnityMath) }, + // { typeof(Vector4Int), new(typeof(Vector4Int), NumType.Int32, dims: 4, TypeSource.Unity) }, // Unity doesn't define this + + // todo: Colors have a bunch of exceptions, disabling for now + // { typeof(Color32), new(typeof(Color32), NumType.Int32, dims: 4, TypeSource.Unity) }, // basically a byte4 type + // { typeof(Color), new(typeof(Color), NumType.Float32, dims: 4, TypeSource.Unity) }, + + // todo: Unity's half types become floats under basic operations, so they require a lot of exceptions in the codegen. Disabling for now + // { typeof(half), new(typeof(half), NumType.Half16, dims: 1, TypeSource.UnityMath) }, // exception: unity defines this + // { typeof(half2), new(typeof(half2), NumType.Half16, dims: 2, TypeSource.UnityMath) }, + // { typeof(half3), new(typeof(half3), NumType.Half16, dims: 3, TypeSource.UnityMath) }, + // { typeof(half4), new(typeof(half4), NumType.Half16, dims: 4, TypeSource.UnityMath) }, + + { typeof(float), new(typeof(float), NumType.Float32, dims: 1, TypeSource.CsNative) }, + { typeof(float2), new(typeof(float2), NumType.Float32, dims: 2, TypeSource.UnityMath) }, + { typeof(Vector2), new(typeof(Vector2), NumType.Float32, dims: 2, TypeSource.Unity) }, // legacy unity float2 + { typeof(float3), new(typeof(float3), NumType.Float32, dims: 3, TypeSource.UnityMath) }, + { typeof(Vector3), new(typeof(Vector3), NumType.Float32, dims: 3, TypeSource.Unity) }, // legacy unity float3 + { typeof(float4), new(typeof(float4), NumType.Float32, dims: 4, TypeSource.UnityMath) }, + { typeof(Vector4), new(typeof(Vector4), NumType.Float32, dims: 4, TypeSource.Unity) }, // legacy unity float4 + + { typeof(double), new(typeof(double), NumType.Double64, dims: 1, TypeSource.CsNative) }, + { typeof(double2), new(typeof(double2), NumType.Double64, dims: 2, TypeSource.UnityMath) }, + { typeof(double3), new(typeof(double3), NumType.Double64, dims: 3, TypeSource.UnityMath) }, + { typeof(double4), new(typeof(double4), NumType.Double64, dims: 4, TypeSource.UnityMath) }, + }; + + // public static IEnumerable<(Type nType,Type iType)> ExternalTypes + + + // v.abs().csum() // taxicab / L1 + // v.abs().cmax() // chebyshev / max norm + + public static IEnumerable InterfacesOfExternalType( Type nType ) { + HashSet interfaces = new HashSet(); + + NumericTypeInfo info = numericInfo[nType]; + Type V = nType; + Type R = info.TypeAfterRounding; // rounded type + Type C = info.ComponentType; // component type + Type D = C; // dot product result type + Type M = V; // complex multiplication result type + + + interfaces.UnionWith( InterfacesOf( typeof(ISignedNumber<>).MakeGenericType( R ) ) ); + if( info.numType is NumType.Double64 or NumType.Float32 or NumType.Half16 or NumType.Rational or NumType.IntHalf ) + interfaces.UnionWith( InterfacesOf( typeof(IRoundable<>).MakeGenericType( R ) ) ); + + switch( info.dims ) { + case 1: + interfaces.UnionWith( InterfacesOf( typeof(INumber<>).MakeGenericType( V ) ) ); + break; + case 2: + Type W2 = D; // wedge product result type + Type ProjSc2 = info.ScalarProjectionType; + interfaces.UnionWith( InterfacesOf( typeof(IVec2<,,,,,>).MakeGenericType( V, C, D, W2, M, ProjSc2 ) ) ); + break; + case 3: + Type W3 = V; // wedge product result type + Type ProjSc3 = info.ScalarProjectionType; + interfaces.UnionWith( InterfacesOf( typeof(IVec3<,,,>).MakeGenericType( V, C, D, W3 ) ) ); + break; + case 4: + interfaces.UnionWith( InterfacesOf( typeof(IVec4<,,>).MakeGenericType( V, C, D ) ) ); + break; + } + + // todo: only 2D vectors so far + // if( info.dims == 2 ) // typeof(IVec2) + // else + + + return interfaces; + } + + static IEnumerable InterfacesOf( Type t ) { + if( t == null ) + yield break; + if( t.IsInterface ) + yield return t; + foreach( Type iType in t.GetInterfaces() ) + yield return iType; + } + + public static string GetCustomImplementation( Type iType, Type nType, string member ) { + Type iTypeDef = iType.IsGenericType ? iType.GetGenericTypeDefinition() : iType; + if( numericInfo.TryGetValue( nType, out NumericTypeInfo info ) == false ) { + Debug.LogWarning( $"Missing custom implementation for nType {nType} with iType {iType} member {member} itypedef: {iTypeDef}" ); + return "default"; + } + + if( iTypeDef == typeof(INumberBase) ) { + if( member == nameof(INumberBase.isInteger) ) { + if( info.IsAlwaysIntegerValue ) + return "true"; + if( info.IsScalar ) + return $"{{0}} == {CsMathClass( nType )}.Truncate( {{0}} )"; + return info.JoinComponents( " && ", ( i, c ) => $"{{0}}.{c}.isInteger()" ); + } + if( member == nameof(INumberBase.isZero) ) { + if( info.IsScalar ) + return "{0} == 0"; + if( info.source == TypeSource.UnityMath ) + return "math.all( {0} == 0 )"; + return info.JoinComponents( " && ", ( i, c ) => $"{{0}}.{c} == 0" ); + } + if( member == nameof(INumberBase.isOrthogonal) ) { + if( info.IsScalar ) + return "true"; + string value = "{0}"; + if( info.source == TypeSource.Unity && info.numType == NumType.Int32 ) { + // Vector2Int and Vector3Int requires a cast + value = $"new int{info.dims}({info.JoinComponents( ", ", ( i, c ) => $"{{0}}.{c}" )})"; + } + string ceilOptionally = info.IsAlwaysIntegerValue ? "" : ".ceilAwayFrom0()"; + return $"({value}{ceilOptionally}.abs() > 0).csum() <= 1"; + } + } else if( iTypeDef == typeof(INumber<>) ) { + switch( member ) { + case nameof(INumber.abs): return StandardMathOp( info, "math.abs({0})", "Math.Abs({0})", "abs()" ); + case nameof(INumber.min): return StandardMathOp( info, "math.min({0},{1})", "Math.Min({0},{1})", "min({0})" ); + case nameof(INumber.max): return StandardMathOp( info, "math.max({0},{1})", "Math.Max({0},{1})", "max({0})" ); + case nameof(INumber.to): return "{1} - {0}"; // b - a + } + } else if( iTypeDef == typeof(IComplex<,>) ) { + switch( member ) { + case nameof(IComplex.complexMul): return "new({0}.x*{1}.x-{0}.y*{1}.y, {0}.x*{1}.y+{0}.y*{1}.x)"; + case nameof(IComplex.complexConj): return "new({0}.x, -{0}.y)"; + } + } else if( iTypeDef == typeof(IDotProduct<,>) ) { + switch( member ) { + case nameof(IDotProduct.dot): + // unity.mathematics does not implement dot() for the half type + bool isNotHalf = info.ComponentType != typeof(half); + if( info.source == TypeSource.UnityMath && isNotHalf ) + return "math.dot( {0}, {1} )"; + return info.CompSum( ( i, c ) => $"{{0}}.{c}*{{1}}.{c}" ); + } + } else if( iTypeDef == typeof(IWedgeProduct<,>) ) { + switch( member ) { + case nameof(IWedgeProduct.wedge): + return info.dims switch { + 2 => "{0}.x*{1}.y - {0}.y*{1}.x", + 3 => "new(" + + "{0}.y * {1}.z - {0}.z * {1}.y," + + "{0}.z * {1}.x - {0}.x * {1}.z," + + "{0}.x * {1}.y - {0}.y * {1}.x" + + ")", + _ => throw new NotImplementedException( $"Wedge product not implemented for elements of dimension {info.dims}" ) + }; + } + } else if( iTypeDef == typeof(ISqrMag<>) ) { + switch( member ) { + case nameof(IDotProduct.magSq): + // unity.mathematics does not implement dot() for the half type + bool isNotHalf = info.ComponentType != typeof(half); + if( info.source == TypeSource.UnityMath && isNotHalf ) + return "math.dot( {0}, {0} )"; + return info.CompSum( ( i, c ) => $"{{0}}.{c}*{{0}}.{c}" ); + } + } else if( iTypeDef == typeof(ISignedNumber<>) ) { + if( member == nameof(ISignedNumber.sign) ) { + if( info.source == TypeSource.CsNative ) + return "Math.Sign({0})"; + if( info.source == TypeSource.UnityMath && info.numType is NumType.Float32 or NumType.Double64 ) + return $"(int{info.dims})math.sign({{0}})"; // floats/doubles + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.sign()" ); + } + } else if( iTypeDef == typeof(IVec2<,,,,,>) ) { + if( member == nameof(IVec2.rot90) ) { + return "new(-{0}.y,{0}.x)"; + } else if( member == nameof(IVec2.rotNeg90) ) { + return "new({0}.y,-{0}.x)"; + } else if( member == nameof(IVec2.rot180) ) { + return "new(-{0}.x,-{0}.y)"; + } + } else if( iTypeDef == typeof(IRoundable<>) ) { + switch( member ) { + case nameof(IRoundable.floorToward0): + if( info.source == TypeSource.CsNative && info.IsScalar ) + return "(int)({0}<0?math.ceil({0}):math.floor({0}))"; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.floorToward0()" ); + case nameof(IRoundable.ceilAwayFrom0): + if( info.source == TypeSource.CsNative && info.IsScalar ) + return "(int)({0}<0?math.floor({0}):math.ceil({0}))"; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.ceilAwayFrom0()" ); + case nameof(IRoundable.floor): + if( info.source == TypeSource.CsNative && info.IsScalar ) + return info.numType switch { + NumType.Double64 => "(int)Math.Floor({0})", + NumType.Float32 => "(int)MathF.Floor({0})", + _ => throw new NotImplementedException( info.numType.ToString() ) + }; + else if( info.source == TypeSource.UnityMath ) + return $"(int{info.dims})math.floor({{0}})"; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.floor()" ); + case nameof(IRoundable.ceil): + if( info.source == TypeSource.CsNative && info.IsScalar ) + return info.numType switch { + NumType.Double64 => "(int)Math.Ceiling({0})", + NumType.Float32 => "(int)MathF.Ceiling({0})", + _ => throw new NotImplementedException( info.numType.ToString() ) + }; + else if( info.source == TypeSource.UnityMath ) + return $"(int{info.dims})math.ceil({{0}})"; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.ceil()" ); + case nameof(IRoundable.round): + if( info.source == TypeSource.CsNative && info.IsScalar ) + return info.numType switch { + NumType.Double64 => "(int)Math.Round( {0}, (MidpointRounding)rounding )", + NumType.Float32 => "(int)MathF.Round( {0}, (MidpointRounding)rounding )", + _ => throw new NotImplementedException( info.numType.ToString() ) + }; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.round(rounding)" ); + } + } else if( iTypeDef == typeof(IVecComponents<>) ) { + switch( member ) { + case "cmin": return UnityMathVecsElseComponentWise( info, "math.cmin({0})", info.CompAggrInstanceFuncs( "min" ) ); + case "cmax": return UnityMathVecsElseComponentWise( info, "math.cmax({0})", info.CompAggrInstanceFuncs( "max" ) ); + case "csum": return UnityMathVecsElseComponentWise( info, "math.csum({0})", info.CompSum( ( i, c ) => $"{{0}}.{c}" ) ); + } + } else if( iTypeDef == typeof(IVec1Base<,,>) && IVecNBase( info, member, 1, out string str1 ) ) { + return str1; + } else if( iTypeDef == typeof(IVec2Base<,,>) && IVecNBase( info, member, 2, out string str2 ) ) { + return str2; + } else if( iTypeDef == typeof(IVec3Base<,,>) && IVecNBase( info, member, 3, out string str3 ) ) { + return str3; + } else if( iTypeDef == typeof(IVec4Base<,,>) && IVecNBase( info, member, 4, out string str4 ) ) { + return str4; + } else if( iTypeDef == typeof(IVec<,,>) ) { + switch( member ) { + case nameof(IVec.magChebyshev): return "{0}.abs().cmax()"; + case nameof(IVec.magTaxicab): return "{0}.abs().csum()"; + case nameof(IVec.pointSideOfPlane): return "({0}-{1}).dot({2}).sign()"; + } + } else if( iTypeDef == typeof(IQuadrant2D) ) { + string rounding = info.numType is NumType.Float32 or NumType.Double64 ? $".{nameof(IRoundable.ceilAwayFrom0)}()" : ""; + return member switch { + nameof(IQuadrant2D.quadrant) => "{0}.y switch {{" + + "> 00 when {0}.x <= 0 => 1," + + "<= 0 when {0}.x < 00 => 2," + + "< 00 when {0}.x >= 0 => 3," + + "_ => 0 }}", + nameof(IQuadrant2D.quadrantBasisX) => $"mathfs.{nameof(mathfs.quadrantToBasisX)}({{0}}{rounding}.quadrant())", + nameof(IQuadrant2D.quadrantBasis) => $"mathfs.{nameof(mathfs.quadrantToBasis)}({{0}}{rounding}.quadrant())", + nameof(IQuadrant2D.signedQuadrant) => $"mathfs.{nameof(mathfs.quadrantToSignedQuadrant)}({{0}}{rounding}.quadrant())", + _ => throw new NotImplementedException() + }; + } else if( iTypeDef == typeof(IVecProjections<,>) ) { + switch( member ) { + case nameof(IVecProjections.projTValue): return "{0}.dot({1})/{1}.dot({1})"; + } + } + + Debug.LogWarning( $"Missing custom implementation for nType {nType} with iType {iType} member {member}" ); + return "default"; + } + + public static bool IVecNBase( NumericTypeInfo info, string member, int dim, out string str ) { + int comp = member[^1] switch { + 'X' => 0, + 'Y' => 1, + 'Z' => 2, + 'W' => 3, + _ => 0, + }; + + if( member is "X" or "Y" or "Z" or "W" ) { + str = $"{{0}}.{member.ToLower()}"; + return true; + } + if( member is "flipX" or "flipY" or "flipZ" or "flipW" ) { + str = info.NewFromComps( ( i, c ) => $"{( i == comp ? "-" : "+" )}{{0}}.{c}" ); + return true; + } + if( member is "zeroX" or "zeroY" or "zeroZ" or "zeroW" ) { + str = info.NewFromComps( ( i, c ) => i == comp ? "0" : $"{{0}}.{c}" ); + return true; + } + + str = default; + return false; + } + + public static string UnityMathVecsElseComponentWise( NumericTypeInfo info, string funcMathematics, string perComp ) { + if( info.source == TypeSource.UnityMath && info.IsScalar == false ) + return funcMathematics; + return perComp; + // if( info.source == TypeSource.CsNative ) + // return $"{funcNative}"; + // return info.NewFromComponents( ( i, c ) => $"{{0}}.{c}.{string.Format( funcCompExtension, $"{{1}}.{c}" )}" ); + } + + public static string StandardMathOp( NumericTypeInfo info, string funcMathematics, string funcNative, string funcCompExtension ) { + if( info.source == TypeSource.UnityMath ) + return $"{funcMathematics}"; + if( info.source == TypeSource.CsNative ) + return $"{funcNative}"; + return info.NewFromComps( ( i, c ) => $"{{0}}.{c}.{string.Format( funcCompExtension, $"{{1}}.{c}" )}" ); + } + + public static string CsMathClass( Type type ) { + if( type == typeof(float) ) + return nameof(MathF); + if( type == typeof(double) ) + return nameof(Math); + throw new NotImplementedException( type.FullName ); + } + + + } + +} \ No newline at end of file diff --git a/Editor/Codegen/StaticAccessExtensions.cs.meta b/Editor/Codegen/StaticAccessExtensions.cs.meta new file mode 100644 index 0000000..de9a42c --- /dev/null +++ b/Editor/Codegen/StaticAccessExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c22c62cb4f5b4f2fad5c6acf58f05c89 +timeCreated: 1775933332 \ No newline at end of file diff --git a/Editor/Codegen/StaticAccessInterfaceGenerator.cs b/Editor/Codegen/StaticAccessInterfaceGenerator.cs new file mode 100644 index 0000000..e4a43e4 --- /dev/null +++ b/Editor/Codegen/StaticAccessInterfaceGenerator.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + public class StaticAccessInterfaceGenerator { + + const string TSelf = "Self"; + const string StaticAccessClassName = nameof(mathfs); + const string StaticAccessClassNameGenerics = StaticAccessClassName + "_generics"; + const string Namespace = nameof(Freya); + + public Type iTypeDef; + public List generics = new(); + public List specifics = new(); + + string FileName => $"{IName}_static.cs"; + bool IsGenericInterface => iTypeDef.IsGenericType; + string IName => IsGenericInterface ? iTypeDef.Name.Split( "`" )[0] : iTypeDef.Name; + IEnumerable GenericTypeDefNames => iTypeDef.GetGenericArguments().Select( x => x.Name ); + string GenericsDefCsv => string.Join( ",", GenericTypeDefNames ); + string GenericsDefWithSelfCsv => string.Join( ",", GenericTypeDefNames.Append( TSelf ) ); + string INameInheritDoc => IName + ( IsGenericInterface ? $"{{{GenericsDefCsv}}}" : "" ); + string INameWithGenerics => IName + ( IsGenericInterface ? $"<{GenericsDefCsv}>" : "" ); + // string INameWithGenericsAndSelf => IName + $"<{GenericsDefWithSelfCsv}>"; + + public void GenerateCode( string targetFolder ) { + if( generics.Count == 0 && specifics.Count == 0 ) + return; // nothing to generate + CodeGenerator code = new(); + code.AppendHeader(); + code.AppendLine( "using System;" ); + code.AppendLine( "using Unity.Mathematics;" ); + code.AppendLine( "using UnityEngine;" ); + using( code.BracketScope( $"namespace {Namespace}" ) ) { + if( generics.Count > 0 ) + using( code.BracketScope( $"public static partial class {StaticAccessClassNameGenerics}" ) ) + generics.ForEach( code.AppendLine ); + if( specifics.Count > 0 ) + using( code.BracketScope( $"public static partial class {StaticAccessClassName}" ) ) + specifics.ForEach( code.AppendLine ); + } + Debug.Log( $"{FileName}:\n{string.Join( "\n", code.content )}" ); + File.WriteAllLines( $"{targetFolder}/{FileName}", code.content ); + } + + public bool PropertyIsValid( PropertyInfo p ) => p.IsSpecialName == false && p.GetIndexParameters().Length == 0; + public bool MethodIsValid( MethodInfo p ) => p.IsSpecialName == false; + + public IEnumerable PropertiesITypeDef => iTypeDef.GetProperties().Where( PropertyIsValid ); + public IEnumerable MethodsITypeDef => iTypeDef.GetMethods().Where( MethodIsValid ); + + public StaticAccessInterfaceGenerator( Type iType ) { + iTypeDef = iType.IsGenericType ? iType.GetGenericTypeDefinition() : iType; + + // Generic implementations of each property, eg: + // /// + // public static bool isInteger( T v ) where T : INumber => v.isInteger; + + // but also, sometimes the Self type is already in the return type + // /// + // public static T abs( T x ) where T : INumber => x.abs; + // incorrectly generates as: + // public static N abs(Self v) where Self : INumber => v.abs; + + + // GENERIC definitions + foreach( PropertyInfo prop in PropertiesITypeDef ) { + string memName = prop.Name; + string access = "public static"; + string retType = GetReturnTypeName( prop ); + string fName = $"{memName}<{GenericsDefWithSelfCsv}>"; + string constraints = $"where {TSelf} : {INameWithGenerics}"; + string parms = $"{TSelf} v"; + string body = $"v.{memName};"; + generics.Add( CodeGenerator.GetInheritdocString( $"{INameInheritDoc}.{memName}" ) ); + generics.Add( $"{access} {retType} {fName}({parms}) {constraints} => {body}" ); + } + + foreach( MethodInfo meth in MethodsITypeDef ) { + CreateStaticAccessForMethod( meth, isGenericDef: true ); + } + } + + + static void CsvAppend( ref string s, string appendage ) { + s = string.IsNullOrEmpty( s ) ? appendage : $"{s}, {appendage}"; + } + + string GetMethodParams( MethodInfo method ) { + return string.Join( ", ", method.GetParameters().Select( p => p.ParameterType.Name + " " + p.Name ) ); + } + + public void GenerateSpecificsForType( Type vType, Type typeImpl ) { + NumericTypeInfo info = StaticAccessExtensions.numericInfo[vType]; + bool addAsExtension = info.IsExternalType; + string param0prefix = addAsExtension ? "this " : ""; + + // /// + // public static bool isZero( rat v ) => v.isZero; + string vTypeName = vType.Name; + + // Specific PROPERTIES + foreach( PropertyInfo prop in typeImpl.GetProperties().Where( PropertyIsValid ) ) { + string memName = prop.Name; + string retTypeName = GetReturnTypeName( prop ); + string paramsInvoc = addAsExtension ? "()" : ""; + + string paramName = "v"; + + string body; + if( addAsExtension ) { + string strToFormat = StaticAccessExtensions.GetCustomImplementation( typeImpl, vType, memName ); + try { + body = string.Format( strToFormat, paramName ); + } catch { + body = "default"; + Debug.LogError( strToFormat ); + } + } else { + body = $"{paramName}.{memName}{paramsInvoc}"; + } + + specifics.Add( CodeGenerator.GetInheritdocString( $"{INameInheritDoc}.{memName}" ) ); + specifics.Add( $"public static {retTypeName} {memName}({param0prefix}{vTypeName} {paramName}) => {body};" ); + } + + // Specific METHODS + foreach( MethodInfo meth in typeImpl.GetMethods().Where( MethodIsValid ) ) { + CreateStaticAccessForMethod( meth, isGenericDef: false, vType, typeImpl ); + } + } + + void CreateStaticAccessForMethod( MethodInfo meth, bool isGenericDef, Type numTypeSelf = null, Type iTypeImpl = null ) { + // binary means we want to name the two input parameters (a,b) + NumericTypeInfo info = numTypeSelf != null ? StaticAccessExtensions.numericInfo[numTypeSelf] : default; + bool addAsExtension = numTypeSelf != null && info.IsExternalType; + string param0prefix = addAsExtension ? "this " : ""; + bool isBinaryOp = meth.CustomAttributes.Any( x => x.AttributeType == typeof(BinaryOpAttribute) ); + string memName = meth.Name; + string access = "public static"; + string retType = GetReturnTypeName( meth, isGenericDef ); + string fName = isGenericDef ? $"{memName}<{GenericsDefWithSelfCsv}>" : memName; + string tSelf = isGenericDef ? TSelf : NameOf( numTypeSelf, isGenericDef ); + if( string.IsNullOrEmpty( tSelf ) ) + Debug.LogWarning( $"Empty param name: {numTypeSelf} in {meth.Name}" ); + string constraints = isGenericDef ? $" where {tSelf} : {INameWithGenerics}" : ""; + string selfParamName = isBinaryOp ? "a" : "v"; + string paramsBody = ""; + string paramsDecl = $"{param0prefix}{tSelf} {selfParamName}"; + List paramNames = new(); + paramNames.Add( selfParamName ); + if( isBinaryOp ) { // f(T a,T b) => a.f(b); + string bTypeName = NameOf( meth.GetParameters().First().ParameterType, isGenericDef ); + if( string.IsNullOrEmpty( bTypeName ) ) + Debug.LogWarning( $"Empty param name: {meth.GetParameters().First()}/{meth.GetParameters().First().ParameterType.Name}/gen: {meth.GetParameters().First().ParameterType.IsGenericType} in meth {meth}" ); + string bName = "b"; + CsvAppend( ref paramsDecl, $"{bTypeName} {bName}" ); + CsvAppend( ref paramsBody, bName ); + paramNames.Add( bName ); + } else { + foreach( ParameterInfo p in meth.GetParameters() ) { + string paramTypeName = NameOf( p.ParameterType, isGenericDef ); + if( string.IsNullOrEmpty( paramTypeName ) ) + Debug.LogWarning( $"Empty param type name: {p.Name} in {meth.Name}" ); + CsvAppend( ref paramsDecl, paramTypeName + " " + p.Name ); + CsvAppend( ref paramsBody, p.Name ); + paramNames.Add( p.Name ); + } + } + string body; + if( addAsExtension ) { + body = StaticAccessExtensions.GetCustomImplementation( iTypeImpl, numTypeSelf, memName ); + if( paramNames.Count > 0 ) + body = string.Format( body, paramNames.ToArray() ); + } else + body = $"{selfParamName}.{memName}({paramsBody})"; + List targetList = isGenericDef ? generics : specifics; + targetList.Add( CodeGenerator.GetInheritdocString( $"{INameInheritDoc}.{memName}" ) ); + targetList.Add( $"{access} {retType} {fName}({paramsDecl}){constraints} => {body};" ); + } + + string NameOf( Type type, bool isGenericDef ) => type.Name; // isGenericDef ? type.Name : type.FullName; + + string GetReturnTypeName( MethodInfo meth, bool isGenericDef ) { + // if( meth.ReturnType.IsGenericParameter ) + return meth.ReturnType.Name; + // return meth.ReturnType.FullName; + } + + string GetReturnTypeName( PropertyInfo prop ) { + Type type = prop.PropertyType; + if( type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ) { + // type.custom + Type[] args = type.GetGenericArguments(); + TupleElementNamesAttribute attr = prop.GetMethod.ReturnParameter.GetCustomAttribute(); + return "(" + string.Join( ",", args.Zip( attr.TransformNames, ( pType, label ) => label != null ? $"{pType} {label}" : pType.Name ) ) + ")"; + } + + return prop.PropertyType.Name; + + // Debug.Log( $"prop.PropertyType.FullName {prop.PropertyType.FullName}" ); + // Debug.Log( $"prop.GetMethod.ReturnType.FullName {prop.GetMethod.ReturnType.FullName}" ); + // if( prop.IsGenericMethod ) + // Debug.Log( $"prop.GetMethod.GetGenericMethodDefinition().ReturnType {prop.GetMethod.GetGenericMethodDefinition().ReturnType}" ); + + + // prop.GetMethod.gene + + // if(prop.typ) + + // return prop.GetMethod.IsGenericMethod ? prop.GetMethod.GetGenericMethodDefinition().ReturnType.FullName : type.FullName; + } + + + } + +} \ No newline at end of file diff --git a/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta b/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta new file mode 100644 index 0000000..c212dc3 --- /dev/null +++ b/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 33527b6429cb4500b0f22c7cfd7c5fff +timeCreated: 1775855648 \ No newline at end of file diff --git a/Editor/Mathfs.Editor.asmdef b/Editor/Mathfs.Editor.asmdef new file mode 100644 index 0000000..63e841f --- /dev/null +++ b/Editor/Mathfs.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Mathfs.Editor", + "rootNamespace": "", + "references": [ + "GUID:6071c9f2ce0a4407c93af459fa416e54", + "GUID:d8b63aba1907145bea998dd612889d6b" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/Mathfs.Editor.asmdef.meta b/Editor/Mathfs.Editor.asmdef.meta new file mode 100644 index 0000000..378efcc --- /dev/null +++ b/Editor/Mathfs.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e310b6bc7297442988d0ba2f3b166f3a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Property Drawers.meta b/Editor/Property Drawers.meta new file mode 100644 index 0000000..9e08f0b --- /dev/null +++ b/Editor/Property Drawers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c50cca08ed628843a5c7077b998eef6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Property Drawers/ClampedIntegerDrawers.cs b/Editor/Property Drawers/ClampedIntegerDrawers.cs new file mode 100644 index 0000000..7f42d16 --- /dev/null +++ b/Editor/Property Drawers/ClampedIntegerDrawers.cs @@ -0,0 +1,81 @@ +namespace Freya { + + using UnityEngine; + using UnityEditor; + + public class ClampedIntegerDrawer : PropertyDrawer { + protected bool hasInitialized; // this flag is used to ensure it's valid when the inspector reveals it + } + + [CustomPropertyDrawer( typeof(PositiveIntegerAttribute) )] public class PositiveIntegerDrawer : ClampedIntegerDrawer { + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, false, false, true ); + } + + [CustomPropertyDrawer( typeof(NegativeIntegerAttribute) )] public class NegativeIntegerDrawer : ClampedIntegerDrawer { + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, false, false ); + } + + [CustomPropertyDrawer( typeof(NonNegativeIntegerAttribute) )] public class NonNegativeIntegerDrawer : ClampedIntegerDrawer { + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, false, true, true ); + } + + [CustomPropertyDrawer( typeof(NonPositiveIntegerAttribute) )] public class NonPositiveIntegerDrawer : ClampedIntegerDrawer { + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, true, false ); + } + + [CustomPropertyDrawer( typeof(NonZeroIntegerAttribute) )] public class NonZeroIntegerDrawer : ClampedIntegerDrawer { + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, false, true ); + } + + static class IntegerDrawerUtils { + + public static void OnGUI( ref bool hasInitialized, Rect rect, SerializedProperty property, GUIContent label, bool allowNegative, bool allowZero, bool allowPositive ) { + EditorGUI.BeginProperty( rect, label, property ); + + if( property.propertyType == SerializedPropertyType.Integer ) { + // PositiveIntegerAttribute range = attribute as PositiveIntegerAttribute; + using( EditorGUI.ChangeCheckScope chChk = new() ) { + int prevValue = property.intValue; + + EditorGUI.PropertyField( rect, property, label ); + if( chChk.changed || hasInitialized == false ) { + hasInitialized = true; + int newValue = property.intValue; + // special case, make it "skip" 0 when scrubbing + if( allowNegative && allowZero == false && allowPositive && newValue == 0 ) { + property.intValue = prevValue > 0 ? -1 : 1; + } else { + // other cases can clamp + int min = GetRangeMin( allowNegative, allowZero, allowPositive ); + int max = GetRangeMax( allowNegative, allowZero, allowPositive ); + if( newValue < min || newValue > max ) + property.intValue = Mathf.Clamp( newValue, min, max ); + } + } + } + } else { + EditorGUI.LabelField( rect, label.text, $"PositiveInteger only works on integer fields. Field is: {property.propertyType.ToString()}" ); + } + + EditorGUI.EndProperty(); + } + + static int GetRangeMin( bool allowNegative, bool allowZero, bool allowPositive ) { + if( allowNegative ) + return int.MinValue; + if( allowZero ) + return 0; + return 1; + } + + static int GetRangeMax( bool allowNegative, bool allowZero, bool allowPositive ) { + if( allowPositive ) + return int.MaxValue; + if( allowZero ) + return 0; + return -1; + } + + } + +} \ No newline at end of file diff --git a/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta b/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta new file mode 100644 index 0000000..bfa43bf --- /dev/null +++ b/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b35a7699a7b00a441a973209a563f913 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Property Drawers/RationalDrawer.cs b/Editor/Property Drawers/RationalDrawer.cs new file mode 100644 index 0000000..49dfa9d --- /dev/null +++ b/Editor/Property Drawers/RationalDrawer.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Freya { + + [CustomPropertyDrawer( typeof(rat) )] + public class IngredientDrawer : PropertyDrawer { + + bool hasInitialized; + + // Draw the property inside the given rect + public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) { + // Using BeginProperty / EndProperty on the parent property means that + // prefab override logic works on the entire property. + EditorGUI.BeginProperty( rect, label, property ); + + // Draw label + rect = EditorGUI.PrefixLabel( rect, GUIUtility.GetControlID( FocusType.Passive ), label ); + + // Don't make child fields be indented + int indent = EditorGUI.indentLevel; + EditorGUI.indentLevel = 0; + + // Calculate rects + int slashWidth = 10; + int inputBoxWidth = Mathfs.FloorToInt( ( rect.width - slashWidth ) / 2 ); + Rect rectNumerator = new Rect( rect.x, rect.y, inputBoxWidth, rect.height ); + Rect rectDivision = new Rect( rect.x + inputBoxWidth, rect.y, slashWidth, rect.height ); + Rect rectDenominator = new Rect( rect.x + inputBoxWidth + slashWidth, rect.y, inputBoxWidth, rect.height ); + + SerializedProperty numerator = property.FindPropertyRelative( "n" ); + SerializedProperty denominator = property.FindPropertyRelative( "d" ); + + using( var chChk = new EditorGUI.ChangeCheckScope() ) { + EditorGUI.DelayedIntField( rectNumerator, numerator, GUIContent.none ); + GUI.Label( rectDivision, "/" ); + EditorGUI.DelayedIntField( rectDenominator, denominator, GUIContent.none ); + if( chChk.changed || hasInitialized == false ) { + hasInitialized = true; + // validation, make sure we're not dividing by 0 + if( denominator.intValue == 0 ) { + denominator.intValue = 1; + Debug.LogWarning( "Rational numbers cannot divide by 0. Setting denominator to 1", property.serializedObject.targetObject ); + } + } + } + + // Set indent back to what it was + EditorGUI.indentLevel = indent; + + EditorGUI.EndProperty(); + } + } + +} \ No newline at end of file diff --git a/Editor/Property Drawers/RationalDrawer.cs.meta b/Editor/Property Drawers/RationalDrawer.cs.meta new file mode 100644 index 0000000..2dc1257 --- /dev/null +++ b/Editor/Property Drawers/RationalDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a6c3402c7b9d5aa4b8518af9657a070c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Extensions.cs b/Extensions.cs deleted file mode 100644 index 5262558..0000000 --- a/Extensions.cs +++ /dev/null @@ -1,513 +0,0 @@ -// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) - -using System.Runtime.CompilerServices; -using UnityEngine; - - -namespace Freya { - - /// Various extensions for floats, vectors and colors - public static class MathfsExtensions { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - #region Vector rotation and angles - - /// Returns the angle of this vector, in radians - /// The vector to get the angle of. It does not have to be normalized - /// - [MethodImpl( INLINE )] public static float Angle( this Vector2 v ) => Mathf.Atan2( v.y, v.x ); - - /// Rotates the vector 90 degrees clockwise (negative Z axis rotation) - [MethodImpl( INLINE )] public static Vector2 Rotate90CW( this Vector2 v ) => new Vector2( v.y, -v.x ); - - /// Rotates the vector 90 degrees counter-clockwise (positive Z axis rotation) - [MethodImpl( INLINE )] public static Vector2 Rotate90CCW( this Vector2 v ) => new Vector2( -v.y, v.x ); - - /// Rotates the vector around pivot with the given angle (in radians) - /// The vector to rotate - /// The point to rotate around - /// The angle to rotate by, in radians - [MethodImpl( INLINE )] public static Vector2 RotateAround( this Vector2 v, Vector2 pivot, float angRad ) => Rotate( v - pivot, angRad ) + pivot; - - /// Rotates the vector around (0,0) with the given angle (in radians) - /// The vector to rotate - /// The angle to rotate by, in radians - public static Vector2 Rotate( this Vector2 v, float angRad ) { - float ca = Mathf.Cos( angRad ); - float sa = Mathf.Sin( angRad ); - return new Vector2( ca * v.x - sa * v.y, sa * v.x + ca * v.y ); - } - - /// Converts an angle in degrees to radians - /// The angle, in degrees, to convert to radians - [MethodImpl( INLINE )] public static float DegToRad( this float angDegrees ) => angDegrees * Mathfs.Deg2Rad; - - /// Converts an angle in radians to degrees - /// The angle, in radians, to convert to degrees - [MethodImpl( INLINE )] public static float RadToDeg( this float angRadians ) => angRadians * Mathfs.Rad2Deg; - - /// Extracts the quaternion components into a Vector4 - /// The quaternion to get the components of - [MethodImpl( INLINE )] public static Vector4 ToVector4( this Quaternion q ) => new Vector4( q.x, q.y, q.z, q.w ); - - #endregion - - #region Swizzling - - /// Returns X and Y as a Vector2, equivalent to new Vector2(v.x,v.y) - [MethodImpl( INLINE )] public static Vector2 XY( this Vector2 v ) => new Vector2( v.y, v.x ); - - /// Returns Y and X as a Vector2, equivalent to new Vector2(v.y,v.x) - [MethodImpl( INLINE )] public static Vector2 YX( this Vector2 v ) => new Vector2( v.y, v.x ); - - /// Returns X and Z as a Vector2, equivalent to new Vector2(v.x,v.z) - [MethodImpl( INLINE )] public static Vector2 XZ( this Vector3 v ) => new Vector2( v.x, v.z ); - - /// Returns this vector as a Vector3, slotting X into X, and Y into Z, and the input value y into Y. - /// Equivalent to new Vector3(v.x,y,v.y) - [MethodImpl( INLINE )] public static Vector3 XZtoXYZ( this Vector2 v, float y = 0 ) => new Vector3( v.x, y, v.y ); - - /// Returns this vector as a Vector3, slotting X into X, and Y into Y, and the input value z into Z. - /// Equivalent to new Vector3(v.x,v.y,z) - [MethodImpl( INLINE )] public static Vector3 XYtoXYZ( this Vector2 v, float z = 0 ) => new Vector3( v.x, v.y, z ); - - /// Sets X to 0 - [MethodImpl( INLINE )] public static Vector2 FlattenX( this Vector2 v ) => new Vector2( 0f, v.y ); - - /// Sets Y to 0 - [MethodImpl( INLINE )] public static Vector2 FlattenY( this Vector2 v ) => new Vector2( v.x, 0f ); - - /// Sets X to 0 - [MethodImpl( INLINE )] public static Vector3 FlattenX( this Vector3 v ) => new Vector3( 0f, v.y, v.z ); - - /// Sets Y to 0 - [MethodImpl( INLINE )] public static Vector3 FlattenY( this Vector3 v ) => new Vector3( v.x, 0f, v.z ); - - /// Sets Z to 0 - [MethodImpl( INLINE )] public static Vector3 FlattenZ( this Vector3 v ) => new Vector3( v.x, v.y, 0f ); - - #endregion - - #region Vector directions & magnitudes - - /// Returns a vector with the same direction, but with the given magnitude. - /// Equivalent to v.normalized*mag - [MethodImpl( INLINE )] public static Vector2 WithMagnitude( this Vector2 v, float mag ) => v.normalized * mag; - - /// Returns a vector with the same direction, but with the given magnitude. - /// Equivalent to v.normalized*mag - [MethodImpl( INLINE )] public static Vector3 WithMagnitude( this Vector3 v, float mag ) => v.normalized * mag; - - /// Returns the vector going from one position to another, also known as the displacement. - /// Equivalent to target-v - [MethodImpl( INLINE )] public static Vector2 To( this Vector2 v, Vector2 target ) => target - v; - - /// Returns the vector going from one position to another, also known as the displacement. - /// Equivalent to target-v - [MethodImpl( INLINE )] public static Vector3 To( this Vector3 v, Vector3 target ) => target - v; - - /// Returns the normalized direction from this vector to the target. - /// Equivalent to (target-v).normalized or v.To(target).normalized - [MethodImpl( INLINE )] public static Vector2 DirTo( this Vector2 v, Vector2 target ) => ( target - v ).normalized; - - /// Returns the normalized direction from this vector to the target. - /// Equivalent to (target-v).normalized or v.To(target).normalized - [MethodImpl( INLINE )] public static Vector3 DirTo( this Vector3 v, Vector3 target ) => ( target - v ).normalized; - - #endregion - - #region Color manipulation - - /// Returns the same color, but with the specified alpha value - /// The source color - /// The new alpha value - [MethodImpl( INLINE )] public static Color WithAlpha( this Color c, float a ) => new Color( c.r, c.g, c.b, a ); - - /// Returns the same color and alpha, but with RGB multiplied by the given value - /// The source color - /// The multiplier for the RGB channels - [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, float m ) => new Color( c.r * m, c.g * m, c.b * m, c.a ); - - /// Returns the same color and alpha, but with the RGB values multiplief by another color - /// The source color - /// The color to multiply RGB by - [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, Color m ) => new Color( c.r * m.r, c.g * m.g, c.b * m.b, c.a ); - - /// Returns the same color, but with the alpha channel multiplied by the given value - /// The source color - /// The multiplier for the alpha - [MethodImpl( INLINE )] public static Color MultiplyA( this Color c, float m ) => new Color( c.r, c.g, c.b, c.a * m ); - - #endregion - - #region Rect - - /// Expands the rectangle to encapsulate the point p - /// The rectangle to expand - /// The point to encapsulate - public static Rect Encapsulate( this Rect r, Vector2 p ) { - r.xMax = Mathf.Max( r.xMax, p.x ); - r.xMin = Mathf.Min( r.xMin, p.x ); - r.yMax = Mathf.Max( r.yMax, p.y ); - r.yMin = Mathf.Min( r.yMin, p.y ); - return r; - } - - #endregion - - #region Simple float and int operations - - /// Returns true if v is between or equal to min & max - /// - [MethodImpl( INLINE )] public static bool Within( this float v, float min, float max ) => v >= min && v <= max; - - /// Returns true if v is between or equal to min & max - /// - [MethodImpl( INLINE )] public static bool Within( this int v, int min, int max ) => v >= min && v <= max; - - /// Returns true if v is between, but not equal to, min & max - /// - [MethodImpl( INLINE )] public static bool Between( this float v, float min, float max ) => v > min && v < max; - - /// Returns true if v is between, but not equal to, min & max - /// - [MethodImpl( INLINE )] public static bool Between( this int v, int min, int max ) => v > min && v < max; - - /// Clamps the value to be at least min - [MethodImpl( INLINE )] public static float AtLeast( this float v, float min ) => v < min ? min : v; - - /// Clamps the value to be at least min - [MethodImpl( INLINE )] public static int AtLeast( this int v, int min ) => v < min ? min : v; - - /// Clamps the value to be at most max - [MethodImpl( INLINE )] public static float AtMost( this float v, float max ) => v > max ? max : v; - - /// Clamps the value to be at most max - [MethodImpl( INLINE )] public static int AtMost( this int v, int max ) => v > max ? max : v; - - /// Squares the value. Equivalent to v*v - [MethodImpl( INLINE )] public static float Square( this float v ) => v * v; - - /// Cubes the value. Equivalent to v*v*v - [MethodImpl( INLINE )] public static float Cube( this float v ) => v * v * v; - - /// Squares the value. Equivalent to v*v - [MethodImpl( INLINE )] public static int Square( this int v ) => v * v; - - /// The next integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc - [MethodImpl( INLINE )] public static int NextMod( this int value, int length ) => ( value + 1 ).Mod( length ); - - /// The previous integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc - [MethodImpl( INLINE )] public static int PrevMod( this int value, int length ) => ( value - 1 ).Mod( length ); - - #endregion - - #region Extension method counterparts of the static Mathfs functions - lots of boilerplate in here - - #region Math operations - - /// - [MethodImpl( INLINE )] public static float Sqrt( this float value ) => Mathfs.Sqrt( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Sqrt( this Vector2 value ) => Mathfs.Sqrt( value ); - - /// - [MethodImpl( INLINE )] public static Vector3 Sqrt( this Vector3 value ) => Mathfs.Sqrt( value ); - - /// - [MethodImpl( INLINE )] public static Vector4 Sqrt( this Vector4 value ) => Mathfs.Sqrt( value ); - - /// - [MethodImpl( INLINE )] public static float Cbrt( this float value ) => Mathfs.Cbrt( value ); - - /// - [MethodImpl( INLINE )] public static float Pow( this float value, float exponent ) => Mathfs.Pow( value, exponent ); - - #endregion - - #region Absolute Values - - /// - [MethodImpl( INLINE )] public static float Abs( this float value ) => Mathfs.Abs( value ); - - /// - [MethodImpl( INLINE )] public static int Abs( this int value ) => Mathfs.Abs( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Abs( this Vector2 v ) => Mathfs.Abs( v ); - - /// - [MethodImpl( INLINE )] public static Vector3 Abs( this Vector3 v ) => Mathfs.Abs( v ); - - /// - [MethodImpl( INLINE )] public static Vector4 Abs( this Vector4 v ) => Mathfs.Abs( v ); - - #endregion - - #region Clamping - - /// - [MethodImpl( INLINE )] public static float Clamp( this float value, float min, float max ) => Mathfs.Clamp( value, min, max ); - - /// - [MethodImpl( INLINE )] public static Vector2 Clamp( this Vector2 v, Vector2 min, Vector2 max ) => Mathfs.Clamp( v, min, max ); - - /// - [MethodImpl( INLINE )] public static Vector3 Clamp( this Vector3 v, Vector3 min, Vector3 max ) => Mathfs.Clamp( v, min, max ); - - /// - [MethodImpl( INLINE )] public static Vector4 Clamp( this Vector4 v, Vector4 min, Vector4 max ) => Mathfs.Clamp( v, min, max ); - - /// - [MethodImpl( INLINE )] public static int Clamp( this int value, int min, int max ) => Mathfs.Clamp( value, min, max ); - - /// - [MethodImpl( INLINE )] public static float Clamp01( this float value ) => Mathfs.Clamp01( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Clamp01( this Vector2 v ) => Mathfs.Clamp01( v ); - - /// - [MethodImpl( INLINE )] public static Vector3 Clamp01( this Vector3 v ) => Mathfs.Clamp01( v ); - - /// - [MethodImpl( INLINE )] public static Vector4 Clamp01( this Vector4 v ) => Mathfs.Clamp01( v ); - - /// - [MethodImpl( INLINE )] public static float ClampNeg1to1( this float value ) => Mathfs.ClampNeg1to1( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 ClampNeg1to1( this Vector2 v ) => Mathfs.ClampNeg1to1( v ); - - /// - [MethodImpl( INLINE )] public static Vector3 ClampNeg1to1( this Vector3 v ) => Mathfs.ClampNeg1to1( v ); - - /// - [MethodImpl( INLINE )] public static Vector4 ClampNeg1to1( this Vector4 v ) => Mathfs.ClampNeg1to1( v ); - - #endregion - - #region Min & Max - - /// - [MethodImpl( INLINE )] public static float Min( this Vector2 v ) => Mathfs.Min( v ); - - /// - [MethodImpl( INLINE )] public static float Min( this Vector3 v ) => Mathfs.Min( v ); - - /// - [MethodImpl( INLINE )] public static float Min( this Vector4 v ) => Mathfs.Min( v ); - - /// - [MethodImpl( INLINE )] public static float Max( this Vector2 v ) => Mathfs.Max( v ); - - /// - [MethodImpl( INLINE )] public static float Max( this Vector3 v ) => Mathfs.Max( v ); - - /// - [MethodImpl( INLINE )] public static float Max( this Vector4 v ) => Mathfs.Max( v ); - - #endregion - - #region Signs & Rounding - - /// - [MethodImpl( INLINE )] public static float Sign( this float value ) => Mathfs.Sign( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Sign( this Vector2 value ) => Mathfs.Sign( value ); - - /// - [MethodImpl( INLINE )] public static Vector3 Sign( this Vector3 value ) => Mathfs.Sign( value ); - - /// - [MethodImpl( INLINE )] public static Vector4 Sign( this Vector4 value ) => Mathfs.Sign( value ); - - /// - [MethodImpl( INLINE )] public static int Sign( this int value ) => Mathfs.Sign( value ); - - /// - [MethodImpl( INLINE )] public static int SignAsInt( this float value ) => Mathfs.SignAsInt( value ); - - /// - [MethodImpl( INLINE )] public static float SignWithZero( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); - - /// - [MethodImpl( INLINE )] public static Vector2 SignWithZero( this Vector2 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); - - /// - [MethodImpl( INLINE )] public static Vector3 SignWithZero( this Vector3 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); - - /// - [MethodImpl( INLINE )] public static Vector4 SignWithZero( this Vector4 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); - - /// - [MethodImpl( INLINE )] public static int SignWithZero( this int value ) => Mathfs.SignWithZero( value ); - - /// - [MethodImpl( INLINE )] public static int SignWithZeroAsInt( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZeroAsInt( value, zeroThreshold ); - - /// - [MethodImpl( INLINE )] public static float Floor( this float value ) => Mathfs.Floor( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Floor( this Vector2 value ) => Mathfs.Floor( value ); - - /// - [MethodImpl( INLINE )] public static Vector3 Floor( this Vector3 value ) => Mathfs.Floor( value ); - - /// - [MethodImpl( INLINE )] public static Vector4 Floor( this Vector4 value ) => Mathfs.Floor( value ); - - /// - [MethodImpl( INLINE )] public static int FloorToInt( this float value ) => Mathfs.FloorToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector2Int FloorToInt( this Vector2 value ) => Mathfs.FloorToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector3Int FloorToInt( this Vector3 value ) => Mathfs.FloorToInt( value ); - - /// - [MethodImpl( INLINE )] public static float Ceil( this float value ) => Mathfs.Ceil( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Ceil( this Vector2 value ) => Mathfs.Ceil( value ); - - /// - [MethodImpl( INLINE )] public static Vector3 Ceil( this Vector3 value ) => Mathfs.Ceil( value ); - - /// - [MethodImpl( INLINE )] public static Vector4 Ceil( this Vector4 value ) => Mathfs.Ceil( value ); - - /// - [MethodImpl( INLINE )] public static int CeilToInt( this float value ) => Mathfs.CeilToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector2Int CeilToInt( this Vector2 value ) => Mathfs.CeilToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector3Int CeilToInt( this Vector3 value ) => Mathfs.CeilToInt( value ); - - /// - [MethodImpl( INLINE )] public static float Round( this float value ) => Mathfs.Round( value ); - - /// - [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value ) => Mathfs.Round( value ); - - /// - [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value ) => Mathfs.Round( value ); - - /// - [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value ) => Mathfs.Round( value ); - - /// - [MethodImpl( INLINE )] public static float Round( this float value, float snapInterval ) => Mathfs.Round( value, snapInterval ); - - /// - [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value, float snapInterval ) => Mathfs.Round( value, snapInterval ); - - /// - [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value, float snapInterval ) => Mathfs.Round( value, snapInterval ); - - /// - [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value, float snapInterval ) => Mathfs.Round( value, snapInterval ); - - /// - [MethodImpl( INLINE )] public static int RoundToInt( this float value ) => Mathfs.RoundToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector2Int RoundToInt( this Vector2 value ) => Mathfs.RoundToInt( value ); - - /// - [MethodImpl( INLINE )] public static Vector3Int RoundToInt( this Vector3 value ) => Mathfs.RoundToInt( value ); - - #endregion - - #region Range Repeating - - /// - [MethodImpl( INLINE )] public static float Frac( this float x ) => Mathfs.Frac( x ); - - /// - [MethodImpl( INLINE )] public static Vector2 Frac( this Vector2 v ) => Mathfs.Frac( v ); - - /// - [MethodImpl( INLINE )] public static Vector3 Frac( this Vector3 v ) => Mathfs.Frac( v ); - - /// - [MethodImpl( INLINE )] public static Vector4 Frac( this Vector4 v ) => Mathfs.Frac( v ); - - /// - [MethodImpl( INLINE )] public static float Repeat( this float value, float length ) => Mathfs.Repeat( value, length ); - - /// - [MethodImpl( INLINE )] public static int Mod( this int value, int length ) => Mathfs.Mod( value, length ); - - #endregion - - #region Smoothing & Easing Curves - - /// - [MethodImpl( INLINE )] public static float Smooth01( this float x ) => Mathfs.Smooth01( x ); - - /// - [MethodImpl( INLINE )] public static float Smoother01( this float x ) => Mathfs.Smoother01( x ); - - /// - [MethodImpl( INLINE )] public static float SmoothCos01( this float x ) => Mathfs.SmoothCos01( x ); - - #endregion - - #region Value & Vector interpolation - - /// - [MethodImpl( INLINE )] public static float Remap( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, value ); - - /// - [MethodImpl( INLINE )] public static float Remap( this int value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerp( iMin, iMax, value ) ); - - /// - [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 v, Vector2 iMin, Vector2 iMax, Vector2 oMin, Vector2 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v ); - - /// - [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 v, Vector3 iMin, Vector3 iMax, Vector3 oMin, Vector3 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v ); - - /// - [MethodImpl( INLINE )] public static Vector4 Remap( this Vector4 v, Vector4 iMin, Vector4 iMax, Vector4 oMin, Vector4 oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerp( iMin, iMax, v ) ); - - /// - [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 iPos, Rect iRect, Rect oRect ) => Mathfs.Remap( iRect.min, iRect.max, oRect.min, oRect.max, iPos ); - - /// - [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 iPos, Bounds iBounds, Bounds oBounds ) => Mathfs.Remap( iBounds.min, iBounds.max, oBounds.min, oBounds.max, iPos ); - - /// - [MethodImpl( INLINE )] public static float RemapClamped( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerpClamped( iMin, iMax, value ) ); - - #endregion - - #region Vector Math - - /// - [MethodImpl( INLINE )] public static (Vector2 dir, float magnitude ) GetDirAndMagnitude( this Vector2 v ) => Mathfs.GetDirAndMagnitude( v ); - - /// - [MethodImpl( INLINE )] public static (Vector3 dir, float magnitude ) GetDirAndMagnitude( this Vector3 v ) => Mathfs.GetDirAndMagnitude( v ); - - /// - [MethodImpl( INLINE )] public static Vector2 ClampMagnitude( this Vector2 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max ); - - /// - [MethodImpl( INLINE )] public static Vector3 ClampMagnitude( this Vector3 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max ); - - #endregion - - #endregion - - - } - -} \ No newline at end of file diff --git a/LICENSE.txt.meta b/LICENSE.txt.meta new file mode 100644 index 0000000..be2250f --- /dev/null +++ b/LICENSE.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e2a7f2371f43d4faca3cdab60b7679a7 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 7196817..f848811 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,26 @@ # Mathfs -Expanded Math Functionality for Unity +Freya's expanded math functionality for Unity! +- This is primarily a way for me to share the math functionality I write and use in my own personal projects +- I will recklessly edit and adapt things without too much thought into backwards compatibility +- Minimum Unity version is currently 2021.2 due to using newer C# version features. It may be possible to auto-downgrade through your IDE if necessary +- Commits with version tags should be relatively stable. Other commits may not be + +## Installation instructions + +There are several ways to install this library into your project: + +- **Plain install** + - Clone or [download](https://github.com/FreyaHolmer/Mathfs/archive/refs/heads/master.zip) this repository and put it somewhere in the Assets folder of your Unity project +- **Unity Package Manager (UPM)**: + - Add either of the the following lines to *Packages/manifest.json*: + - `"com.acegikmo.mathfs": "https://github.com/FreyaHolmer/Mathfs.git#0.1.0",` if you want to target a specific version (recommended) + - `"com.acegikmo.mathfs": "https://github.com/FreyaHolmer/Mathfs.git",` if you want to pull the latest commit (potentially unstable) + - More information about UPM and git [here](https://docs.unity3d.com/Manual/upm-git.html) +- **[OpenUPM](https://openupm.com)** + - After installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command: + - `openupm add com.acegikmo.mathfs` + +After installation you will be able to access the library in scripts by including the namespace `using Freya` ## Features - 2D Intersection tests between all combinations of: @@ -13,7 +34,7 @@ Expanded Math Functionality for Unity - Catmull-Rom - B-Spline (Uniform Cubic & Generalized Non-Uniform) - NURBS (Non-Unifrom Rational B-Spline) - - Trajectory (Cubic) + - Trajectory (Cubic & Generalized) - Trajectory math - GetDisplacement (point in trajectory), given gravity, angle, speed & time - GetLaunchSpeed, given gravity, angle & lateral distance diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..d4e58cb --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c12d18034743d4bd7b7992ffada185ec +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..31d92cb --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 19167c22abdb4380b3b9de2d78ac7819 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves.meta b/Runtime/Curves.meta new file mode 100644 index 0000000..4dbd77f --- /dev/null +++ b/Runtime/Curves.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 06a46a22856424f5184e3ca3d254c609 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Arc2D.cs b/Runtime/Curves/Arc2D.cs new file mode 100644 index 0000000..e188c73 --- /dev/null +++ b/Runtime/Curves/Arc2D.cs @@ -0,0 +1,94 @@ +using System; +using UnityEngine; + +namespace Freya { + + /// a 2D arc with support for straight lines + [Serializable] + public struct Arc2D { + + /// The starting point of the arc + public Transform2D placement; + /// The signed curvature of the arc, equal to 1/radius (0 = straight line, 1 = turning left, -1 = turning right) + public float curvature; + /// The length of the arc + public float length; + + /// The radius of the circle traced by the arc. Returns infinity if this segment is linear, ie: if curvature is 0 + public float Radius => 1f / MathF.Abs( curvature ); + /// The center of the circle traced by the arc. Returns infinity if this segment is linear, ie: if curvature is 0 + public Vector2 CircleCenter => StartNormal / curvature; + /// The normal direction at the start of the arc + public Vector2 StartNormal => placement.AxisY; + /// The tangent direction at the start of the arc + public Vector2 StartTangent => placement.AxisX; + /// The normal direction at the end of the arc + public Vector2 EndNormal => GetNormal( length ); + /// The end point of the arc + public Vector2 EndPoint => GetPosition( length ); + /// The signed angular span covered across the arc. This returns 0 if this segment is linear, ie: if curvature is 0 + public float AngularSpan => length * curvature; // s = ra ▶ s = a/k ▶ sk = a + /// Whether or not this is a straight line rather than an arc, ie: if curvature is 0 + public bool IsStraight => Mathfs.Approximately( curvature, 0 ); + + /// Evaluates the position of this arc at the given arc length s + public Vector2 GetPosition( float s ) => Eval( s, nThDerivative: 0 ); + + /// Evaluates the tangent direction of this arc at the given arc length s + public Vector2 GetTangent( float s ) => s == 0 ? StartTangent : Eval( s, nThDerivative: 1 ); // no need to normalize, it's already arc-length parameterized + + /// Evaluates the normal direction of this arc at the given arc length s + public Vector2 GetNormal( float s ) => Eval( s, nThDerivative: 1 ).Rotate90CCW(); // no need to normalize, it's already arc-length parameterized + + /// Evaluates the given derivative of this arc, by arc length s + public Vector2 Eval( float s, int nThDerivative = 0 ) { + float ang = s * curvature; + float x, y; + + switch( nThDerivative ) { + case 0: + x = s * Mathfs.Sinc( ang ); + y = s * Mathfs.Cosinc( ang ); + return placement.TransformPoint( x, y ); + case 1: + x = MathF.Cos( ang ); + y = MathF.Sin( ang ); + break; + case 2: + x = -curvature * MathF.Sin( ang ); + y = +curvature * MathF.Cos( ang ); + break; + case 3: + float k2 = curvature * curvature; + x = -k2 * MathF.Cos( ang ); + y = -k2 * MathF.Sin( ang ); + break; + case 4: + float k3 = curvature * curvature * curvature; + x = +k3 * MathF.Sin( ang ); + y = -k3 * MathF.Cos( ang ); + break; + case 5: + float _k2 = curvature * curvature; + float k4 = _k2 * _k2; + x = k4 * MathF.Cos( ang ); + y = k4 * MathF.Sin( ang ); + break; + default: + // general form for n > 0 + float scale = MathF.Pow( curvature, nThDerivative - 1 ); + int xSgn = nThDerivative / 2 % 2 == 0 ? 1 : -1; + int ySgn = ( nThDerivative - 1 ) / 2 % 2 == 0 ? 1 : -1; + bool even = nThDerivative % 2 == 0; + x = xSgn * scale * ( even ? MathF.Sin( ang ) : MathF.Cos( ang ) ); + y = ySgn * scale * ( even ? MathF.Cos( ang ) : MathF.Sin( ang ) ); + break; + } + + // space transformation + return placement.TransformVector( x, y ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Arc2D.cs.meta b/Runtime/Curves/Arc2D.cs.meta new file mode 100644 index 0000000..29658a3 --- /dev/null +++ b/Runtime/Curves/Arc2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 723d5270bbbe92b47b5152cbbc8928cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Catenary.cs b/Runtime/Curves/Catenary.cs new file mode 100644 index 0000000..92a8136 --- /dev/null +++ b/Runtime/Curves/Catenary.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine; + +namespace Freya { + + /// Catenary math utility functions + public static class Catenary { + + /// Returns the y coordinate of a catenary at the given x value + /// The x coordinate to evaluate at + /// The a-parameter of the catenary + public static float Eval( float x, float a ) => a * Mathfs.Cosh( x / a ); + + /// 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 + /// The x coordinate to get the length to + /// The a-parameter of the catenary + public static float EvalArcLen( float x, float a ) => a * Mathfs.Sinh( x / a ); + + /// 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 + /// The arc length to get the x coordinate of + /// The a-parameter of the catenary + public static float EvalXByArcLength( float s, float a ) => a * Mathfs.Asinh( s / a ); + + /// 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 + /// The arc length coordinate to get the tangent of + /// The a-parameter of the catenary + 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 ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Catenary.cs.meta b/Runtime/Curves/Catenary.cs.meta new file mode 100644 index 0000000..053d6e7 --- /dev/null +++ b/Runtime/Curves/Catenary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b30c699ff7f0afe4aa0d122f4a3e313f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Catenary2D.cs b/Runtime/Curves/Catenary2D.cs new file mode 100644 index 0000000..a269e4b --- /dev/null +++ b/Runtime/Curves/Catenary2D.cs @@ -0,0 +1,74 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using UnityEngine; + +namespace Freya { + + /// A catenary curve passing through two points with a given an arc length + public struct Catenary2D { + + enum Evaluability { + NotReady, + Ready + } + + // data + Vector2 p1; + CatenaryToPoint catenary; // stores arc length + Transform2D space; // stores p0 and slack direction + Evaluability evaluability; + + public float Length { + get => catenary.Length; + set => catenary.Length = value; // does not change evaluability of this type, since space hasn't changed + } + public Vector2 P0 { + get => space.Origin; + set { + if( value != space.Origin ) + ( space.Origin, evaluability ) = ( value, Evaluability.NotReady ); + } + } + public Vector2 P1 { + get => p1; + set { + if( value != p1 ) + ( p1, evaluability ) = ( value, Evaluability.NotReady ); + } + } + public Vector2 SlackDirection { + get => -space.AxisY; + set { + if( value != SlackDirection ) + ( space.AxisY, evaluability ) = ( -value, Evaluability.NotReady ); + } + } + + /// + public Catenary2D( Vector2 p0, Vector2 p1, float length, Vector2 slackDirection ) { + space = default; + catenary = new CatenaryToPoint( p1 - p0, length ); + ( space.Origin, this.p1 ) = ( p0, p1 ); + evaluability = Evaluability.NotReady; + } + + /// + public Vector3 Eval( float sEval, int n = 1 ) { + ReadyForEvaluation(); + return n switch { + 0 => space.TransformPoint( catenary.Eval( sEval, 0 ) ), + _ => space.TransformVector( catenary.Eval( sEval, n ) ) + }; + } + + // ensures the space transformation is ready + void ReadyForEvaluation() { + if( evaluability == Evaluability.Ready ) + return; + catenary.P = space.InverseTransformPoint( p1 ); + evaluability = Evaluability.Ready; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Catenary2D.cs.meta b/Runtime/Curves/Catenary2D.cs.meta new file mode 100644 index 0000000..4e495e2 --- /dev/null +++ b/Runtime/Curves/Catenary2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04762b34aa4b834489c20d76358b2d7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Catenary3D.cs b/Runtime/Curves/Catenary3D.cs new file mode 100644 index 0000000..9ea9d45 --- /dev/null +++ b/Runtime/Curves/Catenary3D.cs @@ -0,0 +1,80 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using UnityEngine; + +namespace Freya { + + /// A catenary curve passing through two points with a given an arc length + public struct Catenary3D { + + enum Evaluability { + NotReady, + Ready + } + + // data + Vector3 p1; + CatenaryToPoint cat2D; // also stores arc length + Plane2DIn3D space; // stores p0 and slack direction + Evaluability evaluability; + + public float Length { + get => cat2D.Length; + set => cat2D.Length = value; // does not change evaluability of this type, since space hasn't changed + } + public Vector3 P0 { + get => space.origin; + set { + if( value != space.origin ) + ( space.origin, evaluability ) = ( value, Evaluability.NotReady ); + } + } + public Vector3 P1 { + get => p1; + set { + if( value != p1 ) + ( p1, evaluability ) = ( value, Evaluability.NotReady ); + } + } + public Vector3 SlackDirection { + get => -space.axisY; + set { + if( value != SlackDirection ) + ( space.axisY, evaluability ) = ( -value, Evaluability.NotReady ); + } + } + + /// Creates a catenary curve between two points, given an arc length s and a slack direction + /// The start of the curve + /// The end of the curve + /// The length of the curve. note: has to be equal or longer than the distance between the points + /// The direction of "gravity" for the arc + public Catenary3D( Vector3 p0, Vector3 p1, float length, Vector3 slackDirection ) { + cat2D = new CatenaryToPoint( p1 - p0, length ); + space.axisX = default; // set on first evaluation by RotateAroundYToInclude + ( space.origin, space.axisY, this.p1 ) = ( p0, -slackDirection, p1 ); + evaluability = Evaluability.NotReady; + } + + /// + public Vector3 Eval( float sEval, int n = 1 ) { + ReadyForEvaluation(); + return n switch { + 0 => space.TransformPoint( cat2D.Eval( sEval, 0 ) ), + _ => space.TransformVector( cat2D.Eval( sEval, n ) ) + }; + } + + // ensures the space transformation is ready + void ReadyForEvaluation() { + if( evaluability == Evaluability.Ready ) + return; + // ready the embedded plane of the catenary and assign the 2D endpoint + space.RotateAroundYToInclude( P1, out Vector2 p1Local ); + cat2D.P = p1Local; + evaluability = Evaluability.Ready; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Catenary3D.cs.meta b/Runtime/Curves/Catenary3D.cs.meta new file mode 100644 index 0000000..8fad120 --- /dev/null +++ b/Runtime/Curves/Catenary3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b20ae836093c6c4891a5bd00c155a6a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/CatenaryToPoint.cs b/Runtime/Curves/CatenaryToPoint.cs new file mode 100644 index 0000000..212f897 --- /dev/null +++ b/Runtime/Curves/CatenaryToPoint.cs @@ -0,0 +1,215 @@ +using System; +using UnityEngine; + +namespace Freya { + + /// A catenary curve from the origin to a point P + public struct CatenaryToPoint { + + enum Evaluability { + Unknown = 0, + Catenary, + LinearVertical, + LineSegment + } + + const int INTERVAL_SEARCH_ITERATIONS = 12; + const int BISECT_REFINE_COUNT = 14; + + // data + Vector2 p; + float s; + + // cached state + Evaluability evaluability; + float a; + Vector2 delta; + float arcLenSampleOffset; + + public CatenaryToPoint( Vector2 p, float s ) { + ( this.p, this.s ) = ( p, s ); + a = default; + delta = default; + arcLenSampleOffset = default; + evaluability = Evaluability.Unknown; + } + + public Vector2 P { + get => p; + set { + if( value != p ) + ( p, evaluability ) = ( value, Evaluability.Unknown ); + } + } + + public float Length { + get => s; + set { + if( value != s ) + ( s, evaluability ) = ( value, Evaluability.Unknown ); + } + } + + public bool IsVertical => MathF.Abs( p.x ) < 0.001f; + public bool IsStraightLine => s <= p.magnitude * 1.00005f; + + /// Evaluates a position on this catenary curve at the given arc length of sEval + /// The arc length along the curve to sample, relative to the first point + /// The derivative to sample. 1 = first derivative, 2 = second derivative + public Vector2 Eval( float sEval, int nthDerivative = 0 ) { + ReadyForEvaluation(); + return nthDerivative switch { + 0 => 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" ) + }, + _ => evaluability switch { + Evaluability.Catenary => EvalCatDerivByArcLength( sEval, nthDerivative ), + Evaluability.LineSegment => nthDerivative == 1 ? p.normalized : Vector2.zero, + Evaluability.LinearVertical => new Vector2( 0, nthDerivative == 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 ) => p * ( 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 ); + } + + // 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 = Catenary.EvalXByArcLength( sEval + arcLenSampleOffset, a ) + delta.x; + float y = EvalPassingThrough0( x ); + return new Vector2( x, y ); + } + + /// Evaluates the n-th derivative of the catenary at the given arc length + /// The arc length, relative to the first point + /// The derivative to evaluate + 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 Catenary.EvalDerivByArcLength( sEval + arcLenSampleOffset, a, n ); + } + + // Evaluate passing through the origin and p + float EvalPassingThrough0( float x ) => Catenary.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; + + // 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 + + // find bounds of the root + float xRoot = ( p.x * p.x ) / ( 2 * s ); // intial guess based on freya's flawless heuristics + if( TryFindRootBounds( pAbsX, c, xRoot, out FloatRange xRange ) ) { + // refine range, if necessary (which is very likely) + if( Mathfs.Approximately( xRange.Length, 0 ) == false ) + RootFindBisections( pAbsX, c, 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; + } + } + + // root solve function + static float R( float a, float pAbsX, float c ) => 2 * a * Mathfs.Sinh( pAbsX / ( 2 * a ) ) - c; + + // 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 ) => Catenary.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 = -Catenary.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( float pAbsX, float c, float g, out FloatRange xRange ) { + float y = R( g, pAbsX, c ); + 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, pAbsX, c ); + 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, pAbsX, c ); + if( y > 0 ) + return true; // lower bound found! + } + } + + return false; // no root found + } + + static void RootFindBisections( float pAbsX, float c, ref FloatRange xRange, int iterationCount ) { + for( int i = 0; i < iterationCount; i++ ) + RootFindBisection( pAbsX, c, ref xRange ); + } + + static void RootFindBisection( float pAbsX, float c, ref FloatRange xRange ) { + float xInter = xRange.Center; // bisection + float yInter = R( xInter, pAbsX, c ); + if( yInter > 0 ) + xRange.a = xInter; // adjust left bound + else + xRange.b = xInter; // adjust right bound + } + + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/CatenaryToPoint.cs.meta b/Runtime/Curves/CatenaryToPoint.cs.meta new file mode 100644 index 0000000..b7d8d6d --- /dev/null +++ b/Runtime/Curves/CatenaryToPoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 725fcdb7e0f11c54eae09804333f8693 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/GenericTrajectory2D.cs b/Runtime/Curves/GenericTrajectory2D.cs new file mode 100644 index 0000000..3a9e42f --- /dev/null +++ b/Runtime/Curves/GenericTrajectory2D.cs @@ -0,0 +1,24 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using Freya; +using UnityEngine; + +public class GenericTrajectory2D { + + public Vector2[] derivatives; + + public GenericTrajectory2D( params Vector2[] derivatives ) => this.derivatives = derivatives; + + public Vector2 GetPosition( float time ) { + Vector2 pt = derivatives[0]; + for( int i = 1; i < derivatives.Length; i++ ) { + float scale = MathF.Pow( time, i ) / Mathfs.Factorial( (uint)i ); + pt += scale * derivatives[i]; + } + + return pt; + } + + +} \ No newline at end of file diff --git a/Runtime/Curves/GenericTrajectory2D.cs.meta b/Runtime/Curves/GenericTrajectory2D.cs.meta new file mode 100644 index 0000000..236cec4 --- /dev/null +++ b/Runtime/Curves/GenericTrajectory2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39f2a44aef2c7834da92d8e743f7d335 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/IParamCurve.cs b/Runtime/Curves/IParamCurve.cs similarity index 61% rename from Curves/IParamCurve.cs rename to Runtime/Curves/IParamCurve.cs index 1e077b9..320d5c7 100644 --- a/Curves/IParamCurve.cs +++ b/Runtime/Curves/IParamCurve.cs @@ -1,9 +1,21 @@ -using System.Runtime.CompilerServices; +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; using UnityEngine; using static Freya.Mathfs; namespace Freya { + public interface IParamSplineSegment { + /// The curve generated by the control points + P Curve { get; } + + /// The matrix containing the control points of this spline segment + M PointMatrix { get; set; } + + } + /// An interface representing a parametric curve /// The vector type of the curve public interface IParamCurve where V : struct { @@ -11,18 +23,9 @@ public interface IParamCurve where V : struct { /// Returns the degree of this curve. Quadratic = 2, Cubic = 3, etc int Degree { get; } - /// The number of control points in this curve - int Count { get; } - /// Returns the point at the given t-value on the curve /// The t-value along the curve to sample - V GetPoint( float t ); - - /// Returns the starting point of this curve, where t = 0 - V GetStartPoint(); - - /// Returns the end point of this curve, where t = 1 - V GetEndPoint(); + V Eval( float t ); } @@ -31,7 +34,7 @@ public interface IParamCurve where V : struct { public interface IParamCurve1Diff : IParamCurve where V : struct { /// Returns the derivative at the given t-value on the curve. Loosely analogous to "velocity" of the point along the curve /// The t-value along the curve to sample - V GetDerivative( float t ); + V EvalDerivative( float t ); } /// An interface representing a parametric curve of degree 2 or higher @@ -39,14 +42,14 @@ public interface IParamCurve1Diff : IParamCurve where V : struct { public interface IParamCurve2Diff : IParamCurve1Diff where V : struct { /// Returns the second derivative at the given t-value on the curve. Loosely analogous to "acceleration" of the point along the curve /// The t-value along the curve to sample - V GetSecondDerivative( float t ); + V EvalSecondDerivative( float t ); } /// An interface representing a parametric curve of degree 3 or higher /// The vector type of the curve public interface IParamCurve3Diff : IParamCurve2Diff where V : struct { /// Returns the third derivative of the curve. Loosely analogous to "jerk/jolt" (rate of change of acceleration) of the point along the curve - V GetThirdDerivative( float t ); + V EvalThirdDerivative( float t ); } /// Shared functionality for all 2D parametric curves @@ -54,26 +57,29 @@ public static class IParamCurveExt2D { const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - /// Returns the approximate length of the curve + /// Returns the approximate length of the curve in the 0 to 1 interval /// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate - public static float GetLength( this T curve, int accuracy = 8 ) where T : IParamCurve { - if( accuracy <= 2 ) - return ( curve.GetStartPoint() - curve.GetEndPoint() ).magnitude; + [MethodImpl( INLINE )] public static float GetArcLength( this T curve, int accuracy = 8 ) where T : IParamCurve => curve.GetArcLength( FloatRange.unit, accuracy ); + /// Returns the approximate length of the curve in the given interval + /// The parameter interval of the curve to get the length of + /// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate + public static float GetArcLength( this T curve, FloatRange interval, int accuracy = 8 ) where T : IParamCurve { + accuracy = accuracy.AtLeast( 2 ); + bool unit = interval == FloatRange.unit; float totalDist = 0; - Vector2 prev = curve.GetStartPoint(); + Vector2 prev = curve.Eval( interval.a ); for( int i = 1; i < accuracy; i++ ) { float t = i / ( accuracy - 1f ); - Vector2 p = curve.GetPoint( t ); + Vector2 p = curve.Eval( unit ? t : interval.Lerp( t ) ); float dx = p.x - prev.x; float dy = p.y - prev.y; - totalDist += Mathf.Sqrt( dx * dx + dy * dy ); + totalDist += MathF.Sqrt( dx * dx + dy * dy ); prev = p; } return totalDist; } - } /// Shared functionality for all 3D parametric curves @@ -81,26 +87,27 @@ public static class IParamCurveExt3D { const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - /// Returns the approximate length of the curve - /// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate - public static float GetLength( this T curve, int accuracy = 8 ) where T : IParamCurve { - if( accuracy <= 2 ) - return ( curve.GetStartPoint() - curve.GetEndPoint() ).magnitude; + /// + [MethodImpl( INLINE )] public static float GetArcLength( this T curve, int accuracy = 8 ) where T : IParamCurve => curve.GetArcLength( FloatRange.unit, accuracy ); + /// + public static float GetArcLength( this T curve, FloatRange interval, int accuracy = 8 ) where T : IParamCurve { + accuracy = accuracy.AtLeast( 2 ); + bool unit = interval == FloatRange.unit; float totalDist = 0; - Vector3 prev = curve.GetStartPoint(); + Vector3 prev = curve.Eval( interval.a ); for( int i = 1; i < accuracy; i++ ) { float t = i / ( accuracy - 1f ); - Vector3 p = curve.GetPoint( t ); + Vector3 p = curve.Eval( unit ? t : interval.Lerp( t ) ); float dx = p.x - prev.x; float dy = p.y - prev.y; - totalDist += Mathf.Sqrt( dx * dx + dy * dy ); + float dz = p.z - prev.z; + totalDist += MathF.Sqrt( dx * dx + dy * dy + dz * dz ); prev = p; } return totalDist; } - } /// Shared functionality for 2D parametric curves of degree 1 or higher @@ -110,28 +117,28 @@ public static class IParamCurve1DiffExt2D { /// Returns the normalized tangent direction at the given t-value on the curve /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Vector2 GetTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.GetDerivative( t ).normalized; + [MethodImpl( INLINE )] public static Vector2 EvalTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalDerivative( t ).normalized; /// Returns the normal direction at the given t-value on the curve. /// This normal will point to the inner arc of the current curvature /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Vector2 GetNormal( this T curve, float t ) where T : IParamCurve1Diff => curve.GetTangent( t ).Rotate90CCW(); + [MethodImpl( INLINE )] public static Vector2 EvalNormal( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalTangent( t ).Rotate90CCW(); /// Returns the 2D angle of the direction of the curve at the given point, in radians /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static float GetAngle( this T curve, float t ) where T : IParamCurve1Diff => DirToAng( curve.GetDerivative( t ) ); + [MethodImpl( INLINE )] public static float EvalAngle( this T curve, float t ) where T : IParamCurve1Diff => DirToAng( curve.EvalDerivative( t ) ); /// Returns the orientation at the given point t, where the X axis is tangent to the curve /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Quaternion GetOrientation( this T curve, float t ) where T : IParamCurve1Diff => DirToOrientation( curve.GetDerivative( t ) ); + [MethodImpl( INLINE )] public static Quaternion EvalOrientation( this T curve, float t ) where T : IParamCurve1Diff => DirToOrientation( curve.EvalDerivative( t ) ); /// Returns the position and orientation at the given t-value on the curve /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Pose GetPose( this T curve, float t ) where T : IParamCurve1Diff => PointDirToPose( curve.GetPoint( t ), curve.GetTangent( t ) ); + [MethodImpl( INLINE )] public static Pose EvalPose( this T curve, float t ) where T : IParamCurve1Diff => PointDirToPose( curve.Eval( t ), curve.EvalTangent( t ) ); /// Returns the position and orientation at the given t-value on the curve, expressed as a matrix /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Matrix4x4 GetMatrix( this T curve, float t ) where T : IParamCurve1Diff => GetMatrixFrom2DPointDir( curve.GetPoint( t ), curve.GetTangent( t ) ); + [MethodImpl( INLINE )] public static Matrix4x4 EvalMatrix( this T curve, float t ) where T : IParamCurve1Diff => GetMatrixFrom2DPointDir( curve.Eval( t ), curve.EvalTangent( t ) ); } @@ -139,40 +146,40 @@ public static class IParamCurve1DiffExt2D { public static class IParamCurve1DiffExt3D { const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - /// - [MethodImpl( INLINE )] public static Vector3 GetTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.GetDerivative( t ).normalized; + /// + [MethodImpl( INLINE )] public static Vector3 EvalTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalDerivative( t ).normalized; /// Returns a normal of the curve given a reference up vector and t-value on the curve. /// The normal will be perpendicular to both the supplied up vector and the curve /// The t-value along the curve to sample /// The reference up vector. The normal will be perpendicular to both the supplied up vector and the curve - [MethodImpl( INLINE )] public static Vector3 GetNormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetNormalFromLookTangent( curve.GetDerivative( t ), up ); + [MethodImpl( INLINE )] public static Vector3 EvalNormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetNormalFromLookTangent( curve.EvalDerivative( t ), up ); /// Returns the binormal of the curve given a reference up vector and t-value on the curve. /// The binormal will attempt to be as aligned with the reference vector as possible, /// while still being perpendicular to the curve /// The t-value along the curve to sample /// The reference up vector. The binormal will attempt to be as aligned with the reference vector as possible, while still being perpendicular to the curve - [MethodImpl( INLINE )] public static Vector3 GetBinormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetBinormalFromLookTangent( curve.GetDerivative( t ), up ); + [MethodImpl( INLINE )] public static Vector3 EvalBinormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetBinormalFromLookTangent( curve.EvalDerivative( t ), up ); /// Returns the orientation at the given point t, where the Z direction is tangent to the curve. /// The Y axis will attempt to align with the supplied up vector /// The t-value along the curve to sample /// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible - [MethodImpl( INLINE )] public static Quaternion GetOrientation( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => Quaternion.LookRotation( curve.GetDerivative( t ), up ); + [MethodImpl( INLINE )] public static Quaternion EvalOrientation( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => Quaternion.LookRotation( curve.EvalDerivative( t ), up ); /// Returns the position and orientation of curve at the given point t, where the Z direction is tangent to the curve. /// The Y axis will attempt to align with the supplied up vector /// The t-value along the curve to sample /// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible - [MethodImpl( INLINE )] public static Pose GetPose( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => new Pose( curve.GetPoint( t ), Quaternion.LookRotation( curve.GetDerivative( t ), up ) ); + [MethodImpl( INLINE )] public static Pose EvalPose( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => new Pose( curve.Eval( t ), Quaternion.LookRotation( curve.EvalDerivative( t ), up ) ); /// Returns the position and orientation of curve at the given point t, expressed as a matrix, where the Z direction is tangent to the curve. /// The Y axis will attempt to align with the supplied up vector /// The t-value along the curve to sample /// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible - public static Matrix4x4 GetMatrix( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff { - ( Vector3 Pt, Vector3 Tn ) = ( curve.GetPoint( t ), curve.GetTangent( t ) ); + public static Matrix4x4 EvalMatrix( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff { + ( Vector3 Pt, Vector3 Tn ) = ( curve.Eval( t ), curve.EvalTangent( t ) ); Vector3 Nm = Vector3.Cross( up, Tn ).normalized; // X axis Vector3 Bn = Vector3.Cross( Tn, Nm ); // Y axis return new Matrix4x4( @@ -191,11 +198,11 @@ public static class IParamCurve2DiffExt2D { /// Returns the signed curvature at the given t-value on the curve, in radians per distance unit (equivalent to the reciprocal radius of the osculating circle) /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static float GetCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static float EvalCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); /// Returns the osculating circle at the given t-value in the curve, if possible. Osculating circles are defined everywhere except on inflection points, where curvature is 0 /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Circle2D GetOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle2D.GetOsculatingCircle( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Circle2D EvalOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle2D.GetOsculatingCircle( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); } @@ -205,29 +212,29 @@ public static class IParamCurve2DiffExt3D { /// Returns a pseudovector at the given t-value on the curve, where the magnitude is the curvature in radians per distance unit, and the direction is the axis of curvature /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Vector3 GetCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Bivector3 EvalCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); - /// - [MethodImpl( INLINE )] public static Circle3D GetOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle3D.GetOsculatingCircle( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + /// + [MethodImpl( INLINE )] public static Circle3D EvalOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle3D.GetOsculatingCircle( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); /// Returns the frenet-serret (curvature-based) normal direction at the given t-value on the curve /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Vector3 GetArcNormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcNormal( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Vector3 EvalArcNormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcNormal( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); /// Returns the frenet-serret (curvature-based) binormal direction at the given t-value on the curve /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Vector3 GetArcBinormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcBinormal( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Vector3 EvalArcBinormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcBinormal( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); /// Returns the frenet-serret (curvature-based) orientation of curve at the given point t, where the Z direction is tangent to the curve. /// The X axis will point to the inner arc of the current curvature /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Quaternion GetArcOrientation( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcOrientation( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Quaternion EvalArcOrientation( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcOrientation( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); /// Returns the position and the frenet-serret (curvature-based) orientation of curve at the given point t, where the Z direction is tangent to the curve. /// The X axis will point to the inner arc of the current curvature /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static Pose GetArcPose( this T curve, float t ) where T : IParamCurve2Diff { - ( Vector3 pt, Vector3 vel, Vector3 acc ) = ( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) ); + [MethodImpl( INLINE )] public static Pose EvalArcPose( this T curve, float t ) where T : IParamCurve2Diff { + ( Vector3 pt, Vector3 vel, Vector3 acc ) = ( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) ); Vector3 binormal = Vector3.Cross( vel, acc ); return new Pose( pt, Quaternion.LookRotation( vel, binormal ) ); } @@ -235,10 +242,10 @@ public static class IParamCurve2DiffExt3D { /// Returns the position and the frenet-serret (curvature-based) orientation of curve at the given point t, expressed as a matrix, where the Z direction is tangent to the curve. /// The X axis will point to the inner arc of the current curvature /// The t-value along the curve to sample - public static Matrix4x4 GetArcMatrix( this T curve, float t ) where T : IParamCurve2Diff { - Vector3 P = curve.GetPoint( t ); - Vector3 vel = curve.GetDerivative( t ); - Vector3 acc = curve.GetSecondDerivative( t ); + public static Matrix4x4 EvalArcMatrix( this T curve, float t ) where T : IParamCurve2Diff { + Vector3 P = curve.Eval( t ); + Vector3 vel = curve.EvalDerivative( t ); + Vector3 acc = curve.EvalSecondDerivative( t ); Vector3 Tn = vel.normalized; Vector3 B = Vector3.Cross( vel, acc ).normalized; Vector3 N = Vector3.Cross( B, Tn ); @@ -259,7 +266,7 @@ public static class IParamCurve3DiffExt3D { /// Returns the torsion at the given t-value on the curve, in radians per distance unit /// The t-value along the curve to sample - [MethodImpl( INLINE )] public static float GetTorsion( this T curve, float t ) where T : IParamCurve3Diff => Mathfs.GetTorsion( curve.GetDerivative( t ), curve.GetSecondDerivative( t ), curve.GetThirdDerivative( t ) ); + [MethodImpl( INLINE )] public static float EvalTorsion( this T curve, float t ) where T : IParamCurve3Diff => Mathfs.GetTorsion( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ), curve.EvalThirdDerivative( t ) ); } diff --git a/Runtime/Curves/IParamCurve.cs.meta b/Runtime/Curves/IParamCurve.cs.meta new file mode 100644 index 0000000..c3d9b5b --- /dev/null +++ b/Runtime/Curves/IParamCurve.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b0d319ba2d8a94359ad22777db36c523 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/IPolynomialCubic.cs b/Runtime/Curves/IPolynomialCubic.cs new file mode 100644 index 0000000..5d8186e --- /dev/null +++ b/Runtime/Curves/IPolynomialCubic.cs @@ -0,0 +1,54 @@ +namespace Freya { + + public interface IPolynomialCubic { + + /// The constant coefficient + public V C0 { get; set; } + /// The linear coefficient + public V C1 { get; set; } + /// The quadratic coefficient + public V C2 { get; set; } + /// The cubic coefficient + public V C3 { get; set; } + + /// The degree of the polynomial + public int Degree { get; } + + /// Returns the component polynomial of the given axis/dimension + /// Index of axis/dimension. 0 = x. 1 = y, etc + public Polynomial this[ int i ] { get; set; } + + /// Gets the coefficient of the given degree + /// The degree of the coefficient you want to get. For example, 0 will return the constant coefficient, 3 will return the cubic coefficient + V GetCoefficient( int degree ); + + /// Sets the coefficient of the given degree + /// The degree of the coefficient you want to set. For example, 0 will return the constant coefficient, 3 will return the cubic coefficient + /// The value to set it to + public void SetCoefficient( int degree, V value ); + + /// Evaluates the polynomial at the given value t + /// The value to sample at + public V Eval( float t ); + + /// Evaluates the n:th derivative of the polynomial at the given value t + /// The value to sample at + /// The derivative to evaluate + public V Eval( float t, int n ); + + /// Differentiates this function, returning the n-th derivative of this polynomial + /// The number of times to differentiate this function. 0 returns the function itself, 1 returns the first derivative + public P Differentiate( int n = 1 ); + + /// Scales the parameter space by a factor. For example, the output of the polynomial in the input interval [0 to 1] will now be in the range [0 to factor] + /// The factor to scale the input parameters by + public P ScaleParameterSpace( float factor ); + + /// Given an inner function g(x), returns f(g(x)) + /// The constant coefficient of the inner function g(x) + /// The linear coefficient of the inner function g(x) + public P Compose( float g0, float g1 ); + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/IPolynomialCubic.cs.meta b/Runtime/Curves/IPolynomialCubic.cs.meta new file mode 100644 index 0000000..6e2ecf4 --- /dev/null +++ b/Runtime/Curves/IPolynomialCubic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d508c7c494c2a34f9d18768260e71e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/IPolynomialMath.cs b/Runtime/Curves/IPolynomialMath.cs new file mode 100644 index 0000000..20efc91 --- /dev/null +++ b/Runtime/Curves/IPolynomialMath.cs @@ -0,0 +1,39 @@ +using UnityEngine; + +namespace Freya { + + public interface IPolynomialMath { + public P NaN { get; } + public P FitCubicFrom0( float x1, float x2, float x3, V y0, V y1, V y2, V y3 ); + } + + public struct PolynomialMath1D : IPolynomialMath { + public Polynomial NaN => Polynomial.NaN; + + /// + public Polynomial FitCubicFrom0( float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) => Polynomial.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 ); + } + + public struct PolynomialMath2D : IPolynomialMath { + public Polynomial2D NaN => Polynomial2D.NaN; + + /// + public Polynomial2D FitCubicFrom0( float x1, float x2, float x3, Vector2 y0, Vector2 y1, Vector2 y2, Vector2 y3 ) => Polynomial2D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 ); + } + + public struct PolynomialMath3D : IPolynomialMath { + public Polynomial3D NaN => Polynomial3D.NaN; + + /// + public Polynomial3D FitCubicFrom0( float x1, float x2, float x3, Vector3 y0, Vector3 y1, Vector3 y2, Vector3 y3 ) => Polynomial3D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 ); + } + + public struct PolynomialMath4D : IPolynomialMath { + public Polynomial4D NaN => Polynomial4D.NaN; + + /// + public Polynomial4D FitCubicFrom0( float x1, float x2, float x3, Vector4 y0, Vector4 y1, Vector4 y2, Vector4 y3 ) => Polynomial4D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 ); + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/IPolynomialMath.cs.meta b/Runtime/Curves/IPolynomialMath.cs.meta new file mode 100644 index 0000000..42cd070 --- /dev/null +++ b/Runtime/Curves/IPolynomialMath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 47c9d879bc3df8e4abdda485b1cc9254 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/LagrangePolynomial2D.cs b/Runtime/Curves/LagrangePolynomial2D.cs new file mode 100644 index 0000000..86abd3f --- /dev/null +++ b/Runtime/Curves/LagrangePolynomial2D.cs @@ -0,0 +1,39 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System.Collections.Generic; +using UnityEngine; + +namespace Freya { + + public class LagrangePolynomial2D { + + public List points = new List(); + public List knots = null; + public bool Uniform => knots == null; + public FloatRange InternalKnotRange => Uniform ? ( 0, points.Count - 1 ) : ( knots[0], knots[^1] ); + + public Vector2 Eval( float u ) { + float l( int j ) { + float prod = 1; + for( int i = 0; i < points.Count; i++ ) { + if( i == j ) + continue; + if( Uniform ) + prod *= ( u - i ) / ( j - i ); + else + prod *= Mathfs.InverseLerp( knots[i], knots[j], u ); + } + + return prod; + } + + Vector2 sum = Vector2.zero; + for( int j = 0; j < points.Count; j++ ) + sum += points[j] * l( j ); + + return sum; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/LagrangePolynomial2D.cs.meta b/Runtime/Curves/LagrangePolynomial2D.cs.meta new file mode 100644 index 0000000..33f9f81 --- /dev/null +++ b/Runtime/Curves/LagrangePolynomial2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: def04ed833bc2244996ce588be8fb734 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/MobiusTf.cs b/Runtime/Curves/MobiusTf.cs new file mode 100644 index 0000000..7f7a263 --- /dev/null +++ b/Runtime/Curves/MobiusTf.cs @@ -0,0 +1,64 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine.Assertions; + +namespace Freya { + + /// The Möbius Transformation as an equation of the form f(x) = (ax+b)/(cx+d) + [Serializable] public struct MobiusTf { + + public float a, b, c, d; + + public static readonly MobiusTf identity = new MobiusTf( 1, 1, 0, 0 ); + public bool IsIdentity => b == 0 && c == 0 && a == d; + + float Determinant => a * d - b * c; + public bool IsValid => Determinant != 0; + public bool IsAffine => c == 0; + public MobiusTf Inverse => new(d, -b, -c, a); + + public MobiusTf( float a, float b, float c, float d ) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + Assert.IsTrue( IsValid ); + } + + /// Evaluates the nth derivative of the mobius transform + /// The parameter value to evaluate at + /// The nth derivative. 0 = evaluates the function. 1 = evaluates the first derivative, etc. + public float eval( float x, int n = 0 ) { + float D = c * x + d; + switch( n ) { + case 0: return ( a * x + b ) / D; + case 1: return Determinant / ( D * D ); + case 2: return -2 * Determinant / ( D * D * D ); + default: + int scale = -Mathfs.Factorial( (uint)n ) * n.esign(); + float num = Determinant * c.pow( n - 1 ); + return scale * ( num / D.pow( n + 1 ) ); + } + } + + /// Evaluates the definite integral from x0 to x1 + public float integrate( float x0, float x1 ) { + float r = ( x1 - x0 ) / ( c * x0 + d ); + float rect = r * ( a * x0 + b ); + float curv = r * r * Determinant * logrem( c * r ); + return rect + curv; + } + + /// A weird natural log remainder type thing (x-ln(x+1))/(x*x) + static float logrem( float x ) { + return x.abs() switch { + 0 => 0.5f, // singularity at 0 + < 0.001f => 0.5f - x / 3 + ( x * x ) / 4, // approximation near 0 + _ => ( x - MathF.Log( 1 + x ) ) / ( x * x ) + }; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/MobiusTf.cs.meta b/Runtime/Curves/MobiusTf.cs.meta new file mode 100644 index 0000000..fbd5737 --- /dev/null +++ b/Runtime/Curves/MobiusTf.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f01d18fb05654c1eb34c0354a370a322 +timeCreated: 1784991861 \ No newline at end of file diff --git a/Runtime/Curves/Polynomial.cs b/Runtime/Curves/Polynomial.cs new file mode 100644 index 0000000..2f3b1bd --- /dev/null +++ b/Runtime/Curves/Polynomial.cs @@ -0,0 +1,405 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using System.Text; +using Unity.Mathematics; +using UnityEngine.Serialization; + +namespace Freya { + + /// A polynomial in the form ax³+bx²+cx+d, up to a cubic, with functions like derivatives, root finding, and more + [Serializable] public struct Polynomial : IPolynomialCubic { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// A polynomial with all 0 coefficients. f(x) = 0 + public static readonly Polynomial zero = new Polynomial( 0, 0, 0, 0 ); + + /// A polynomial with all NaN coefficients + public static readonly Polynomial NaN = new Polynomial( float.NaN, float.NaN, float.NaN, float.NaN ); + + /// The constant coefficient + [FormerlySerializedAs( "fConstant" )] public float c0; + + /// The linear coefficient + [FormerlySerializedAs( "fLinear" )] public float c1; + + /// The quadratic coefficient + [FormerlySerializedAs( "fQuadratic" )] public float c2; + + /// The cubic coefficient + [FormerlySerializedAs( "fCubic" )] public float c3; + + /// Creates a polynomial up to a cubic + /// The constant coefficient + /// The linear coefficient + /// The quadratic coefficient + /// The cubic coefficient + public Polynomial( float c0, float c1, float c2, float c3 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, c2, c3 ); + + /// Creates a polynomial up to a quadratic + /// The constant coefficient + /// The linear coefficient + /// The quadratic coefficient + public Polynomial( float c0, float c1, float c2 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, c2, 0 ); + + /// Creates a polynomial up to a linear + /// The constant coefficient + /// The linear coefficient + public Polynomial( float c0, float c1 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, 0, 0 ); + + /// Creates a polynomial + /// The coefficients to use + public Polynomial( float4 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.x, coefficients.y, coefficients.z, coefficients.w ); + + /// + public Polynomial( Matrix4x1 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.m0, coefficients.m1, coefficients.m2, coefficients.m3 ); + + /// + public Polynomial( Matrix3x1 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.m0, coefficients.m1, coefficients.m2, 0 ); + + /// + public Polynomial( (float c0, float c1, float c2, float c3) coefficients ) => ( c0, c1, c2, c3 ) = coefficients; + + /// + public Polynomial( (float c0, float c1, float c2) coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.c0, coefficients.c1, coefficients.c2, 0 ); + + #region IPolynomialCubic + + public float C0 { + [MethodImpl( INLINE )] get => c0; + [MethodImpl( INLINE )] set => c0 = value; + } + public float C1 { + [MethodImpl( INLINE )] get => c1; + [MethodImpl( INLINE )] set => c1 = value; + } + public float C2 { + [MethodImpl( INLINE )] get => c2; + [MethodImpl( INLINE )] set => c2 = value; + } + public float C3 { + [MethodImpl( INLINE )] get => c3; + [MethodImpl( INLINE )] set => c3 = value; + } + public Polynomial this[ int i ] { + [MethodImpl( INLINE )] get => i == 0 ? this : throw new IndexOutOfRangeException( "float polynomials don't have vector components" ); + [MethodImpl( INLINE )] set => this = value; + } + + public int Degree => GetPolynomialDegree( c0, c1, c2, c3 ); + + [MethodImpl( INLINE )] public float GetCoefficient( int degree ) => + degree switch { + 0 => c0, + 1 => c1, + 2 => c2, + 3 => c3, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + + [MethodImpl( INLINE )] public void SetCoefficient( int degree, float value ) { + _ = degree switch { + 0 => c0 = value, + 1 => c1 = value, + 2 => c2 = value, + 3 => c3 = value, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + } + + public float Eval( float t ) { + float t2 = t * t; + float t3 = t * t2; + return c3 * t3 + c2 * t2 + c1 * t + c0; + } + + [MethodImpl( INLINE )] public float Eval( float t, int n ) => Differentiate( n ).Eval( t ); + + [MethodImpl( INLINE )] public readonly Polynomial Differentiate( int n = 1 ) { + return n switch { + 0 => this, + 1 => new Polynomial( c1, 2 * c2, 3 * c3, 0 ), + 2 => new Polynomial( 2 * c2, 6 * c3, 0, 0 ), + 3 => new Polynomial( 6 * c3, 0, 0, 0 ), + _ => n > 3 ? zero : throw new IndexOutOfRangeException( "Cannot differentiate a negative amount of times" ) + }; + } + + public Polynomial ScaleParameterSpace( float factor ) { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if( factor == 1f ) + return this; + float factor2 = factor * factor; + float factor3 = factor2 * factor; + return new Polynomial( + c0, + c1 / factor, + c2 / factor2, + c3 / factor3 + ); + } + + /// Given an inner function g(x), returns f(g(x)) + /// The constant coefficient of the inner function g(x) + /// The linear coefficient of the inner function g(x) + public Polynomial Compose( float g0, float g1 ) { + float g0_2 = g0 * g0; + float g0_3 = g0 * g0_2; + float g1_2 = g1 * g1; + float g1_3 = g1 * g1_2; + return new Polynomial( + c0 + c1 * g0 + c2 * g0_2 + c3 * g0_3, + c1 * g1 + c2 * 2 * g0 * g1 + c3 * 3 * g0_2 * g1, + c2 * g1_2 + c3 * 3 * g0 * g1_2, + c3 * g1_3 + ); + } + + #endregion + + /// Fits a cubic polynomial to pass through the given coordinates + public static Polynomial FitCubic( float x0, float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) { + // precalcs + float i01 = x1 - x0; + float i02 = x2 - x0; + float i03 = x3 - x0; + float i12 = x2 - x1; + float i13 = x3 - x1; + float i23 = x3 - x2; + float x0x1 = x0 * x1; + float x0x2 = x0 * x2; + float x0x3 = x0 * x3; + float x1x2 = x1 * x2; + float x1x3 = x1 * x3; + float x2x3 = x2 * x3; + float x1x2x3 = x1 * x2x3; + float x0x2x3 = x0 * x2x3; + float x0x1x3 = x0 * x1x3; + float x0x1x2 = x0 * x1x2; + float x0plusx1 = x0 + x1; + float x0plusx1plusx2 = x0plusx1 + x2; + float x0plusx1plusx3 = x0plusx1 + x3; + float x2plusx3 = x2 + x3; + float x0plusx2plusx3 = x0 + x2plusx3; + float x1plusx2plusx3 = x1 + x2plusx3; + float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 ); + float x0x2plusx0x3plusx2x3 = ( x0x2 + x0x3 + x2x3 ); + float x0x1plusx0x3plusx1x3 = ( x0x1 + x0x3 + x1x3 ); + float x0x1plusx0x2plusx1x2 = ( x0x1 + x0x2 + x1x2 ); + + // scale factors + float scl0 = -( y0 / ( i01 * i02 * i03 ) ); + float scl1 = +( y1 / ( i01 * i12 * i13 ) ); + float scl2 = -( y2 / ( i02 * i12 * i23 ) ); + float scl3 = +( y3 / ( i03 * i13 * i23 ) ); + + // polynomial form + float c0 = -( scl0 * x1x2x3 + scl1 * x0x2x3 + scl2 * x0x1x3 + scl3 * x0x1x2 ); + float c1 = scl0 * x1x2plusx1x3plusx2x3 + scl1 * x0x2plusx0x3plusx2x3 + scl2 * x0x1plusx0x3plusx1x3 + scl3 * x0x1plusx0x2plusx1x2; + float c2 = -( scl0 * x1plusx2plusx3 + scl1 * x0plusx2plusx3 + scl2 * x0plusx1plusx3 + scl3 * x0plusx1plusx2 ); + float c3 = scl0 + scl1 + scl2 + scl3; + + return new Polynomial( (float)c0, (float)c1, (float)c2, (float)c3 ); + } + + /// Fits a cubic polynomial to pass through the given coordinates, assuming x0 = 0 + public static Polynomial FitCubicFrom0( float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) { + // precalcs + float i12 = x2 - x1; + float i13 = x3 - x1; + float i23 = x3 - x2; + float x1x2 = x1 * x2; + float x1x3 = x1 * x3; + float x2x3 = x2 * x3; + float x1x2x3 = x1 * x2x3; + float x2plusx3 = x2 + x3; + + // scale factors + float scl0 = -( y0 / ( x1 * x2 * x3 ) ); + float scl1 = +( y1 / ( x1 * i12 * i13 ) ); + float scl2 = -( y2 / ( x2 * i12 * i23 ) ); + float scl3 = +( y3 / ( x3 * i13 * i23 ) ); + + // polynomial form + float c0 = -( scl0 * x1x2x3 ); + float c1 = scl0 * ( x1x2 + x1x3 + x2x3 ) + scl1 * x2x3 + scl2 * x1x3 + scl3 * x1x2; + float c2 = -( scl0 * ( x2plusx3 + x1 ) + scl1 * ( x2plusx3 ) + scl2 * ( x1 + x3 ) + scl3 * ( x1 + x2 ) ); + float c3 = scl0 + scl1 + scl2 + scl3; + + return new Polynomial( c0, c1, c2, c3 ); + } + + + /// Splits the 0-1 range into two distinct polynomials at the given parameter value u, where both new curves cover the same total range with their individual 0-1 ranges + /// The parameter value to split at + public (Polynomial pre, Polynomial post) Split01( float u ) { + float d = 1f - u; + float dd = d * d; + float ddd = d * d * d; + float uu = u * u; + float uuu = u * u * u; + + Polynomial pre = new Polynomial( c0, c1 * u, c2 * uu, c3 * uuu ); + Polynomial post = new Polynomial( + Eval( u ), + d * Differentiate( 1 ).Eval( u ), + dd / 2 * Differentiate( 2 ).Eval( u ), + ddd / 6 * Differentiate( 3 ).Eval( u ) + ); + return ( pre, post ); + } + + /// Calculates the roots (values where this polynomial = 0) + public ResultsMax3 Roots => Solve.Polynomial( c0, c1, c2, c3 ); + + /// Calculates the local extrema of this polynomial + public ResultsMax2 LocalExtrema => (ResultsMax2)Differentiate().Roots; + + /// Calculates the local extrema of this polynomial in the unit interval + public ResultsMax2 LocalExtrema01 { + get { + ResultsMax2 all = LocalExtrema; + ResultsMax2 valids = new ResultsMax2(); + for( int i = 0; i < all.count; i++ ) { + float t = all[i]; + if( t.Within( 0, 1 ) ) + valids = valids.Add( all[i] ); + } + + return valids; + } + } + + /// Returns the output value range within the unit interval + public FloatRange OutputRange01 { + get { + FloatRange range = ( Eval( 0 ), Eval( 1 ) ); + foreach( float t in LocalExtrema01 ) + range = range.Encapsulate( Eval( t ) ); + return range; + } + } + + #region Statics + + /// Creates a constant polynomial + /// The constant coefficient + public static Polynomial Constant( float constant ) => new Polynomial( constant, 0, 0, 0 ); + + /// Creates a linear polynomial of the form ax+b + /// The constant coefficient b in ax+b + /// The linear coefficient a in ax+b + public static Polynomial Linear( float c0, float c1 ) => new Polynomial( c0, c1, 0, 0 ); + + /// Creates a linear polynomial of the form ax+b from two points a and b + /// The first point + /// The second point + public static Polynomial Linear( float2 a, float2 b ) => Linear( a.x, a.y, b.x, b.y ); + + /// Creates a linear polynomial of the form ax+b from two points + /// The coordinate of the first point + /// The value of the first point + /// The coordinate of the second point + /// The value of the second point + public static Polynomial Linear( float x0, float y0, float x1, float y1 ) { + float d = ( y1 - y0 ) / ( x1 - x0 ); + return new Polynomial( y0 - d * x0, d, 0, 0 ); + } + + /// Creates a quadratic polynomial + /// The constant coefficient + /// The linear coefficient + /// The quadratic coefficient + public static Polynomial Quadratic( float c0, float c1, float c2 ) => new Polynomial( c0, c1, c2, 0 ); + + /// Creates a cubic polynomial + /// The constant coefficient + /// The linear coefficient + /// The quadratic coefficient + /// The cubic coefficient + public static Polynomial Cubic( float c0, float c1, float c2, float c3 ) => new Polynomial( c0, c1, c2, c3 ); + + /// Given the coefficients for a cubic polynomial, returns the net polynomial type/degree, accounting for values very close to 0 + /// The constant coefficient + /// The linear coefficient + /// The quadratic coefficient + /// The cubic coefficient + /// The quartic coefficient + [MethodImpl( INLINE )] public static int GetPolynomialDegree( float c0, float c1 = 0, float c2 = 0, float c3 = 0, float c4 = 0 ) { + if( Mathfs.Approximately( c4, 0 ) == false ) return 4; + if( Mathfs.Approximately( c3, 0 ) == false ) return 3; + if( Mathfs.Approximately( c2, 0 ) == false ) return 2; + if( Mathfs.Approximately( c1, 0 ) == false ) return 1; + return 0; + } + + /// Linearly interpolates between two polynomials + /// The first polynomial to blend from + /// The second polynomial to blend to + /// The blend value, typically from 0 to 1 + public static Polynomial Lerp( Polynomial a, Polynomial b, float t ) => + new( + t.Lerp( a.c0, b.c0 ), + t.Lerp( a.c1, b.c1 ), + t.Lerp( a.c2, b.c2 ), + t.Lerp( a.c3, b.c3 ) + ); + + #endregion + + #region Typecasting & Operators + + public static Polynomial operator /( Polynomial p, float v ) => new(p.c0 / v, p.c1 / v, p.c2 / v, p.c3 / v); + public static Polynomial operator /( float v, Polynomial p ) => new(v / p.c0, v / p.c1, v / p.c2, v / p.c3); + public static Polynomial operator *( Polynomial p, float v ) => new(p.c0 * v, p.c1 * v, p.c2 * v, p.c3 * v); + public static Polynomial operator *( float v, Polynomial p ) => p * v; + public static Polynomial operator +( Polynomial a, Polynomial b ) => new(a.c0 + b.c0, a.c1 + b.c1, a.c2 + b.c2, a.c3 + b.c3); + public static Polynomial operator -( Polynomial a, Polynomial b ) => new(a.c0 - b.c0, a.c1 - b.c1, a.c2 - b.c2, a.c3 - b.c3); + + public static explicit operator Matrix3x1( Polynomial poly ) => new(poly.c0, poly.c1, poly.c2); + public static explicit operator Matrix4x1( Polynomial poly ) => new(poly.c0, poly.c1, poly.c2, poly.c3); + public static explicit operator BezierQuad1D( Polynomial poly ) => poly.Degree < 3 ? new BezierQuad1D( CharMatrix.quadraticBezierInverse * (Matrix3x1)poly ) : throw new InvalidCastException( "Cannot cast a cubic polynomial to a quadratic curve" ); + public static explicit operator BezierCubic1D( Polynomial poly ) => new(CharMatrix.cubicBezierInverse * (Matrix4x1)poly); + public static explicit operator CatRomCubic1D( Polynomial poly ) => new(CharMatrix.cubicCatmullRomInverse * (Matrix4x1)poly); + public static explicit operator HermiteCubic1D( Polynomial poly ) => new(CharMatrix.cubicHermiteInverse * (Matrix4x1)poly); + public static explicit operator UBSCubic1D( Polynomial poly ) => new(CharMatrix.cubicUniformBsplineInverse * (Matrix4x1)poly); + + #endregion + + static StringBuilder strBuilder = new StringBuilder( 64 ); + static string[] tPowerSuffixStr = new[] { "", "x", "x²", "x³" }; + + public override string ToString() { + strBuilder.Clear(); + + bool hasAddedFirstTerm = false; + for( int c = 0; c < 4; c++ ) { + float value = GetCoefficient( c ); + if( value != 0 ) { + if( hasAddedFirstTerm == false ) { + hasAddedFirstTerm = true; + strBuilder.Append( GetCoefficient( c ) ); + } else { + if( value > 0 ) + strBuilder.Append( "+" ); + strBuilder.Append( GetCoefficient( c ) ); + if( c > 0 ) + strBuilder.Append( tPowerSuffixStr[c] ); + } + } + } + + if( hasAddedFirstTerm == false ) + return "0"; // no terms. constant 0 + + return strBuilder.ToString(); + } + + public string ToStringCoefficients() => $"({c0},{c1},{c2},{c3})"; + + } + + +} \ No newline at end of file diff --git a/Runtime/Curves/Polynomial.cs.meta b/Runtime/Curves/Polynomial.cs.meta new file mode 100644 index 0000000..3f7dd11 --- /dev/null +++ b/Runtime/Curves/Polynomial.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d77cc56ff77094210bc83f74c421077c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Polynomial2D.cs b/Runtime/Curves/Polynomial2D.cs new file mode 100644 index 0000000..59eddca --- /dev/null +++ b/Runtime/Curves/Polynomial2D.cs @@ -0,0 +1,413 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + [Serializable] + public struct Polynomial2D : IPolynomialCubic, IParamCurve3Diff { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// + public static readonly Polynomial2D NaN = new Polynomial2D { x = Polynomial.NaN, y = Polynomial.NaN }; + + public Polynomial x, y; + + public Polynomial2D( Polynomial x, Polynomial y ) => ( this.x, this.y ) = ( x, y ); + + /// + public Polynomial2D( Vector2 c0, Vector2 c1, Vector2 c2, Vector2 c3 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x, c3.x ); + this.y = new Polynomial( c0.y, c1.y, c2.y, c3.y ); + } + + /// + public Polynomial2D( Vector2 c0, Vector2 c1, Vector2 c2 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x ); + this.y = new Polynomial( c0.y, c1.y, c2.y ); + } + + /// + public Polynomial2D( Vector2 c0, Vector2 c1 ) { + this.x = new Polynomial( c0.x, c1.x, 0, 0 ); + this.y = new Polynomial( c0.y, c1.y, 0, 0 ); + } + + /// + public Polynomial2D( Vector2Matrix4x1 coefficients ) => ( x, y ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ) ); + + /// + public Polynomial2D( Vector2Matrix3x1 coefficients ) => ( x, y ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ) ); + + #region IPolynomialCubic + + public Vector2 C0 { + [MethodImpl( INLINE )] get => new(x.c0, y.c0); + [MethodImpl( INLINE )] set => ( x.c0, y.c0 ) = ( value.x, value.y ); + } + public Vector2 C1 { + [MethodImpl( INLINE )] get => new(x.c1, y.c1); + [MethodImpl( INLINE )] set => ( x.c1, y.c1 ) = ( value.x, value.y ); + } + public Vector2 C2 { + [MethodImpl( INLINE )] get => new(x.c2, y.c2); + [MethodImpl( INLINE )] set => ( x.c2, y.c2 ) = ( value.x, value.y ); + } + public Vector2 C3 { + [MethodImpl( INLINE )] get => new(x.c3, y.c3); + [MethodImpl( INLINE )] set => ( x.c3, y.c3 ) = ( value.x, value.y ); + } + + public Polynomial this[ int i ] { + get { return i switch { 0 => x, 1 => y, _ => throw new IndexOutOfRangeException( "Polynomial2D component index has to be either 0 or 1" ) }; } + set => _ = i switch { 0 => x = value, 1 => y = value, _ => throw new IndexOutOfRangeException() }; + } + + [MethodImpl( INLINE )] public Vector2 GetCoefficient( int degree ) => + degree switch { + 0 => C0, + 1 => C1, + 2 => C2, + 3 => C3, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + + [MethodImpl( INLINE )] public void SetCoefficient( int degree, Vector2 value ) { + _ = degree switch { + 0 => C0 = value, + 1 => C1 = value, + 2 => C2 = value, + 3 => C3 = value, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + } + + public Vector2 Eval( float t ) { + float t2 = t * t; + float t3 = t2 * t; + return new Vector2( + x.c3 * t3 + x.c2 * t2 + x.c1 * t + x.c0, + y.c3 * t3 + y.c2 * t2 + y.c1 * t + y.c0 + ); + } + + [MethodImpl( INLINE )] public Vector2 Eval( float t, int n ) => Differentiate( n ).Eval( t ); + + [MethodImpl( INLINE )] public Polynomial2D Differentiate( int n = 1 ) => new(x.Differentiate( n ), y.Differentiate( n )); + + public Polynomial2D ScaleParameterSpace( float factor ) { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if( factor == 1f ) + return this; + float factor2 = factor * factor; + float factor3 = factor2 * factor; + return new Polynomial2D( + new Polynomial( x.c0, x.c1 / factor, x.c2 / factor2, x.c3 / factor3 ), + new Polynomial( y.c0, y.c1 / factor, y.c2 / factor2, y.c3 / factor3 ) + ); + } + + public Polynomial2D Compose( float g0, float g1 ) => new(x.Compose( g0, g1 ), y.Compose( g0, g1 )); + + #endregion + + /// + public static Polynomial2D FitCubicFrom0( float x1, float x2, float x3, Vector2 y0, Vector2 y1, Vector2 y2, Vector2 y3 ) { + // precalcs + float i12 = x2 - x1; + float i13 = x3 - x1; + float i23 = x3 - x2; + float x1x2 = x1 * x2; + float x1x3 = x1 * x3; + float x2x3 = x2 * x3; + float x1x2x3 = x1 * x2x3; + float x0plusx1plusx2 = x1 + x2; + float x0plusx1plusx3 = x1 + x3; + float x2plusx3 = x2 + x3; + float x1plusx2plusx3 = x1 + x2plusx3; + float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 ); + + // scale factors + Vector2 scl0 = y0 / -( x1 * x2 * x3 ); + Vector2 scl1 = y1 / +( x1 * i12 * i13 ); + Vector2 scl2 = y2 / -( x2 * i12 * i23 ); + Vector2 scl3 = y3 / +( x3 * i13 * i23 ); + + // polynomial form + Vector2 c0 = new( + -( scl0.x * x1x2x3 ), + -( scl0.y * x1x2x3 ) + ); + Vector2 c1 = new( + scl0.x * x1x2plusx1x3plusx2x3 + scl1.x * x2x3 + scl2.x * x1x3 + scl3.x * x1x2, + scl0.y * x1x2plusx1x3plusx2x3 + scl1.y * x2x3 + scl2.y * x1x3 + scl3.y * x1x2 + ); + Vector2 c2 = new( + -( scl0.x * x1plusx2plusx3 + scl1.x * x2plusx3 + scl2.x * x0plusx1plusx3 + scl3.x * x0plusx1plusx2 ), + -( scl0.y * x1plusx2plusx3 + scl1.y * x2plusx3 + scl2.y * x0plusx1plusx3 + scl3.y * x0plusx1plusx2 ) + ); + Vector2 c3 = new( + scl0.x + scl1.x + scl2.x + scl3.x, + scl0.y + scl1.y + scl2.y + scl3.y + ); + + return new Polynomial2D( c0, c1, c2, c3 ); + } + + + /// Returns the tight axis-aligned bounds of the curve in the unit interval + public Rect GetBounds01() => FloatRange.ToRect( x.OutputRange01, y.OutputRange01 ); + + /// + public (Polynomial2D pre, Polynomial2D post) Split01( float u ) { + ( Polynomial xPre, Polynomial xPost ) = x.Split01( u ); + ( Polynomial yPre, Polynomial yPost ) = y.Split01( u ); + return ( new Polynomial2D( xPre, yPre ), new Polynomial2D( xPost, yPost ) ); + } + + #region IParamCurve3Diff interface implementations + + public int Degree => Mathfs.Max( x.Degree, y.Degree ); + public Vector2 EvalDerivative( float t ) => Differentiate().Eval( t ); + public Vector2 EvalSecondDerivative( float t ) => Differentiate( 2 ).Eval( t ); + public Vector2 EvalThirdDerivative( float t = 0 ) => Differentiate( 3 ).Eval( 0 ); + + #endregion + + #region Project Point + + /// Returns the (approximate) point on the curve closest to the input point + /// The point to project against the curve + /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive. + /// This is how many subdivisions to split the curve into, to find candidates for the closest point. + /// If your curves are complex, you might need to use around 16 subdivisions. + /// If they are usually very simple, then around 8 subdivisions is likely fine + /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive. + /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary + public Vector2 ProjectPoint( Vector2 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations ); + + struct PointProjectSample { + public float t; + public float distDeltaSq; + public Vector2 f; + public Vector2 fp; + } + + static PointProjectSample[] pointProjectGuesses = { default, default, default }; + + /// Returns the (approximate) point on the curve closest to the input point + /// The point to project against the curve + /// The t-value at the projected point on the curve + /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive. + /// This is how many subdivisions to split the curve into, to find candidates for the closest point. + /// If your curves are complex, you might need to use around 16 subdivisions. + /// If they are usually very simple, then around 8 subdivisions is likely fine + /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive. + /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary + public Vector2 ProjectPoint( Vector2 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) { + // define a curve relative to the test point + Polynomial2D curve = this; + curve.x.c0 -= point.x; // constant coefficient defines the start position + curve.y.c0 -= point.y; + Polynomial2D vel = curve.Differentiate(); + Polynomial2D acc = vel.Differentiate(); + Vector2 curveStart = curve.Eval( 0 ); + Vector2 curveEnd = curve.Eval( 1 ); + + PointProjectSample SampleDistSqDelta( float tSmp ) { + PointProjectSample s = new PointProjectSample { + t = tSmp, + f = curve.Eval( tSmp ), + fp = vel.Eval( tSmp ) + }; + s.distDeltaSq = Vector2.Dot( s.f, s.fp ); + return s; + } + + // find initial candidates + int candidatesFound = 0; + PointProjectSample prevSmp = SampleDistSqDelta( 0 ); + + for( int i = 1; i < initialSubdivisions; i++ ) { + float ti = i / ( initialSubdivisions - 1f ); + PointProjectSample smp = SampleDistSqDelta( ti ); + if( Mathfs.SignAsInt( smp.distDeltaSq ) != Mathfs.SignAsInt( prevSmp.distDeltaSq ) ) { + pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 ); + if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree + } + + prevSmp = smp; + } + + // refine each guess w. Newton-Raphson iterations + void Refine( ref PointProjectSample smp ) { + Vector2 fpp = acc.Eval( smp.t ); + float tNew = smp.t - Vector2.Dot( smp.f, smp.fp ) / ( Vector2.Dot( smp.f, fpp ) + Vector2.Dot( smp.fp, smp.fp ) ); + smp = SampleDistSqDelta( tNew ); + } + + for( int p = 0; p < candidatesFound; p++ ) + for( int i = 0; i < refinementIterations; i++ ) + Refine( ref pointProjectGuesses[p] ); + + // Now find closest. First include the endpoints + float sqDist0 = curveStart.sqrMagnitude; // include endpoints + float sqDist1 = curveEnd.sqrMagnitude; + bool firstClosest = sqDist0 < sqDist1; + float tClosest = firstClosest ? 0 : 1; + Vector2 ptClosest = ( firstClosest ? curveStart : curveEnd ) + point; + float distSqClosest = firstClosest ? sqDist0 : sqDist1; + + // then check internal roots + for( int i = 0; i < candidatesFound; i++ ) { + float pSqmag = pointProjectGuesses[i].f.sqrMagnitude; + if( pSqmag < distSqClosest ) { + distSqClosest = pSqmag; + tClosest = pointProjectGuesses[i].t; + ptClosest = pointProjectGuesses[i].f + point; + } + } + + t = tClosest; + return ptClosest; + } + + #endregion + + #region Intersection Tests + + // Internal - used by all other intersections + private ResultsMax3 Intersect( Vector2 origin, Vector2 direction, bool rangeLimited = false, float minRayT = float.NaN, float maxRayT = float.NaN ) { + Polynomial2D rel = this; + rel.C0 -= origin; + float y0 = Mathfs.Determinant( rel.C0, direction ); // transform polynomial into the line space y components + float y1 = Mathfs.Determinant( rel.C1, direction ); + float y2 = Mathfs.Determinant( rel.C2, direction ); + float y3 = Mathfs.Determinant( rel.C3, direction ); + Polynomial polynomY = new Polynomial( y0, y1, y2, y3 ); + ResultsMax3 roots = polynomY.Roots; // t values of the function + + Polynomial polynomX = default; + if( rangeLimited ) { + // if we're range limited, we need to verify position along the ray/line/lineSegment + // and if we do, we need to be able to go from t -> x coord + float x0 = Vector2.Dot( rel.C0, direction ); // transform into the line space x components + float x1 = Vector2.Dot( rel.C1, direction ); + float x2 = Vector2.Dot( rel.C2, direction ); + float x3 = Vector2.Dot( rel.C3, direction ); + polynomX = new Polynomial( x0, x1, x2, x3 ); + } + + float CurveTtoRayT( float t ) => polynomX.Eval( t ); + + ResultsMax3 returnVals = default; + + for( int i = 0; i < roots.count; i++ ) { + if( roots[i].Between( 0, 1 ) && ( rangeLimited == false || CurveTtoRayT( roots[i] ).Within( minRayT, maxRayT ) ) ) + returnVals = returnVals.Add( roots[i] ); + } + + return returnVals; + } + + // Internal - to unpack from curve t values to points + private ResultsMax3 TtoPoints( ResultsMax3 tVals ) { + ResultsMax3 pts = default; + for( int i = 0; i < tVals.count; i++ ) + pts = pts.Add( Eval( tVals[i] ) ); + return pts; + } + + /// Returns the t-values at which the given line intersects with the curve + /// The line to test intersection against + public ResultsMax3 Intersect( Line2D line ) => Intersect( line.origin, line.dir ); + + /// Returns the t-values at which the given ray intersects with the curve + /// The ray to test intersection against + public ResultsMax3 Intersect( Ray2D ray ) => Intersect( ray.origin, ray.dir, rangeLimited: true, 0, float.MaxValue ); + + /// Returns the t-values at which the given line segment intersects with the curve + /// The line segment to test intersection against + public ResultsMax3 Intersect( LineSegment2D lineSegment ) => Intersect( lineSegment.start, lineSegment.end - lineSegment.start, rangeLimited: true, 0, lineSegment.LengthSquared ); + + /// Returns the points at which the given line intersects with the curve + /// The line to test intersection against + public ResultsMax3 IntersectionPoints( Line2D line ) => TtoPoints( Intersect( line.origin, line.dir ) ); + + /// Returns the points at which the given ray intersects with the curve + /// The ray to test intersection against + public ResultsMax3 IntersectionPoints( Ray2D ray ) => TtoPoints( Intersect( ray.origin, ray.dir, rangeLimited: true, 0, float.MaxValue ) ); + + /// Returns the points at which the given line segment intersects with the curve + /// The line segment to test intersection against + public ResultsMax3 IntersectionPoints( LineSegment2D lineSegment ) => TtoPoints( Intersect( lineSegment.start, lineSegment.end - lineSegment.start, rangeLimited: true, 0, lineSegment.LengthSquared ) ); + + /// Raycasts and returns whether or not it hit, along with the closest hit point + /// The ray to use when raycasting + /// The closest point on the curve the ray hit + /// The maximum length of the ray + public bool Raycast( Ray2D ray, out Vector2 hitPoint, float maxDist = float.MaxValue ) => Raycast( ray, out hitPoint, out _, maxDist ); + + /// Raycasts and returns whether or not it hit, along with the closest hit point and the t-value on the curve + /// The ray to use when raycasting + /// The closest point on the curve the ray hit + /// The t-value of the curve at the point the ray hit + /// The maximum length of the ray + public bool Raycast( Ray2D ray, out Vector2 hitPoint, out float t, float maxDist = float.MaxValue ) { + float closestDist = float.MaxValue; + ResultsMax3 tPts = Intersect( ray ); + ResultsMax3 pts = TtoPoints( tPts ); + + // find closest point + bool didHit = false; + hitPoint = default; + t = default; + for( int i = 0; i < pts.count; i++ ) { + Vector2 pt = pts[i]; + float dist = Vector2.Dot( ray.dir, pt - ray.origin ); + if( dist < closestDist && dist <= maxDist ) { + closestDist = dist; + hitPoint = pt; + t = tPts[i]; + didHit = true; + } + } + + return didHit; + } + + #endregion + + /// Returns the polynomial, rotated around the origin + /// The polynomial to rotate + /// The angle to rotate by (in radians) + public static Polynomial2D Rotate( Polynomial2D poly, float angle ) => + new( + poly.C0.Rotate( angle ), + poly.C1.Rotate( angle ), + poly.C2.Rotate( angle ), + poly.C3.Rotate( angle ) + ); + + #region Typecasting & Operators + + public static Polynomial2D operator /( Polynomial2D p, float v ) => new(p.C0 / v, p.C1 / v, p.C2 / v, p.C3 / v); + public static Polynomial2D operator *( Polynomial2D p, float v ) => new(p.C0 * v, p.C1 * v, p.C2 * v, p.C3 * v); + public static Polynomial2D operator *( float v, Polynomial2D p ) => p * v; + + public static explicit operator Vector2Matrix3x1( Polynomial2D poly ) => new(poly.C0, poly.C1, poly.C2); + public static explicit operator Vector2Matrix4x1( Polynomial2D poly ) => new(poly.C0, poly.C1, poly.C2, poly.C3); + public static explicit operator BezierQuad2D( Polynomial2D poly ) => poly.Degree < 3 ? new BezierQuad2D( CharMatrix.quadraticBezierInverse * (Vector2Matrix3x1)poly ) : throw new InvalidCastException( "Cannot cast a cubic polynomial to a quadratic curve" ); + public static explicit operator BezierCubic2D( Polynomial2D poly ) => new(CharMatrix.cubicBezierInverse * (Vector2Matrix4x1)poly); + public static explicit operator CatRomCubic2D( Polynomial2D poly ) => new(CharMatrix.cubicCatmullRomInverse * (Vector2Matrix4x1)poly); + public static explicit operator HermiteCubic2D( Polynomial2D poly ) => new(CharMatrix.cubicHermiteInverse * (Vector2Matrix4x1)poly); + public static explicit operator UBSCubic2D( Polynomial2D poly ) => new(CharMatrix.cubicUniformBsplineInverse * (Vector2Matrix4x1)poly); + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Polynomial2D.cs.meta b/Runtime/Curves/Polynomial2D.cs.meta new file mode 100644 index 0000000..8ea9ff9 --- /dev/null +++ b/Runtime/Curves/Polynomial2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d8cebec39f574d0cb2d29824afc2220 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Polynomial3D.cs b/Runtime/Curves/Polynomial3D.cs new file mode 100644 index 0000000..0e2e7f0 --- /dev/null +++ b/Runtime/Curves/Polynomial3D.cs @@ -0,0 +1,298 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + [Serializable] + public struct Polynomial3D : IPolynomialCubic, IParamCurve3Diff { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// + public static readonly Polynomial3D NaN = new Polynomial3D { x = Polynomial.NaN, y = Polynomial.NaN, z = Polynomial.NaN }; + + public Polynomial x, y, z; + + public Polynomial3D( Polynomial x, Polynomial y, Polynomial z ) => ( this.x, this.y, this.z ) = ( x, y, z ); + + /// + public Polynomial3D( Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x, c3.x ); + this.y = new Polynomial( c0.y, c1.y, c2.y, c3.y ); + this.z = new Polynomial( c0.z, c1.z, c2.z, c3.z ); + } + + /// + public Polynomial3D( Vector3 c0, Vector3 c1, Vector3 c2 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x, 0 ); + this.y = new Polynomial( c0.y, c1.y, c2.y, 0 ); + this.z = new Polynomial( c0.z, c1.z, c2.z, 0 ); + } + + /// + public Polynomial3D( Vector3 c0, Vector3 c1 ) { + this.x = new Polynomial( c0.x, c1.x, 0, 0 ); + this.y = new Polynomial( c0.y, c1.y, 0, 0 ); + this.z = new Polynomial( c0.z, c1.z, 0, 0 ); + } + + /// + public Polynomial3D( Vector3Matrix4x1 coefficients ) => ( x, y, z ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ), new Polynomial( coefficients.Z ) ); + + /// + public Polynomial3D( Vector3Matrix3x1 coefficients ) => ( x, y, z ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ), new Polynomial( coefficients.Z ) ); + + #region IPolynomialCubic + + public Vector3 C0 { + [MethodImpl( INLINE )] get => new(x.c0, y.c0, z.c0); + [MethodImpl( INLINE )] set => ( x.c0, y.c0, z.c0 ) = ( value.x, value.y, value.z ); + } + public Vector3 C1 { + [MethodImpl( INLINE )] get => new(x.c1, y.c1, z.c1); + [MethodImpl( INLINE )] set => ( x.c1, y.c1, z.c1 ) = ( value.x, value.y, value.z ); + } + public Vector3 C2 { + [MethodImpl( INLINE )] get => new(x.c2, y.c2, z.c2); + [MethodImpl( INLINE )] set => ( x.c2, y.c2, z.c2 ) = ( value.x, value.y, value.z ); + } + public Vector3 C3 { + [MethodImpl( INLINE )] get => new(x.c3, y.c3, z.c3); + [MethodImpl( INLINE )] set => ( x.c3, y.c3, z.c3 ) = ( value.x, value.y, value.z ); + } + + public Polynomial this[ int i ] { + get { return i switch { 0 => x, 1 => y, 2 => z, _ => throw new IndexOutOfRangeException( "Polynomial3D component index has to be either 0, 1, or 2" ) }; } + set => _ = i switch { 0 => x = value, 1 => y = value, 2 => z = value, _ => throw new IndexOutOfRangeException() }; + } + + [MethodImpl( INLINE )] public Vector3 GetCoefficient( int degree ) => + degree switch { + 0 => C0, + 1 => C1, + 2 => C2, + 3 => C3, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + + [MethodImpl( INLINE )] public void SetCoefficient( int degree, Vector3 value ) { + _ = degree switch { + 0 => C0 = value, + 1 => C1 = value, + 2 => C2 = value, + 3 => C3 = value, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + } + + public readonly Vector3 Eval( float t ) { + float t2 = t * t; + float t3 = t2 * t; + return new Vector3( + x.c3 * t3 + x.c2 * t2 + x.c1 * t + x.c0, + y.c3 * t3 + y.c2 * t2 + y.c1 * t + y.c0, + z.c3 * t3 + z.c2 * t2 + z.c1 * t + z.c0 + ); + } + + [MethodImpl( INLINE )] public Vector3 Eval( float t, int n ) => Differentiate( n ).Eval( t ); + + [MethodImpl( INLINE )] public readonly Polynomial3D Differentiate( int n = 1 ) => new(x.Differentiate( n ), y.Differentiate( n ), z.Differentiate( n )); + + public Polynomial3D ScaleParameterSpace( float factor ) { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if( factor == 1f ) + return this; + float factor2 = factor * factor; + float factor3 = factor2 * factor; + return new Polynomial3D( + new Polynomial( x.c0, x.c1 / factor, x.c2 / factor2, x.c3 / factor3 ), + new Polynomial( y.c0, y.c1 / factor, y.c2 / factor2, y.c3 / factor3 ), + new Polynomial( z.c0, z.c1 / factor, z.c2 / factor2, z.c3 / factor3 ) + ); + } + + public Polynomial3D Compose( float g0, float g1 ) => new(x.Compose( g0, g1 ), y.Compose( g0, g1 ), z.Compose( g0, g1 )); + + #endregion + + /// + public static Polynomial3D FitCubicFrom0( float x1, float x2, float x3, Vector3 y0, Vector3 y1, Vector3 y2, Vector3 y3 ) { + // precalcs + float i12 = x2 - x1; + float i13 = x3 - x1; + float i23 = x3 - x2; + float x1x2 = x1 * x2; + float x1x3 = x1 * x3; + float x2x3 = x2 * x3; + float x1x2x3 = x1 * x2x3; + float x0plusx1plusx2 = x1 + x2; + float x0plusx1plusx3 = x1 + x3; + float x2plusx3 = x2 + x3; + float x1plusx2plusx3 = x1 + x2plusx3; + float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 ); + + // scale factors + Vector3 scl0 = y0 / -( x1 * x2 * x3 ); + Vector3 scl1 = y1 / +( x1 * i12 * i13 ); + Vector3 scl2 = y2 / -( x2 * i12 * i23 ); + Vector3 scl3 = y3 / +( x3 * i13 * i23 ); + + // polynomial form + Vector3 c0 = new( + -( scl0.x * x1x2x3 ), + -( scl0.y * x1x2x3 ), + -( scl0.z * x1x2x3 ) + ); + Vector3 c1 = new( + scl0.x * x1x2plusx1x3plusx2x3 + scl1.x * x2x3 + scl2.x * x1x3 + scl3.x * x1x2, + scl0.y * x1x2plusx1x3plusx2x3 + scl1.y * x2x3 + scl2.y * x1x3 + scl3.y * x1x2, + scl0.z * x1x2plusx1x3plusx2x3 + scl1.z * x2x3 + scl2.z * x1x3 + scl3.z * x1x2 + ); + Vector3 c2 = new( + -( scl0.x * x1plusx2plusx3 + scl1.x * x2plusx3 + scl2.x * x0plusx1plusx3 + scl3.x * x0plusx1plusx2 ), + -( scl0.y * x1plusx2plusx3 + scl1.y * x2plusx3 + scl2.y * x0plusx1plusx3 + scl3.y * x0plusx1plusx2 ), + -( scl0.z * x1plusx2plusx3 + scl1.z * x2plusx3 + scl2.z * x0plusx1plusx3 + scl3.z * x0plusx1plusx2 ) + ); + Vector3 c3 = new( + scl0.x + scl1.x + scl2.x + scl3.x, + scl0.y + scl1.y + scl2.y + scl3.y, + scl0.z + scl1.z + scl2.z + scl3.z + ); + + return new Polynomial3D( c0, c1, c2, c3 ); + } + + + /// + public Bounds GetBounds01() => FloatRange.ToBounds( x.OutputRange01, y.OutputRange01, z.OutputRange01 ); + + /// + public (Polynomial3D pre, Polynomial3D post) Split01( float u ) { + ( Polynomial xPre, Polynomial xPost ) = x.Split01( u ); + ( Polynomial yPre, Polynomial yPost ) = y.Split01( u ); + ( Polynomial zPre, Polynomial zPost ) = z.Split01( u ); + return ( new Polynomial3D( xPre, yPre, zPre ), new Polynomial3D( xPost, yPost, zPost ) ); + } + + #region IParamCurve3Diff interface implementations + + public int Degree => Mathfs.Max( x.Degree, y.Degree, z.Degree ); + public readonly Vector3 EvalDerivative( float t ) => Differentiate().Eval( t ); + public Vector3 EvalSecondDerivative( float t ) => Differentiate( 2 ).Eval( t ); + public Vector3 EvalThirdDerivative( float t = 0 ) => Differentiate( 3 ).Eval( 0 ); + + #endregion + + #region Project Point + + /// + public Vector3 ProjectPoint( Vector3 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations ); + + struct PointProjectSample { + public float t; + public float distDeltaSq; + public Vector3 f; + public Vector3 fp; + } + + static PointProjectSample[] pointProjectGuesses = { default, default, default }; + + /// + public Vector3 ProjectPoint( Vector3 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) { + // define a bezier relative to the test point + Polynomial3D curve = this; + curve.x.c0 -= point.x; // constant coefficient defines the start position + curve.y.c0 -= point.y; + curve.z.c0 -= point.z; + Vector3 curveStart = curve.Eval( 0 ); + Vector3 curveEnd = curve.Eval( 1 ); + + PointProjectSample SampleDistSqDelta( float tSmp ) { + PointProjectSample s = new PointProjectSample { t = tSmp }; + ( s.f, s.fp ) = ( curve.Eval( tSmp ), curve.EvalDerivative( tSmp ) ); + s.distDeltaSq = Vector3.Dot( s.f, s.fp ); + return s; + } + + // find initial candidates + int candidatesFound = 0; + PointProjectSample prevSmp = SampleDistSqDelta( 0 ); + + for( int i = 1; i < initialSubdivisions; i++ ) { + float ti = i / ( initialSubdivisions - 1f ); + PointProjectSample smp = SampleDistSqDelta( ti ); + if( Mathfs.SignAsInt( smp.distDeltaSq ) != Mathfs.SignAsInt( prevSmp.distDeltaSq ) ) { + pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 ); + if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree + } + + prevSmp = smp; + } + + // refine each guess w. Newton-Raphson iterations + void Refine( ref PointProjectSample smp ) { + Vector3 fpp = curve.EvalSecondDerivative( smp.t ); + float tNew = smp.t - Vector3.Dot( smp.f, smp.fp ) / ( Vector3.Dot( smp.f, fpp ) + Vector3.Dot( smp.fp, smp.fp ) ); + smp = SampleDistSqDelta( tNew ); + } + + for( int p = 0; p < candidatesFound; p++ ) + for( int i = 0; i < refinementIterations; i++ ) + Refine( ref pointProjectGuesses[p] ); + + // Now find closest. First include the endpoints + float sqDist0 = curveStart.sqrMagnitude; // include endpoints + float sqDist1 = curveEnd.sqrMagnitude; + bool firstClosest = sqDist0 < sqDist1; + float tClosest = firstClosest ? 0 : 1; + Vector3 ptClosest = ( firstClosest ? curveStart : curveEnd ) + point; + float distSqClosest = firstClosest ? sqDist0 : sqDist1; + + // then check internal roots + for( int i = 0; i < candidatesFound; i++ ) { + float pSqmag = pointProjectGuesses[i].f.sqrMagnitude; + if( pSqmag < distSqClosest ) { + distSqClosest = pSqmag; + tClosest = pointProjectGuesses[i].t; + ptClosest = pointProjectGuesses[i].f + point; + } + } + + t = tClosest; + return ptClosest; + } + + #endregion + + #region Typecasting & Operators + + public static Polynomial3D operator /( Polynomial3D p, float v ) => new(p.C0 / v, p.C1 / v, p.C2 / v, p.C3 / v); + public static Polynomial3D operator *( Polynomial3D p, float v ) => new(p.C0 * v, p.C1 * v, p.C2 * v, p.C3 * v); + public static Polynomial3D operator *( float v, Polynomial3D p ) => p * v; + + public override string ToString() { + string s = ""; + s += x + "\n"; + s += y + "\n"; + s += z; + return s; + } + + public static explicit operator Polynomial2D( Polynomial3D p ) => new(p.x, p.y); + public static explicit operator Vector3Matrix3x1( Polynomial3D poly ) => new(poly.C0, poly.C1, poly.C2); + public static explicit operator Vector3Matrix4x1( Polynomial3D poly ) => new(poly.C0, poly.C1, poly.C2, poly.C3); + public static explicit operator BezierQuad3D( Polynomial3D poly ) => poly.Degree < 3 ? new BezierQuad3D( CharMatrix.quadraticBezierInverse * (Vector3Matrix3x1)poly ) : throw new InvalidCastException( "Cannot cast a cubic polynomial to a quadratic curve" ); + public static explicit operator BezierCubic3D( Polynomial3D poly ) => new(CharMatrix.cubicBezierInverse * (Vector3Matrix4x1)poly); + public static explicit operator CatRomCubic3D( Polynomial3D poly ) => new(CharMatrix.cubicCatmullRomInverse * (Vector3Matrix4x1)poly); + public static explicit operator HermiteCubic3D( Polynomial3D poly ) => new(CharMatrix.cubicHermiteInverse * (Vector3Matrix4x1)poly); + public static explicit operator UBSCubic3D( Polynomial3D poly ) => new(CharMatrix.cubicUniformBsplineInverse * (Vector3Matrix4x1)poly); + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Polynomial3D.cs.meta b/Runtime/Curves/Polynomial3D.cs.meta new file mode 100644 index 0000000..d5b8bcf --- /dev/null +++ b/Runtime/Curves/Polynomial3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db7bebf9d29d7450b978e9e990eed482 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Polynomial4D.cs b/Runtime/Curves/Polynomial4D.cs new file mode 100644 index 0000000..e0bb790 --- /dev/null +++ b/Runtime/Curves/Polynomial4D.cs @@ -0,0 +1,296 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + public struct Polynomial4D : IPolynomialCubic, IParamCurve3Diff { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// + public static readonly Polynomial4D NaN = new Polynomial4D { x = Polynomial.NaN, y = Polynomial.NaN, z = Polynomial.NaN, w = Polynomial.NaN }; + + public Polynomial x, y, z, w; + + public Polynomial4D( Polynomial x, Polynomial y, Polynomial z, Polynomial w ) => ( this.x, this.y, this.z, this.w ) = ( x, y, z, w ); + + /// + public Polynomial4D( Vector4 c0, Vector4 c1, Vector4 c2, Vector4 c3 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x, c3.x ); + this.y = new Polynomial( c0.y, c1.y, c2.y, c3.y ); + this.z = new Polynomial( c0.z, c1.z, c2.z, c3.z ); + this.w = new Polynomial( c0.w, c1.w, c2.w, c3.w ); + } + + /// + public Polynomial4D( Vector4 c0, Vector4 c1, Vector4 c2 ) { + this.x = new Polynomial( c0.x, c1.x, c2.x, 0 ); + this.y = new Polynomial( c0.y, c1.y, c2.y, 0 ); + this.z = new Polynomial( c0.z, c1.z, c2.z, 0 ); + this.w = new Polynomial( c0.w, c1.w, c2.w, 0 ); + } + + /// + public Polynomial4D( Vector4 c0, Vector4 c1 ) { + this.x = new Polynomial( c0.x, c1.x, 0, 0 ); + this.y = new Polynomial( c0.y, c1.y, 0, 0 ); + this.z = new Polynomial( c0.z, c1.z, 0, 0 ); + this.w = new Polynomial( c0.w, c1.w, 0, 0 ); + } + + /// + public Polynomial4D( Vector4Matrix4x1 coefficients ) => ( x, y, z, w ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ), new Polynomial( coefficients.Z ), new Polynomial( coefficients.W ) ); + + /// + public Polynomial4D( Vector4Matrix3x1 coefficients ) => ( x, y, z, w ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ), new Polynomial( coefficients.Z ), new Polynomial( coefficients.W ) ); + + #region IPolynomialCubic + + public Vector4 C0 { + [MethodImpl( INLINE )] get => new(x.c0, y.c0, z.c0, w.c0); + [MethodImpl( INLINE )] set => ( x.c0, y.c0, z.c0, w.c0 ) = ( value.x, value.y, value.z, value.w ); + } + public Vector4 C1 { + [MethodImpl( INLINE )] get => new(x.c1, y.c1, z.c1, w.c1); + [MethodImpl( INLINE )] set => ( x.c1, y.c1, z.c1, w.c1 ) = ( value.x, value.y, value.z, value.w ); + } + public Vector4 C2 { + [MethodImpl( INLINE )] get => new(x.c2, y.c2, z.c2, w.c2); + [MethodImpl( INLINE )] set => ( x.c2, y.c2, z.c2, w.c2 ) = ( value.x, value.y, value.z, value.w ); + } + public Vector4 C3 { + [MethodImpl( INLINE )] get => new(x.c3, y.c3, z.c3, w.c3); + [MethodImpl( INLINE )] set => ( x.c3, y.c3, z.c3, w.c3 ) = ( value.x, value.y, value.z, value.w ); + } + + public Polynomial this[ int i ] { + get => i switch { 0 => x, 1 => y, 2 => z, 4 => w, _ => throw new IndexOutOfRangeException( "Polynomial4D component index has to be either 0, 1, 2, or 3" ) }; + set => _ = i switch { 0 => x = value, 1 => y = value, 2 => z = value, 3 => w = value, _ => throw new IndexOutOfRangeException() }; + } + + [MethodImpl( INLINE )] public Vector4 GetCoefficient( int degree ) => + degree switch { + 0 => C0, + 1 => C1, + 2 => C2, + 3 => C3, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + + [MethodImpl( INLINE )] public void SetCoefficient( int degree, Vector4 value ) { + _ = degree switch { + 0 => C0 = value, + 1 => C1 = value, + 2 => C2 = value, + 3 => C3 = value, + _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" ) + }; + } + + public Vector4 Eval( float t ) { + float t2 = t * t; + float t3 = t2 * t; + return new Vector4( + x.c3 * t3 + x.c2 * t2 + x.c1 * t + x.c0, + y.c3 * t3 + y.c2 * t2 + y.c1 * t + y.c0, + z.c3 * t3 + z.c2 * t2 + z.c1 * t + z.c0, + w.c3 * t3 + w.c2 * t2 + w.c1 * t + w.c0 + ); + } + + [MethodImpl( INLINE )] public Vector4 Eval( float t, int n ) => Differentiate( n ).Eval( t ); + + [MethodImpl( INLINE )] public Polynomial4D Differentiate( int n = 1 ) => new(x.Differentiate( n ), y.Differentiate( n ), z.Differentiate( n ), w.Differentiate( n )); + + public Polynomial4D ScaleParameterSpace( float factor ) { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if( factor == 1f ) + return this; + float factor2 = factor * factor; + float factor3 = factor2 * factor; + return new Polynomial4D( + new Polynomial( x.c0, x.c1 / factor, x.c2 / factor2, x.c3 / factor3 ), + new Polynomial( y.c0, y.c1 / factor, y.c2 / factor2, y.c3 / factor3 ), + new Polynomial( z.c0, z.c1 / factor, z.c2 / factor2, z.c3 / factor3 ), + new Polynomial( w.c0, w.c1 / factor, w.c2 / factor2, w.c3 / factor3 ) + ); + } + + public Polynomial4D Compose( float g0, float g1 ) => new(x.Compose( g0, g1 ), y.Compose( g0, g1 ), z.Compose( g0, g1 ), w.Compose( g0, g1 )); + + #endregion + + public static Polynomial4D FitCubicFrom0( float x1, float x2, float x3, Vector4 y0, Vector4 y1, Vector4 y2, Vector4 y3 ) { + // precalcs + float i12 = x2 - x1; + float i13 = x3 - x1; + float i23 = x3 - x2; + float x1x2 = x1 * x2; + float x1x3 = x1 * x3; + float x2x3 = x2 * x3; + float x1x2x3 = x1 * x2x3; + float x0plusx1plusx2 = x1 + x2; + float x0plusx1plusx3 = x1 + x3; + float x2plusx3 = x2 + x3; + float x1plusx2plusx3 = x1 + x2plusx3; + float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 ); + + // scale factors + Vector4 scl0 = y0 / -( x1 * x2 * x3 ); + Vector4 scl1 = y1 / +( x1 * i12 * i13 ); + Vector4 scl2 = y2 / -( x2 * i12 * i23 ); + Vector4 scl3 = y3 / +( x3 * i13 * i23 ); + + // polynomial form + Vector4 c0 = new( + -( scl0.x * x1x2x3 ), + -( scl0.y * x1x2x3 ), + -( scl0.z * x1x2x3 ), + -( scl0.w * x1x2x3 ) + ); + Vector4 c1 = new( + scl0.x * x1x2plusx1x3plusx2x3 + scl1.x * x2x3 + scl2.x * x1x3 + scl3.x * x1x2, + scl0.y * x1x2plusx1x3plusx2x3 + scl1.y * x2x3 + scl2.y * x1x3 + scl3.y * x1x2, + scl0.z * x1x2plusx1x3plusx2x3 + scl1.z * x2x3 + scl2.z * x1x3 + scl3.z * x1x2, + scl0.w * x1x2plusx1x3plusx2x3 + scl1.w * x2x3 + scl2.w * x1x3 + scl3.w * x1x2 + ); + Vector4 c2 = new( + -( scl0.x * x1plusx2plusx3 + scl1.x * x2plusx3 + scl2.x * x0plusx1plusx3 + scl3.x * x0plusx1plusx2 ), + -( scl0.y * x1plusx2plusx3 + scl1.y * x2plusx3 + scl2.y * x0plusx1plusx3 + scl3.y * x0plusx1plusx2 ), + -( scl0.z * x1plusx2plusx3 + scl1.z * x2plusx3 + scl2.z * x0plusx1plusx3 + scl3.z * x0plusx1plusx2 ), + -( scl0.w * x1plusx2plusx3 + scl1.w * x2plusx3 + scl2.w * x0plusx1plusx3 + scl3.w * x0plusx1plusx2 ) + ); + Vector4 c3 = new( + scl0.x + scl1.x + scl2.x + scl3.x, + scl0.y + scl1.y + scl2.y + scl3.y, + scl0.z + scl1.z + scl2.z + scl3.z, + scl0.w + scl1.w + scl2.w + scl3.w + ); + + return new Polynomial4D( c0, c1, c2, c3 ); + } + + /// + public (FloatRange x, FloatRange y, FloatRange z, FloatRange w) GetBounds01() => ( x.OutputRange01, y.OutputRange01, z.OutputRange01, w.OutputRange01 ); + + /// + public (Polynomial4D pre, Polynomial4D post) Split01( float u ) { + ( Polynomial xPre, Polynomial xPost ) = x.Split01( u ); + ( Polynomial yPre, Polynomial yPost ) = y.Split01( u ); + ( Polynomial zPre, Polynomial zPost ) = z.Split01( u ); + ( Polynomial wPre, Polynomial wPost ) = w.Split01( u ); + return ( new Polynomial4D( xPre, yPre, zPre, wPre ), new Polynomial4D( xPost, yPost, zPost, wPost ) ); + } + + #region IParamCurve3Diff interface implementations + + public int Degree => Mathfs.Max( x.Degree, y.Degree, z.Degree, w.Degree ); + public Vector4 EvalDerivative( float t ) => Differentiate().Eval( t ); + public Vector4 EvalSecondDerivative( float t ) => Differentiate( 2 ).Eval( t ); + public Vector4 EvalThirdDerivative( float t = 0 ) => Differentiate( 3 ).Eval( 0 ); + + #endregion + + #region Project Point + + /// + public Vector4 ProjectPoint( Vector4 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations ); + + struct PointProjectSample { + public float t; + public float distDeltaSq; + public Vector4 f; + public Vector4 fp; + } + + static PointProjectSample[] pointProjectGuesses = { default, default, default }; + + /// + public Vector4 ProjectPoint( Vector4 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) { + // define a bezier relative to the test point + Polynomial4D curve = this; + curve.x.c0 -= point.x; // constant coefficient defines the start position + curve.y.c0 -= point.y; + curve.z.c0 -= point.z; + Vector4 curveStart = curve.Eval( 0 ); + Vector4 curveEnd = curve.Eval( 1 ); + + PointProjectSample SampleDistSqDelta( float tSmp ) { + PointProjectSample s = new PointProjectSample { t = tSmp }; + ( s.f, s.fp ) = ( curve.Eval( tSmp ), curve.EvalDerivative( tSmp ) ); + s.distDeltaSq = Vector4.Dot( s.f, s.fp ); + return s; + } + + // find initial candidates + int candidatesFound = 0; + PointProjectSample prevSmp = SampleDistSqDelta( 0 ); + + for( int i = 1; i < initialSubdivisions; i++ ) { + float ti = i / ( initialSubdivisions - 1f ); + PointProjectSample smp = SampleDistSqDelta( ti ); + if( Mathfs.SignAsInt( smp.distDeltaSq ) != Mathfs.SignAsInt( prevSmp.distDeltaSq ) ) { + pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 ); + if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree + } + + prevSmp = smp; + } + + // refine each guess w. Newton-Raphson iterations + void Refine( ref PointProjectSample smp ) { + Vector4 fpp = curve.EvalSecondDerivative( smp.t ); + float tNew = smp.t - Vector4.Dot( smp.f, smp.fp ) / ( Vector4.Dot( smp.f, fpp ) + Vector4.Dot( smp.fp, smp.fp ) ); + smp = SampleDistSqDelta( tNew ); + } + + for( int p = 0; p < candidatesFound; p++ ) + for( int i = 0; i < refinementIterations; i++ ) + Refine( ref pointProjectGuesses[p] ); + + // Now find closest. First include the endpoints + float sqDist0 = curveStart.sqrMagnitude; // include endpoints + float sqDist1 = curveEnd.sqrMagnitude; + bool firstClosest = sqDist0 < sqDist1; + float tClosest = firstClosest ? 0 : 1; + Vector4 ptClosest = ( firstClosest ? curveStart : curveEnd ) + point; + float distSqClosest = firstClosest ? sqDist0 : sqDist1; + + // then check internal roots + for( int i = 0; i < candidatesFound; i++ ) { + float pSqmag = pointProjectGuesses[i].f.sqrMagnitude; + if( pSqmag < distSqClosest ) { + distSqClosest = pSqmag; + tClosest = pointProjectGuesses[i].t; + ptClosest = pointProjectGuesses[i].f + point; + } + } + + t = tClosest; + return ptClosest; + } + + #endregion + + #region Typecasting & Operators + + public static Polynomial4D operator /( Polynomial4D p, float v ) => new(p.C0 / v, p.C1 / v, p.C2 / v, p.C3 / v); + public static Polynomial4D operator *( Polynomial4D p, float v ) => new(p.C0 * v, p.C1 * v, p.C2 * v, p.C3 * v); + public static Polynomial4D operator *( float v, Polynomial4D p ) => p * v; + + public static explicit operator Vector4Matrix3x1( Polynomial4D poly ) => new(poly.C0, poly.C1, poly.C2); + public static explicit operator Vector4Matrix4x1( Polynomial4D poly ) => new(poly.C0, poly.C1, poly.C2, poly.C3); + public static explicit operator BezierQuad4D( Polynomial4D poly ) => poly.Degree < 3 ? new BezierQuad4D( CharMatrix.quadraticBezierInverse * (Vector4Matrix3x1)poly ) : throw new InvalidCastException( "Cannot cast a cubic polynomial to a quadratic curve" ); + public static explicit operator BezierCubic4D( Polynomial4D poly ) => new(CharMatrix.cubicBezierInverse * (Vector4Matrix4x1)poly); + public static explicit operator CatRomCubic4D( Polynomial4D poly ) => new(CharMatrix.cubicCatmullRomInverse * (Vector4Matrix4x1)poly); + public static explicit operator HermiteCubic4D( Polynomial4D poly ) => new(CharMatrix.cubicHermiteInverse * (Vector4Matrix4x1)poly); + public static explicit operator UBSCubic4D( Polynomial4D poly ) => new(CharMatrix.cubicUniformBsplineInverse * (Vector4Matrix4x1)poly); + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Polynomial4D.cs.meta b/Runtime/Curves/Polynomial4D.cs.meta new file mode 100644 index 0000000..9f90047 --- /dev/null +++ b/Runtime/Curves/Polynomial4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4abb3aedb35e5495eb7a39471860fc50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Curves/Solve.cs b/Runtime/Curves/Solve.cs new file mode 100644 index 0000000..da67300 --- /dev/null +++ b/Runtime/Curves/Solve.cs @@ -0,0 +1,187 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + public static class Solve { + + /// Solves for x in a polynomial equation c₀ + c₁x = rhs + public static float? Eq( float c0, float c1, float equals ) => Polynomial( c0 - equals, c1 ); + + /// Solves for x in a polynomial equation c₀ + c₁x + c₂x² = rhs. Roots are sorted in increasing order + public static ResultsMax2 Eq( float c0, float c1, float c2, float equals ) => Polynomial( c0 - equals, c1, c2 ); + + /// Solves for x in a polynomial equation c₀ + c₁x + c₂x² + c₃x³ = rhs. Roots are sorted in increasing order + public static ResultsMax3 Eq( float c0, float c1, float c2, float c3, float equals ) => Polynomial( c0 - equals, c1, c2, c3 ); + + /// Solves for x in a polynomial equation c₀ + c₁x + c₂x² + c₃x³ + c₄x⁴ = rhs. Roots are sorted in increasing order + public static ResultsMax4 Eq( float c0, float c1, float c2, float c3, float c4, float equals ) => Polynomial( c0 - equals, c1, c2, c3, c4 ); + + /// Finds roots/x values where a polynomial c₀ + c₁x = 0 + public static float? Polynomial( float c0, float c1 ) => Mathf.Approximately( c1, 0 ) ? null : -c0 / c1; + + /// Finds roots/x values where a polynomial c₀ + c₁x + c₂x² = 0. Roots are sorted in increasing order + public static ResultsMax2 Polynomial( float c0, float c1, float c2 ) { + if( Mathf.Approximately( c2, 0 ) ) + return Polynomial( c0, c1 ); // curve is lower order + float disc = c1 * c1 - 4 * c2 * c0; + if( Mathf.Approximately( disc, 0 ) ) + return Polynomial( c1, 2 * c2 ); // one root + if( disc < 0 ) + return default; // no roots + // two roots: + float q = -( c1 + MathF.Sqrt( disc ) * c1.Sign() ) / 2; + float r0 = q / c2; + float r1 = c0 / q; + return new ResultsMax2( MathF.Min( r0, r1 ), MathF.Max( r0, r1 ) ); + } + + /// Finds roots/x values where a polynomial c₀ + c₁x + c₂x² + c₃x³ = 0. Roots are sorted in increasing order + public static ResultsMax3 Polynomial( float c0, float c1, float c2, float c3 ) { + if( Mathf.Approximately( c3, 0 ) ) + return Polynomial( c0, c1, c2 ); // curve is lower order + if( c2 == 0f && c3 == 1f ) + return SolveDepressedCubicRoots( c0, c1 ); // It's a depressed cubic :( c₀ + c₁t + t³ + + // first, depress the cubic to make it easier to solve + float aa = c3 * c3; + float ac = c3 * c1; + float bb = c2 * c2; + float p_c1 = ( 3 * ac - bb ) / ( 3 * aa ); + float q_c0 = ( 2 * bb * c2 - 9 * ac * c2 + 27 * aa * c0 ) / ( 27 * aa * c3 ); + + ResultsMax3 dpr = SolveDepressedCubicRoots( q_c0, p_c1 ); + + // we now have the roots of the depressed cubic, now convert back to the normal cubic + ResultsMax3 results = default; + for( int i = 0; i < dpr.count; i++ ) + results = results.InsertSorted( dpr[i] - c2 / ( 3 * c3 ) ); + return results; + } + + /// Finds roots/x values where a polynomial c₀ + c₁x + c₂x² + c₃x³ + c₄x⁴ = 0. Roots are sorted in increasing order + static ResultsMax4 Polynomial( float c0, float c1, float c2, float c3, float c4 ) { + if( Mathf.Approximately( c4, 0 ) ) + return Polynomial( c0, c1, c2, c3 ); // curve is lower order + if( Mathf.Approximately( c1, 0 ) && Mathf.Approximately( c3, 0 ) ) + return SolveBiquadraticRoots( c0, c2, c4 ); // curve is biquadratic -> c0 + c2x² + c4x⁴= 0 + + float iA = 1f / c4; + float BoA = c3 * iA; + float BoA2 = BoA * BoA; + float BoA3 = BoA * BoA * BoA; + float BoA4 = BoA2 * BoA2; + + float a = -( 3f / 8f ) * BoA2 + c2 * iA; + float b = BoA3 / 8f - ( c2 / 2 ) * BoA * iA + c1 * iA; + float c = -( 3f / 256 ) * BoA4 + ( c2 / 16f ) * BoA2 * iA - ( c1 / 4 ) * BoA * iA + c0 * iA; + + if( Mathf.Approximately( b, 0 ) ) + return SolveBiquadraticRoots( c, a, 1 ); + + // not biquadratic oh boy + ResultsMax3 yRoots = Solve.Polynomial( ( a * c ) / 2f - ( b * b ) / 8f, -c, -a / 2f, 1 ); + if( yRoots.count == 0 ) + return default; // no roots + + // filter roots + float y = float.NaN; + float vBest = float.NegativeInfinity; + for( int i = 0; i < yRoots.count; i++ ) { + float v = 2 * yRoots[i] - a; + if( Mathf.Approximately( v, 0 ) ) + continue; + if( v >= 0f && v > vBest ) { + y = yRoots[i]; + vBest = v; + } + } + if( float.IsNaN( y ) ) + return default; // no roots + + float R = math.sqrt( vBest ); + float i0 = b / ( 2f * R ); + ResultsMax2 q0 = Solve.Polynomial( y / 2f - i0, +R / 2, 1f ); + ResultsMax2 q1 = Solve.Polynomial( y / 2f + i0, -R / 2, 1f ); + + float offset = -c3 / ( 4 * c4 ); + ResultsMax4 results = default; + for( int i = 0; i < q0.count; i++ ) + results = results.InsertSorted( q0[i] + offset ); + for( int i = 0; i < q1.count; i++ ) + results = results.InsertSorted( q1[i] + offset ); + return results; + } + + /// t³ + c₁t + c₀ = 0 + static ResultsMax3 SolveDepressedCubicRoots( float c0, float c1 ) { + if( Mathf.Approximately( c1, 0 ) ) // triple root - one solution. solve x³+q = 0 => x = cr(-q) + return new ResultsMax3( Mathfs.Cbrt( -c0 ) ); + float discriminant = 4 * c1 * c1 * c1 + 27 * c0 * c0; + if( discriminant < 0.00001 ) { // two or three roots guaranteed, use trig solution + float pre = 2 * MathF.Sqrt( -c1 / 3 ); + float acosInner = ( ( 3 * c0 ) / ( 2 * c1 ) ) * MathF.Sqrt( -3 / c1 ); + + float GetRoot( int k ) => pre * MathF.Cos( ( 1f / 3f ) * Mathfs.Acos( acosInner.ClampNeg1to1() ) - ( Mathfs.TAU / 3f ) * k ); + // if acos hits 0 or TAU/2, the offsets will have the same value, + // which means we have a double root plus one regular root on our hands + if( acosInner >= 0.9999f ) + return new ResultsMax3( GetRoot( 0 ), GetRoot( 2 ) ); // two roots - one single and one double root + if( acosInner <= -0.9999f ) + return new ResultsMax3( GetRoot( 1 ), GetRoot( 2 ) ); // two roots - one single and one double root + return new ResultsMax3( GetRoot( 0 ), GetRoot( 1 ), GetRoot( 2 ) ); // three roots + } + + if( discriminant > 0 && c1 < 0 ) { // one root + float coshInner = ( 1f / 3f ) * Mathfs.Acosh( ( -3 * c0.Abs() / ( 2 * c1 ) ) * MathF.Sqrt( -3 / c1 ) ); + float r = -2 * Mathfs.Sign( c0 ) * MathF.Sqrt( -c1 / 3 ) * Mathfs.Cosh( coshInner ); + return new ResultsMax3( r ); + } + + if( c1 > 0 ) { // one root + float sinhInner = ( 1f / 3f ) * Mathfs.Asinh( ( ( 3 * c0 ) / ( 2 * c1 ) ) * MathF.Sqrt( 3 / c1 ) ); + float r = ( -2 * MathF.Sqrt( c1 / 3 ) ) * Mathfs.Sinh( sinhInner ); + return new ResultsMax3( r ); + } + + // no roots + return default; + } + + /// c4x⁴ + c2x² + c0 = 0 + static ResultsMax4 SolveBiquadraticRoots( float c0, float c2, float c4 ) { + ResultsMax2 z = Polynomial( c0, c2, c4 ); + + // filter roots, keep positive only + if( z.count == 2 ) { + if( z.b < 0 && z.a < 0 ) + return default; // no roots + if( z.a >= 0 && z.b < 0 ) + z = new ResultsMax2( z.a ); + if( z.b >= 0 && z.a < 0 ) + z = new ResultsMax2( z.b ); + } else if( z.count == 1 && z.a < 0 ) { + return default; // no roots + } + + if( z.count == 2 ) { + ( float small, float big ) = z.a < z.b ? ( z.a, z.b ) : ( z.b, z.a ); + big = math.sqrt( big ); + if( Mathf.Approximately( c0, 0 ) ) // three roots + return new ResultsMax4( -big, 0, big ); + // four roots + small = math.sqrt( small ); + return new ResultsMax4( -big, -small, small, big ); + } else if( z.count == 1 ) { + if( Mathf.Approximately( c0, 0 ) ) + return 0; // one root at 0 + // two roots + float x = math.sqrt( z.a ); + return new ResultsMax4( -x, x ); + } + return default; + } + } + +} \ No newline at end of file diff --git a/Runtime/Curves/Solve.cs.meta b/Runtime/Curves/Solve.cs.meta new file mode 100644 index 0000000..5cc9500 --- /dev/null +++ b/Runtime/Curves/Solve.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6926dbd05cff4b04a255583a2d403d49 +timeCreated: 1785466382 \ No newline at end of file diff --git a/Curves/Trajectory2D.cs b/Runtime/Curves/Trajectory2D.cs similarity index 99% rename from Curves/Trajectory2D.cs rename to Runtime/Curves/Trajectory2D.cs index 2a1c141..b72ce36 100644 --- a/Curves/Trajectory2D.cs +++ b/Runtime/Curves/Trajectory2D.cs @@ -1,4 +1,4 @@ -// collected and expended upon by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) using UnityEngine; using static Freya.Mathfs; diff --git a/Runtime/Curves/Trajectory2D.cs.meta b/Runtime/Curves/Trajectory2D.cs.meta new file mode 100644 index 0000000..e9c84a9 --- /dev/null +++ b/Runtime/Curves/Trajectory2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77d647eaddaf740dfa0ede98c36474f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Enums.meta b/Runtime/Enums.meta new file mode 100644 index 0000000..7cd31b1 --- /dev/null +++ b/Runtime/Enums.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f78df51a080d0a0469ef09acab10afce +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Enums/Axis.cs b/Runtime/Enums/Axis.cs new file mode 100644 index 0000000..78275cb --- /dev/null +++ b/Runtime/Enums/Axis.cs @@ -0,0 +1,11 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +namespace Freya { + + public enum Axis { + X = 0, + Y = 1, + Z = 2 + } + +} \ No newline at end of file diff --git a/Runtime/Enums/Axis.cs.meta b/Runtime/Enums/Axis.cs.meta new file mode 100644 index 0000000..8e1dfaa --- /dev/null +++ b/Runtime/Enums/Axis.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c486966d4fb6a6f46b350341c26bb8a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Enums/RotationSpace.cs b/Runtime/Enums/RotationSpace.cs new file mode 100644 index 0000000..a3370db --- /dev/null +++ b/Runtime/Enums/RotationSpace.cs @@ -0,0 +1,12 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +namespace Freya { + + public enum RotationSpace { + /// An intrinsic rotation around its own local axes, usually called "local" or "self" space. Equivalent to q*rotation + Self, + /// Rotation around its pre-rotation axes, usually "world" space. Equivalent to rotation*q + Extrinsic + } + +} \ No newline at end of file diff --git a/Runtime/Enums/RotationSpace.cs.meta b/Runtime/Enums/RotationSpace.cs.meta new file mode 100644 index 0000000..8177792 --- /dev/null +++ b/Runtime/Enums/RotationSpace.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9ea6b7f7cd84d743ad4651c7f27967a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Extensions.cs b/Runtime/Extensions.cs new file mode 100644 index 0000000..eb20faf --- /dev/null +++ b/Runtime/Extensions.cs @@ -0,0 +1,1109 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using Unity.Mathematics; +using UnityEngine; +using static Unity.Mathematics.math; + +namespace Freya { + + /// Various extensions for floats, vectors and colors + public static class MathfsExtensions { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + #region Vector rotation and angles + + /// Returns the angle of this vector, in radians + /// The vector to get the angle of. It does not have to be normalized + /// + [MethodImpl( INLINE )] public static float Angle( this Vector2 v ) => MathF.Atan2( v.y, v.x ); + + /// + [MethodImpl( INLINE )] public static float Angle( this float2 v ) => atan2( v.y, v.x ); + + /// Rotates the vector 90 degrees clockwise (negative Z axis rotation) + [MethodImpl( INLINE )] public static Vector2 Rotate90CW( this Vector2 v ) => new Vector2( v.y, -v.x ); + + /// Rotates the vector 90 degrees counter-clockwise (positive Z axis rotation) + [MethodImpl( INLINE )] public static Vector2 Rotate90CCW( this Vector2 v ) => new Vector2( -v.y, v.x ); + + /// Rotates the vector around pivot with the given angle (in radians) + /// The vector to rotate + /// The point to rotate around + /// The angle to rotate by, in radians + [MethodImpl( INLINE )] public static Vector2 RotateAround( this Vector2 v, Vector2 pivot, float angRad ) => Rotate( v - pivot, angRad ) + pivot; + + /// Rotates the vector around (0,0) with the given angle (in radians) + /// The vector to rotate + /// The angle to rotate by, in radians + public static Vector2 Rotate( this Vector2 v, float angRad ) { + float ca = MathF.Cos( angRad ); + float sa = MathF.Sin( angRad ); + return new Vector2( ca * v.x - sa * v.y, sa * v.x + ca * v.y ); + } + + /// Converts an angle in degrees to radians + /// The angle, in degrees, to convert to radians + [MethodImpl( INLINE )] public static float DegToRad( this float angDegrees ) => angDegrees * Mathfs.Deg2Rad; + + /// Converts an angle in radians to degrees + /// The angle, in radians, to convert to degrees + [MethodImpl( INLINE )] public static float RadToDeg( this float angRadians ) => angRadians * Mathfs.Rad2Deg; + + /// Extracts the quaternion components into a Vector4 + /// The quaternion to get the components of + [MethodImpl( INLINE )] public static Vector4 ToVector4( this Quaternion q ) => new Vector4( q.x, q.y, q.z, q.w ); + + /// Converts to a rotation vector (axis-angle where the angle is embedded in the magnitude, in radians) + /// The quaternion to get the rotation vector of + [MethodImpl( INLINE )] public static Vector3 ToRotationVector( this Quaternion q ) { + q.ToAngleAxis( out float angDeg, out Vector3 axis ); + return axis * ( angDeg * Mathf.Deg2Rad ); + } + + #endregion + + #region Swizzling + + /// Returns X and Y as a Vector2, equivalent to new Vector2(v.x,v.y) + [MethodImpl( INLINE )] public static Vector2 XY( this Vector3 v ) => new(v.x, v.y); + + /// Returns X and Y as a Vector2, equivalent to new Vector2(v.x,v.y) + [MethodImpl( INLINE )] public static Vector2 XY( this Vector4 v ) => new(v.x, v.y); + + /// Returns Y and X as a Vector2, equivalent to new Vector2(v.y,v.x) + [MethodImpl( INLINE )] public static Vector2 YX( this Vector3 v ) => new(v.y, v.x); + + /// Returns X and Z as a Vector2, equivalent to new Vector2(v.x,v.z) + [MethodImpl( INLINE )] public static Vector2 XZ( this Vector3 v ) => new(v.x, v.z); + + /// Returns this vector as a Vector3, slotting X into X, and Y into Z, and the input value y into Y. + /// Equivalent to new Vector3(v.x,y,v.y) + [MethodImpl( INLINE )] public static Vector3 XZtoXYZ( this Vector2 v, float y = 0 ) => new Vector3( v.x, y, v.y ); + + /// Returns this vector as a Vector3, slotting X into X, and Y into Y, and the input value z into Z. + /// Equivalent to new Vector3(v.x,v.y,z) + [MethodImpl( INLINE )] public static Vector3 XYtoXYZ( this Vector2 v, float z = 0 ) => new Vector3( v.x, v.y, z ); + + /// Sets X to 0 + [MethodImpl( INLINE )] public static Vector2 FlattenX( this Vector2 v ) => new Vector2( 0f, v.y ); + + /// Sets Y to 0 + [MethodImpl( INLINE )] public static Vector2 FlattenY( this Vector2 v ) => new Vector2( v.x, 0f ); + + /// Sets X to 0 + [MethodImpl( INLINE )] public static Vector3 FlattenX( this Vector3 v ) => new Vector3( 0f, v.y, v.z ); + + /// Sets Y to 0 + [MethodImpl( INLINE )] public static Vector3 FlattenY( this Vector3 v ) => new Vector3( v.x, 0f, v.z ); + + /// Sets Z to 0 + [MethodImpl( INLINE )] public static Vector3 FlattenZ( this Vector3 v ) => new Vector3( v.x, v.y, 0f ); + + #endregion + + #region Vector directions & magnitudes + + /// Returns the chebyshev magnitude of this vector + [MethodImpl( INLINE )] public static float ChebyshevMagnitude( this Vector3 v ) => Mathfs.Max( Abs( v.x ), Abs( v.y ), Abs( v.z ) ); + + /// Returns the taxicab/rectilinear magnitude of this vector + [MethodImpl( INLINE )] public static float TaxicabMagnitude( this Vector3 v ) => Abs( v.x ) + Abs( v.y ) + Abs( v.z ); + + /// + [MethodImpl( INLINE )] public static float ChebyshevMagnitude( this Vector2 v ) => Mathfs.Max( Abs( v.x ), Abs( v.y ) ); + + /// + [MethodImpl( INLINE )] public static float TaxicabMagnitude( this Vector2 v ) => Abs( v.x ) + Abs( v.y ); + + /// Returns a vector with the same direction, but with the given magnitude. + /// Equivalent to v.normalized*mag + [MethodImpl( INLINE )] public static Vector2 WithMagnitude( this Vector2 v, float mag ) => v.normalized * mag; + + /// + [MethodImpl( INLINE )] public static Vector3 WithMagnitude( this Vector3 v, float mag ) => v.normalized * mag; + + /// Returns a vector with the same direction, but extending the magnitude by the given amount + [MethodImpl( INLINE )] public static Vector2 AddMagnitude( this Vector2 v, float extraMagnitude ) => v * ( 1 + extraMagnitude / v.magnitude ); + + /// + [MethodImpl( INLINE )] public static Vector3 AddMagnitude( this Vector3 v, float extraMagnitude ) => v * ( 1 + extraMagnitude / v.magnitude ); + + /// Returns the vector going from one position to another, also known as the displacement. + /// Equivalent to target-v + [MethodImpl( INLINE )] public static Vector2 To( this Vector2 v, Vector2 target ) => target - v; + + /// Returns the vector going from one position to another, also known as the displacement. + /// Equivalent to target-v + [MethodImpl( INLINE )] public static Vector3 To( this Vector3 v, Vector3 target ) => target - v; + + /// Returns the normalized direction from this vector to the target. + /// Equivalent to (target-v).normalized or v.To(target).normalized + [MethodImpl( INLINE )] public static Vector2 DirTo( this Vector2 v, Vector2 target ) => ( target - v ).normalized; + + /// Returns the normalized direction from this vector to the target. + /// Equivalent to (target-v).normalized or v.To(target).normalized + [MethodImpl( INLINE )] public static Vector3 DirTo( this Vector3 v, Vector3 target ) => ( target - v ).normalized; + + /// Mirrors this vector around another point. Equivalent to rotating the vector 180° around the point + /// The point to mirror + /// The point to mirror around + [MethodImpl( INLINE )] public static Vector2 MirrorAround( this Vector2 p, Vector2 pivot ) => new(2 * pivot.x - p.x, 2 * pivot.y - p.y); + + /// Mirrors this vector around an x coordinate + /// The point to mirror + /// The x coordinate to mirror around + [MethodImpl( INLINE )] public static Vector2 MirrorAroundX( this Vector2 p, float xPivot ) => new(2 * xPivot - p.x, p.y); + + /// Mirrors this vector around a y coordinate + /// The point to mirror + /// The y coordinate to mirror around + [MethodImpl( INLINE )] public static Vector2 MirrorAroundY( this Vector2 p, float yPivot ) => new(p.x, 2 * yPivot - p.y); + + /// + [MethodImpl( INLINE )] public static Vector3 MirrorAroundX( this Vector3 p, float xPivot ) => new(2 * xPivot - p.x, p.y, p.z); + + /// + [MethodImpl( INLINE )] public static Vector3 MirrorAroundY( this Vector3 p, float yPivot ) => new(p.x, 2 * yPivot - p.y, p.z); + + /// Mirrors this vector around a y coordinate + /// The point to mirror + /// The z coordinate to mirror around + [MethodImpl( INLINE )] public static Vector3 MirrorAroundZ( this Vector3 p, float zPivot ) => new(p.x, p.y, 2 * zPivot - p.z); + + /// + [MethodImpl( INLINE )] public static Vector3 MirrorAround( this Vector3 p, Vector3 pivot ) => new(2 * pivot.x - p.x, 2 * pivot.y - p.y, 2 * pivot.z - p.z); + + /// Scale the point p around pivot by scale + /// The point to scale + /// The pivot to scale around + /// The scale to scale by + [MethodImpl( INLINE )] public static Vector2 ScaleAround( this Vector2 p, Vector2 pivot, Vector2 scale ) => new(pivot.x + ( p.x - pivot.x ) * scale.x, pivot.y + ( p.y - pivot.y ) * scale.y); + + /// + [MethodImpl( INLINE )] public static Vector3 ScaleAround( this Vector3 p, Vector3 pivot, Vector3 scale ) => new(pivot.x + ( p.x - pivot.x ) * scale.x, pivot.y + ( p.y - pivot.y ) * scale.y, pivot.z + ( p.z - pivot.z ) * scale.z); + + /// Projects the vector perpendicularly onto the other vector B + /// The vector to project with + /// The vector to project perpendicularly against. The resulting vector is along this vector + public static Vector3 Project( this Vector3 a, Vector3 b ) { + float denom = Vector3.Dot( b, b ); + if( Mathfs.Approximately( denom, 0 ) ) + throw new DivideByZeroException( "Can't project to a vector with 0 length" ); + return b * ( Vector3.Dot( a, b ) / denom ); + } + + /// Projects the vector perpendicularly *from* the initial vector, onto the other vector B + /// The vector to project perpendicularly from + /// The vector to project against. The resulting vector is along this vector + public static Vector3 ProjectPerpFrom( this Vector3 a, Vector3 b ) { + float denom = Vector3.Dot( a, b ); + if( Mathfs.Approximately( denom, 0 ) ) + throw new DivideByZeroException( "Can't project to a vector with 0 length" ); + return b * ( Vector3.Dot( a, a ) / denom ); + } + + #endregion + + #region Quaternions + + /// Rotates 180° around the extrinsic pre-rotation X axis, sometimes this is interpreted as a world space rotation, as opposed to rotating around its own axes + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundExtrX( this Quaternion q ) => new(q.w, -q.z, q.y, -q.x); + + /// Rotates 180° around the extrinsic pre-rotation Y axis, sometimes this is interpreted as a world space rotation, as opposed to rotating around its own axes + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundExtrY( this Quaternion q ) => new(q.z, q.w, -q.x, -q.y); + + /// Rotates 180° around the extrinsic pre-rotation Z axis, sometimes this is interpreted as a world space rotation, as opposed to rotating around its own axes + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundExtrZ( this Quaternion q ) => new(-q.y, q.x, q.w, -q.z); + + /// Rotates 180° around its local X axis + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundSelfX( this Quaternion q ) => new(q.w, q.z, -q.y, -q.x); + + /// Rotates 180° around its local Y axis + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundSelfY( this Quaternion q ) => new(-q.z, q.w, q.x, -q.y); + + /// Rotates 180° around its local Z axis + [MethodImpl( INLINE )] public static Quaternion Rotate180AroundSelfZ( this Quaternion q ) => new(q.y, -q.x, q.w, -q.z); + + /// Rotates this quaternion 180° so that its Y and Z axes are swapped + [MethodImpl( INLINE )] public static Quaternion SwapYZ( this Quaternion q ) { + return new Quaternion( + Mathfs.RSQRT2 * ( q.y - q.z ), + Mathfs.RSQRT2 * ( q.w - q.x ), + Mathfs.RSQRT2 * ( q.w + q.x ), + Mathfs.RSQRT2 * ( -q.y - q.z ) + ); + } + + /// Rotates this quaternion 180° so that its Z and X axes are swapped + [MethodImpl( INLINE )] public static Quaternion SwapZX( this Quaternion q ) { + return new Quaternion( + Mathfs.RSQRT2 * ( q.w + q.y ), + Mathfs.RSQRT2 * ( q.z - q.x ), + Mathfs.RSQRT2 * ( q.w - q.y ), + Mathfs.RSQRT2 * ( -q.x - q.z ) + ); + } + + /// Rotates this quaternion 180° so that its X and Y axes are swapped + [MethodImpl( INLINE )] public static Quaternion SwapXY( this Quaternion q ) { + return new Quaternion( + Mathfs.RSQRT2 * ( q.w - q.z ), + Mathfs.RSQRT2 * ( q.w + q.z ), + Mathfs.RSQRT2 * ( q.x - q.y ), + Mathfs.RSQRT2 * ( -q.x - q.y ) + ); + } + + /// Returns an 180° rotated version of this quaternion around the given axis + /// The quaternion to rotate + /// The axis to rotate around + /// The rotation space of the axis, if it should be intrinsic/self/local or extrinsic/"world" + public static Quaternion Rotate180Around( this Quaternion q, Axis axis, RotationSpace space = RotationSpace.Self ) { + return axis switch { + Axis.X => space == RotationSpace.Self ? Rotate180AroundSelfX( q ) : Rotate180AroundExtrX( q ), + Axis.Y => space == RotationSpace.Self ? Rotate180AroundSelfY( q ) : Rotate180AroundExtrY( q ), + Axis.Z => space == RotationSpace.Self ? Rotate180AroundSelfZ( q ) : Rotate180AroundExtrZ( q ), + _ => throw new ArgumentOutOfRangeException( nameof(axis), $"Invalid axis: {axis}. Expected 0, 1 or 2" ) + }; + } + + /// Returns the quaternion rotated around the given axis by the given angle in radians + /// The quaternion to rotate + /// The axis to rotate around + /// The angle to rotate by (in radians) + /// The rotation space of the axis, if it should be intrinsic/self/local or extrinsic/"world" + public static Quaternion RotateAround( this Quaternion q, Axis axis, float angRad, RotationSpace space = RotationSpace.Self ) { + float aHalf = angRad / 2; + float c = MathF.Cos( aHalf ); + float s = MathF.Sin( aHalf ); + float xc = q.x * c; + float yc = q.y * c; + float zc = q.z * c; + float wc = q.w * c; + float xs = q.x * s; + float ys = q.y * s; + float zs = q.z * s; + float ws = q.w * s; + + return space switch { + RotationSpace.Self => axis switch { + Axis.X => new Quaternion( xc + ws, yc + zs, zc - ys, wc - xs ), + Axis.Y => new Quaternion( xc - zs, yc + ws, zc + xs, wc - ys ), + Axis.Z => new Quaternion( xc + ys, yc - xs, zc + ws, wc - zs ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + RotationSpace.Extrinsic => axis switch { + Axis.X => new Quaternion( xc + ws, yc - zs, zc + ys, wc - xs ), + Axis.Y => new Quaternion( xc + zs, yc + ws, zc - xs, wc - ys ), + Axis.Z => new Quaternion( xc - ys, yc + xs, zc + ws, wc - zs ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + _ => throw new ArgumentOutOfRangeException( nameof(space) ) + }; + } + + /// Returns the quaternion rotated around the given axis by 90° + /// The quaternion to rotate + /// The axis to rotate around + /// The rotation space of the axis, if it should be intrinsic/self/local or extrinsic/"world" + public static Quaternion Rotate90Around( this Quaternion q, Axis axis, RotationSpace space = RotationSpace.Self ) { + const float v = Mathfs.RSQRT2; // cos(90°/2) = sin(90°/2) + float x = q.x; + float y = q.y; + float z = q.z; + float w = q.w; + + return space switch { + RotationSpace.Self => axis switch { + Axis.X => new Quaternion( v * ( x + w ), v * ( y + z ), v * ( z - y ), v * ( w - x ) ), + Axis.Y => new Quaternion( v * ( x - z ), v * ( y + w ), v * ( z + x ), v * ( w - y ) ), + Axis.Z => new Quaternion( v * ( x + y ), v * ( y - x ), v * ( z + w ), v * ( w - z ) ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + RotationSpace.Extrinsic => axis switch { + Axis.X => new Quaternion( v * ( x + w ), v * ( y - z ), v * ( z + y ), v * ( w - x ) ), + Axis.Y => new Quaternion( v * ( x + z ), v * ( y + w ), v * ( z - x ), v * ( w - y ) ), + Axis.Z => new Quaternion( v * ( x - y ), v * ( y + x ), v * ( z + w ), v * ( w - z ) ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + _ => throw new ArgumentOutOfRangeException( nameof(space) ) + }; + } + + /// Returns the quaternion rotated around the given axis by -90° + /// The quaternion to rotate + /// The axis to rotate around + /// The rotation space of the axis, if it should be intrinsic/self/local or extrinsic/"world" + public static Quaternion RotateNeg90Around( this Quaternion q, Axis axis, RotationSpace space = RotationSpace.Self ) { + const float v = Mathfs.RSQRT2; // cos(90°/2) = sin(90°/2) + float x = q.x; + float y = q.y; + float z = q.z; + float w = q.w; + + return space switch { + RotationSpace.Self => axis switch { + Axis.X => new Quaternion( v * ( x - w ), v * ( y - z ), v * ( z + y ), v * ( w + x ) ), + Axis.Y => new Quaternion( v * ( x + z ), v * ( y - w ), v * ( z - x ), v * ( w + y ) ), + Axis.Z => new Quaternion( v * ( x - y ), v * ( y + x ), v * ( z - w ), v * ( w + z ) ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + RotationSpace.Extrinsic => axis switch { + Axis.X => new Quaternion( v * ( x - w ), v * ( y + z ), v * ( z - y ), v * ( w + x ) ), + Axis.Y => new Quaternion( v * ( x - z ), v * ( y - w ), v * ( z + x ), v * ( w + y ) ), + Axis.Z => new Quaternion( v * ( x + y ), v * ( y - x ), v * ( z - w ), v * ( w + z ) ), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }, + _ => throw new ArgumentOutOfRangeException( nameof(space) ) + }; + } + + /// Returns the given axis of this rotation (assumes this quaternion is normalized) + public static Vector3 GetAxis( this Quaternion q, Axis axis ) { + return axis switch { + Axis.X => q.Right(), + Axis.Y => q.Up(), + Axis.Z => q.Forward(), + _ => throw new ArgumentOutOfRangeException( nameof(axis) ) + }; + } + + /// Returns the X axis of this rotation (assumes this quaternion is normalized) + [MethodImpl( INLINE )] public static Vector3 Right( this Quaternion q ) => + new( + q.x * q.x - q.y * q.y - q.z * q.z + q.w * q.w, + 2 * ( q.x * q.y + q.z * q.w ), + 2 * ( q.x * q.z - q.y * q.w ) + ); + + /// Returns the Y axis of this rotation (assumes this quaternion is normalized) + [MethodImpl( INLINE )] public static Vector3 Up( this Quaternion q ) => + new( + 2 * ( q.x * q.y - q.z * q.w ), + -q.x * q.x + q.y * q.y - q.z * q.z + q.w * q.w, + 2 * ( q.x * q.w + q.y * q.z ) + ); + + /// Returns the Z axis of this rotation (assumes this quaternion is normalized) + [MethodImpl( INLINE )] public static Vector3 Forward( this Quaternion q ) => + new( + 2 * ( q.x * q.z + q.y * q.w ), + 2 * ( q.y * q.z - q.x * q.w ), + -q.x * q.x - q.y * q.y + q.z * q.z + q.w * q.w + ); + + /// Converts this quaternion to a rotation matrix + public static Matrix4x4 ToMatrix( this Quaternion q ) { + // you could just use Matrix4x4.Rotate( q ) but that's not as fun as doing this math myself + float xx = q.x * q.x; + float yy = q.y * q.y; + float zz = q.z * q.z; + float ww = q.w * q.w; + float xy = q.x * q.y; + float yz = q.y * q.z; + float zw = q.z * q.w; + float wx = q.w * q.x; + float xz = q.x * q.z; + float yw = q.y * q.w; + + return new Matrix4x4 { + m00 = xx - yy - zz + ww, // X + m10 = 2 * ( xy + zw ), + m20 = 2 * ( xz - yw ), + m01 = 2 * ( xy - zw ), // Y + m11 = -xx + yy - zz + ww, + m21 = 2 * ( wx + yz ), + m02 = 2 * ( xz + yw ), // Z + m12 = 2 * ( yz - wx ), + m22 = -xx - yy + zz + ww, + m33 = 1 + }; + } + + /// Returns the natural logarithm of a quaternion + public static Quaternion Ln( this Quaternion q ) { + double vMagSq = (double)q.x * q.x + (double)q.y * q.y + (double)q.z * q.z; + double vMag = Math.Sqrt( vMagSq ); + double qMag = Math.Sqrt( vMagSq + (double)q.w * q.w ); + double theta = Math.Atan2( vMag, q.w ); + double scV = vMag < 0.001 ? Mathfs.SincRcp( theta ) / qMag : theta / vMag; + return new Quaternion( + (float)( scV * q.x ), + (float)( scV * q.y ), + (float)( scV * q.z ), + (float)Math.Log( qMag ) + ); + } + + /// Returns the natural logarithm of a unit quaternion + public static Quaternion LnUnit( this Quaternion q ) { + double vMagSq = (double)q.x * q.x + (double)q.y * q.y + (double)q.z * q.z; + double vMag = Math.Sqrt( vMagSq ); + double theta = Math.Atan2( vMag, q.w ); + double scV = vMag < 0.001 ? Mathfs.SincRcp( theta ) : theta / vMag; + return new Quaternion( + (float)( scV * q.x ), + (float)( scV * q.y ), + (float)( scV * q.z ), + 0f + ); + } + + /// Returns the natural exponent of a quaternion + public static Quaternion Exp( this Quaternion q ) { + double vMag = Math.Sqrt( (double)q.x * q.x + (double)q.y * q.y + (double)q.z * q.z ); + double sc = Math.Exp( q.w ); + double scV = sc * Mathfs.Sinc( vMag ); + return new Quaternion( (float)( scV * q.x ), (float)( scV * q.y ), (float)( scV * q.z ), (float)( sc * Math.Cos( vMag ) ) ); + } + + /// Returns the natural exponent of a pure imaginary quaternion + public static Quaternion ExpPureIm( this Quaternion q ) { + double vMag = Math.Sqrt( (double)q.x * q.x + (double)q.y * q.y + (double)q.z * q.z ); + double scV = Mathfs.Sinc( vMag ); + return new Quaternion( (float)( scV * q.x ), (float)( scV * q.y ), (float)( scV * q.z ), (float)Math.Cos( vMag ) ); + } + + /// Returns the quaternion raised to a real power + public static Quaternion Pow( this Quaternion q, float x ) { + return x switch { + 0f => new Quaternion( 0, 0, 0, 1 ), + 1f => q, + _ => q.Ln().Mul( x ).Exp() + }; + } + + /// Returns the unit quaternion raised to a real power + public static Quaternion PowUnit( this Quaternion q, float x ) { + return x switch { + 0f => new Quaternion( 0, 0, 0, 1 ), + 1f => q, + _ => q.LnUnit().Mul( x ).ExpPureIm() + }; + } + + /// Returns the imaginary part of a quaternion as a vector + public static Vector3 Imag( this Quaternion q ) => new Vector3( q.x, q.y, q.z ); + + /// Returns the squared magnitude of this quaternion + public static float SqrMagnitude( this Quaternion q ) => (float)( (double)q.w * q.w + (double)q.x * q.x + (double)q.y * q.y + (double)q.z * q.z ); + + /// Returns the magnitude of this quaternion + public static float Magnitude( this Quaternion q ) => MathF.Sqrt( q.SqrMagnitude() ); + + /// Multiplies a quaternion by a scalar + /// The quaternion to multiply + /// The scalar value to multiply with + public static Quaternion Mul( this Quaternion q, float c ) => new Quaternion( c * q.x, c * q.y, c * q.z, c * q.w ); + + /// Adds a quaternion to an existing quaternion + public static Quaternion Add( this Quaternion a, Quaternion b ) => new Quaternion( a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w ); + + /// Subtracts a quaternion from an existing quaternion + public static Quaternion Sub( this Quaternion a, Quaternion b ) => new Quaternion( a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w ); + + /// The conjugate of a quaternion + /// The quaternion to conjugate + public static Quaternion Conjugate( this Quaternion q ) => new Quaternion( -q.x, -q.y, -q.z, q.w ); + + /// + public static Quaternion Inverse( this Quaternion q ) => Quaternion.Inverse( q ); + + /// The inverse of a unit quaternion, equivalent to the quaternion conjugate + public static Quaternion InverseUnit( this Quaternion q ) { + return new Quaternion( -q.x, -q.y, -q.z, q.w ); + } + + /// The inverse of a pure imaginary unit quaternion, where w is assumed to be 0 + public static Quaternion InversePureIm( this Quaternion q ) { + float sqMag = q.x * q.x + q.y * q.y + q.z * q.z; + return new Quaternion( -q.x / sqMag, -q.y / sqMag, -q.z / sqMag, 0 ); + } + + /// Add to the magnitude of this quaternion + public static Quaternion AddMagnitude( this Quaternion q, float amount ) => amount == 0f ? q : q.Mul( 1 + amount / q.Magnitude() ); + + public static quaternion Mirror( this quaternion q, float3 nPlane ) { + float4 v = q.value; + float4 n = new float4( nPlane, 0 ); + return conjugate( reflect( v, n ) ); + } + + #endregion + + #region Transform extensions + + /// Transforms a rotation from local space to world space + /// The transform to use + /// The local space rotation + public static Quaternion TransformRotation( this Transform tf, Quaternion quat ) => tf.rotation * quat; + + /// Transforms a bivector from local space to world space + /// The transform to use + /// The local space bivector + public static Bivector3 TransformBivector( this Transform tf, Bivector3 b ) => new Matrix3x3( tf.localToWorldMatrix ).CofactorMatrix * b; + + /// Transforms a bivector from local space to world space + /// The transform to use + /// The local space bivector + public static float TransformBivector( this Transform tf, float b ) => new Matrix3x3( tf.localToWorldMatrix ).Determinant * b; + + /// Transforms a rotation from world space to local space + /// The transform to use + /// The world space rotation + public static Quaternion InverseTransformRotation( this Transform tf, Quaternion quat ) => Quaternion.Inverse( tf.rotation ) * quat; + + /// Transforms a ray from world space to local space + /// The transform to use + /// The world space ray + public static Ray InverseTransformRay( this Transform tf, Ray ray ) => new(tf.InverseTransformPoint( ray.origin ), tf.InverseTransformDirection( ray.direction )); + + /// Transforms a ray from local space to world space + /// The transform to use + /// The local space ray + public static Ray TransformRay( this Transform tf, Ray ray ) => new(tf.TransformPoint( ray.origin ), tf.TransformDirection( ray.direction )); + + #endregion + + #region Color manipulation + + /// Returns the same color, but with the specified alpha value + /// The source color + /// The new alpha value + [MethodImpl( INLINE )] public static Color WithAlpha( this Color c, float a ) => new Color( c.r, c.g, c.b, a ); + + /// Returns the same color and alpha, but with RGB multiplied by the given value + /// The source color + /// The multiplier for the RGB channels + [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, float m ) => new Color( c.r * m, c.g * m, c.b * m, c.a ); + + /// Returns the same color and alpha, but with the RGB values multiplief by another color + /// The source color + /// The color to multiply RGB by + [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, Color m ) => new Color( c.r * m.r, c.g * m.g, c.b * m.b, c.a ); + + /// Returns the same color, but with the alpha channel multiplied by the given value + /// The source color + /// The multiplier for the alpha + [MethodImpl( INLINE )] public static Color MultiplyA( this Color c, float m ) => new Color( c.r, c.g, c.b, c.a * m ); + + /// Converts this color to the nearest 32 bit hex string, including the alpha channel. + /// A pure red color of (1,0,0,1) returns "FF0000FF" + /// The color to get the hex string of + /// + [MethodImpl( INLINE )] public static string ToHexString( this Color c ) => ColorUtility.ToHtmlStringRGBA( c ); + + #endregion + + #region Rect + + /// Expands the rectangle to encapsulate the point p + /// The rectangle to expand + /// The point to encapsulate + public static Rect Encapsulate( this Rect r, Vector2 p ) { + r.xMax = MathF.Max( r.xMax, p.x ); + r.xMin = MathF.Min( r.xMin, p.x ); + r.yMax = MathF.Max( r.yMax, p.y ); + r.yMin = MathF.Min( r.yMin, p.y ); + return r; + } + + /// Interpolates a position within this rectangle, given a normalized position + /// The rectangle to get a position within + /// The normalized position within this rectangle + public static Vector2 Lerp( this Rect r, Vector2 tPos ) => + new( + Mathfs.Lerp( r.xMin, r.xMax, tPos.x ), + Mathfs.Lerp( r.yMin, r.yMax, tPos.y ) + ); + + /// The x axis range of this rectangle + /// The rectangle to get the x range of + public static FloatRange RangeX( this Rect rect ) => ( rect.xMin, rect.xMax ); + + /// The y axis range of this rectangle + /// The rectangle to get the y range of + public static FloatRange RangeY( this Rect rect ) => ( rect.yMin, rect.yMax ); + + /// Places the center of this rectangle at its position, + /// useful together with the constructor to define it by center instead of by corner + public static Rect ByCenter( this Rect r ) => new Rect( r ) { center = r.position }; + + #endregion + + #region Simple float and int operations + + /// Returns true if v is between or equal to min & max + /// + [MethodImpl( INLINE )] public static bool Within( this float v, float min, float max ) => v >= min && v <= max; + + /// Returns true if v is between or equal to min & max + /// + [MethodImpl( INLINE )] public static bool Within( this int v, int min, int max ) => v >= min && v <= max; + + /// Returns true if v is between, but not equal to, min & max + /// + [MethodImpl( INLINE )] public static bool Between( this float v, float min, float max ) => v > min && v < max; + + /// Returns true if v is between, but not equal to, min & max + /// + [MethodImpl( INLINE )] public static bool Between( this int v, int min, int max ) => v > min && v < max; + + /// Clamps the value to be at least min + [MethodImpl( INLINE )] public static float AtLeast( this float v, float min ) => v < min ? min : v; + + /// Clamps the value to be at least min + [MethodImpl( INLINE )] public static int AtLeast( this int v, int min ) => v < min ? min : v; + + /// Clamps the value to be at most max + [MethodImpl( INLINE )] public static float AtMost( this float v, float max ) => v > max ? max : v; + + /// Clamps the value to be at most max + [MethodImpl( INLINE )] public static int AtMost( this int v, int max ) => v > max ? max : v; + + /// Squares the value. Equivalent to v*v + [MethodImpl( INLINE )] public static float Square( this float v ) => v * v; + + /// Cubes the value. Equivalent to v*v*v + [MethodImpl( INLINE )] public static float Cube( this float v ) => v * v * v; + + /// Squares the value. Equivalent to v*v + [MethodImpl( INLINE )] public static int Square( this int v ) => v * v; + + /// The next integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc + [MethodImpl( INLINE )] public static int NextMod( this int value, int length ) => ( value + 1 ).Mod( length ); + + /// The previous integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc + [MethodImpl( INLINE )] public static int PrevMod( this int value, int length ) => ( value - 1 ).Mod( length ); + + #endregion + + #region String extensions + + public static string ToValueTableString( this string[,] m ) { + int rowCount = m.GetLength( 0 ); + int colCount = m.GetLength( 1 ); + string[] r = new string[rowCount]; + for( int i = 0; i < rowCount; i++ ) + r[i] = ""; + + for( int c = 0; c < colCount; c++ ) { + string endBit = c == colCount - 1 ? "" : ", "; + + int colWidth = 4; // min width + string[] columnEntries = new string[rowCount]; + for( int row = 0; row < rowCount; row++ ) { + string s = m[row, c].StartsWith( '-' ) ? "" : " "; + columnEntries[row] = $"{s}{m[row, c]}{endBit}"; + colWidth = Mathfs.Max( colWidth, columnEntries[row].Length ); + } + + for( int row = 0; row < rowCount; row++ ) { + r[row] += columnEntries[row].PadRight( colWidth, ' ' ); + } + } + + return string.Join( '\n', r ); + } + + #endregion + + #region Matrix extensions + + public static float AverageScale( this Matrix4x4 m ) { + return ( + ( (Vector3)m.GetColumn( 0 ) ).magnitude + + ( (Vector3)m.GetColumn( 1 ) ).magnitude + + ( (Vector3)m.GetColumn( 2 ) ).magnitude + ) / 3; + } + + public static Matrix4x1 MultiplyColumnVector( this Matrix4x4 m, Matrix4x1 v ) => + new Matrix4x1( + m.m00 * v.m0 + m.m01 * v.m1 + m.m02 * v.m2 + m.m03 * v.m3, + m.m10 * v.m0 + m.m11 * v.m1 + m.m12 * v.m2 + m.m13 * v.m3, + m.m20 * v.m0 + m.m21 * v.m1 + m.m22 * v.m2 + m.m23 * v.m3, + m.m30 * v.m0 + m.m31 * v.m1 + m.m32 * v.m2 + m.m33 * v.m3 + ); + + public static Vector2Matrix4x1 MultiplyColumnVector( this Matrix4x4 m, Vector2Matrix4x1 v ) => new(m.MultiplyColumnVector( v.X ), m.MultiplyColumnVector( v.Y )); + public static Vector3Matrix4x1 MultiplyColumnVector( this Matrix4x4 m, Vector3Matrix4x1 v ) => new(m.MultiplyColumnVector( v.X ), m.MultiplyColumnVector( v.Y ), m.MultiplyColumnVector( v.Z )); + public static Vector4Matrix4x1 MultiplyColumnVector( this Matrix4x4 m, Vector4Matrix4x1 v ) => new(m.MultiplyColumnVector( v.X ), m.MultiplyColumnVector( v.Y ), m.MultiplyColumnVector( v.Z ), m.MultiplyColumnVector( v.W )); + + /// Transforms a ray by this matrix + /// The matrix to use + /// The ray to transform + public static Ray MultiplyRay( this Matrix4x4 mtx, Ray ray ) => new(mtx.MultiplyPoint3x4( ray.origin ), mtx.MultiplyVector( ray.direction )); + + #endregion + + #region Extension method counterparts of the static Mathfs functions - lots of boilerplate in here + + #region Math operations + + /// + [MethodImpl( INLINE )] public static float Sqrt( this float value ) => MathF.Sqrt( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Sqrt( this Vector2 value ) => Mathfs.Sqrt( value ); + + /// + [MethodImpl( INLINE )] public static Vector3 Sqrt( this Vector3 value ) => Mathfs.Sqrt( value ); + + /// + [MethodImpl( INLINE )] public static Vector4 Sqrt( this Vector4 value ) => Mathfs.Sqrt( value ); + + /// + [MethodImpl( INLINE )] public static float Cbrt( this float value ) => MathF.Cbrt( value ); + + /// + [MethodImpl( INLINE )] public static float Pow( this float value, float exponent ) => MathF.Pow( value, exponent ); + + /// Calculates exact positive integer powers + /// + /// A positive integer power + [MethodImpl( INLINE )] public static int Pow( this int value, int pow ) { + if( pow < 0 ) + throw new ArithmeticException( "int.Pow(int) doesn't support negative powers" ); + checked { + switch( pow ) { + case 0: return 1; + case 1: return value; + case 2: return value * value; + case 3: return value * value * value; + default: + if( value == 2 ) + return 1 << pow; + // from: https://stackoverflow.com/questions/383587/how-do-you-do-integer-exponentiation-in-c + int ret = 1; + while( pow != 0 ) { + if( ( pow & 1 ) == 1 ) + ret *= value; + value *= value; + pow >>= 1; + } + + return ret; + } + } + } + + #endregion + + #region Absolute Values + + /// + [MethodImpl( INLINE )] public static float Abs( this float value ) => MathF.Abs( value ); + + /// + [MethodImpl( INLINE )] public static int Abs( this int value ) => Mathfs.Abs( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Abs( this Vector2 v ) => Mathfs.Abs( v ); + + /// + [MethodImpl( INLINE )] public static Vector3 Abs( this Vector3 v ) => Mathfs.Abs( v ); + + /// + [MethodImpl( INLINE )] public static Vector4 Abs( this Vector4 v ) => Mathfs.Abs( v ); + + #endregion + + #region Clamping + + /// + [MethodImpl( INLINE )] public static float Clamp( this float value, float min, float max ) => Mathfs.Clamp( value, min, max ); + + /// + [MethodImpl( INLINE )] public static Vector2 Clamp( this Vector2 v, Vector2 min, Vector2 max ) => Mathfs.Clamp( v, min, max ); + + /// + [MethodImpl( INLINE )] public static Vector3 Clamp( this Vector3 v, Vector3 min, Vector3 max ) => Mathfs.Clamp( v, min, max ); + + /// + [MethodImpl( INLINE )] public static Vector4 Clamp( this Vector4 v, Vector4 min, Vector4 max ) => Mathfs.Clamp( v, min, max ); + + /// + [MethodImpl( INLINE )] public static int Clamp( this int value, int min, int max ) => Mathfs.Clamp( value, min, max ); + + /// + [MethodImpl( INLINE )] public static float Clamp01( this float value ) => Mathfs.Clamp01( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Clamp01( this Vector2 v ) => Mathfs.Clamp01( v ); + + /// + [MethodImpl( INLINE )] public static Vector3 Clamp01( this Vector3 v ) => Mathfs.Clamp01( v ); + + /// + [MethodImpl( INLINE )] public static Vector4 Clamp01( this Vector4 v ) => Mathfs.Clamp01( v ); + + /// + [MethodImpl( INLINE )] public static float ClampNeg1to1( this float value ) => Mathfs.ClampNeg1to1( value ); + + /// + [MethodImpl( INLINE )] public static double ClampNeg1to1( this double value ) => Mathfs.ClampNeg1to1( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 ClampNeg1to1( this Vector2 v ) => Mathfs.ClampNeg1to1( v ); + + /// + [MethodImpl( INLINE )] public static Vector3 ClampNeg1to1( this Vector3 v ) => Mathfs.ClampNeg1to1( v ); + + /// + [MethodImpl( INLINE )] public static Vector4 ClampNeg1to1( this Vector4 v ) => Mathfs.ClampNeg1to1( v ); + + #endregion + + #region Min & Max + + /// + [MethodImpl( INLINE )] public static float Min( this Vector2 v ) => Mathfs.Min( v ); + + /// + [MethodImpl( INLINE )] public static float Min( this Vector3 v ) => Mathfs.Min( v ); + + /// + [MethodImpl( INLINE )] public static float Min( this Vector4 v ) => Mathfs.Min( v ); + + /// + [MethodImpl( INLINE )] public static float Max( this Vector2 v ) => Mathfs.Max( v ); + + /// + [MethodImpl( INLINE )] public static float Max( this Vector3 v ) => Mathfs.Max( v ); + + /// + [MethodImpl( INLINE )] public static float Max( this Vector4 v ) => Mathfs.Max( v ); + + #endregion + + #region Signs & Rounding + + /// + [MethodImpl( INLINE )] public static float Sign( this float value ) => Mathfs.Sign( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Sign( this Vector2 value ) => Mathfs.Sign( value ); + + /// + [MethodImpl( INLINE )] public static Vector3 Sign( this Vector3 value ) => Mathfs.Sign( value ); + + /// + [MethodImpl( INLINE )] public static Vector4 Sign( this Vector4 value ) => Mathfs.Sign( value ); + + /// + [MethodImpl( INLINE )] public static int Sign( this int value ) => Mathfs.Sign( value ); + + /// + [MethodImpl( INLINE )] public static int SignAsInt( this float value ) => Mathfs.SignAsInt( value ); + + /// + [MethodImpl( INLINE )] public static float SignWithZero( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); + + /// + [MethodImpl( INLINE )] public static Vector2 SignWithZero( this Vector2 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); + + /// + [MethodImpl( INLINE )] public static Vector3 SignWithZero( this Vector3 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); + + /// + [MethodImpl( INLINE )] public static Vector4 SignWithZero( this Vector4 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold ); + + /// + [MethodImpl( INLINE )] public static int SignWithZero( this int value ) => Mathfs.SignWithZero( value ); + + /// + [MethodImpl( INLINE )] public static int SignWithZeroAsInt( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZeroAsInt( value, zeroThreshold ); + + /// + [MethodImpl( INLINE )] public static float Floor( this float value ) => Mathfs.Floor( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Floor( this Vector2 value ) => Mathfs.Floor( value ); + + /// + [MethodImpl( INLINE )] public static Vector3 Floor( this Vector3 value ) => Mathfs.Floor( value ); + + /// + [MethodImpl( INLINE )] public static Vector4 Floor( this Vector4 value ) => Mathfs.Floor( value ); + + /// + [MethodImpl( INLINE )] public static int FloorToInt( this float value ) => Mathfs.FloorToInt( value ); + + /// + [MethodImpl( INLINE )] public static Vector2Int FloorToInt( this Vector2 value ) => Mathfs.FloorToInt( value ); + + /// + [MethodImpl( INLINE )] public static Vector3Int FloorToInt( this Vector3 value ) => Mathfs.FloorToInt( value ); + + /// + [MethodImpl( INLINE )] public static float Ceil( this float value ) => Mathfs.Ceil( value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Ceil( this Vector2 value ) => Mathfs.Ceil( value ); + + /// + [MethodImpl( INLINE )] public static Vector3 Ceil( this Vector3 value ) => Mathfs.Ceil( value ); + + /// + [MethodImpl( INLINE )] public static Vector4 Ceil( this Vector4 value ) => Mathfs.Ceil( value ); + + /// + [MethodImpl( INLINE )] public static int CeilToInt( this float value ) => Mathfs.CeilToInt( value ); + + /// + [MethodImpl( INLINE )] public static Vector2Int CeilToInt( this Vector2 value ) => Mathfs.CeilToInt( value ); + + /// + [MethodImpl( INLINE )] public static Vector3Int CeilToInt( this Vector3 value ) => Mathfs.CeilToInt( value ); + + /// + [MethodImpl( INLINE )] public static float Round( this float value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static float Round( this float value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, snapInterval, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, snapInterval, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, snapInterval, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.Round( value, snapInterval, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static int RoundToInt( this float value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.RoundToInt( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector2Int RoundToInt( this Vector2 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.RoundToInt( value, midpointRounding ); + + /// + [MethodImpl( INLINE )] public static Vector3Int RoundToInt( this Vector3 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => Mathfs.RoundToInt( value, midpointRounding ); + + #endregion + + #region Range Repeating + + /// + [MethodImpl( INLINE )] public static float Frac( this float x ) => Mathfs.Frac( x ); + + /// + [MethodImpl( INLINE )] public static Vector2 Frac( this Vector2 v ) => Mathfs.Frac( v ); + + /// + [MethodImpl( INLINE )] public static Vector3 Frac( this Vector3 v ) => Mathfs.Frac( v ); + + /// + [MethodImpl( INLINE )] public static Vector4 Frac( this Vector4 v ) => Mathfs.Frac( v ); + + /// + [MethodImpl( INLINE )] public static float Repeat( this float value, float length ) => Mathfs.Repeat( value, length ); + + /// + [MethodImpl( INLINE )] public static int Mod( this int value, int length ) => Mathfs.Mod( value, length ); + + #endregion + + #region Smoothing & Easing Curves + + /// + [MethodImpl( INLINE )] public static float Smooth01( this float x ) => Mathfs.Smooth01( x ); + + /// + [MethodImpl( INLINE )] public static float Smoother01( this float x ) => Mathfs.Smoother01( x ); + + /// + [MethodImpl( INLINE )] public static float SmoothCos01( this float x ) => Mathfs.SmoothCos01( x ); + + #endregion + + #region Value & Vector interpolation + + /// + [MethodImpl( INLINE )] public static float Remap( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, value ); + + /// + [MethodImpl( INLINE )] public static float RemapClamped( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.RemapClamped( iMin, iMax, oMin, oMax, value ); + + /// + [MethodImpl( INLINE )] public static float Remap( this float value, FloatRange inRange, FloatRange outRange ) => Mathfs.Remap( inRange.a, inRange.b, outRange.a, outRange.b, value ); + + /// + [MethodImpl( INLINE )] public static float RemapClamped( this float value, FloatRange inRange, FloatRange outRange ) => Mathfs.RemapClamped( inRange.a, inRange.b, outRange.a, outRange.b, value ); + + /// + [MethodImpl( INLINE )] public static float Remap( this int value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, value ); + + /// + [MethodImpl( INLINE )] public static float RemapClamped( this int value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.RemapClamped( iMin, iMax, oMin, oMax, value ); + + /// + [MethodImpl( INLINE )] public static float Lerp( this float t, float a, float b ) => Mathfs.Lerp( a, b, t ); + + /// + [MethodImpl( INLINE )] public static float InverseLerp( this float value, float a, float b ) => Mathfs.InverseLerp( a, b, value ); + + /// + [MethodImpl( INLINE )] public static float LerpClamped( this float t, float a, float b ) => Mathfs.LerpClamped( a, b, t ); + + /// + [MethodImpl( INLINE )] public static float InverseLerpClamped( this float value, float a, float b ) => Mathfs.InverseLerpClamped( a, b, value ); + + /// + [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 v, Vector2 iMin, Vector2 iMax, Vector2 oMin, Vector2 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v ); + + /// + [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 v, Vector3 iMin, Vector3 iMax, Vector3 oMin, Vector3 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v ); + + /// + [MethodImpl( INLINE )] public static Vector4 Remap( this Vector4 v, Vector4 iMin, Vector4 iMax, Vector4 oMin, Vector4 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v ); + + /// + [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 iPos, Rect iRect, Rect oRect ) => Mathfs.Remap( iRect.min, iRect.max, oRect.min, oRect.max, iPos ); + + /// + [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 iPos, Bounds iBounds, Bounds oBounds ) => Mathfs.Remap( iBounds.min, iBounds.max, oBounds.min, oBounds.max, iPos ); + + /// + [MethodImpl( INLINE )] public static float Eerp( this float t, float a, float b ) => Mathfs.Eerp( a, b, t ); + + /// + [MethodImpl( INLINE )] public static float InverseEerp( this float v, float a, float b ) => Mathfs.InverseEerp( a, b, v ); + + #endregion + + #region Vector Math + + /// + [MethodImpl( INLINE )] public static (Vector2 dir, float magnitude ) GetDirAndMagnitude( this Vector2 v ) => Mathfs.GetDirAndMagnitude( v ); + + /// + [MethodImpl( INLINE )] public static (Vector3 dir, float magnitude ) GetDirAndMagnitude( this Vector3 v ) => Mathfs.GetDirAndMagnitude( v ); + + /// + [MethodImpl( INLINE )] public static Vector2 ClampMagnitude( this Vector2 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max ); + + /// + [MethodImpl( INLINE )] public static Vector3 ClampMagnitude( this Vector3 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max ); + + #endregion + + #endregion + + + } + +} \ No newline at end of file diff --git a/Runtime/Extensions.cs.meta b/Runtime/Extensions.cs.meta new file mode 100644 index 0000000..300ed17 --- /dev/null +++ b/Runtime/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e671a7c3c775403485f53cdb833fd46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Generated static functions.meta b/Runtime/Generated static functions.meta new file mode 100644 index 0000000..6f56a6b --- /dev/null +++ b/Runtime/Generated static functions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e1bef33040b8787439791e0d49d13da7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Generated static functions/IComplex_static.cs b/Runtime/Generated static functions/IComplex_static.cs new file mode 100644 index 0000000..90c8d89 --- /dev/null +++ b/Runtime/Generated static functions/IComplex_static.cs @@ -0,0 +1,44 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static V complexConj(Self v) where Self : IComplex => v.complexConj; + /// + public static M complexMul(Self a, V b) where Self : IComplex => a.complexMul(b); + } + public static partial class mathfs { + /// + public static rat2 complexConj(rat2 v) => v.complexConj; + /// + public static rat2 complexMul(rat2 a, rat2 b) => a.complexMul(b); + /// + public static inth2 complexConj(inth2 v) => v.complexConj; + /// + public static rat2 complexMul(inth2 a, inth2 b) => a.complexMul(b); + /// + public static int2 complexConj(this int2 v) => new(v.x, -v.y); + /// + public static int2 complexMul(this int2 a, int2 b) => new(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); + /// + public static Vector2Int complexConj(this Vector2Int v) => new(v.x, -v.y); + /// + public static Vector2Int complexMul(this Vector2Int a, Vector2Int b) => new(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); + /// + public static float2 complexConj(this float2 v) => new(v.x, -v.y); + /// + public static float2 complexMul(this float2 a, float2 b) => new(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); + /// + public static Vector2 complexConj(this Vector2 v) => new(v.x, -v.y); + /// + public static Vector2 complexMul(this Vector2 a, Vector2 b) => new(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); + /// + public static double2 complexConj(this double2 v) => new(v.x, -v.y); + /// + public static double2 complexMul(this double2 a, double2 b) => new(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); + } +} diff --git a/Runtime/Generated static functions/IComplex_static.cs.meta b/Runtime/Generated static functions/IComplex_static.cs.meta new file mode 100644 index 0000000..d5abcc3 --- /dev/null +++ b/Runtime/Generated static functions/IComplex_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7a2daf25b51121469a4a2b91830a68b \ No newline at end of file diff --git a/Runtime/Generated static functions/IDotProduct_static.cs b/Runtime/Generated static functions/IDotProduct_static.cs new file mode 100644 index 0000000..c44aeff --- /dev/null +++ b/Runtime/Generated static functions/IDotProduct_static.cs @@ -0,0 +1,48 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static D dot(Self a, B b) where Self : IDotProduct => a.dot(b); + } + public static partial class mathfs { + /// + public static rat dot(rat2 a, int2 b) => a.dot(b); + /// + public static rat dot(rat2 a, rat2 b) => a.dot(b); + /// + public static rat dot(inth2 a, inth2 b) => a.dot(b); + /// + public static Int32 dot(this int2 a, int2 b) => math.dot( a, b ); + /// + public static Int32 dot(this Vector2Int a, Vector2Int b) => a.x*b.x+a.y*b.y; + /// + public static Int32 dot(this int3 a, int3 b) => math.dot( a, b ); + /// + public static Int32 dot(this Vector3Int a, Vector3Int b) => a.x*b.x+a.y*b.y+a.z*b.z; + /// + public static Int32 dot(this int4 a, int4 b) => math.dot( a, b ); + /// + public static Single dot(this float2 a, float2 b) => math.dot( a, b ); + /// + public static Single dot(this Vector2 a, Vector2 b) => a.x*b.x+a.y*b.y; + /// + public static Single dot(this float3 a, float3 b) => math.dot( a, b ); + /// + public static Single dot(this Vector3 a, Vector3 b) => a.x*b.x+a.y*b.y+a.z*b.z; + /// + public static Single dot(this float4 a, float4 b) => math.dot( a, b ); + /// + public static Single dot(this Vector4 a, Vector4 b) => a.x*b.x+a.y*b.y+a.z*b.z+a.w*b.w; + /// + public static Double dot(this double2 a, double2 b) => math.dot( a, b ); + /// + public static Double dot(this double3 a, double3 b) => math.dot( a, b ); + /// + public static Double dot(this double4 a, double4 b) => math.dot( a, b ); + } +} diff --git a/Runtime/Generated static functions/IDotProduct_static.cs.meta b/Runtime/Generated static functions/IDotProduct_static.cs.meta new file mode 100644 index 0000000..bf4e742 --- /dev/null +++ b/Runtime/Generated static functions/IDotProduct_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d486d8c1ce4144a429e7756d6d09b4f3 \ No newline at end of file diff --git a/Runtime/Generated static functions/IHalfNumber_static.cs b/Runtime/Generated static functions/IHalfNumber_static.cs new file mode 100644 index 0000000..bc1e1eb --- /dev/null +++ b/Runtime/Generated static functions/IHalfNumber_static.cs @@ -0,0 +1,18 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static F times2(Self v) where Self : IHalfNumber => v.times2; + } + public static partial class mathfs { + /// + public static Int32 times2(inth v) => v.times2; + /// + public static int2 times2(inth2 v) => v.times2; + } +} diff --git a/Runtime/Generated static functions/IHalfNumber_static.cs.meta b/Runtime/Generated static functions/IHalfNumber_static.cs.meta new file mode 100644 index 0000000..07378e9 --- /dev/null +++ b/Runtime/Generated static functions/IHalfNumber_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eefe704695a4d93409b842f68024ecf7 \ No newline at end of file diff --git a/Runtime/Generated static functions/INumberBase_static.cs b/Runtime/Generated static functions/INumberBase_static.cs new file mode 100644 index 0000000..30c8845 --- /dev/null +++ b/Runtime/Generated static functions/INumberBase_static.cs @@ -0,0 +1,144 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static Boolean isInteger(Self v) where Self : INumberBase => v.isInteger; + /// + public static Boolean isZero(Self v) where Self : INumberBase => v.isZero; + /// + public static Boolean isOrthogonal(Self v) where Self : INumberBase => v.isOrthogonal; + } + public static partial class mathfs { + /// + public static Boolean isInteger(rat v) => v.isInteger; + /// + public static Boolean isZero(rat v) => v.isZero; + /// + public static Boolean isOrthogonal(rat v) => v.isOrthogonal; + /// + public static Boolean isInteger(rat2 v) => v.isInteger; + /// + public static Boolean isZero(rat2 v) => v.isZero; + /// + public static Boolean isOrthogonal(rat2 v) => v.isOrthogonal; + /// + public static Boolean isInteger(inth v) => v.isInteger; + /// + public static Boolean isZero(inth v) => v.isZero; + /// + public static Boolean isOrthogonal(inth v) => v.isOrthogonal; + /// + public static Boolean isInteger(inth2 v) => v.isInteger; + /// + public static Boolean isZero(inth2 v) => v.isZero; + /// + public static Boolean isOrthogonal(inth2 v) => v.isOrthogonal; + /// + public static Boolean isInteger(this Int32 v) => true; + /// + public static Boolean isZero(this Int32 v) => v == 0; + /// + public static Boolean isOrthogonal(this Int32 v) => true; + /// + public static Boolean isInteger(this int2 v) => true; + /// + public static Boolean isZero(this int2 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this int2 v) => (v.abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Vector2Int v) => true; + /// + public static Boolean isZero(this Vector2Int v) => v.x == 0 && v.y == 0; + /// + public static Boolean isOrthogonal(this Vector2Int v) => (new int2(v.x, v.y).abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this int3 v) => true; + /// + public static Boolean isZero(this int3 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this int3 v) => (v.abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Vector3Int v) => true; + /// + public static Boolean isZero(this Vector3Int v) => v.x == 0 && v.y == 0 && v.z == 0; + /// + public static Boolean isOrthogonal(this Vector3Int v) => (new int3(v.x, v.y, v.z).abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this int4 v) => true; + /// + public static Boolean isZero(this int4 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this int4 v) => (v.abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Single v) => v == MathF.Truncate( v ); + /// + public static Boolean isZero(this Single v) => v == 0; + /// + public static Boolean isOrthogonal(this Single v) => true; + /// + public static Boolean isInteger(this float2 v) => v.x.isInteger() && v.y.isInteger(); + /// + public static Boolean isZero(this float2 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this float2 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Vector2 v) => v.x.isInteger() && v.y.isInteger(); + /// + public static Boolean isZero(this Vector2 v) => v.x == 0 && v.y == 0; + /// + public static Boolean isOrthogonal(this Vector2 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this float3 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger(); + /// + public static Boolean isZero(this float3 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this float3 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Vector3 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger(); + /// + public static Boolean isZero(this Vector3 v) => v.x == 0 && v.y == 0 && v.z == 0; + /// + public static Boolean isOrthogonal(this Vector3 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this float4 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger() && v.w.isInteger(); + /// + public static Boolean isZero(this float4 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this float4 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Vector4 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger() && v.w.isInteger(); + /// + public static Boolean isZero(this Vector4 v) => v.x == 0 && v.y == 0 && v.z == 0 && v.w == 0; + /// + public static Boolean isOrthogonal(this Vector4 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this Double v) => v == Math.Truncate( v ); + /// + public static Boolean isZero(this Double v) => v == 0; + /// + public static Boolean isOrthogonal(this Double v) => true; + /// + public static Boolean isInteger(this double2 v) => v.x.isInteger() && v.y.isInteger(); + /// + public static Boolean isZero(this double2 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this double2 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this double3 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger(); + /// + public static Boolean isZero(this double3 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this double3 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + /// + public static Boolean isInteger(this double4 v) => v.x.isInteger() && v.y.isInteger() && v.z.isInteger() && v.w.isInteger(); + /// + public static Boolean isZero(this double4 v) => math.all( v == 0 ); + /// + public static Boolean isOrthogonal(this double4 v) => (v.ceilAwayFrom0().abs() > 0).csum() <= 1; + } +} diff --git a/Runtime/Generated static functions/INumberBase_static.cs.meta b/Runtime/Generated static functions/INumberBase_static.cs.meta new file mode 100644 index 0000000..11ce42a --- /dev/null +++ b/Runtime/Generated static functions/INumberBase_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b8364fe0fa70dab45901f44ca8f3c2c0 \ No newline at end of file diff --git a/Runtime/Generated static functions/INumber_static.cs b/Runtime/Generated static functions/INumber_static.cs new file mode 100644 index 0000000..3e1c957 --- /dev/null +++ b/Runtime/Generated static functions/INumber_static.cs @@ -0,0 +1,188 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static N abs(Self v) where Self : INumber => v.abs; + /// + public static N min(Self a, N b) where Self : INumber => a.min(b); + /// + public static N max(Self a, N b) where Self : INumber => a.max(b); + /// + public static N to(Self v, N target) where Self : INumber => v.to(target); + } + public static partial class mathfs { + /// + public static rat abs(rat v) => v.abs; + /// + public static rat min(rat a, rat b) => a.min(b); + /// + public static rat max(rat a, rat b) => a.max(b); + /// + public static rat to(rat v, rat target) => v.to(target); + /// + public static rat2 abs(rat2 v) => v.abs; + /// + public static rat2 min(rat2 a, rat2 b) => a.min(b); + /// + public static rat2 max(rat2 a, rat2 b) => a.max(b); + /// + public static rat2 to(rat2 v, rat2 target) => v.to(target); + /// + public static inth abs(inth v) => v.abs; + /// + public static inth min(inth a, inth b) => a.min(b); + /// + public static inth max(inth a, inth b) => a.max(b); + /// + public static inth to(inth v, inth target) => v.to(target); + /// + public static inth2 abs(inth2 v) => v.abs; + /// + public static inth2 min(inth2 a, inth2 b) => a.min(b); + /// + public static inth2 max(inth2 a, inth2 b) => a.max(b); + /// + public static inth2 to(inth2 v, inth2 target) => v.to(target); + /// + public static Int32 abs(this Int32 v) => Math.Abs(v); + /// + public static Int32 min(this Int32 a, Int32 b) => Math.Min(a,b); + /// + public static Int32 max(this Int32 a, Int32 b) => Math.Max(a,b); + /// + public static Int32 to(this Int32 v, Int32 target) => target - v; + /// + public static int2 abs(this int2 v) => math.abs(v); + /// + public static int2 min(this int2 a, int2 b) => math.min(a,b); + /// + public static int2 max(this int2 a, int2 b) => math.max(a,b); + /// + public static int2 to(this int2 v, int2 target) => target - v; + /// + public static Vector2Int abs(this Vector2Int v) => new(v.x.abs(), v.y.abs()); + /// + public static Vector2Int min(this Vector2Int a, Vector2Int b) => new(a.x.min(b.x), a.y.min(b.y)); + /// + public static Vector2Int max(this Vector2Int a, Vector2Int b) => new(a.x.max(b.x), a.y.max(b.y)); + /// + public static Vector2Int to(this Vector2Int v, Vector2Int target) => target - v; + /// + public static int3 abs(this int3 v) => math.abs(v); + /// + public static int3 min(this int3 a, int3 b) => math.min(a,b); + /// + public static int3 max(this int3 a, int3 b) => math.max(a,b); + /// + public static int3 to(this int3 v, int3 target) => target - v; + /// + public static Vector3Int abs(this Vector3Int v) => new(v.x.abs(), v.y.abs(), v.z.abs()); + /// + public static Vector3Int min(this Vector3Int a, Vector3Int b) => new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)); + /// + public static Vector3Int max(this Vector3Int a, Vector3Int b) => new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)); + /// + public static Vector3Int to(this Vector3Int v, Vector3Int target) => target - v; + /// + public static int4 abs(this int4 v) => math.abs(v); + /// + public static int4 min(this int4 a, int4 b) => math.min(a,b); + /// + public static int4 max(this int4 a, int4 b) => math.max(a,b); + /// + public static int4 to(this int4 v, int4 target) => target - v; + /// + public static Single abs(this Single v) => Math.Abs(v); + /// + public static Single min(this Single a, Single b) => Math.Min(a,b); + /// + public static Single max(this Single a, Single b) => Math.Max(a,b); + /// + public static Single to(this Single v, Single target) => target - v; + /// + public static float2 abs(this float2 v) => math.abs(v); + /// + public static float2 min(this float2 a, float2 b) => math.min(a,b); + /// + public static float2 max(this float2 a, float2 b) => math.max(a,b); + /// + public static float2 to(this float2 v, float2 target) => target - v; + /// + public static Vector2 abs(this Vector2 v) => new(v.x.abs(), v.y.abs()); + /// + public static Vector2 min(this Vector2 a, Vector2 b) => new(a.x.min(b.x), a.y.min(b.y)); + /// + public static Vector2 max(this Vector2 a, Vector2 b) => new(a.x.max(b.x), a.y.max(b.y)); + /// + public static Vector2 to(this Vector2 v, Vector2 target) => target - v; + /// + public static float3 abs(this float3 v) => math.abs(v); + /// + public static float3 min(this float3 a, float3 b) => math.min(a,b); + /// + public static float3 max(this float3 a, float3 b) => math.max(a,b); + /// + public static float3 to(this float3 v, float3 target) => target - v; + /// + public static Vector3 abs(this Vector3 v) => new(v.x.abs(), v.y.abs(), v.z.abs()); + /// + public static Vector3 min(this Vector3 a, Vector3 b) => new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z)); + /// + public static Vector3 max(this Vector3 a, Vector3 b) => new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z)); + /// + public static Vector3 to(this Vector3 v, Vector3 target) => target - v; + /// + public static float4 abs(this float4 v) => math.abs(v); + /// + public static float4 min(this float4 a, float4 b) => math.min(a,b); + /// + public static float4 max(this float4 a, float4 b) => math.max(a,b); + /// + public static float4 to(this float4 v, float4 target) => target - v; + /// + public static Vector4 abs(this Vector4 v) => new(v.x.abs(), v.y.abs(), v.z.abs(), v.w.abs()); + /// + public static Vector4 min(this Vector4 a, Vector4 b) => new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z), a.w.min(b.w)); + /// + public static Vector4 max(this Vector4 a, Vector4 b) => new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z), a.w.max(b.w)); + /// + public static Vector4 to(this Vector4 v, Vector4 target) => target - v; + /// + public static Double abs(this Double v) => Math.Abs(v); + /// + public static Double min(this Double a, Double b) => Math.Min(a,b); + /// + public static Double max(this Double a, Double b) => Math.Max(a,b); + /// + public static Double to(this Double v, Double target) => target - v; + /// + public static double2 abs(this double2 v) => math.abs(v); + /// + public static double2 min(this double2 a, double2 b) => math.min(a,b); + /// + public static double2 max(this double2 a, double2 b) => math.max(a,b); + /// + public static double2 to(this double2 v, double2 target) => target - v; + /// + public static double3 abs(this double3 v) => math.abs(v); + /// + public static double3 min(this double3 a, double3 b) => math.min(a,b); + /// + public static double3 max(this double3 a, double3 b) => math.max(a,b); + /// + public static double3 to(this double3 v, double3 target) => target - v; + /// + public static double4 abs(this double4 v) => math.abs(v); + /// + public static double4 min(this double4 a, double4 b) => math.min(a,b); + /// + public static double4 max(this double4 a, double4 b) => math.max(a,b); + /// + public static double4 to(this double4 v, double4 target) => target - v; + } +} diff --git a/Runtime/Generated static functions/INumber_static.cs.meta b/Runtime/Generated static functions/INumber_static.cs.meta new file mode 100644 index 0000000..eedeea1 --- /dev/null +++ b/Runtime/Generated static functions/INumber_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 69270ddc049a38b4fa5da7df273018b7 \ No newline at end of file diff --git a/Runtime/Generated static functions/IQuadrant2D_static.cs b/Runtime/Generated static functions/IQuadrant2D_static.cs new file mode 100644 index 0000000..122b217 --- /dev/null +++ b/Runtime/Generated static functions/IQuadrant2D_static.cs @@ -0,0 +1,76 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static Int32 quadrant(Self v) where Self : IQuadrant2D => v.quadrant; + /// + public static Int32 signedQuadrant(Self v) where Self : IQuadrant2D => v.signedQuadrant; + /// + public static int2 quadrantBasisX(Self v) where Self : IQuadrant2D => v.quadrantBasisX; + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(Self v) where Self : IQuadrant2D => v.quadrantBasis; + } + public static partial class mathfs { + /// + public static Int32 quadrant(rat2 v) => v.quadrant; + /// + public static Int32 signedQuadrant(rat2 v) => v.signedQuadrant; + /// + public static int2 quadrantBasisX(rat2 v) => v.quadrantBasisX; + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(rat2 v) => v.quadrantBasis; + /// + public static Int32 quadrant(inth2 v) => v.quadrant; + /// + public static Int32 signedQuadrant(inth2 v) => v.signedQuadrant; + /// + public static int2 quadrantBasisX(inth2 v) => v.quadrantBasisX; + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(inth2 v) => v.quadrantBasis; + /// + public static Int32 quadrant(this int2 v) => v.y switch {> 00 when v.x <= 0 => 1,<= 0 when v.x < 00 => 2,< 00 when v.x >= 0 => 3,_ => 0 }; + /// + public static Int32 signedQuadrant(this int2 v) => mathfs.quadrantToSignedQuadrant(v.quadrant()); + /// + public static int2 quadrantBasisX(this int2 v) => mathfs.quadrantToBasisX(v.quadrant()); + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(this int2 v) => mathfs.quadrantToBasis(v.quadrant()); + /// + public static Int32 quadrant(this Vector2Int v) => v.y switch {> 00 when v.x <= 0 => 1,<= 0 when v.x < 00 => 2,< 00 when v.x >= 0 => 3,_ => 0 }; + /// + public static Int32 signedQuadrant(this Vector2Int v) => mathfs.quadrantToSignedQuadrant(v.quadrant()); + /// + public static int2 quadrantBasisX(this Vector2Int v) => mathfs.quadrantToBasisX(v.quadrant()); + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(this Vector2Int v) => mathfs.quadrantToBasis(v.quadrant()); + /// + public static Int32 quadrant(this float2 v) => v.y switch {> 00 when v.x <= 0 => 1,<= 0 when v.x < 00 => 2,< 00 when v.x >= 0 => 3,_ => 0 }; + /// + public static Int32 signedQuadrant(this float2 v) => mathfs.quadrantToSignedQuadrant(v.ceilAwayFrom0().quadrant()); + /// + public static int2 quadrantBasisX(this float2 v) => mathfs.quadrantToBasisX(v.ceilAwayFrom0().quadrant()); + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(this float2 v) => mathfs.quadrantToBasis(v.ceilAwayFrom0().quadrant()); + /// + public static Int32 quadrant(this Vector2 v) => v.y switch {> 00 when v.x <= 0 => 1,<= 0 when v.x < 00 => 2,< 00 when v.x >= 0 => 3,_ => 0 }; + /// + public static Int32 signedQuadrant(this Vector2 v) => mathfs.quadrantToSignedQuadrant(v.ceilAwayFrom0().quadrant()); + /// + public static int2 quadrantBasisX(this Vector2 v) => mathfs.quadrantToBasisX(v.ceilAwayFrom0().quadrant()); + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(this Vector2 v) => mathfs.quadrantToBasis(v.ceilAwayFrom0().quadrant()); + /// + public static Int32 quadrant(this double2 v) => v.y switch {> 00 when v.x <= 0 => 1,<= 0 when v.x < 00 => 2,< 00 when v.x >= 0 => 3,_ => 0 }; + /// + public static Int32 signedQuadrant(this double2 v) => mathfs.quadrantToSignedQuadrant(v.ceilAwayFrom0().quadrant()); + /// + public static int2 quadrantBasisX(this double2 v) => mathfs.quadrantToBasisX(v.ceilAwayFrom0().quadrant()); + /// + public static (Unity.Mathematics.int2 x,Unity.Mathematics.int2 y) quadrantBasis(this double2 v) => mathfs.quadrantToBasis(v.ceilAwayFrom0().quadrant()); + } +} diff --git a/Runtime/Generated static functions/IQuadrant2D_static.cs.meta b/Runtime/Generated static functions/IQuadrant2D_static.cs.meta new file mode 100644 index 0000000..96e790a --- /dev/null +++ b/Runtime/Generated static functions/IQuadrant2D_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fe329ea050547bc4e86684778a02f71f \ No newline at end of file diff --git a/Runtime/Generated static functions/IRoundable_static.cs b/Runtime/Generated static functions/IRoundable_static.cs new file mode 100644 index 0000000..15cb285 --- /dev/null +++ b/Runtime/Generated static functions/IRoundable_static.cs @@ -0,0 +1,172 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static R floorToward0(Self v) where Self : IRoundable => v.floorToward0; + /// + public static R ceilAwayFrom0(Self v) where Self : IRoundable => v.ceilAwayFrom0; + /// + public static R floor(Self v) where Self : IRoundable => v.floor; + /// + public static R ceil(Self v) where Self : IRoundable => v.ceil; + /// + public static R round(Self v, RoundingDirection rounding) where Self : IRoundable => v.round(rounding); + } + public static partial class mathfs { + /// + public static Int32 floorToward0(rat v) => v.floorToward0; + /// + public static Int32 ceilAwayFrom0(rat v) => v.ceilAwayFrom0; + /// + public static Int32 floor(rat v) => v.floor; + /// + public static Int32 ceil(rat v) => v.ceil; + /// + public static Int32 round(rat v, RoundingDirection rounding) => v.round(rounding); + /// + public static int2 floorToward0(rat2 v) => v.floorToward0; + /// + public static int2 ceilAwayFrom0(rat2 v) => v.ceilAwayFrom0; + /// + public static int2 floor(rat2 v) => v.floor; + /// + public static int2 ceil(rat2 v) => v.ceil; + /// + public static int2 round(rat2 v, RoundingDirection rounding) => v.round(rounding); + /// + public static Int32 floorToward0(inth v) => v.floorToward0; + /// + public static Int32 ceilAwayFrom0(inth v) => v.ceilAwayFrom0; + /// + public static Int32 floor(inth v) => v.floor; + /// + public static Int32 ceil(inth v) => v.ceil; + /// + public static Int32 round(inth v, RoundingDirection rounding) => v.round(rounding); + /// + public static int2 floorToward0(inth2 v) => v.floorToward0; + /// + public static int2 ceilAwayFrom0(inth2 v) => v.ceilAwayFrom0; + /// + public static int2 floor(inth2 v) => v.floor; + /// + public static int2 ceil(inth2 v) => v.ceil; + /// + public static int2 round(inth2 v, RoundingDirection rounding) => v.round(rounding); + /// + public static Int32 floorToward0(this Single v) => (int)(v<0?math.ceil(v):math.floor(v)); + /// + public static Int32 ceilAwayFrom0(this Single v) => (int)(v<0?math.floor(v):math.ceil(v)); + /// + public static Int32 floor(this Single v) => (int)MathF.Floor(v); + /// + public static Int32 ceil(this Single v) => (int)MathF.Ceiling(v); + /// + public static Int32 round(this Single v, RoundingDirection rounding) => (int)MathF.Round( v, (MidpointRounding)rounding ); + /// + public static int2 floorToward0(this float2 v) => new(v.x.floorToward0(), v.y.floorToward0()); + /// + public static int2 ceilAwayFrom0(this float2 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0()); + /// + public static int2 floor(this float2 v) => (int2)math.floor(v); + /// + public static int2 ceil(this float2 v) => (int2)math.ceil(v); + /// + public static int2 round(this float2 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding)); + /// + public static int2 floorToward0(this Vector2 v) => new(v.x.floorToward0(), v.y.floorToward0()); + /// + public static int2 ceilAwayFrom0(this Vector2 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0()); + /// + public static int2 floor(this Vector2 v) => new(v.x.floor(), v.y.floor()); + /// + public static int2 ceil(this Vector2 v) => new(v.x.ceil(), v.y.ceil()); + /// + public static int2 round(this Vector2 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding)); + /// + public static int3 floorToward0(this float3 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0()); + /// + public static int3 ceilAwayFrom0(this float3 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0()); + /// + public static int3 floor(this float3 v) => (int3)math.floor(v); + /// + public static int3 ceil(this float3 v) => (int3)math.ceil(v); + /// + public static int3 round(this float3 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding)); + /// + public static int3 floorToward0(this Vector3 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0()); + /// + public static int3 ceilAwayFrom0(this Vector3 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0()); + /// + public static int3 floor(this Vector3 v) => new(v.x.floor(), v.y.floor(), v.z.floor()); + /// + public static int3 ceil(this Vector3 v) => new(v.x.ceil(), v.y.ceil(), v.z.ceil()); + /// + public static int3 round(this Vector3 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding)); + /// + public static int4 floorToward0(this float4 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0(), v.w.floorToward0()); + /// + public static int4 ceilAwayFrom0(this float4 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0(), v.w.ceilAwayFrom0()); + /// + public static int4 floor(this float4 v) => (int4)math.floor(v); + /// + public static int4 ceil(this float4 v) => (int4)math.ceil(v); + /// + public static int4 round(this float4 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding), v.w.round(rounding)); + /// + public static int4 floorToward0(this Vector4 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0(), v.w.floorToward0()); + /// + public static int4 ceilAwayFrom0(this Vector4 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0(), v.w.ceilAwayFrom0()); + /// + public static int4 floor(this Vector4 v) => new(v.x.floor(), v.y.floor(), v.z.floor(), v.w.floor()); + /// + public static int4 ceil(this Vector4 v) => new(v.x.ceil(), v.y.ceil(), v.z.ceil(), v.w.ceil()); + /// + public static int4 round(this Vector4 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding), v.w.round(rounding)); + /// + public static Int32 floorToward0(this Double v) => (int)(v<0?math.ceil(v):math.floor(v)); + /// + public static Int32 ceilAwayFrom0(this Double v) => (int)(v<0?math.floor(v):math.ceil(v)); + /// + public static Int32 floor(this Double v) => (int)Math.Floor(v); + /// + public static Int32 ceil(this Double v) => (int)Math.Ceiling(v); + /// + public static Int32 round(this Double v, RoundingDirection rounding) => (int)Math.Round( v, (MidpointRounding)rounding ); + /// + public static int2 floorToward0(this double2 v) => new(v.x.floorToward0(), v.y.floorToward0()); + /// + public static int2 ceilAwayFrom0(this double2 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0()); + /// + public static int2 floor(this double2 v) => (int2)math.floor(v); + /// + public static int2 ceil(this double2 v) => (int2)math.ceil(v); + /// + public static int2 round(this double2 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding)); + /// + public static int3 floorToward0(this double3 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0()); + /// + public static int3 ceilAwayFrom0(this double3 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0()); + /// + public static int3 floor(this double3 v) => (int3)math.floor(v); + /// + public static int3 ceil(this double3 v) => (int3)math.ceil(v); + /// + public static int3 round(this double3 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding)); + /// + public static int4 floorToward0(this double4 v) => new(v.x.floorToward0(), v.y.floorToward0(), v.z.floorToward0(), v.w.floorToward0()); + /// + public static int4 ceilAwayFrom0(this double4 v) => new(v.x.ceilAwayFrom0(), v.y.ceilAwayFrom0(), v.z.ceilAwayFrom0(), v.w.ceilAwayFrom0()); + /// + public static int4 floor(this double4 v) => (int4)math.floor(v); + /// + public static int4 ceil(this double4 v) => (int4)math.ceil(v); + /// + public static int4 round(this double4 v, RoundingDirection rounding) => new(v.x.round(rounding), v.y.round(rounding), v.z.round(rounding), v.w.round(rounding)); + } +} diff --git a/Runtime/Generated static functions/IRoundable_static.cs.meta b/Runtime/Generated static functions/IRoundable_static.cs.meta new file mode 100644 index 0000000..c620889 --- /dev/null +++ b/Runtime/Generated static functions/IRoundable_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60d521fa58ac8be449b220f00b3a5718 \ No newline at end of file diff --git a/Runtime/Generated static functions/ISignedNumber_static.cs b/Runtime/Generated static functions/ISignedNumber_static.cs new file mode 100644 index 0000000..445420c --- /dev/null +++ b/Runtime/Generated static functions/ISignedNumber_static.cs @@ -0,0 +1,56 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static R sign(Self v) where Self : ISignedNumber => v.sign; + } + public static partial class mathfs { + /// + public static Int32 sign(rat v) => v.sign; + /// + public static int2 sign(rat2 v) => v.sign; + /// + public static Int32 sign(inth v) => v.sign; + /// + public static int2 sign(inth2 v) => v.sign; + /// + public static Int32 sign(this Int32 v) => Math.Sign(v); + /// + public static int2 sign(this int2 v) => new(v.x.sign(), v.y.sign()); + /// + public static int2 sign(this Vector2Int v) => new(v.x.sign(), v.y.sign()); + /// + public static int3 sign(this int3 v) => new(v.x.sign(), v.y.sign(), v.z.sign()); + /// + public static int3 sign(this Vector3Int v) => new(v.x.sign(), v.y.sign(), v.z.sign()); + /// + public static int4 sign(this int4 v) => new(v.x.sign(), v.y.sign(), v.z.sign(), v.w.sign()); + /// + public static Int32 sign(this Single v) => Math.Sign(v); + /// + public static int2 sign(this float2 v) => (int2)math.sign(v); + /// + public static int2 sign(this Vector2 v) => new(v.x.sign(), v.y.sign()); + /// + public static int3 sign(this float3 v) => (int3)math.sign(v); + /// + public static int3 sign(this Vector3 v) => new(v.x.sign(), v.y.sign(), v.z.sign()); + /// + public static int4 sign(this float4 v) => (int4)math.sign(v); + /// + public static int4 sign(this Vector4 v) => new(v.x.sign(), v.y.sign(), v.z.sign(), v.w.sign()); + /// + public static Int32 sign(this Double v) => Math.Sign(v); + /// + public static int2 sign(this double2 v) => (int2)math.sign(v); + /// + public static int3 sign(this double3 v) => (int3)math.sign(v); + /// + public static int4 sign(this double4 v) => (int4)math.sign(v); + } +} diff --git a/Runtime/Generated static functions/ISignedNumber_static.cs.meta b/Runtime/Generated static functions/ISignedNumber_static.cs.meta new file mode 100644 index 0000000..2d0458f --- /dev/null +++ b/Runtime/Generated static functions/ISignedNumber_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7894f569523cf8a4bbc22370ab9b661d \ No newline at end of file diff --git a/Runtime/Generated static functions/ISqrMag_static.cs b/Runtime/Generated static functions/ISqrMag_static.cs new file mode 100644 index 0000000..31b16b3 --- /dev/null +++ b/Runtime/Generated static functions/ISqrMag_static.cs @@ -0,0 +1,46 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static D magSq(Self v) where Self : ISqrMag => v.magSq; + } + public static partial class mathfs { + /// + public static rat magSq(rat2 v) => v.magSq; + /// + public static rat magSq(inth2 v) => v.magSq; + /// + public static Int32 magSq(this int2 v) => math.dot( v, v ); + /// + public static Int32 magSq(this Vector2Int v) => v.x*v.x+v.y*v.y; + /// + public static Int32 magSq(this int3 v) => math.dot( v, v ); + /// + public static Int32 magSq(this Vector3Int v) => v.x*v.x+v.y*v.y+v.z*v.z; + /// + public static Int32 magSq(this int4 v) => math.dot( v, v ); + /// + public static Single magSq(this float2 v) => math.dot( v, v ); + /// + public static Single magSq(this Vector2 v) => v.x*v.x+v.y*v.y; + /// + public static Single magSq(this float3 v) => math.dot( v, v ); + /// + public static Single magSq(this Vector3 v) => v.x*v.x+v.y*v.y+v.z*v.z; + /// + public static Single magSq(this float4 v) => math.dot( v, v ); + /// + public static Single magSq(this Vector4 v) => v.x*v.x+v.y*v.y+v.z*v.z+v.w*v.w; + /// + public static Double magSq(this double2 v) => math.dot( v, v ); + /// + public static Double magSq(this double3 v) => math.dot( v, v ); + /// + public static Double magSq(this double4 v) => math.dot( v, v ); + } +} diff --git a/Runtime/Generated static functions/ISqrMag_static.cs.meta b/Runtime/Generated static functions/ISqrMag_static.cs.meta new file mode 100644 index 0000000..b37a9ce --- /dev/null +++ b/Runtime/Generated static functions/ISqrMag_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 65fb0c42dc933ca45a1146f1360c6208 \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec1Base_static.cs b/Runtime/Generated static functions/IVec1Base_static.cs new file mode 100644 index 0000000..5118bde --- /dev/null +++ b/Runtime/Generated static functions/IVec1Base_static.cs @@ -0,0 +1,114 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C X(Self v) where Self : IVec1Base => v.X; + /// + public static V flipX(Self v) where Self : IVec1Base => v.flipX; + /// + public static V zeroX(Self v) where Self : IVec1Base => v.zeroX; + } + public static partial class mathfs { + /// + public static rat X(rat2 v) => v.X; + /// + public static rat2 flipX(rat2 v) => v.flipX; + /// + public static rat2 zeroX(rat2 v) => v.zeroX; + /// + public static inth X(inth2 v) => v.X; + /// + public static inth2 flipX(inth2 v) => v.flipX; + /// + public static inth2 zeroX(inth2 v) => v.zeroX; + /// + public static Int32 X(this int2 v) => v.x; + /// + public static int2 flipX(this int2 v) => new(-v.x, +v.y); + /// + public static int2 zeroX(this int2 v) => new(0, v.y); + /// + public static Int32 X(this Vector2Int v) => v.x; + /// + public static Vector2Int flipX(this Vector2Int v) => new(-v.x, +v.y); + /// + public static Vector2Int zeroX(this Vector2Int v) => new(0, v.y); + /// + public static Int32 X(this int3 v) => v.x; + /// + public static int3 flipX(this int3 v) => new(-v.x, +v.y, +v.z); + /// + public static int3 zeroX(this int3 v) => new(0, v.y, v.z); + /// + public static Int32 X(this Vector3Int v) => v.x; + /// + public static Vector3Int flipX(this Vector3Int v) => new(-v.x, +v.y, +v.z); + /// + public static Vector3Int zeroX(this Vector3Int v) => new(0, v.y, v.z); + /// + public static Int32 X(this int4 v) => v.x; + /// + public static int4 flipX(this int4 v) => new(-v.x, +v.y, +v.z, +v.w); + /// + public static int4 zeroX(this int4 v) => new(0, v.y, v.z, v.w); + /// + public static Single X(this float2 v) => v.x; + /// + public static float2 flipX(this float2 v) => new(-v.x, +v.y); + /// + public static float2 zeroX(this float2 v) => new(0, v.y); + /// + public static Single X(this Vector2 v) => v.x; + /// + public static Vector2 flipX(this Vector2 v) => new(-v.x, +v.y); + /// + public static Vector2 zeroX(this Vector2 v) => new(0, v.y); + /// + public static Single X(this float3 v) => v.x; + /// + public static float3 flipX(this float3 v) => new(-v.x, +v.y, +v.z); + /// + public static float3 zeroX(this float3 v) => new(0, v.y, v.z); + /// + public static Single X(this Vector3 v) => v.x; + /// + public static Vector3 flipX(this Vector3 v) => new(-v.x, +v.y, +v.z); + /// + public static Vector3 zeroX(this Vector3 v) => new(0, v.y, v.z); + /// + public static Single X(this float4 v) => v.x; + /// + public static float4 flipX(this float4 v) => new(-v.x, +v.y, +v.z, +v.w); + /// + public static float4 zeroX(this float4 v) => new(0, v.y, v.z, v.w); + /// + public static Single X(this Vector4 v) => v.x; + /// + public static Vector4 flipX(this Vector4 v) => new(-v.x, +v.y, +v.z, +v.w); + /// + public static Vector4 zeroX(this Vector4 v) => new(0, v.y, v.z, v.w); + /// + public static Double X(this double2 v) => v.x; + /// + public static double2 flipX(this double2 v) => new(-v.x, +v.y); + /// + public static double2 zeroX(this double2 v) => new(0, v.y); + /// + public static Double X(this double3 v) => v.x; + /// + public static double3 flipX(this double3 v) => new(-v.x, +v.y, +v.z); + /// + public static double3 zeroX(this double3 v) => new(0, v.y, v.z); + /// + public static Double X(this double4 v) => v.x; + /// + public static double4 flipX(this double4 v) => new(-v.x, +v.y, +v.z, +v.w); + /// + public static double4 zeroX(this double4 v) => new(0, v.y, v.z, v.w); + } +} diff --git a/Runtime/Generated static functions/IVec1Base_static.cs.meta b/Runtime/Generated static functions/IVec1Base_static.cs.meta new file mode 100644 index 0000000..9e896ba --- /dev/null +++ b/Runtime/Generated static functions/IVec1Base_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b162f4ffab01ef447b46fb232e67a4af \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec2Base_static.cs b/Runtime/Generated static functions/IVec2Base_static.cs new file mode 100644 index 0000000..0d59d64 --- /dev/null +++ b/Runtime/Generated static functions/IVec2Base_static.cs @@ -0,0 +1,114 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C Y(Self v) where Self : IVec2Base => v.Y; + /// + public static V flipY(Self v) where Self : IVec2Base => v.flipY; + /// + public static V zeroY(Self v) where Self : IVec2Base => v.zeroY; + } + public static partial class mathfs { + /// + public static rat Y(rat2 v) => v.Y; + /// + public static rat2 flipY(rat2 v) => v.flipY; + /// + public static rat2 zeroY(rat2 v) => v.zeroY; + /// + public static inth Y(inth2 v) => v.Y; + /// + public static inth2 flipY(inth2 v) => v.flipY; + /// + public static inth2 zeroY(inth2 v) => v.zeroY; + /// + public static Int32 Y(this int2 v) => v.y; + /// + public static int2 flipY(this int2 v) => new(+v.x, -v.y); + /// + public static int2 zeroY(this int2 v) => new(v.x, 0); + /// + public static Int32 Y(this Vector2Int v) => v.y; + /// + public static Vector2Int flipY(this Vector2Int v) => new(+v.x, -v.y); + /// + public static Vector2Int zeroY(this Vector2Int v) => new(v.x, 0); + /// + public static Int32 Y(this int3 v) => v.y; + /// + public static int3 flipY(this int3 v) => new(+v.x, -v.y, +v.z); + /// + public static int3 zeroY(this int3 v) => new(v.x, 0, v.z); + /// + public static Int32 Y(this Vector3Int v) => v.y; + /// + public static Vector3Int flipY(this Vector3Int v) => new(+v.x, -v.y, +v.z); + /// + public static Vector3Int zeroY(this Vector3Int v) => new(v.x, 0, v.z); + /// + public static Int32 Y(this int4 v) => v.y; + /// + public static int4 flipY(this int4 v) => new(+v.x, -v.y, +v.z, +v.w); + /// + public static int4 zeroY(this int4 v) => new(v.x, 0, v.z, v.w); + /// + public static Single Y(this float2 v) => v.y; + /// + public static float2 flipY(this float2 v) => new(+v.x, -v.y); + /// + public static float2 zeroY(this float2 v) => new(v.x, 0); + /// + public static Single Y(this Vector2 v) => v.y; + /// + public static Vector2 flipY(this Vector2 v) => new(+v.x, -v.y); + /// + public static Vector2 zeroY(this Vector2 v) => new(v.x, 0); + /// + public static Single Y(this float3 v) => v.y; + /// + public static float3 flipY(this float3 v) => new(+v.x, -v.y, +v.z); + /// + public static float3 zeroY(this float3 v) => new(v.x, 0, v.z); + /// + public static Single Y(this Vector3 v) => v.y; + /// + public static Vector3 flipY(this Vector3 v) => new(+v.x, -v.y, +v.z); + /// + public static Vector3 zeroY(this Vector3 v) => new(v.x, 0, v.z); + /// + public static Single Y(this float4 v) => v.y; + /// + public static float4 flipY(this float4 v) => new(+v.x, -v.y, +v.z, +v.w); + /// + public static float4 zeroY(this float4 v) => new(v.x, 0, v.z, v.w); + /// + public static Single Y(this Vector4 v) => v.y; + /// + public static Vector4 flipY(this Vector4 v) => new(+v.x, -v.y, +v.z, +v.w); + /// + public static Vector4 zeroY(this Vector4 v) => new(v.x, 0, v.z, v.w); + /// + public static Double Y(this double2 v) => v.y; + /// + public static double2 flipY(this double2 v) => new(+v.x, -v.y); + /// + public static double2 zeroY(this double2 v) => new(v.x, 0); + /// + public static Double Y(this double3 v) => v.y; + /// + public static double3 flipY(this double3 v) => new(+v.x, -v.y, +v.z); + /// + public static double3 zeroY(this double3 v) => new(v.x, 0, v.z); + /// + public static Double Y(this double4 v) => v.y; + /// + public static double4 flipY(this double4 v) => new(+v.x, -v.y, +v.z, +v.w); + /// + public static double4 zeroY(this double4 v) => new(v.x, 0, v.z, v.w); + } +} diff --git a/Runtime/Generated static functions/IVec2Base_static.cs.meta b/Runtime/Generated static functions/IVec2Base_static.cs.meta new file mode 100644 index 0000000..5d7ba59 --- /dev/null +++ b/Runtime/Generated static functions/IVec2Base_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5d1005de541dd9f4ea3c895f2c744050 \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec2_static.cs b/Runtime/Generated static functions/IVec2_static.cs new file mode 100644 index 0000000..764bdb6 --- /dev/null +++ b/Runtime/Generated static functions/IVec2_static.cs @@ -0,0 +1,60 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static V rot90(Self v) where Self : IVec2 => v.rot90; + /// + public static V rotNeg90(Self v) where Self : IVec2 => v.rotNeg90; + /// + public static V rot180(Self v) where Self : IVec2 => v.rot180; + } + public static partial class mathfs { + /// + public static rat2 rot90(rat2 v) => v.rot90; + /// + public static rat2 rotNeg90(rat2 v) => v.rotNeg90; + /// + public static rat2 rot180(rat2 v) => v.rot180; + /// + public static inth2 rot90(inth2 v) => v.rot90; + /// + public static inth2 rotNeg90(inth2 v) => v.rotNeg90; + /// + public static inth2 rot180(inth2 v) => v.rot180; + /// + public static int2 rot90(this int2 v) => new(-v.y,v.x); + /// + public static int2 rotNeg90(this int2 v) => new(v.y,-v.x); + /// + public static int2 rot180(this int2 v) => new(-v.x,-v.y); + /// + public static Vector2Int rot90(this Vector2Int v) => new(-v.y,v.x); + /// + public static Vector2Int rotNeg90(this Vector2Int v) => new(v.y,-v.x); + /// + public static Vector2Int rot180(this Vector2Int v) => new(-v.x,-v.y); + /// + public static float2 rot90(this float2 v) => new(-v.y,v.x); + /// + public static float2 rotNeg90(this float2 v) => new(v.y,-v.x); + /// + public static float2 rot180(this float2 v) => new(-v.x,-v.y); + /// + public static Vector2 rot90(this Vector2 v) => new(-v.y,v.x); + /// + public static Vector2 rotNeg90(this Vector2 v) => new(v.y,-v.x); + /// + public static Vector2 rot180(this Vector2 v) => new(-v.x,-v.y); + /// + public static double2 rot90(this double2 v) => new(-v.y,v.x); + /// + public static double2 rotNeg90(this double2 v) => new(v.y,-v.x); + /// + public static double2 rot180(this double2 v) => new(-v.x,-v.y); + } +} diff --git a/Runtime/Generated static functions/IVec2_static.cs.meta b/Runtime/Generated static functions/IVec2_static.cs.meta new file mode 100644 index 0000000..2d0294a --- /dev/null +++ b/Runtime/Generated static functions/IVec2_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2982c80d636593d4a829b1e71935cc8a \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec3Base_static.cs b/Runtime/Generated static functions/IVec3Base_static.cs new file mode 100644 index 0000000..44063d0 --- /dev/null +++ b/Runtime/Generated static functions/IVec3Base_static.cs @@ -0,0 +1,72 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C Z(Self v) where Self : IVec3Base => v.Z; + /// + public static V flipZ(Self v) where Self : IVec3Base => v.flipZ; + /// + public static V zeroZ(Self v) where Self : IVec3Base => v.zeroZ; + } + public static partial class mathfs { + /// + public static Int32 Z(this int3 v) => v.z; + /// + public static int3 flipZ(this int3 v) => new(+v.x, +v.y, -v.z); + /// + public static int3 zeroZ(this int3 v) => new(v.x, v.y, 0); + /// + public static Int32 Z(this Vector3Int v) => v.z; + /// + public static Vector3Int flipZ(this Vector3Int v) => new(+v.x, +v.y, -v.z); + /// + public static Vector3Int zeroZ(this Vector3Int v) => new(v.x, v.y, 0); + /// + public static Int32 Z(this int4 v) => v.z; + /// + public static int4 flipZ(this int4 v) => new(+v.x, +v.y, -v.z, +v.w); + /// + public static int4 zeroZ(this int4 v) => new(v.x, v.y, 0, v.w); + /// + public static Single Z(this float3 v) => v.z; + /// + public static float3 flipZ(this float3 v) => new(+v.x, +v.y, -v.z); + /// + public static float3 zeroZ(this float3 v) => new(v.x, v.y, 0); + /// + public static Single Z(this Vector3 v) => v.z; + /// + public static Vector3 flipZ(this Vector3 v) => new(+v.x, +v.y, -v.z); + /// + public static Vector3 zeroZ(this Vector3 v) => new(v.x, v.y, 0); + /// + public static Single Z(this float4 v) => v.z; + /// + public static float4 flipZ(this float4 v) => new(+v.x, +v.y, -v.z, +v.w); + /// + public static float4 zeroZ(this float4 v) => new(v.x, v.y, 0, v.w); + /// + public static Single Z(this Vector4 v) => v.z; + /// + public static Vector4 flipZ(this Vector4 v) => new(+v.x, +v.y, -v.z, +v.w); + /// + public static Vector4 zeroZ(this Vector4 v) => new(v.x, v.y, 0, v.w); + /// + public static Double Z(this double3 v) => v.z; + /// + public static double3 flipZ(this double3 v) => new(+v.x, +v.y, -v.z); + /// + public static double3 zeroZ(this double3 v) => new(v.x, v.y, 0); + /// + public static Double Z(this double4 v) => v.z; + /// + public static double4 flipZ(this double4 v) => new(+v.x, +v.y, -v.z, +v.w); + /// + public static double4 zeroZ(this double4 v) => new(v.x, v.y, 0, v.w); + } +} diff --git a/Runtime/Generated static functions/IVec3Base_static.cs.meta b/Runtime/Generated static functions/IVec3Base_static.cs.meta new file mode 100644 index 0000000..3e372d6 --- /dev/null +++ b/Runtime/Generated static functions/IVec3Base_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a2313d7ddb5772b46b263caf79b61be4 \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec4Base_static.cs b/Runtime/Generated static functions/IVec4Base_static.cs new file mode 100644 index 0000000..cf25429 --- /dev/null +++ b/Runtime/Generated static functions/IVec4Base_static.cs @@ -0,0 +1,42 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C W(Self v) where Self : IVec4Base => v.W; + /// + public static V flipW(Self v) where Self : IVec4Base => v.flipW; + /// + public static V zeroW(Self v) where Self : IVec4Base => v.zeroW; + } + public static partial class mathfs { + /// + public static Int32 W(this int4 v) => v.w; + /// + public static int4 flipW(this int4 v) => new(+v.x, +v.y, +v.z, -v.w); + /// + public static int4 zeroW(this int4 v) => new(v.x, v.y, v.z, 0); + /// + public static Single W(this float4 v) => v.w; + /// + public static float4 flipW(this float4 v) => new(+v.x, +v.y, +v.z, -v.w); + /// + public static float4 zeroW(this float4 v) => new(v.x, v.y, v.z, 0); + /// + public static Single W(this Vector4 v) => v.w; + /// + public static Vector4 flipW(this Vector4 v) => new(+v.x, +v.y, +v.z, -v.w); + /// + public static Vector4 zeroW(this Vector4 v) => new(v.x, v.y, v.z, 0); + /// + public static Double W(this double4 v) => v.w; + /// + public static double4 flipW(this double4 v) => new(+v.x, +v.y, +v.z, -v.w); + /// + public static double4 zeroW(this double4 v) => new(v.x, v.y, v.z, 0); + } +} diff --git a/Runtime/Generated static functions/IVec4Base_static.cs.meta b/Runtime/Generated static functions/IVec4Base_static.cs.meta new file mode 100644 index 0000000..1d0b974 --- /dev/null +++ b/Runtime/Generated static functions/IVec4Base_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ffe9f8449b895c545902650e7116df95 \ No newline at end of file diff --git a/Runtime/Generated static functions/IVecComponents_static.cs b/Runtime/Generated static functions/IVecComponents_static.cs new file mode 100644 index 0000000..90c2a3c --- /dev/null +++ b/Runtime/Generated static functions/IVecComponents_static.cs @@ -0,0 +1,114 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C cmin(Self v) where Self : IVecComponents => v.cmin; + /// + public static C cmax(Self v) where Self : IVecComponents => v.cmax; + /// + public static C csum(Self v) where Self : IVecComponents => v.csum; + } + public static partial class mathfs { + /// + public static rat cmin(rat2 v) => v.cmin; + /// + public static rat cmax(rat2 v) => v.cmax; + /// + public static rat csum(rat2 v) => v.csum; + /// + public static inth cmin(inth2 v) => v.cmin; + /// + public static inth cmax(inth2 v) => v.cmax; + /// + public static inth csum(inth2 v) => v.csum; + /// + public static Int32 cmin(this int2 v) => math.cmin(v); + /// + public static Int32 cmax(this int2 v) => math.cmax(v); + /// + public static Int32 csum(this int2 v) => math.csum(v); + /// + public static Int32 cmin(this Vector2Int v) => v.y.min(v.x); + /// + public static Int32 cmax(this Vector2Int v) => v.y.max(v.x); + /// + public static Int32 csum(this Vector2Int v) => v.x+v.y; + /// + public static Int32 cmin(this int3 v) => math.cmin(v); + /// + public static Int32 cmax(this int3 v) => math.cmax(v); + /// + public static Int32 csum(this int3 v) => math.csum(v); + /// + public static Int32 cmin(this Vector3Int v) => v.z.min(v.y.min(v.x)); + /// + public static Int32 cmax(this Vector3Int v) => v.z.max(v.y.max(v.x)); + /// + public static Int32 csum(this Vector3Int v) => v.x+v.y+v.z; + /// + public static Int32 cmin(this int4 v) => math.cmin(v); + /// + public static Int32 cmax(this int4 v) => math.cmax(v); + /// + public static Int32 csum(this int4 v) => math.csum(v); + /// + public static Single cmin(this float2 v) => math.cmin(v); + /// + public static Single cmax(this float2 v) => math.cmax(v); + /// + public static Single csum(this float2 v) => math.csum(v); + /// + public static Single cmin(this Vector2 v) => v.y.min(v.x); + /// + public static Single cmax(this Vector2 v) => v.y.max(v.x); + /// + public static Single csum(this Vector2 v) => v.x+v.y; + /// + public static Single cmin(this float3 v) => math.cmin(v); + /// + public static Single cmax(this float3 v) => math.cmax(v); + /// + public static Single csum(this float3 v) => math.csum(v); + /// + public static Single cmin(this Vector3 v) => v.z.min(v.y.min(v.x)); + /// + public static Single cmax(this Vector3 v) => v.z.max(v.y.max(v.x)); + /// + public static Single csum(this Vector3 v) => v.x+v.y+v.z; + /// + public static Single cmin(this float4 v) => math.cmin(v); + /// + public static Single cmax(this float4 v) => math.cmax(v); + /// + public static Single csum(this float4 v) => math.csum(v); + /// + public static Single cmin(this Vector4 v) => v.w.min(v.z.min(v.y.min(v.x))); + /// + public static Single cmax(this Vector4 v) => v.w.max(v.z.max(v.y.max(v.x))); + /// + public static Single csum(this Vector4 v) => v.x+v.y+v.z+v.w; + /// + public static Double cmin(this double2 v) => math.cmin(v); + /// + public static Double cmax(this double2 v) => math.cmax(v); + /// + public static Double csum(this double2 v) => math.csum(v); + /// + public static Double cmin(this double3 v) => math.cmin(v); + /// + public static Double cmax(this double3 v) => math.cmax(v); + /// + public static Double csum(this double3 v) => math.csum(v); + /// + public static Double cmin(this double4 v) => math.cmin(v); + /// + public static Double cmax(this double4 v) => math.cmax(v); + /// + public static Double csum(this double4 v) => math.csum(v); + } +} diff --git a/Runtime/Generated static functions/IVecComponents_static.cs.meta b/Runtime/Generated static functions/IVecComponents_static.cs.meta new file mode 100644 index 0000000..62428d2 --- /dev/null +++ b/Runtime/Generated static functions/IVecComponents_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d299effa917f60d4b83ce7e7dd05dedf \ No newline at end of file diff --git a/Runtime/Generated static functions/IVecProjections_static.cs b/Runtime/Generated static functions/IVecProjections_static.cs new file mode 100644 index 0000000..a129be8 --- /dev/null +++ b/Runtime/Generated static functions/IVecProjections_static.cs @@ -0,0 +1,28 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static Proj projTValue(Self v, V n) where Self : IVecProjections => v.projTValue(n); + } + public static partial class mathfs { + /// + public static rat projTValue(rat2 v, rat2 n) => v.projTValue(n); + /// + public static rat projTValue(inth2 v, inth2 n) => v.projTValue(n); + /// + public static rat projTValue(this int2 v, int2 n) => v.dot(n)/n.dot(n); + /// + public static rat projTValue(this Vector2Int v, Vector2Int n) => v.dot(n)/n.dot(n); + /// + public static Single projTValue(this float2 v, float2 n) => v.dot(n)/n.dot(n); + /// + public static Single projTValue(this Vector2 v, Vector2 n) => v.dot(n)/n.dot(n); + /// + public static Double projTValue(this double2 v, double2 n) => v.dot(n)/n.dot(n); + } +} diff --git a/Runtime/Generated static functions/IVecProjections_static.cs.meta b/Runtime/Generated static functions/IVecProjections_static.cs.meta new file mode 100644 index 0000000..f5fc6d0 --- /dev/null +++ b/Runtime/Generated static functions/IVecProjections_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 11d10ab84ebdfdd46873c692b2bcc55e \ No newline at end of file diff --git a/Runtime/Generated static functions/IVec_static.cs b/Runtime/Generated static functions/IVec_static.cs new file mode 100644 index 0000000..b5f5acf --- /dev/null +++ b/Runtime/Generated static functions/IVec_static.cs @@ -0,0 +1,114 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static C magChebyshev(Self v) where Self : IVec => v.magChebyshev; + /// + public static C magTaxicab(Self v) where Self : IVec => v.magTaxicab; + /// + public static Int32 pointSideOfPlane(Self v, V planePos, V planeNormal) where Self : IVec => v.pointSideOfPlane(planePos, planeNormal); + } + public static partial class mathfs { + /// + public static rat magChebyshev(rat2 v) => v.magChebyshev; + /// + public static rat magTaxicab(rat2 v) => v.magTaxicab; + /// + public static Int32 pointSideOfPlane(rat2 v, rat2 planePos, rat2 planeNormal) => v.pointSideOfPlane(planePos, planeNormal); + /// + public static inth magChebyshev(inth2 v) => v.magChebyshev; + /// + public static inth magTaxicab(inth2 v) => v.magTaxicab; + /// + public static Int32 pointSideOfPlane(inth2 v, inth2 planePos, inth2 planeNormal) => v.pointSideOfPlane(planePos, planeNormal); + /// + public static Int32 magChebyshev(this int2 v) => v.abs().cmax(); + /// + public static Int32 magTaxicab(this int2 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this int2 v, int2 planePos, int2 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Int32 magChebyshev(this Vector2Int v) => v.abs().cmax(); + /// + public static Int32 magTaxicab(this Vector2Int v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this Vector2Int v, Vector2Int planePos, Vector2Int planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Int32 magChebyshev(this int3 v) => v.abs().cmax(); + /// + public static Int32 magTaxicab(this int3 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this int3 v, int3 planePos, int3 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Int32 magChebyshev(this Vector3Int v) => v.abs().cmax(); + /// + public static Int32 magTaxicab(this Vector3Int v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this Vector3Int v, Vector3Int planePos, Vector3Int planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Int32 magChebyshev(this int4 v) => v.abs().cmax(); + /// + public static Int32 magTaxicab(this int4 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this int4 v, int4 planePos, int4 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this float2 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this float2 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this float2 v, float2 planePos, float2 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this Vector2 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this Vector2 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this Vector2 v, Vector2 planePos, Vector2 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this float3 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this float3 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this float3 v, float3 planePos, float3 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this Vector3 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this Vector3 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this Vector3 v, Vector3 planePos, Vector3 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this float4 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this float4 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this float4 v, float4 planePos, float4 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Single magChebyshev(this Vector4 v) => v.abs().cmax(); + /// + public static Single magTaxicab(this Vector4 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this Vector4 v, Vector4 planePos, Vector4 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Double magChebyshev(this double2 v) => v.abs().cmax(); + /// + public static Double magTaxicab(this double2 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this double2 v, double2 planePos, double2 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Double magChebyshev(this double3 v) => v.abs().cmax(); + /// + public static Double magTaxicab(this double3 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this double3 v, double3 planePos, double3 planeNormal) => (v-planePos).dot(planeNormal).sign(); + /// + public static Double magChebyshev(this double4 v) => v.abs().cmax(); + /// + public static Double magTaxicab(this double4 v) => v.abs().csum(); + /// + public static Int32 pointSideOfPlane(this double4 v, double4 planePos, double4 planeNormal) => (v-planePos).dot(planeNormal).sign(); + } +} diff --git a/Runtime/Generated static functions/IVec_static.cs.meta b/Runtime/Generated static functions/IVec_static.cs.meta new file mode 100644 index 0000000..a98bb5f --- /dev/null +++ b/Runtime/Generated static functions/IVec_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0a43132aa99fb064b82c4429d717e757 \ No newline at end of file diff --git a/Runtime/Generated static functions/IWedgeProduct_static.cs b/Runtime/Generated static functions/IWedgeProduct_static.cs new file mode 100644 index 0000000..666243b --- /dev/null +++ b/Runtime/Generated static functions/IWedgeProduct_static.cs @@ -0,0 +1,38 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using Unity.Mathematics; +using UnityEngine; +namespace Freya { + public static partial class mathfs_generics { + /// + public static W wedge(Self a, V b) where Self : IWedgeProduct => a.wedge(b); + } + public static partial class mathfs { + /// + public static rat wedge(rat2 a, rat2 b) => a.wedge(b); + /// + public static rat wedge(inth2 a, inth2 b) => a.wedge(b); + /// + public static Int32 wedge(this int2 a, int2 b) => a.x*b.y - a.y*b.x; + /// + public static Int32 wedge(this Vector2Int a, Vector2Int b) => a.x*b.y - a.y*b.x; + /// + public static int3 wedge(this int3 a, int3 b) => new(a.y * b.z - a.z * b.y,a.z * b.x - a.x * b.z,a.x * b.y - a.y * b.x); + /// + public static Vector3Int wedge(this Vector3Int a, Vector3Int b) => new(a.y * b.z - a.z * b.y,a.z * b.x - a.x * b.z,a.x * b.y - a.y * b.x); + /// + public static Single wedge(this float2 a, float2 b) => a.x*b.y - a.y*b.x; + /// + public static Single wedge(this Vector2 a, Vector2 b) => a.x*b.y - a.y*b.x; + /// + public static float3 wedge(this float3 a, float3 b) => new(a.y * b.z - a.z * b.y,a.z * b.x - a.x * b.z,a.x * b.y - a.y * b.x); + /// + public static Vector3 wedge(this Vector3 a, Vector3 b) => new(a.y * b.z - a.z * b.y,a.z * b.x - a.x * b.z,a.x * b.y - a.y * b.x); + /// + public static Double wedge(this double2 a, double2 b) => a.x*b.y - a.y*b.x; + /// + public static double3 wedge(this double3 a, double3 b) => new(a.y * b.z - a.z * b.y,a.z * b.x - a.x * b.z,a.x * b.y - a.y * b.x); + } +} diff --git a/Runtime/Generated static functions/IWedgeProduct_static.cs.meta b/Runtime/Generated static functions/IWedgeProduct_static.cs.meta new file mode 100644 index 0000000..0fab0c4 --- /dev/null +++ b/Runtime/Generated static functions/IWedgeProduct_static.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 03e8288e5c11ccd44a34de518786894a \ No newline at end of file diff --git a/Runtime/Geometric Algebra.meta b/Runtime/Geometric Algebra.meta new file mode 100644 index 0000000..c8cbbd2 --- /dev/null +++ b/Runtime/Geometric Algebra.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 27c902a03e0fe7d45bd17d786cd48ddd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Algebra/Bivector3.cs b/Runtime/Geometric Algebra/Bivector3.cs new file mode 100644 index 0000000..d131b13 --- /dev/null +++ b/Runtime/Geometric Algebra/Bivector3.cs @@ -0,0 +1,107 @@ +using System; +using UnityEngine; + +namespace Freya { + + [Serializable] + public struct Bivector3 { + + public static readonly Bivector3 zero = new Bivector3( 0, 0, 0 ); + public float yz, zx, xy; + + public float this[ int i ] => i switch { 0 => yz, 1 => zx, 2 => xy, _ => throw new IndexOutOfRangeException() }; + + public Bivector3( float yz, float zx, float xy ) { + this.yz = yz; + this.zx = zx; + this.xy = xy; + } + + public Bivector3( Vector3 a, Vector3 b ) { + Bivector3 bv = Mathfs.Wedge( a, b ); + this.yz = bv.yz; + this.zx = bv.zx; + this.xy = bv.xy; + } + + public float Magnitude => MathF.Sqrt( SqrMagnitude ); + public Bivector3 Normalized => new Bivector3( yz, zx, xy ) / Magnitude; + public Vector3 Normal => HodgeDual.normalized; + public Vector3 HodgeDual => new Vector3( yz, zx, xy ); + public float SqrMagnitude => yz * yz + zx * zx + xy * xy; + + /// + public float Dot( Bivector3 b ) => Dot( this, b ); + + /// + public Bivector3 Wedge( Bivector3 b ) => Wedge( this, b ); + + /// The real part when multiplying two bivectors + public static float Dot( Bivector3 a, Bivector3 b ) => -a.yz * b.yz - a.zx * b.zx - a.xy * b.xy; + + /// The bivector part when multiplying two bivectors + public static Bivector3 Wedge( Bivector3 a, Bivector3 b ) => + new Bivector3( + yz: a.xy * b.zx - a.zx * b.xy, + zx: a.yz * b.xy - a.xy * b.yz, + xy: a.zx * b.yz - a.yz * b.zx ); + + /// Returns the normal of this bivector plane and its area + public (Vector3 normal, float area) GetNormalAndArea() => HodgeDual.GetDirAndMagnitude(); + + // Multiplication + public static Bivector3 operator -( Bivector3 b ) => new Bivector3( -b.yz, -b.zx, -b.xy ); + public static Bivector3 operator *( float a, Bivector3 b ) => b * a; + public static Bivector3 operator *( Bivector3 a, float b ) => new Bivector3( a.yz * b, a.zx * b, a.xy * b ); + + public static Rotor3 operator *( Bivector3 a, Bivector3 b ) => + new( + r: Dot( a, b ), + b: Wedge( a, b ) + ); + + public Rotor3 Square() => + new( + -yz * yz - zx * zx - xy * xy, + yz: xy * zx - zx * xy, + zx: yz * xy - xy * yz, + xy: zx * yz - yz * zx + ); + + public static Multivector3 operator *( Bivector3 a, Vector3 b ) { + return new Multivector3( + 0, // real + a.xy * b.y - a.zx * b.z, // vector + a.yz * b.z - a.xy * b.x, + a.zx * b.x - a.yz * b.y, + 0, 0, 0, // bivector + a.yz * b.x + a.zx * b.y + a.xy * b.z // trivector + ); + } + + public static Multivector3 operator *( Vector3 a, Bivector3 b ) { + return new Multivector3( + 0, // real + a.z * b.zx - a.y * b.xy, // vector + a.x * b.xy - a.z * b.yz, + a.y * b.yz - a.x * b.zx, + 0, 0, 0, // bivector + a.x * b.yz + a.y * b.zx + a.z * b.xy // trivector + ); + } + + // division + public static Bivector3 operator /( Bivector3 a, float b ) => new Bivector3( a.yz / b, a.zx / b, a.xy / b ); + + // addition + public static Bivector3 operator +( Bivector3 a, Bivector3 b ) => new Bivector3( a.yz * b.yz, a.zx * b.zx, a.xy * b.xy ); + public static Multivector3 operator +( Bivector3 a, Trivector3 b ) => new Multivector3( 0, Vector3.zero, a, b ); + public static Multivector3 operator +( Trivector3 a, Bivector3 b ) => new Multivector3( 0, Vector3.zero, b, a ); + + // casting + public static explicit operator Vector3( Bivector3 bv ) => new Vector3( bv.yz, bv.zx, bv.xy ); + public static explicit operator Bivector3( Vector3 v ) => new Bivector3( v.x, v.y, v.z ); + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Algebra/Bivector3.cs.meta b/Runtime/Geometric Algebra/Bivector3.cs.meta new file mode 100644 index 0000000..df2bab5 --- /dev/null +++ b/Runtime/Geometric Algebra/Bivector3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc21a5a6fd4afbb4bb345d2004897f94 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Algebra/Multivector3.cs b/Runtime/Geometric Algebra/Multivector3.cs new file mode 100644 index 0000000..5014b20 --- /dev/null +++ b/Runtime/Geometric Algebra/Multivector3.cs @@ -0,0 +1,118 @@ +using UnityEngine; + +namespace Freya { + + public struct Multivector3 { + + public float r; + public Vector3 v; + public Bivector3 b; + public Trivector3 t; + + public float x { + get => v.x; + set => v.x = value; + } + public float y { + get => v.y; + set => v.y = value; + } + public float z { + get => v.z; + set => v.z = value; + } + public float yz { + get => b.yz; + set => b.yz = value; + } + public float zx { + get => b.zx; + set => b.zx = value; + } + public float xy { + get => b.xy; + set => b.xy = value; + } + public float xyz { + get => t.xyz; + set => t.xyz = value; + } + + public Multivector3( float r, float x, float y, float z, float yz, float zx, float xy, float xyz ) + : this( + r, + new Vector3( x, y, z ), + new Bivector3( yz, zx, xy ), + new Trivector3( xyz ) ) {} + + public Multivector3( float r, Vector3 v ) : this( r, v, Bivector3.zero, Trivector3.zero ) {} + + public Multivector3( float r, Vector3 v, Bivector3 b, Trivector3 t ) { + this.r = r; + this.v = v; + this.b = b; + this.t = t; + } + + // multiplication + public static Multivector3 operator *( Multivector3 a, Multivector3 b ) { + // 64 multiplications, 56 add/sub + // R: a_r*b_r +a_x*b_x +a_y*b_y +a_z*b_z +a_yz*b_yz +a_zx*b_zx +a_xy*b_xy +a_xyz*b_xyz + // X: a_r*b_x +a_x*b_r -a_y*b_xy +a_z*b_zx -a_yz*b_xyz -a_zx*b_z +a_xy*b_y -a_xyz*b_yz + // Y: a_r*b_y +a_x*b_xy +a_y*b_r -a_z*b_yz +a_yz*b_z -a_zx*b_xyz -a_xy*b_x -a_xyz*b_zx + // Z: a_r*b_z -a_x*b_zx +a_y*b_yz +a_z*b_r -a_yz*b_y +a_zx*b_x -a_xy*b_xyz -a_xyz*b_xy + // YZ: a_r*b_yz +a_x*b_xyz +a_y*b_z -a_z*b_y +a_yz*b_r -a_zx*b_xy +a_xy*b_zx +a_xyz*b_x + // ZX: a_r*b_zx -a_x*b_z +a_y*b_xyz +a_z*b_x +a_yz*b_xy +a_zx*b_r -a_xy*b_yz +a_xyz*b_y + // XY: a_r*b_xy +a_x*b_y -a_y*b_x +a_z*b_xyz -a_yz*b_zx +a_zx*b_yz +a_xy*b_r +a_xyz*b_z + // XYZ: a_r*b_xyz +a_x*byz +a_y*b_zx +a_z*xy +a_yz*b_x +a_zx*b_y +a_xy*b_z +a_xyz*b_r + return new Multivector3( + a.r * b.r + a.x * b.x + a.y * b.y + a.z * b.z + a.yz * b.yz + a.zx * b.zx + a.xy * b.xy + a.xyz * b.xyz, + a.r * b.x + a.x * b.r - a.y * b.xy + a.z * b.zx - a.yz * b.xyz - a.zx * b.z + a.xy * b.y - a.xyz * b.yz, + a.r * b.y + a.x * b.xy + a.y * b.r - a.z * b.yz + a.yz * b.z - a.zx * b.xyz - a.xy * b.x - a.xyz * b.zx, + a.r * b.z - a.x * b.zx + a.y * b.yz + a.z * b.r - a.yz * b.y + a.zx * b.x - a.xy * b.xyz - a.xyz * b.xy, + a.r * b.yz + a.x * b.xyz + a.y * b.z - a.z * b.y + a.yz * b.r - a.zx * b.xy + a.xy * b.zx + a.xyz * b.x, + a.r * b.zx - a.x * b.z + a.y * b.xyz + a.z * b.x + a.yz * b.xy + a.zx * b.r - a.xy * b.yz + a.xyz * b.y, + a.r * b.xy + a.x * b.y - a.y * b.x + a.z * b.xyz - a.yz * b.zx + a.zx * b.yz + a.xy * b.r + a.xyz * b.z, + a.r * b.xyz + a.x * b.yz + a.y * b.zx + a.z * b.xy + a.yz * b.x + a.zx * b.y + a.xy * b.z + a.xyz * b.r + ); + } + + public static Multivector3 operator *( Multivector3 m, Rotor3 r ) { + return new Multivector3( + m.r * r.r + m.yz * r.yz + m.zx * r.zx + m.xy * r.xy, + m.x * r.r - m.y * r.xy + m.z * r.zx - m.xyz * r.yz, + +m.x * r.xy + m.y * r.r - m.z * r.yz - m.xyz * r.zx, + -m.x * r.zx + m.y * r.yz + m.z * r.r - m.xyz * r.xy, + m.r * r.yz + m.yz * r.r - m.zx * r.xy + m.xy * r.zx, + m.r * r.zx + m.yz * r.xy + m.zx * r.r - m.xy * r.yz, + m.r * r.xy - m.yz * r.zx + m.zx * r.yz + m.xy * r.r, + m.x * r.yz + m.y * r.zx + m.z * r.xy + m.xyz * r.r + ); + } + + public static Bivector3 Wedge( Multivector3 a, Multivector3 b ) => + new Bivector3( + a.r * b.yz + a.x * b.xyz + a.y * b.z - a.z * b.y + a.yz * b.r - a.zx * b.xy + a.xy * b.zx + a.xyz * b.x, + a.r * b.zx - a.x * b.z + a.y * b.xyz + a.z * b.x + a.yz * b.xy + a.zx * b.r - a.xy * b.yz + a.xyz * b.y, + a.r * b.xy + a.x * b.y - a.y * b.x + a.z * b.xyz - a.yz * b.zx + a.zx * b.yz + a.xy * b.r + a.xyz * b.z ); + + public static float Dot( Multivector3 a, Multivector3 b ) => a.r * b.r + a.x * b.x + a.y * b.y + a.z * b.z + a.yz * b.yz + a.zx * b.zx + a.xy * b.xy + a.xyz * b.xyz; + + // addition + public static Multivector3 operator +( Multivector3 a, float b ) => new(a.r + b, a.v, a.b, a.t); + public static Multivector3 operator +( float a, Multivector3 b ) => b + a; + public static Multivector3 operator +( Multivector3 a, Vector3 b ) => new(a.r, a.v + b, a.b, a.t); + public static Multivector3 operator +( Vector3 a, Multivector3 b ) => b + a; + public static Multivector3 operator +( Multivector3 a, Bivector3 b ) => new(a.r, a.v, a.b + b, a.t); + public static Multivector3 operator +( Bivector3 a, Multivector3 b ) => b + a; + public static Multivector3 operator +( Multivector3 a, Trivector3 b ) => new(a.r, a.v, a.b, a.t + b); + public static Multivector3 operator +( Trivector3 a, Multivector3 b ) => b + a; + public static Multivector3 operator +( Multivector3 a, Multivector3 b ) => new(a.r + b.r, a.v + b.v, a.b + b.b, a.t + b.t); + + // combined + public static Multivector3 operator +( Multivector3 a, Rotor3 b ) => new(a.r + b.r, a.v, a.b + b.b, a.t); + public static Multivector3 operator +( Rotor3 a, Multivector3 b ) => b + a; + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Algebra/Multivector3.cs.meta b/Runtime/Geometric Algebra/Multivector3.cs.meta new file mode 100644 index 0000000..6feb424 --- /dev/null +++ b/Runtime/Geometric Algebra/Multivector3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae2bcddcbde33d74a83fa78413d60774 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Algebra/Rotor3.cs b/Runtime/Geometric Algebra/Rotor3.cs new file mode 100644 index 0000000..ceddb53 --- /dev/null +++ b/Runtime/Geometric Algebra/Rotor3.cs @@ -0,0 +1,158 @@ +using System; +using UnityEngine; + +namespace Freya { + + /// The even subalgebra of 3D VGA, isomorphic to Quaternions + public struct Rotor3 { + + public float r; + public Bivector3 b; + public float yz { + get => b.yz; + set => b.yz = value; + } + public float zx { + get => b.zx; + set => b.zx = value; + } + public float xy { + get => b.xy; + set => b.xy = value; + } + + public Rotor3( float r, float yz, float zx, float xy ) : this( r, new Bivector3( yz, zx, xy ) ) {} + + public Rotor3( float r, Bivector3 b ) { + this.r = r; + this.b = b; + } + + /// Creates a rotation representing twice the angle from a to b. + /// This is equivalent to multiplying the two vectors a*b. + /// Note: Assumes both input vectors are normalized + public static Rotor3 FromToRotationDouble( Vector3 a, Vector3 b ) => new Rotor3( Vector3.Dot( a, b ), Mathfs.Wedge( a, b ) ); + + /// Creates a rotation from a to b. Note: Assumes both input vectors are normalized + public static Rotor3 FromToRotation( Vector3 a, Vector3 b ) => new Rotor3( Vector3.Dot( a, b ) + 1, Mathfs.Wedge( a, b ) ).Normalized(); + + /// Constructs a unit rotor representing a rotation + public Rotor3( float angle, Vector3 axis ) { + Bivector3 axisDual = new Bivector3( axis.x, axis.y, axis.z ); + float halfAngle = angle / 2; + r = MathF.Cos( halfAngle ); + b = axisDual * MathF.Sin( halfAngle ); + } + + public float Magnitude => MathF.Sqrt( SqrMagnitude ); + public float SqrMagnitude => r * r + b.SqrMagnitude; + + public Rotor3 Normalized() => this / Magnitude; + + public Quaternion ToQuaternion() => new(yz, zx, xy, r); + + /// Negates the bivector, which is equivalent to reversing the rotation, + /// if this is normalized an interpreted as a rotation + public Rotor3 Conjugate => new Rotor3( r, -b ); + + /// Sandwich product, equivalent to ⭐(R* ⭐v R) (where R* is the conjugate of R and ⭐v is the hodge dual of v). + /// Commonly used to rotate vectors with unit rotors + /// The vector to multiply (or rotate) + public Vector3 Rotate( Vector3 v ) { + // hodge variant + Bivector3 vHodge = new(v.x, v.y, v.z); + return ( this.Conjugate * vHodge * this ).b.HodgeDual; + // // todo: does not work - this does R ⭐v R* instead of R* ⭐v R + // float r2 = r * r; + // float yz2 = yz * yz; + // float zx2 = zx * zx; + // float xy2 = xy * xy; + // float yzzx = yz * zx; + // float zxxy = zx * xy; + // float xyyz = xy * yz; + // float rxy = r * xy; + // float rzx = r * zx; + // float ryz = r * yz; + // + // return new Vector3( + // v.x * ( r2 + yz2 - zx2 - xy2 ) + // + 2 * v.y * ( yzzx + rxy ) + // + 2 * v.z * ( xyyz - rzx ), + // v.y * ( r2 - yz2 + zx2 - xy2 ) + // + 2 * v.x * ( yzzx - rxy ) + // + 2 * v.z * ( ryz + zxxy ), + // v.z * ( r2 - yz2 - zx2 + xy2 ) + // + 2 * v.x * ( rzx + xyyz ) + // + 2 * v.y * ( zxxy - ryz ) + // ); + } + + // multiplication + public static Rotor3 operator *( Rotor3 a, Rotor3 b ) { + return new Rotor3( + a.r * b.r - a.yz * b.yz - a.zx * b.zx - a.xy * b.xy, + a.r * b.yz + a.yz * b.r - a.zx * b.xy + a.xy * b.zx, + a.r * b.zx + a.yz * b.xy + a.zx * b.r - a.xy * b.yz, + a.r * b.xy - a.yz * b.zx + a.zx * b.yz + a.xy * b.r + ); + } + + public static Rotor3 operator *( Bivector3 b, Rotor3 r ) { + return new Rotor3( + -b.yz * r.yz - b.zx * r.zx - b.xy * r.xy, + +b.yz * r.r - b.zx * r.xy + b.xy * r.zx, + +b.yz * r.xy + b.zx * r.r - b.xy * r.yz, + -b.yz * r.zx + b.zx * r.yz + b.xy * r.r + ); + } + + public static Rotor3 operator *( Rotor3 a, Bivector3 b ) { + return new Rotor3( + a.yz * b.yz + a.zx * b.zx + a.xy * b.xy, + a.r * b.yz - a.zx * b.xy + a.xy * b.zx, + a.r * b.zx + a.yz * b.xy + -a.xy * b.yz, + a.r * b.xy - a.yz * b.zx + a.zx * b.yz + ); + } + + public static Multivector3 operator *( Rotor3 a, Vector3 b ) { + return new Multivector3( + 0, + a.r * b.x - a.zx * b.z + a.xy * b.y, + a.r * b.y + a.yz * b.z - a.xy * b.x, + a.r * b.z - a.yz * b.y + a.zx * b.x, + 0, + 0, + 0, + a.yz * b.x + a.zx * b.y + a.xy * b.z + ); + } + + public static Multivector3 operator *( Vector3 b, Rotor3 a ) { + return new Multivector3( + 0, + a.r * b.x - a.zx * b.z + a.xy * b.y, + a.r * b.y + a.yz * b.z - a.xy * b.x, + a.r * b.z - a.yz * b.y + a.zx * b.x, + 0, + 0, + 0, + a.yz * b.x + a.zx * b.y + a.xy * b.z + ); + } + + + public static Rotor3 operator /( Rotor3 a, float b ) { + return new Rotor3( a.r / b, a.yz / b, a.zx / b, a.xy / b ); + } + + // addition + public static Rotor3 operator +( Rotor3 a, float b ) => new Rotor3( a.r + b, a.b ); + public static Rotor3 operator +( float a, Rotor3 b ) => b + a; + public static Rotor3 operator +( Rotor3 a, Bivector3 b ) => new Rotor3( a.r, a.b + b ); + public static Rotor3 operator +( Bivector3 a, Rotor3 b ) => b + a; + public static Rotor3 operator +( Rotor3 a, Rotor3 b ) => new Rotor3( a.r + b.r, a.b + b.b ); + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Algebra/Rotor3.cs.meta b/Runtime/Geometric Algebra/Rotor3.cs.meta new file mode 100644 index 0000000..a93a430 --- /dev/null +++ b/Runtime/Geometric Algebra/Rotor3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eea02c7bcbb44ac4680152501f9ae6cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Algebra/Trivector3.cs b/Runtime/Geometric Algebra/Trivector3.cs new file mode 100644 index 0000000..c731fee --- /dev/null +++ b/Runtime/Geometric Algebra/Trivector3.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace Freya { + + public struct Trivector3 { + public static readonly Trivector3 zero = new Trivector3( 0 ); + public float xyz; + public Trivector3( float xyz ) => this.xyz = xyz; + public static Trivector3 operator +( Trivector3 a, Trivector3 b ) => new Trivector3( a.xyz + b.xyz ); + public static Trivector3 operator *( Trivector3 a, float b ) => new Trivector3( a.xyz * b ); + public static Trivector3 operator *( float a, Trivector3 b ) => b * a; + + public static Bivector3 operator *( Trivector3 a, Vector3 b ) => new Bivector3( a.xyz * b.x, a.xyz * b.y, a.xyz * b.z ); + public static Bivector3 operator *( Vector3 a, Trivector3 b ) => new Bivector3( a.x * b.xyz, a.y * b.xyz, a.z * b.xyz ); + public static Vector3 operator *( Bivector3 a, Trivector3 b ) => new Vector3( -a.yz * b.xyz, -a.zx * b.xyz, -a.xy * b.xyz ); + public static Vector3 operator *( Trivector3 a, Bivector3 b ) => new Vector3( -a.xyz * b.yz, -a.xyz * b.zx, -a.xyz * b.xy ); + + public static float operator *( Trivector3 a, Trivector3 b ) => -a.xyz * b.xyz; + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Algebra/Trivector3.cs.meta b/Runtime/Geometric Algebra/Trivector3.cs.meta new file mode 100644 index 0000000..519a45a --- /dev/null +++ b/Runtime/Geometric Algebra/Trivector3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2506c44a177d8b3418ba2b081ffefeff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes.meta b/Runtime/Geometric Shapes.meta new file mode 100644 index 0000000..e691a22 --- /dev/null +++ b/Runtime/Geometric Shapes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38fb77b76c22f4e9da81e718f7f273ef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Box.cs b/Runtime/Geometric Shapes/Box.cs similarity index 98% rename from Geometric Shapes/Box.cs rename to Runtime/Geometric Shapes/Box.cs index 19a56fb..aa61841 100644 --- a/Geometric Shapes/Box.cs +++ b/Runtime/Geometric Shapes/Box.cs @@ -184,12 +184,12 @@ public float SurfaceArea { public partial struct Box2D { /// Returns whether or not a point is inside this box /// The point to test if it's inside - [MethodImpl( INLINE )] public bool Contains( Vector2 point ) => Abs( point.x ) - extents.x <= 0 && Abs( point.y ) - extents.y <= 0; + [MethodImpl( INLINE )] public bool Contains( Vector2 point ) => Abs( point.x - center.x ) - extents.x <= 0 && Abs( point.y - center.y ) - extents.y <= 0; } public partial struct Box3D { /// - [MethodImpl( INLINE )] public bool Contains( Vector3 point ) => Abs( point.x ) - extents.x <= 0 && Abs( point.y ) - extents.y <= 0 && Abs( point.z ) - extents.z <= 0; + [MethodImpl( INLINE )] public bool Contains( Vector3 point ) => Abs( point.x - center.x ) - extents.x <= 0 && Abs( point.y - center.y ) - extents.y <= 0 && Abs( point.z - center.z ) - extents.z <= 0; } #endregion diff --git a/Runtime/Geometric Shapes/Box.cs.meta b/Runtime/Geometric Shapes/Box.cs.meta new file mode 100644 index 0000000..9149437 --- /dev/null +++ b/Runtime/Geometric Shapes/Box.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53d1cb856f6234d75b97c79eaeeb9851 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Circle.cs b/Runtime/Geometric Shapes/Circle.cs similarity index 95% rename from Geometric Shapes/Circle.cs rename to Runtime/Geometric Shapes/Circle.cs index 1d419f3..66cbcfe 100644 --- a/Geometric Shapes/Circle.cs +++ b/Runtime/Geometric Shapes/Circle.cs @@ -1,5 +1,6 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +using System; using System.Runtime.CompilerServices; using UnityEngine; using static Freya.Mathfs; @@ -183,7 +184,7 @@ public static bool FromThreePoints( Vector3 a, Vector3 b, Vector3 c, out Circle3 if( Circle2D.FromThreePoints( default, b2D, c2D, out Circle2D circle2D ) ) { Vector3 origin = xAxis * circle2D.center.x + yAxis * circle2D.center.y; - circle = new Circle3D( origin, normal, circle2D.radius ); + circle = new Circle3D( a + origin, normal, circle2D.radius ); return true; } @@ -226,10 +227,10 @@ public static bool FromPointTangentPoint( Vector3 startPt, Vector3 startTangent, if( Vector3.Dot( xAxis, startTangent ).Abs() < 0.9999f ) { float h = d / 2; float ang = AngleBetween( xAxis, startTangent ); - float fh = h * Mathf.Tan( ang + TAU / 4 ); + float fh = h * MathF.Tan( ang + TAU / 4 ); float x2D = h; float y2D = fh; - float r = Mathf.Sqrt( h * h + fh * fh ); + float r = MathF.Sqrt( h * h + fh * fh ); Vector3 normal = Vector3.Cross( xAxis, startTangent ).normalized; Vector3 yAxis = Vector3.Cross( normal, xAxis ); Vector3 center = startPt + xAxis * x2D + yAxis * y2D; @@ -308,14 +309,17 @@ public partial struct Circle2D { return new Circle2D( point + normal * signedRadius, Abs( signedRadius ) ); } + public static Circle2D operator *( Circle2D circle, float value ) => new(circle.center * value, circle.radius * value); + public static Circle2D operator *( float value, Circle2D circle ) => new(circle.center * value, circle.radius * value); + } public partial struct Circle3D { /// public static Circle3D GetOsculatingCircle( Vector3 point, Vector3 velocity, Vector3 acceleration ) { - Vector3 curvatureVector = GetCurvature( velocity, acceleration ); - ( Vector3 axis, float curvature ) = curvatureVector.GetDirAndMagnitude(); + Bivector3 curvatureBivector = GetCurvature( velocity, acceleration ); + ( Vector3 axis, float curvature ) = curvatureBivector.GetNormalAndArea(); Vector3 normal = Vector3.Cross( velocity, Vector3.Cross( acceleration, velocity ) ).normalized; float signedRadius = 1f / curvature; return new Circle3D( point + normal * signedRadius, axis, Abs( signedRadius ) ); diff --git a/Runtime/Geometric Shapes/Circle.cs.meta b/Runtime/Geometric Shapes/Circle.cs.meta new file mode 100644 index 0000000..376abac --- /dev/null +++ b/Runtime/Geometric Shapes/Circle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02ae58e19ebdf4a009bb3e77ee7873c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/ILinear2D.cs b/Runtime/Geometric Shapes/ILinear2D.cs similarity index 84% rename from Geometric Shapes/ILinear2D.cs rename to Runtime/Geometric Shapes/ILinear2D.cs index ad88058..fea1811 100644 --- a/Geometric Shapes/ILinear2D.cs +++ b/Runtime/Geometric Shapes/ILinear2D.cs @@ -1,5 +1,6 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +using System; using System.Runtime.CompilerServices; using UnityEngine; @@ -30,31 +31,27 @@ public static class ExtILinear2D { /// Gets a point along this line /// The linear object to get a point along (Ray2D, Line2D or LineSegment2D) /// The t-value along the ray to get the point of. If the ray direction is normalized, t is equivalent to distance - [MethodImpl( INLINE )] public static Vector2 GetPoint( this T linear, float t ) where T : ILinear2D { - return linear.Origin + linear.Dir * t; - } + [MethodImpl( INLINE )] public static Vector2 GetPoint( this T linear, float t ) where T : ILinear2D => linear.Origin + linear.Dir * t; /// Returns the t-value of a point projected onto this line /// The linear object to project onto (Ray2D, Line2D or LineSegment2D) /// The point to use when projecting - [MethodImpl( INLINE )] public static float ProjectPointTValue( this T linear, Vector2 point ) where T : ILinear2D { - float t = Line2D.ProjectPointToLineTValue( linear.Origin, linear.Dir, point ); - return linear.ClampTValue( t ); - } + [MethodImpl( INLINE )] public static float ProjectPointTValue( this T linear, Vector2 point ) where T : ILinear2D => linear.ClampTValue( Line2D.ProjectPointToLineTValue( linear.Origin, linear.Dir, point ) ); /// Projects a point onto this line /// The linear object to project onto (Ray2D, Line2D or LineSegment2D) /// The point to project - [MethodImpl( INLINE )] public static Vector2 ProjectPoint( this T linear, Vector2 point ) where T : ILinear2D { - return linear.GetPoint( linear.ProjectPointTValue( point ) ); - } + [MethodImpl( INLINE )] public static Vector2 ProjectPoint( this T linear, Vector2 point ) where T : ILinear2D => linear.GetPoint( linear.ProjectPointTValue( point ) ); /// The shortest distance from this line to a point /// The linear object to check distance from (Ray2D, Line2D or LineSegment2D) /// The point to check the distance to - [MethodImpl( INLINE )] public static float Distance( this T linear, Vector2 point ) where T : ILinear2D { - return Vector2.Distance( point, linear.ProjectPoint( point ) ); - } + [MethodImpl( INLINE )] public static float Distance( this T linear, Vector2 point ) where T : ILinear2D => MathF.Sqrt( DistanceSqr( linear, point ) ); + + /// The shortest squared distance from this line to a point + /// The linear object to check distance from (Ray2D, Line2D or LineSegment2D) + /// The point to check the distance to + [MethodImpl( INLINE )] public static float DistanceSqr( this T linear, Vector2 point ) where T : ILinear2D => ( point - linear.ProjectPoint( point ) ).sqrMagnitude; #region Intersection Tests diff --git a/Runtime/Geometric Shapes/ILinear2D.cs.meta b/Runtime/Geometric Shapes/ILinear2D.cs.meta new file mode 100644 index 0000000..7b490da --- /dev/null +++ b/Runtime/Geometric Shapes/ILinear2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51f75430748c44d949919836cbc44e75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/ILinear3D.cs b/Runtime/Geometric Shapes/ILinear3D.cs new file mode 100644 index 0000000..d481f93 --- /dev/null +++ b/Runtime/Geometric Shapes/ILinear3D.cs @@ -0,0 +1,76 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// A shared interface between Ray3D, Line3D and LineSegment3D + public interface ILinear3D { + + /// The origin of this linear 3D object (Ray3D, Line3D or LineSegment3D) + Vector3 Origin { get; } + + /// The direction of this linear 3D object (Ray3D, Line3D or LineSegment3D). Note: this vector may or may not be normalized + Vector3 Dir { get; } + + /// Returns whether or not this t-value is within this linear 3D object (Ray3D, Line3D or LineSegment3D) + /// The t-value along the linear 3D object + bool IsValidTValue( float t ); + + /// Clamps the value into the range of this linear 3D object (Ray3D, Line3D or LineSegment3D) + /// The t-value along the linear 3D object + float ClampTValue( float t ); + } + + public static class ExtILinear3D { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// Gets a point along this line + /// The linear object to get a point along (Ray3D, Line3D or LineSegment3D) + /// The t-value along the ray to get the point of. If the ray direction is normalized, t is equivalent to distance + [MethodImpl( INLINE )] public static Vector3 GetPoint( this T linear, float t ) where T : ILinear3D => linear.Origin + linear.Dir * t; + + /// Returns the t-value of a point projected onto this line + /// The linear object to project onto (Ray3D, Line3D or LineSegment3D) + /// The point to use when projecting + [MethodImpl( INLINE )] public static float ProjectPointTValue( this T linear, Vector3 point ) where T : ILinear3D => linear.ClampTValue( Line3D.ProjectPointToLineTValue( linear.Origin, linear.Dir, point ) ); + + /// The t-values at the shortest squared distance between two linear objects + /// The linear object to check distance from (Ray3D, Line3D or LineSegment3D) + /// The other linear object to check the distance to + [MethodImpl( INLINE )] public static (float,float) LinearTValues( this A linear, B other ) where A : ILinear3D where B : ILinear3D { + ( float tA, float tB ) = Line3D.ClosestPointBetweenLinesTValues( linear.Origin, linear.Dir, other.Origin, other.Dir ); + tA = linear.ClampTValue( tA ); + tB = other.ClampTValue( tB ); + return ( tA, tB ); + } + + /// Projects a point onto this line + /// The linear object to project onto (Ray3D, Line3D or LineSegment3D) + /// The point to project + [MethodImpl( INLINE )] public static Vector3 ProjectPoint( this T linear, Vector3 point ) where T : ILinear3D => linear.GetPoint( linear.ProjectPointTValue( point ) ); + + /// The shortest distance from this line to a point + /// The linear object to check distance from (Ray3D, Line3D or LineSegment3D) + /// The point to check the distance to + [MethodImpl( INLINE )] public static float Distance( this T linear, Vector3 point ) where T : ILinear3D => MathF.Sqrt( DistanceSqr( linear, point ) ); + + /// The shortest squared distance from this line to a point + /// The linear object to check distance from (Ray3D, Line3D or LineSegment3D) + /// The point to check the distance to + [MethodImpl( INLINE )] public static float DistanceSqr( this T linear, Vector3 point ) where T : ILinear3D => ( point - linear.ProjectPoint( point ) ).sqrMagnitude; + + /// The shortest squared distance from this line to another line + /// The linear object to check distance from (Ray3D, Line3D or LineSegment3D) + /// The other linear object to check the distance to + [MethodImpl( INLINE )] public static float DistanceSqr( this A linear, B other ) where A : ILinear3D where B : ILinear3D { + ( float tA, float tB ) = LinearTValues( linear, other ); + return ( linear.GetPoint( tA ) - other.GetPoint( tB ) ).sqrMagnitude; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/ILinear3D.cs.meta b/Runtime/Geometric Shapes/ILinear3D.cs.meta new file mode 100644 index 0000000..735a3a3 --- /dev/null +++ b/Runtime/Geometric Shapes/ILinear3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35cc3b65d9cb6e84e99bed822ec6fe09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Line2D.cs b/Runtime/Geometric Shapes/Line2D.cs similarity index 92% rename from Geometric Shapes/Line2D.cs rename to Runtime/Geometric Shapes/Line2D.cs index deca79d..4634352 100644 --- a/Geometric Shapes/Line2D.cs +++ b/Runtime/Geometric Shapes/Line2D.cs @@ -27,6 +27,10 @@ namespace Freya { /// The direction of the line. It does not have to be normalized, but if it is, the t-value when sampling will correspond to distance along the ray public Line2D( Vector2 origin, Vector2 dir ) => ( this.origin, this.dir ) = ( origin, dir ); + /// The signed distance from this line to a point. Points to the left of this line are positive + /// The point to check the signed distance to + [MethodImpl( INLINE )] public float SignedDistance( Vector2 point ) => Determinant( dir.normalized, point - origin ); + #region Interface stuff for generic line tests [MethodImpl( INLINE )] bool ILinear2D.IsValidTValue( float t ) => true; // just always valid uwu diff --git a/Runtime/Geometric Shapes/Line2D.cs.meta b/Runtime/Geometric Shapes/Line2D.cs.meta new file mode 100644 index 0000000..1380d9d --- /dev/null +++ b/Runtime/Geometric Shapes/Line2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a212ccfc9b024c46bf134163234992a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/Line3D.cs b/Runtime/Geometric Shapes/Line3D.cs new file mode 100644 index 0000000..7b7aa12 --- /dev/null +++ b/Runtime/Geometric Shapes/Line3D.cs @@ -0,0 +1,118 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +using static Freya.Mathfs; + +namespace Freya { + + // 3D line math + /// A structure representing an infinitely long 3D line + [Serializable] public struct Line3D : ILinear3D { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// The origin of this line + public Vector3 origin; + + /// The direction of the ray. Note: Line3D allows non-normalized direction vectors + public Vector3 dir; + + /// Returns a normalized version of this line. Normalized lines ensure t-values correspond to distance + public Line3D Normalized => new Line3D( origin, dir ); + + /// Creates an infinitely long 3D line, given an origin and a direction + /// The origin of the line + /// The direction of the line. It does not have to be normalized, but if it is, the t-value when sampling will correspond to distance along the ray + public Line3D( Vector3 origin, Vector3 dir ) => ( this.origin, this.dir ) = ( origin, dir ); + + /// The signed distance from this line to a point. Points to the left of this line are positive + /// The point to check the signed distance to + [MethodImpl( INLINE )] public float SignedDistance( Vector3 point ) => Determinant( dir.normalized, point - origin ); + + #region Interface stuff for generic line tests + + [MethodImpl( INLINE )] bool ILinear3D.IsValidTValue( float t ) => true; // just always valid uwu + [MethodImpl( INLINE )] float ILinear3D.ClampTValue( float t ) => t; // :) + Vector3 ILinear3D.Origin { + [MethodImpl( INLINE )] get => origin; + } + Vector3 ILinear3D.Dir { + [MethodImpl( INLINE )] get => dir; + } + + #endregion + + #region Statics (general linear 3D methods) + + /// Projects a point onto an infinite line, returning the t-value along the line + /// Line origin + /// Line direction (does not have to be normalized) + /// The point to project onto the line + [MethodImpl( INLINE )] public static float ProjectPointToLineTValue( Vector3 lineOrigin, Vector3 lineDir, Vector3 point ) { + return Vector3.Dot( lineDir, point - lineOrigin ) / Vector3.Dot( lineDir, lineDir ); + } + + /// Gets the t-values of the closest point between two infinite lines, returning the two t-values along the line + /// Line A origin + /// Line A direction (does not have to be normalized) + /// Line B origin + /// Line B direction (does not have to be normalized) + [MethodImpl( INLINE )] public static (float tA, float tB) ClosestPointBetweenLinesTValues( Vector3 aOrigin, Vector3 aDir, Vector3 bOrigin, Vector3 bDir ) { + // source: https://math.stackexchange.com/questions/2213165/find-shortest-distance-between-lines-in-3d + Vector3 a = aOrigin; + Vector3 b = aDir; + Vector3 c = bOrigin; + Vector3 d = bDir; + Vector3 e = a - c; + float be = Vector3.Dot( b, e ); + float de = Vector3.Dot( d, e ); + float bd = Vector3.Dot( b, d ); + float b2 = Vector3.Dot( b, b ); + float d2 = Vector3.Dot( d, d ); + float A = -b2 * d2 + bd * bd; + + float s = ( -b2 * de + be * bd ) / A; + float t = ( d2 * be - de * bd ) / A; + + return ( t, s ); + + // Vector3 n = Vector3.Cross( aDir, bDir ); + // float nMag = n.magnitude; + // float dist = Vector3.Dot( n, aOrigin - bOrigin ) / nMag; + } + + /// Projects a point onto an infinite line + /// Line origin + /// Line direction (does not have to be normalized) + /// The point to project onto the line + [MethodImpl( INLINE )] public static Vector3 ProjectPointToLine( Vector3 lineOrigin, Vector3 lineDir, Vector3 point ) { + return lineOrigin + lineDir * ProjectPointToLineTValue( lineOrigin, lineDir, point ); + } + + /// Projects a point onto an infinite line + /// Line to project onto + /// The point to project onto the line + [MethodImpl( INLINE )] public static Vector3 ProjectPointToLine( Line3D line, Vector3 point ) => ProjectPointToLine( line.origin, line.dir, point ); + + /// Returns the signed distance to a 3D plane + /// Plane origin + /// Plane normal (has to be normalized for a true distance) + /// The point to use when checking distance to the plane + [MethodImpl( INLINE )] public static float PointToPlaneSignedDistance( Vector3 planeOrigin, Vector3 planeNormal, Vector3 point ) { + return Vector3.Dot( point - planeOrigin, planeNormal ); + } + + /// Returns the distance to a 3D plane + /// Plane origin + /// Plane normal (has to be normalized for a true distance) + /// The point to use when checking distance to the plane + [MethodImpl( INLINE )] public static float PointToPlaneDistance( Vector3 planeOrigin, Vector3 planeNormal, Vector3 point ) => Abs( PointToPlaneSignedDistance( planeOrigin, planeNormal, point ) ); + + #endregion + + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/Line3D.cs.meta b/Runtime/Geometric Shapes/Line3D.cs.meta new file mode 100644 index 0000000..8f0fd07 --- /dev/null +++ b/Runtime/Geometric Shapes/Line3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6a2c5d66a5446344b60a60f51987f49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/LineSegment2D.cs b/Runtime/Geometric Shapes/LineSegment2D.cs similarity index 100% rename from Geometric Shapes/LineSegment2D.cs rename to Runtime/Geometric Shapes/LineSegment2D.cs diff --git a/Runtime/Geometric Shapes/LineSegment2D.cs.meta b/Runtime/Geometric Shapes/LineSegment2D.cs.meta new file mode 100644 index 0000000..b4c2838 --- /dev/null +++ b/Runtime/Geometric Shapes/LineSegment2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b97d865bcd2f448eae8e4a1089aa6ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/LineSegment3D.cs b/Runtime/Geometric Shapes/LineSegment3D.cs new file mode 100644 index 0000000..fd42510 --- /dev/null +++ b/Runtime/Geometric Shapes/LineSegment3D.cs @@ -0,0 +1,81 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +using Plane = System.Numerics.Plane; + +namespace Freya { + + /// Represents a line segment, similar to a line but with a defined start and end + [Serializable] public struct LineSegment3D : ILinear3D { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// The start point of the line segment + public Vector3 start; + + /// The end point of the line segment + public Vector3 end; + + /// Creates a line segment with a defined start and end point + /// The start point of the line segment + /// The end point of the line segment + public LineSegment3D( Vector3 start, Vector3 end ) => ( this.start, this.end ) = ( start, end ); + + /// Returns the displacement vector from start to end of this line. Equivalent to end-start + public Vector3 Displacement { + [MethodImpl( INLINE )] get => end - start; + } + + /// Returns the normalized direction of this line. Equivalent to (end-start).normalized + public Vector3 Direction { + [MethodImpl( INLINE )] get => Displacement.normalized; + } + + /// Calculates the length of the line segment + public float Length { + [MethodImpl( INLINE )] get { + float dx = end.x - start.x; + float dy = end.y - start.y; + float dz = end.z - start.z; + return (float)Math.Sqrt( dx * dx + dy * dy + dz * dz ); + } + } + + /// Calculates the length squared (faster than calculating the actual length) + public float LengthSquared { + [MethodImpl( INLINE )] get { + float dx = end.x - start.x; + float dy = end.y - start.y; + float dz = end.z - start.z; + return dx * dx + dy * dy + dz * dz; + } + } + + /// Returns the perpendicular bisector. Note: the returned normal is not normalized to save performance. Use Bisector.Normalized if you want to make sure it is normalized + public Plane3D Bisector { + [MethodImpl( INLINE )] get => GetBisector( start, end ); + } + + /// Returns the perpendicular bisector of the input line segment + /// Starting point of the line segment + /// Endpoint of the line segment + [MethodImpl( INLINE )] public static Plane3D GetBisector( Vector3 startPoint, Vector3 endPoint ) => new Plane3D( ( endPoint - startPoint ).normalized, ( endPoint + startPoint ) / 2 ); + + #region Interface stuff for generic line tests + + [MethodImpl( INLINE )] bool ILinear3D.IsValidTValue( float t ) => t >= 0 && t <= 1; + [MethodImpl( INLINE )] float ILinear3D.ClampTValue( float t ) => t < 0 ? 0 : t > 1 ? 1 : t; + Vector3 ILinear3D.Origin { + [MethodImpl( INLINE )] get => start; + } + Vector3 ILinear3D.Dir { + [MethodImpl( INLINE )] get => end - start; + } + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/LineSegment3D.cs.meta b/Runtime/Geometric Shapes/LineSegment3D.cs.meta new file mode 100644 index 0000000..8c8eef6 --- /dev/null +++ b/Runtime/Geometric Shapes/LineSegment3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 906a5dc9fe0ac30479250ca70e09d037 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/Plane2DIn3D.cs b/Runtime/Geometric Shapes/Plane2DIn3D.cs new file mode 100644 index 0000000..7633af4 --- /dev/null +++ b/Runtime/Geometric Shapes/Plane2DIn3D.cs @@ -0,0 +1,75 @@ +using UnityEngine; + +namespace Freya { + + /// An oriented 2D plane embedded in 3D space + public struct Plane2DIn3D { + + public static readonly Plane2DIn3D XY = new(default, Vector3.right, Vector3.up); + public static readonly Plane2DIn3D YZ = new(default, Vector3.up, Vector3.forward); + public static readonly Plane2DIn3D ZX = new(default, Vector3.forward, Vector3.right); + + public Vector3 origin, axisX, axisY; + + /// Creates an oriented 2D plane embedded in 3D space + /// The origin of the plane + /// The x axis direction of the plane + /// The y axis direction of the plane + public Plane2DIn3D( Vector3 origin, Vector3 axisX, Vector3 axisY ) => ( this.origin, this.axisX, this.axisY ) = ( origin, axisX.normalized, axisY.normalized ); + + /// Rotates this plane around the Y axis, setting the X axis, + /// so that the given point p is in the plane where x > 0 + /// The point to include in the plane + /// The included point in the 2D local space + public void RotateAroundYToInclude( Vector3 p, out Vector2 pLocal ) { + Vector3 pRel = p - origin; + float yProj = Vector3.Dot( axisY, pRel ); + axisX = ( pRel - axisY * yProj ).normalized; + float xProj = Vector3.Dot( axisX, pRel ); + pLocal = new Vector2( xProj, yProj ); + } + + /// Transforms a local 2D point to a 3D world space point + /// The local space point to transform + public Vector3 TransformPoint( Vector2 pt ) { + return new( // unrolled for performance + origin.x + axisX.x * pt.x + axisY.x * pt.y, + origin.y + axisX.y * pt.x + axisY.y * pt.y, + origin.z + axisX.z * pt.x + axisY.z * pt.y + ); + } + + /// Transforms a local 2D vector to a 3D world space vector, not taking position into account + /// The local space vector to transform + public Vector3 TransformVector( Vector2 vec ) { + return new( // unrolled for performance + axisX.x * vec.x + axisY.x * vec.y, + axisX.y * vec.x + axisY.y * vec.y, + axisX.z * vec.x + axisY.z * vec.y + ); + } + + /// Transform a 3D world space point to a local 2D point + /// World space point + public Vector2 InverseTransformPoint( Vector3 pt ) { + float rx = pt.x - origin.x; + float ry = pt.y - origin.y; + float rz = pt.z - origin.z; + return new( + axisX.x * rx + axisX.y * ry + axisX.z * rz, + axisY.x * rx + axisY.y * ry + axisY.z * rz + ); + } + + /// Transform a 3D world space vector to a local 2D vector + /// World space vector + public Vector2 InverseTransformVector( Vector3 vec ) { + return new( + axisX.x * vec.x + axisX.y * vec.y + axisX.z * vec.z, + axisY.x * vec.x + axisY.y * vec.y + axisY.z * vec.z + ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/Plane2DIn3D.cs.meta b/Runtime/Geometric Shapes/Plane2DIn3D.cs.meta new file mode 100644 index 0000000..828d594 --- /dev/null +++ b/Runtime/Geometric Shapes/Plane2DIn3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f19b780d08262e46bd6fae1ce9f7275 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/Plane3D.cs b/Runtime/Geometric Shapes/Plane3D.cs new file mode 100644 index 0000000..4ace889 --- /dev/null +++ b/Runtime/Geometric Shapes/Plane3D.cs @@ -0,0 +1,35 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using UnityEngine; + +namespace Freya { + + /// A mathematical plane in 3D space + public struct Plane3D { + + /// The normal of the plane. Note that this type lets you assign non-normalized vectors + public Vector3 normal; + + /// The signed distance from the world origin + public float distance; + + public Vector3 PointClosestToOrigin => normal * distance; + + public Plane3D( Vector3 normal, float distance ) { + this.normal = normal; + this.distance = distance; + } + + public Plane3D( Vector3 normal, Vector3 point ) { + this.normal = normal; + this.distance = Vector3.Dot( normal, point ); + } + + // public static Line3D Intersect( Plane3D a, Plane3D b ) { + // return default; + // } + + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/Plane3D.cs.meta b/Runtime/Geometric Shapes/Plane3D.cs.meta new file mode 100644 index 0000000..ed6e984 --- /dev/null +++ b/Runtime/Geometric Shapes/Plane3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 449dff60f3928394abea47bcd9791a76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Polygon.cs b/Runtime/Geometric Shapes/Polygon.cs similarity index 52% rename from Geometric Shapes/Polygon.cs rename to Runtime/Geometric Shapes/Polygon.cs index f7bbb26..1b37823 100644 --- a/Geometric Shapes/Polygon.cs +++ b/Runtime/Geometric Shapes/Polygon.cs @@ -1,5 +1,6 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +using System; using System.Collections.Generic; using UnityEngine; using static Freya.Mathfs; @@ -7,7 +8,7 @@ namespace Freya { /// Polygon with various math functions to test if a point is inside, calculate area, etc. - public struct Polygon { + public class Polygon { /// The points in this polygon public IReadOnlyList points; @@ -16,11 +17,18 @@ public struct Polygon { /// The points in the polygon public Polygon( IReadOnlyList points ) => this.points = points; + /// Get a point by index. Indices cannot be out of range, as they will wrap/cycle in the polygon + /// The index of the point + public Vector2 this[ int i ] => points[i.Mod( Count )]; + + /// The number of points in this polygon + public int Count => points.Count; + /// Returns whether or not this polygon is defined clockwise public bool IsClockwise => SignedArea > 0; /// Returns the area of this polygon - public float Area => Mathf.Abs( SignedArea ); + public float Area => MathF.Abs( SignedArea ); /// Returns the signed area of this polygon public float SignedArea { @@ -47,7 +55,7 @@ public float Perimeter { Vector2 b = points[( i + 1 ) % count]; float dx = a.x - b.x; float dy = a.y - b.y; - totalDist += Mathf.Sqrt( dx * dx + dy * dy ); // unrolled for speed + totalDist += MathF.Sqrt( dx * dx + dy * dy ); // unrolled for speed } return totalDist; @@ -62,10 +70,10 @@ public Rect Bounds { float xMin = p.x, xMax = p.x, yMin = p.y, yMax = p.y; for( int i = 1; i < count; i++ ) { p = points[i]; - xMin = Mathf.Min( xMin, p.x ); - xMax = Mathf.Max( xMax, p.x ); - yMin = Mathf.Min( yMin, p.y ); - yMax = Mathf.Max( yMax, p.y ); + xMin = MathF.Min( xMin, p.x ); + xMax = MathF.Max( xMax, p.x ); + yMin = MathF.Min( yMin, p.y ); + yMax = MathF.Max( yMax, p.y ); } return new Rect( xMin, yMin, xMax - xMin, yMax - yMin ); @@ -79,7 +87,7 @@ public Rect Bounds { // modified version of the code from here: // http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm // Copyright 2000 softSurfer, 2012 Dan Sunday. This code may be freely used and modified for any purpose providing that this copyright notice is included with it. SoftSurfer makes no warranty for this code, and cannot be held liable for any real or imagined damage resulting from its use. Users of this code must verify correctness for their application. - /// Returns the winding number for this polygon, given a point + /// Returns the winding number for this polygon, around a given point /// The point to check winding around public int WindingNumber( Vector2 point ) { int winding = 0; @@ -99,6 +107,70 @@ public int WindingNumber( Vector2 point ) { return winding; } + + /// Returns the resulting polygons when clipping this polygon by a line + /// The line/plane to clip by. Points on its left side will be kept + /// The resulting array of clipped polygons (if any) + public PolygonClipper.ResultState Clip( Line2D line, out List clippedPolygons ) => PolygonClipper.Clip( this, line, out clippedPolygons ); + + public Polygon GetMiterPolygon( float offset ) { + List miterPts = new List(); + + Line2D GetMiterLine( int i ) { + Vector2 tangent = ( this[i + 1] - this[i] ).normalized; + Vector2 normal = tangent.Rotate90CCW(); + return new Line2D( this[i] + normal * offset, tangent ); + } + + // Line2D prev = GetMiterLine( -1 ); + for( int i = 0; i < Count; i++ ) { + Line2D line = GetMiterLine( i ); + Line2D line2 = GetMiterLine( i + 1 ); + if( line.Intersect( line2, out Vector2 pt ) ) + miterPts.Add( pt ); + else { + Debug.LogError( $"{line.origin},{line.dir}\n{line2.origin},{line2.dir}\nPoints:{string.Join( '\n', points )}" ); + throw new Exception( "Line intersection failed" ); + } + // prev = line; + } + + return new Polygon( miterPts ); + } + + // from: https://en.wikipedia.org/wiki/Centroid + /// The centroid of this polygon, also known as the center of mass + public Vector2 Centroid { + get { + Vector2 centroid = Vector2.zero; + float signedArea = 0; + for( int i = 0; i < Count; i++ ) { + Vector2 a = points[i]; + Vector2 b = points[( i + 1 ) % Count]; + float det = a.x * b.y - b.x * a.y; + signedArea += det; + centroid.x += ( a.x + b.x ) * det; + centroid.y += ( b.y + a.y ) * det; + } + return centroid / ( 3 * signedArea ); + } + } + + public Vector2 WeightedEdgeCenter { + get { + Vector2 eCenter = Vector2.zero; + float totalLength = 0; + for( int i = 0; i < Count; i++ ) { + Vector2 a = points[i]; + Vector2 b = points[( i + 1 ) % Count]; + float length = Vector2.Distance( a, b ); + totalLength += length; + eCenter += ( a + b ) * length; + } + return eCenter / ( 2 * totalLength ); + } + } + } } \ No newline at end of file diff --git a/Runtime/Geometric Shapes/Polygon.cs.meta b/Runtime/Geometric Shapes/Polygon.cs.meta new file mode 100644 index 0000000..0183321 --- /dev/null +++ b/Runtime/Geometric Shapes/Polygon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14a0a8a5c63dc4b35848977e18cb4a0d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/PolygonClipper.cs b/Runtime/Geometric Shapes/PolygonClipper.cs new file mode 100644 index 0000000..d9debb4 --- /dev/null +++ b/Runtime/Geometric Shapes/PolygonClipper.cs @@ -0,0 +1,158 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Freya { + + /// Utility type to clip polygons + public static class PolygonClipper { + + enum PointSideState { + Discard = -1, + Edge = 0, + Keep = 1, + Handled = 2 + } + + public enum ResultState { + OriginalLeftIntact, + Clipped, + FullyDiscarded + } + + public class PolygonSection : IComparable { + public FloatRange tRange; + public List points; + public PolygonSection( FloatRange tRange, List points ) => ( this.tRange, this.points ) = ( tRange, points ); + public int CompareTo( PolygonSection other ) => tRange.Min.CompareTo( other.tRange.Min ); + } + + static List states = new List(); + + public static ResultState Clip( Polygon poly, Line2D line, out List clippedPolygons ) { + states.Clear(); + + // first, figure out which side all points are on + bool hasDiscards = false; + int startIndex = -1; + for( int i = 0; i < poly.Count; i++ ) { + float sd = line.SignedDistance( poly[i] ); + if( Mathfs.Approximately( sd, 0 ) ) + states.Add( PointSideState.Edge ); + else if( sd > 0 ) { + if( startIndex == -1 ) + startIndex = i; + states.Add( PointSideState.Keep ); + } else { + hasDiscards = true; + states.Add( PointSideState.Discard ); + } + } + + if( hasDiscards == false ) { + clippedPolygons = null; + return ResultState.OriginalLeftIntact; + } + + if( startIndex == -1 ) { + clippedPolygons = null; + return ResultState.FullyDiscarded; + } + + // find keep points, spread outwards until it's cut off from the rest + SortedSet sections = new SortedSet(); + for( int i = 0; i < poly.Count; i++ ) { + if( states[i] == PointSideState.Keep ) { + sections.Add( ExtractPolygonSection( poly, line, i ) ); + } + } + + // combine all clipped polygonal regions + clippedPolygons = new List(); + while( sections.Count > 0 ) { + // find solid polygon + PolygonSection solid = sections.First(); + sections.Remove( solid ); + int solidDir = solid.tRange.Direction; + + // find holes in that polygon + float referencePoint = solid.tRange.Min; + while( true ) { // should break early anyway + FloatRange checkRange = new FloatRange( referencePoint, solid.tRange.Max ); + PolygonSection hole = sections.FirstOrDefault( s => s.tRange.Direction != solidDir && checkRange.Contains( s.tRange ) ); + if( hole == null ) { + // nothing inside - we're done with this solid + clippedPolygons.Add( new Polygon( solid.points ) ); + break; + } else { + // append the hole polygon to the solid points + sections.Remove( hole ); + if( solidDir == 1 ) + solid.points.InsertRange( 0, hole.points ); + else + solid.points.AddRange( hole.points ); + + referencePoint = hole.tRange.Max; // skip everything inside the hole by shifting forward + } + } + } + + return ResultState.Clipped; + } + + static PolygonSection ExtractPolygonSection( Polygon poly, Line2D line, int sourceIndex ) { + List points = new List(); + + void AddBack( int i ) { + states[i.Mod( states.Count )] = PointSideState.Handled; + points.Insert( 0, poly[i] ); + } + + void AddFront( int i ) { + states[i.Mod( states.Count )] = PointSideState.Handled; + points.Add( poly[i] ); + } + + AddFront( sourceIndex ); + + float tStart = 0, tEnd = 0; + for( int dir = -1; dir <= 1; dir += 2 ) { + for( int i = 1; i < poly.Count; i++ ) { + int index = sourceIndex + dir * i; + if( states[index.Mod( states.Count )] is PointSideState.Discard or PointSideState.Edge ) { + // hit the front edge + Line2D edge = new Line2D( poly[index - dir], poly[index] - poly[index - dir] ); + if( IntersectionTest.LinearTValues( line, edge, out float tLine, out float tEdge ) ) { + Vector2 intPt = edge.GetPoint( tEdge ); + if( dir == 1 ) { + tEnd = tLine; + points.Add( intPt ); + } else { + tStart = tLine; + points.Insert( 0, intPt ); + } + + break; + } + + throw new Exception( "Polygon clipping failed due to line intersection not working as expected. You may have duplicate points or a degenerate polygon in general" ); + } + + // haven't hit the end yet, add current point + if( dir == 1 ) + AddFront( index ); + else + AddBack( index ); + } + } + + return new PolygonSection( ( tStart, tEnd ), points ); + } + + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/PolygonClipper.cs.meta b/Runtime/Geometric Shapes/PolygonClipper.cs.meta new file mode 100644 index 0000000..daf7a4d --- /dev/null +++ b/Runtime/Geometric Shapes/PolygonClipper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 433af421925ac41db9300c8146938f22 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Ray2D.cs b/Runtime/Geometric Shapes/Ray2D.cs similarity index 100% rename from Geometric Shapes/Ray2D.cs rename to Runtime/Geometric Shapes/Ray2D.cs diff --git a/Runtime/Geometric Shapes/Ray2D.cs.meta b/Runtime/Geometric Shapes/Ray2D.cs.meta new file mode 100644 index 0000000..9f5524b --- /dev/null +++ b/Runtime/Geometric Shapes/Ray2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f7ef092194dc43aebdd9a3d63b6b1c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Geometric Shapes/Transform2D.cs b/Runtime/Geometric Shapes/Transform2D.cs new file mode 100644 index 0000000..9ce9981 --- /dev/null +++ b/Runtime/Geometric Shapes/Transform2D.cs @@ -0,0 +1,99 @@ +using System; +using UnityEngine; + +namespace Freya { + + /// An orthonormal affine 2D transformation + [Serializable] + public struct Transform2D { + + public float origin_x, origin_y; + public float axisX_x, axisX_y; + + public Vector2 Origin { + get => new(origin_x, origin_y); + set => ( origin_x, origin_y ) = ( value.x, value.y ); + } + public Vector2 AxisX { + get => new(axisX_x, axisX_y); + set => ( axisX_x, axisX_y ) = ( value.x, value.y ); + } + public Vector2 AxisY { + get => new(AxisY_x, AxisY_y); + set => ( axisX_x, axisX_y ) = ( +value.y, -value.x ); + } + public float AxisY_x => -axisX_y; + public float AxisY_y => +axisX_x; + + /// Creates an orthonormal affine 2D transformation + public Transform2D( Vector2 origin, Vector2 axisX ) { + this.origin_x = origin.x; + this.origin_y = origin.y; + this.axisX_x = axisX.x; + this.axisX_y = axisX.y; + } + + /// Transforms a local point to a world space point + /// The local space point to transform + public Vector2 TransformPoint( Vector2 pt ) { + return new( // unrolled for performance + origin_x + axisX_x * pt.x + AxisY_x * pt.y, + origin_y + axisX_y * pt.x + AxisY_y * pt.y + ); + } + + /// + public Vector2 TransformPoint( float x, float y ) { + return new( // unrolled for performance + origin_x + axisX_x * x + AxisY_x * y, + origin_y + axisX_y * x + AxisY_y * y + ); + } + + /// Transforms a local vector to a world space vector, not taking position into account + /// The local space vector to transform + public Vector2 TransformVector( Vector2 vec ) { + return new( // unrolled for performance + axisX_x * vec.x + AxisY_x * vec.y, + axisX_y * vec.x + AxisY_y * vec.y + ); + } + + /// + public Vector2 TransformVector( float x, float y ) { + return new( // unrolled for performance + axisX_x * x + AxisY_x * y, + axisX_y * x + AxisY_y * y + ); + } + + /// Transform a world space point to a local point + /// World space point + public Vector2 InverseTransformPoint( Vector2 pt ) { + float rx = pt.x - origin_x; + float ry = pt.y - origin_y; + return new( + axisX_x * rx + axisX_y * ry, + AxisY_x * rx + AxisY_y * ry + ); + } + + /// Transform a world space vector to a local vector + /// World space vector + public Vector2 InverseTransformVector( Vector2 vec ) { + return new( + axisX_x * vec.x + axisX_y * vec.y, + AxisY_x * vec.x + AxisY_y * vec.y + ); + } + + public static Transform2D operator *( Transform2D a, Transform2D b ) { + return new Transform2D( + a.TransformPoint( b.origin_x, b.origin_y ), + a.TransformVector( b.axisX_x, b.axisX_y ) + ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Geometric Shapes/Transform2D.cs.meta b/Runtime/Geometric Shapes/Transform2D.cs.meta new file mode 100644 index 0000000..1cc15ab --- /dev/null +++ b/Runtime/Geometric Shapes/Transform2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77cf644ac96832c4aa1be178553ec82f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Geometric Shapes/Triangle.cs b/Runtime/Geometric Shapes/Triangle.cs similarity index 98% rename from Geometric Shapes/Triangle.cs rename to Runtime/Geometric Shapes/Triangle.cs index 6e0c4c7..6fe3191 100644 --- a/Geometric Shapes/Triangle.cs +++ b/Runtime/Geometric Shapes/Triangle.cs @@ -100,7 +100,7 @@ public Vector2 this[ int i ] { public partial struct Triangle2D { /// The area of the triangle - public float Area => Mathf.Abs( SignedArea ); + public float Area => MathF.Abs( SignedArea ); // todo: verify clockwise vs ccw /// The signed area of the triangle. When the triangle is defined clockwise, the area will be negative @@ -323,8 +323,8 @@ public float GetAngle( int index ) { Vector2 abDir = ( b - a ).normalized; Vector2 acDir = ( c - a ).normalized; Vector2 bcDir = ( c - b ).normalized; - float angA = Mathf.Acos( Vector2.Dot( abDir, acDir ).ClampNeg1to1() ); - float angB = Mathf.Acos( Vector2.Dot( -abDir, bcDir ).ClampNeg1to1() ); + float angA = MathF.Acos( Vector2.Dot( abDir, acDir ).ClampNeg1to1() ); + float angB = MathF.Acos( Vector2.Dot( -abDir, bcDir ).ClampNeg1to1() ); float angC = PI - angA - angB; return ( angA, angB, angC ); } @@ -375,8 +375,8 @@ public float GetAngle( int index ) { Vector2 abDir = ( b - a ).normalized; Vector2 acDir = ( c - a ).normalized; Vector2 bcDir = ( c - b ).normalized; - float angA = Mathf.Acos( Vector2.Dot( abDir, acDir ).ClampNeg1to1() ); - float angB = Mathf.Acos( Vector2.Dot( -abDir, bcDir ).ClampNeg1to1() ); + float angA = MathF.Acos( Vector2.Dot( abDir, acDir ).ClampNeg1to1() ); + float angB = MathF.Acos( Vector2.Dot( -abDir, bcDir ).ClampNeg1to1() ); float angC = PI - angA - angB; return ( angA, angB, angC ); } diff --git a/Runtime/Geometric Shapes/Triangle.cs.meta b/Runtime/Geometric Shapes/Triangle.cs.meta new file mode 100644 index 0000000..868f3ba --- /dev/null +++ b/Runtime/Geometric Shapes/Triangle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 524889b1a2a414d7e8c0bf355912df9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntersectionTestCore.cs b/Runtime/IntersectionTestCore.cs similarity index 98% rename from IntersectionTestCore.cs rename to Runtime/IntersectionTestCore.cs index bfabdd4..4437ff7 100644 --- a/IntersectionTestCore.cs +++ b/Runtime/IntersectionTestCore.cs @@ -1,5 +1,6 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +using System; using System.Runtime.CompilerServices; using UnityEngine; using static Freya.Mathfs; @@ -70,7 +71,7 @@ public static ResultsMax2 CirclesIntersectionPoints( Vector2 aPos, floa bool differentPosition = dist > 0.00001f; float maxRad = Max( aRadius, bRadius ); float minRad = Min( aRadius, bRadius ); - bool ringsTouching = Mathf.Abs( dist - maxRad ) < minRad; + bool ringsTouching = MathF.Abs( dist - maxRad ) < minRad; if( ringsTouching && differentPosition ) { float aRadSq = aRadius * aRadius; @@ -100,7 +101,7 @@ public static bool CirclesOverlap( Vector2 aPos, float aRadius, Vector2 bPos, fl float dist = Vector2.Distance( aPos, bPos ); float maxRad = Max( aRadius, bRadius ); float minRad = Min( aRadius, bRadius ); - return Mathf.Abs( dist - maxRad ) < minRad; + return MathF.Abs( dist - maxRad ) < minRad; } /// Returns whether or not a line passes through a box centered at (0,0) diff --git a/Runtime/IntersectionTestCore.cs.meta b/Runtime/IntersectionTestCore.cs.meta new file mode 100644 index 0000000..264f20c --- /dev/null +++ b/Runtime/IntersectionTestCore.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b59506ec891ae4eecb84750ddd5fe0ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntersectionTestWrappers.cs b/Runtime/IntersectionTestWrappers.cs similarity index 100% rename from IntersectionTestWrappers.cs rename to Runtime/IntersectionTestWrappers.cs diff --git a/Runtime/IntersectionTestWrappers.cs.meta b/Runtime/IntersectionTestWrappers.cs.meta new file mode 100644 index 0000000..78cfbef --- /dev/null +++ b/Runtime/IntersectionTestWrappers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0fe72063fc6ef488fb5995d7a9043e5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MathfsAsmdef.asmdef b/Runtime/Mathfs.asmdef similarity index 74% rename from MathfsAsmdef.asmdef rename to Runtime/Mathfs.asmdef index 68ca406..bafca79 100644 --- a/MathfsAsmdef.asmdef +++ b/Runtime/Mathfs.asmdef @@ -1,6 +1,9 @@ { "name": "MathfsAsmdef", - "references": [], + "rootNamespace": "", + "references": [ + "GUID:d8b63aba1907145bea998dd612889d6b" + ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, diff --git a/Runtime/Mathfs.asmdef.meta b/Runtime/Mathfs.asmdef.meta new file mode 100644 index 0000000..954f77f --- /dev/null +++ b/Runtime/Mathfs.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6071c9f2ce0a4407c93af459fa416e54 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Mathfs.cs b/Runtime/Mathfs.cs similarity index 76% rename from Mathfs.cs rename to Runtime/Mathfs.cs index dbb2be4..710b504 100644 --- a/Mathfs.cs +++ b/Runtime/Mathfs.cs @@ -1,1215 +1,1517 @@ -// Some of this code is similar to Unity's original Mathf source to match functionality. -// The original Mathf.cs source https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Mathf.cs -// ...and the trace amounts of it left in here is copyright (c) Unity Technologies with license: https://unity3d.com/legal/licenses/Unity_Reference_Only_License -// -// Collected and expanded upon to by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) - -using System; -using UnityEngine; -using Uei = UnityEngine.Internal; -using System.Linq; // used for arbitrary count min/max functions, so it's safe and won't allocate garbage don't worry~ -using System.Runtime.CompilerServices; - -namespace Freya { - - /// The core math helper class. It has functions mostly for single values, but also vector helpers - public static class Mathfs { - - const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; - - #region Constants - - /// The circle constant. Defined as the circumference of a circle divided by its radius. Equivalent to 2*pi - public const float TAU = 6.28318530717959f; - - /// An obscure circle constant. Defined as the circumference of a circle divided by its diameter. Equivalent to 0.5*tau - public const float PI = 3.14159265359f; - - /// Euler's number. The base of the natural logarithm. f(x)=e^x is equal to its own derivative - public const float E = 2.71828182846f; - - /// The golden ratio. It is the value of a/b where a/b = (a+b)/a. It's the positive root of x^2-x-1 - public const float GOLDEN_RATIO = 1.61803398875f; - - /// The square root of two. The length of the vector (1,1) - public const float SQRT2 = 1.41421356237f; - - /// The reciprocal of the square root of two. The components of the vector (1,1) - public const float RSQRT2 = 1f / SQRT2; - - /// Multiply an angle in degrees by this, to convert it to radians - public const float Deg2Rad = TAU / 360f; - - /// Multiply an angle in radians by this, to convert it to degrees - public const float Rad2Deg = 360f / TAU; - - #endregion - - #region Math operations - - /// Returns the square root of the given value - [MethodImpl( INLINE )] public static float Sqrt( float value ) => (float)Math.Sqrt( value ); - - /// Returns the square root of each component - [MethodImpl( INLINE )] public static Vector2 Sqrt( Vector2 v ) => new Vector2( Sqrt( v.x ), Sqrt( v.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Sqrt( Vector3 v ) => new Vector3( Sqrt( v.x ), Sqrt( v.y ), Sqrt( v.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Sqrt( Vector4 v ) => new Vector4( Sqrt( v.x ), Sqrt( v.y ), Sqrt( v.z ), Sqrt( v.w ) ); - - /// Returns the cube root of the given value, properly handling negative values unlike Pow(v,1/3) - [MethodImpl( INLINE )] public static float Cbrt( float value ) => value < 0 ? -Pow( -value, 1f / 3f ) : Pow( value, 1f / 3f ); - - /// Returns value raised to the power of exponent - [MethodImpl( INLINE )] public static float Pow( float value, float exponent ) => (float)Math.Pow( value, exponent ); - - /// Returns e to the power of the given value - [MethodImpl( INLINE )] public static float Exp( float power ) => (float)Math.Exp( power ); - - /// Returns the logarithm of a value, with the given base - [MethodImpl( INLINE )] public static float Log( float value, float @base ) => (float)Math.Log( value, @base ); - - /// Returns the natural logarithm of the given value - [MethodImpl( INLINE )] public static float Log( float value ) => (float)Math.Log( value ); - - /// Returns the base 10 logarithm of the given value - [MethodImpl( INLINE )] public static float Log10( float value ) => (float)Math.Log10( value ); - - /// Returns the binomial coefficient n over k - public static ulong BinomialCoef( uint n, uint k ) { - // source: https://blog.plover.com/math/choose.html - ulong r = 1; - if( k > n ) return 0; - for( ulong d = 1; d <= k; d++ ) { - r *= n--; - r /= d; - } - - return r; - // mathematically clean but extremely prone to overflow - //return Factorial( n ) / ( Factorial( k ) * Factorial( n - k ) ); - } - - /// Returns the Factorial of a given value from 0 to 12 - /// A value between 0 and 12 (integers can't store the factorial of 13 or above) - [MethodImpl( INLINE )] public static int Factorial( uint value ) { - if( value <= 12 ) - return factorialInt[value]; - if( value <= 20 ) - throw new OverflowException( $"The Factorial of {value} is too big for integer representation, please use {nameof(FactorialLong)} instead" ); - throw new OverflowException( $"The Factorial of {value} is too big for integer representation" ); - } - - /// Returns the Factorial of a given value from 0 to 20 - /// A value between 0 and 20 (neither long nor ulong can store values large enough for the factorial of 21) - [MethodImpl( INLINE )] public static long FactorialLong( uint value ) { - if( value <= 20 ) - return factorialLong[value]; - throw new OverflowException( $"The Factorial of {value} is too big for integer representation, even unsigned longs, soooo, rip" ); - } - - static readonly long[] factorialLong = { - /*0*/ 1, - /*1*/ 1, - /*2*/ 2, - /*3*/ 6, - /*4*/ 24, - /*5*/ 120, - /*6*/ 720, - /*7*/ 5040, - /*8*/ 40320, - /*9*/ 362880, - /*10*/ 3628800, - /*11*/ 39916800, - /*12*/ 479001600, - /*13*/ 6227020800, - /*14*/ 87178291200, - /*15*/ 1307674368000, - /*16*/ 20922789888000, - /*17*/ 355687428096000, - /*18*/ 6402373705728000, - /*19*/ 121645100408832000, - /*20*/ 2432902008176640000 - }; - - static readonly int[] factorialInt = { - /*0*/ 1, - /*1*/ 1, - /*2*/ 2, - /*3*/ 6, - /*4*/ 24, - /*5*/ 120, - /*6*/ 720, - /*7*/ 5040, - /*8*/ 40320, - /*9*/ 362880, - /*10*/ 3628800, - /*11*/ 39916800, - /*12*/ 479001600 - }; - - #endregion - - #region Floating point shenanigans - - /// A very small value, used for various floating point inaccuracy thresholds - public static readonly float Epsilon = UnityEngineInternal.MathfInternal.IsFlushToZeroEnabled ? UnityEngineInternal.MathfInternal.FloatMinNormal : UnityEngineInternal.MathfInternal.FloatMinDenormal; - - /// float.PositiveInfinity - public const float Infinity = float.PositiveInfinity; - - /// float.NegativeInfinity - public const float NegativeInfinity = float.NegativeInfinity; - - /// Returns whether or not two values are approximately equal. - /// They are considered equal if they are within a Mathfs.Epsilon*8 or max(a,b)*0.000001f range of each other - /// The first value to compare - /// The second value to compare - [MethodImpl( INLINE )] public static bool Approximately( float a, float b ) => Abs( b - a ) < Max( 0.000001f * Max( Abs( a ), Abs( b ) ), Epsilon * 8 ); - - /// - [MethodImpl( INLINE )] public static bool Approximately( Vector2 a, Vector2 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ); - - /// - [MethodImpl( INLINE )] public static bool Approximately( Vector3 a, Vector3 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ) && Approximately( a.z, b.z ); - - /// - [MethodImpl( INLINE )] public static bool Approximately( Vector4 a, Vector4 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ) && Approximately( a.z, b.z ) && Approximately( a.w, b.w ); - - /// - [MethodImpl( INLINE )] public static bool Approximately( Color a, Color b ) => Approximately( a.r, b.r ) && Approximately( a.g, b.g ) && Approximately( a.b, b.b ) && Approximately( a.a, b.a ); - - #endregion - - #region Trigonometry - - /// Returns the cosine of the given angle. Equivalent to the x-component of a unit vector with the same angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Cos( float angRad ) => (float)Math.Cos( angRad ); - - /// Returns the sine of the given angle. Equivalent to the y-component of a unit vector with the same angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Sin( float angRad ) => (float)Math.Sin( angRad ); - - /// Returns the tangent of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Tan( float angRad ) => (float)Math.Tan( angRad ); - - /// Returns the arc cosine of the given value, in radians - /// A value between -1 and 1 - [MethodImpl( INLINE )] public static float Acos( float value ) => (float)Math.Acos( value ); - - /// Returns the arc sine of the given value, in radians - /// A value between -1 and 1 - [MethodImpl( INLINE )] public static float Asin( float value ) => (float)Math.Asin( value ); - - /// Returns the arc tangent of the given value, in radians - /// A value between -1 and 1 - [MethodImpl( INLINE )] public static float Atan( float value ) => (float)Math.Atan( value ); - - /// Returns the angle of a vector. I don't recommend using this function, it's confusing~ Use Mathfs.DirToAng instead - /// The y component of the vector. They're flipped yeah I know but this is how everyone implements if for some godforsaken reason - /// The x component of the vector. They're flipped yeah I know but this is how everyone implements if for some godforsaken reason - [MethodImpl( INLINE )] public static float Atan2( float y, float x ) => (float)Math.Atan2( y, x ); - - /// Returns the cosecant of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Csc( float angRad ) => 1f / (float)Math.Sin( angRad ); - - /// Returns the secant of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Sec( float angRad ) => 1f / (float)Math.Cos( angRad ); - - /// Returns the cotangent of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Cot( float angRad ) => 1f / (float)Math.Tan( angRad ); - - /// Returns the versine of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Ver( float angRad ) => 1 - (float)Math.Cos( angRad ); - - /// Returns the coversine of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Cvs( float angRad ) => 1 - (float)Math.Sin( angRad ); - - /// Returns the chord of the given angle - /// Angle in radians - [MethodImpl( INLINE )] public static float Crd( float angRad ) => 2 * (float)Math.Sin( angRad / 2 ); - - #endregion - - #region Hyperbolic Trigonometry - - /// Returns the hyperbolic cosine of the given hyperbolic angle - [MethodImpl( INLINE )] public static float Cosh( float x ) => (float)Math.Cosh( x ); - - /// Returns the hyperbolic sine of the given hyperbolic angle - [MethodImpl( INLINE )] public static float Sinh( float x ) => (float)Math.Sinh( x ); - - /// Returns the hyperbolic tangent of the given hyperbolic angle - [MethodImpl( INLINE )] public static float Tanh( float x ) => (float)Math.Tanh( x ); - - /// Returns the hyperbolic arc cosine of the given value - [MethodImpl( INLINE )] public static float Acosh( float x ) => (float)Math.Log( x + Mathf.Sqrt( x * x - 1 ) ); - - /// Returns the hyperbolic arc sine of the given value - [MethodImpl( INLINE )] public static float Asinh( float x ) => (float)Math.Log( x + Mathf.Sqrt( x * x + 1 ) ); - - /// Returns the hyperbolic arc tangent of the given value - [MethodImpl( INLINE )] public static float Atanh( float x ) => (float)( 0.5 * Math.Log( ( 1 + x ) / ( 1 - x ) ) ); - - #endregion - - #region Absolute Values - - /// Returns the absolute value. Basically makes negative numbers positive - [MethodImpl( INLINE )] public static float Abs( float value ) => Math.Abs( value ); - - /// - [MethodImpl( INLINE )] public static int Abs( int value ) => Math.Abs( value ); - - /// Returns the absolute value, per component. Basically makes negative numbers positive - [MethodImpl( INLINE )] public static Vector2 Abs( Vector2 v ) => new Vector2( Abs( v.x ), Abs( v.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Abs( Vector3 v ) => new Vector3( Abs( v.x ), Abs( v.y ), Abs( v.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Abs( Vector4 v ) => new Vector4( Abs( v.x ), Abs( v.y ), Abs( v.z ), Abs( v.w ) ); - - #endregion - - #region Clamping - - /// Returns the value clamped between min and max - /// The value to clamp - /// The minimum value - /// The maximum value - public static float Clamp( float value, float min, float max ) => value < min ? min : value > max ? max : value; - - /// Clamps each component between min and max - public static Vector2 Clamp( Vector2 v, Vector2 min, Vector2 max ) => - new Vector2( - v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, - v.y < min.y ? min.y : v.y > max.y ? max.y : v.y - ); - - /// - public static Vector3 Clamp( Vector3 v, Vector3 min, Vector3 max ) => - new Vector3( - v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, - v.y < min.y ? min.y : v.y > max.y ? max.y : v.y, - v.z < min.z ? min.z : v.z > max.z ? max.z : v.z - ); - - /// - public static Vector4 Clamp( Vector4 v, Vector4 min, Vector4 max ) => - new Vector4( - v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, - v.y < min.y ? min.y : v.y > max.y ? max.y : v.y, - v.z < min.z ? min.z : v.z > max.z ? max.z : v.z, - v.w < min.w ? min.w : v.w > max.w ? max.w : v.w - ); - - /// - public static int Clamp( int value, int min, int max ) => value < min ? min : value > max ? max : value; - - /// Returns the value clamped between 0 and 1 - public static float Clamp01( float value ) => value < 0f ? 0f : value > 1f ? 1f : value; - - /// Clamps each component between 0 and 1 - public static Vector2 Clamp01( Vector2 v ) => - new Vector2( - v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, - v.y < 0f ? 0f : v.y > 1f ? 1f : v.y - ); - - /// - public static Vector3 Clamp01( Vector3 v ) => - new Vector3( - v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, - v.y < 0f ? 0f : v.y > 1f ? 1f : v.y, - v.z < 0f ? 0f : v.z > 1f ? 1f : v.z - ); - - /// - public static Vector4 Clamp01( Vector4 v ) => - new Vector4( - v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, - v.y < 0f ? 0f : v.y > 1f ? 1f : v.y, - v.z < 0f ? 0f : v.z > 1f ? 1f : v.z, - v.w < 0f ? 0f : v.w > 1f ? 1f : v.w - ); - - /// Clamps the value between -1 and 1 - public static float ClampNeg1to1( float value ) => value < -1f ? -1f : value > 1f ? 1f : value; - - /// Clamps each component between -1 and 1 - public static Vector2 ClampNeg1to1( Vector2 v ) => - new Vector2( - v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, - v.y < -1f ? -1f : v.y > 1f ? 1f : v.y - ); - - /// Clamps each component between -1 and 1 - public static Vector3 ClampNeg1to1( Vector3 v ) => - new Vector3( - v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, - v.y < -1f ? -1f : v.y > 1f ? 1f : v.y, - v.z < -1f ? -1f : v.z > 1f ? 1f : v.z - ); - - /// Clamps each component between -1 and 1 - public static Vector4 ClampNeg1to1( Vector4 v ) => - new Vector4( - v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, - v.y < -1f ? -1f : v.y > 1f ? 1f : v.y, - v.z < -1f ? -1f : v.z > 1f ? 1f : v.z, - v.w < -1f ? -1f : v.w > 1f ? 1f : v.w - ); - - #endregion - - #region Min & Max - - /// Returns the smallest of the two values - [MethodImpl( INLINE )] public static float Min( float a, float b ) => a < b ? a : b; - - /// Returns the smallest of the three values - [MethodImpl( INLINE )] public static float Min( float a, float b, float c ) => Min( Min( a, b ), c ); - - /// Returns the smallest of the four values - [MethodImpl( INLINE )] public static float Min( float a, float b, float c, float d ) => Min( Min( a, b ), Min( c, d ) ); - - /// Returns the largest of the two values - [MethodImpl( INLINE )] public static float Max( float a, float b ) => a > b ? a : b; - - /// Returns the largest of the three values - [MethodImpl( INLINE )] public static float Max( float a, float b, float c ) => Max( Max( a, b ), c ); - - /// Returns the largest of the four values - [MethodImpl( INLINE )] public static float Max( float a, float b, float c, float d ) => Max( Max( a, b ), Max( c, d ) ); - - /// Returns the smallest of the two values - [MethodImpl( INLINE )] public static int Min( int a, int b ) => a < b ? a : b; - - /// Returns the smallest of the three values - [MethodImpl( INLINE )] public static int Min( int a, int b, int c ) => Min( Min( a, b ), c ); - - /// Returns the smallest of the four values - [MethodImpl( INLINE )] public static int Min( int a, int b, int c, int d ) => Min( Min( a, b ), Min( c, d ) ); - - /// Returns the largest of the two values - [MethodImpl( INLINE )] public static int Max( int a, int b ) => a > b ? a : b; - - /// Returns the largest of the three values - [MethodImpl( INLINE )] public static int Max( int a, int b, int c ) => Max( Max( a, b ), c ); - - /// Returns the largest of the four values - [MethodImpl( INLINE )] public static int Max( int a, int b, int c, int d ) => Max( Max( a, b ), Max( c, d ) ); - - /// Returns the smallest of the given values - [MethodImpl( INLINE )] public static float Min( params float[] values ) => values.Min(); - - /// Returns the largest of the given values - [MethodImpl( INLINE )] public static float Max( params float[] values ) => values.Max(); - - /// Returns the smallest of the given values - [MethodImpl( INLINE )] public static int Min( params int[] values ) => values.Min(); - - /// Returns the largest of the given values - [MethodImpl( INLINE )] public static int Max( params int[] values ) => values.Max(); - - /// Returns the minimum value of all components in the vector - [MethodImpl( INLINE )] public static float Min( Vector2 v ) => Min( v.x, v.y ); - - /// - [MethodImpl( INLINE )] public static float Min( Vector3 v ) => Min( v.x, v.y, v.z ); - - /// - [MethodImpl( INLINE )] public static float Min( Vector4 v ) => Min( v.x, v.y, v.z, v.w ); - - /// Returns the maximum value of all components in the vector - [MethodImpl( INLINE )] public static float Max( Vector2 v ) => Max( v.x, v.y ); - - /// - [MethodImpl( INLINE )] public static float Max( Vector3 v ) => Max( v.x, v.y, v.z ); - - /// - [MethodImpl( INLINE )] public static float Max( Vector4 v ) => Max( v.x, v.y, v.z, v.w ); - - #endregion - - #region Signs & Rounding - - /// The sign of the value. Returns -1 if negative, returns 1 if greater than or equal to 0 - [MethodImpl( INLINE )] public static float Sign( float value ) => value >= 0f ? 1 : -1; - - /// The sign of each component. Returns -1 if negative, returns 1 if greater than or equal to 0 - [MethodImpl( INLINE )] public static Vector2 Sign( Vector2 value ) => new Vector2( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1 ); - - /// - [MethodImpl( INLINE )] public static Vector3 Sign( Vector3 value ) => new Vector3( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1, value.z >= 0f ? 1 : -1 ); - - /// - [MethodImpl( INLINE )] public static Vector4 Sign( Vector4 value ) => new Vector4( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1, value.z >= 0f ? 1 : -1, value.w >= 0f ? 1 : -1 ); - - /// Returns the sign of the value, either -1 if negative, or 1 if positive or 0 - [MethodImpl( INLINE )] public static int Sign( int value ) => value >= 0 ? 1 : -1; - - /// The sign of the value as an integer. Returns -1 if negative, returns 1 if greater than or equal to 0 - [MethodImpl( INLINE )] public static int SignAsInt( float value ) => value >= 0f ? 1 : -1; - - /// The sign of the value. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive - [MethodImpl( INLINE )] public static float SignWithZero( float value, float zeroThreshold = 0.000001f ) => Abs( value ) < zeroThreshold ? 0 : Sign( value ); - - /// The sign of each component. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive - [MethodImpl( INLINE )] public static Vector2 SignWithZero( Vector2 value, float zeroThreshold = 0.000001f ) => - new Vector2( - Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), - Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ) - ); - - /// - [MethodImpl( INLINE )] public static Vector3 SignWithZero( Vector3 value, float zeroThreshold = 0.000001f ) => - new Vector3( - Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), - Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ), - Abs( value.z ) < zeroThreshold ? 0 : Sign( value.z ) - ); - - /// - [MethodImpl( INLINE )] public static Vector4 SignWithZero( Vector4 value, float zeroThreshold = 0.000001f ) => - new Vector4( - Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), - Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ), - Abs( value.z ) < zeroThreshold ? 0 : Sign( value.z ), - Abs( value.w ) < zeroThreshold ? 0 : Sign( value.w ) - ); - - /// Returns the sign of the value, either -1 if negative, 0 if zero, 1 if positive - [MethodImpl( INLINE )] public static int SignWithZero( int value ) => value == 0 ? 0 : Sign( value ); - - /// The sign of the value. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive - [MethodImpl( INLINE )] public static int SignWithZeroAsInt( float value, float zeroThreshold = 0.000001f ) => Abs( value ) < zeroThreshold ? 0 : SignAsInt( value ); - - /// Rounds the value down to the nearest integer - [MethodImpl( INLINE )] public static float Floor( float value ) => (float)Math.Floor( value ); - - /// Rounds the vector components down to the nearest integer - [MethodImpl( INLINE )] public static Vector2 Floor( Vector2 value ) => new Vector2( (float)Math.Floor( value.x ), (float)Math.Floor( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Floor( Vector3 value ) => new Vector3( (float)Math.Floor( value.x ), (float)Math.Floor( value.y ), (float)Math.Floor( value.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Floor( Vector4 value ) => new Vector4( (float)Math.Floor( value.x ), (float)Math.Floor( value.y ), (float)Math.Floor( value.z ), (float)Math.Floor( value.w ) ); - - /// Rounds the value down to the nearest integer, returning an int value - [MethodImpl( INLINE )] public static int FloorToInt( float value ) => (int)Math.Floor( value ); - - /// Rounds the vector components down to the nearest integer, returning an integer vector - [MethodImpl( INLINE )] public static Vector2Int FloorToInt( Vector2 value ) => new Vector2Int( (int)Math.Floor( value.x ), (int)Math.Floor( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3Int FloorToInt( Vector3 value ) => new Vector3Int( (int)Math.Floor( value.x ), (int)Math.Floor( value.y ), (int)Math.Floor( value.z ) ); - - /// Rounds the value up to the nearest integer - [MethodImpl( INLINE )] public static float Ceil( float value ) => (float)Math.Ceiling( value ); - - /// Rounds the vector components up to the nearest integer - [MethodImpl( INLINE )] public static Vector2 Ceil( Vector2 value ) => new Vector2( (float)Math.Ceiling( value.x ), (float)Math.Ceiling( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Ceil( Vector3 value ) => new Vector3( (float)Math.Ceiling( value.x ), (float)Math.Ceiling( value.y ), (float)Math.Ceiling( value.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Ceil( Vector4 value ) => new Vector4( (float)Math.Ceiling( value.x ), (float)Math.Ceiling( value.y ), (float)Math.Ceiling( value.z ), (float)Math.Ceiling( value.w ) ); - - /// Rounds the value up to the nearest integer, returning an int value - [MethodImpl( INLINE )] public static int CeilToInt( float value ) => (int)Math.Ceiling( value ); - - /// Rounds the vector components up to the nearest integer, returning an integer vector - [MethodImpl( INLINE )] public static Vector2Int CeilToInt( Vector2 value ) => new Vector2Int( (int)Math.Ceiling( value.x ), (int)Math.Ceiling( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3Int CeilToInt( Vector3 value ) => new Vector3Int( (int)Math.Ceiling( value.x ), (int)Math.Ceiling( value.y ), (int)Math.Ceiling( value.z ) ); - - /// Rounds the value to the nearest integer - [MethodImpl( INLINE )] public static float Round( float value ) => (float)Math.Round( value ); - - /// Rounds the vector components to the nearest integer - [MethodImpl( INLINE )] public static Vector2 Round( Vector2 value ) => new Vector2( (float)Math.Round( value.x ), (float)Math.Round( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Round( Vector3 value ) => new Vector3( (float)Math.Round( value.x ), (float)Math.Round( value.y ), (float)Math.Round( value.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Round( Vector4 value ) => new Vector4( (float)Math.Round( value.x ), (float)Math.Round( value.y ), (float)Math.Round( value.z ), (float)Math.Round( value.w ) ); - - /// Rounds the value to the nearest value, snapped to the given interval size - [MethodImpl( INLINE )] public static float Round( float value, float snapInterval ) => Mathf.Round( value / snapInterval ) * snapInterval; - - /// Rounds the vector components to the nearest value, snapped to the given interval size - [MethodImpl( INLINE )] public static Vector2 Round( Vector2 value, float snapInterval ) => new Vector2( Round( value.x, snapInterval ), Round( value.y, snapInterval ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Round( Vector3 value, float snapInterval ) => new Vector3( Round( value.x, snapInterval ), Round( value.y, snapInterval ), Round( value.z, snapInterval ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Round( Vector4 value, float snapInterval ) => new Vector4( Round( value.x, snapInterval ), Round( value.y, snapInterval ), Round( value.z, snapInterval ), Round( value.w, snapInterval ) ); - - /// Rounds the value to the nearest integer, returning an int value - [MethodImpl( INLINE )] public static int RoundToInt( float value ) => (int)Math.Round( value ); - - /// Rounds the vector components to the nearest integer, returning an integer vector - [MethodImpl( INLINE )] public static Vector2Int RoundToInt( Vector2 value ) => new Vector2Int( (int)Math.Round( value.x ), (int)Math.Round( value.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3Int RoundToInt( Vector3 value ) => new Vector3Int( (int)Math.Round( value.x ), (int)Math.Round( value.y ), (int)Math.Round( value.z ) ); - - #endregion - - #region Range Repeating - - /// Returns the fractional part of the value. Equivalent to x - floor(x) - [MethodImpl( INLINE )] public static float Frac( float x ) => x - Floor( x ); - - /// Returns the fractional part of the value for each component. Equivalent to v - floor(v) - [MethodImpl( INLINE )] public static Vector2 Frac( Vector2 v ) => new Vector2( v.x - Floor( v.x ), v.y - Floor( v.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Frac( Vector3 v ) => new Vector3( v.x - Floor( v.x ), v.y - Floor( v.y ), v.z - Floor( v.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Frac( Vector4 v ) => new Vector4( v.x - Floor( v.x ), v.y - Floor( v.y ), v.z - Floor( v.z ), v.w - Floor( v.w ) ); - - /// Repeats the given value in the interval specified by length - [MethodImpl( INLINE )] public static float Repeat( float value, float length ) => Clamp( value - Floor( value / length ) * length, 0.0f, length ); - - /// Modulo, but, behaves the way you want with negative values, for stuff like array[(n+1)%length] etc. - [MethodImpl( INLINE )] public static int Mod( int value, int length ) => ( value % length + length ) % length; - - /// Repeats a value within a range, going back and forth - [MethodImpl( INLINE )] public static float PingPong( float t, float length ) => length - Abs( Repeat( t, length * 2f ) - length ); - - /// Returns the height of in a triangle wave at time t going from 0 to 1 and back to 0 within the the given period - [MethodImpl( INLINE )] public static float TriangleWave( float t, float period = 1f ) { - float x = t / period; - return 1f - Abs( 2 * ( x - Floor( x ) ) - 1 ); - } - - #endregion - - #region Smoothing & Easing Curves - - /// Applies cubic smoothing to the 0-1 interval, also known as the smoothstep function. Similar to an EaseInOut operation - [MethodImpl( INLINE )] public static float Smooth01( float x ) => x * x * ( 3 - 2 * x ); - - /// Applies quintic smoothing to the 0-1 interval, also known as the smootherstep function. Similar to an EaseInOut operation - [MethodImpl( INLINE )] public static float Smoother01( float x ) => x * x * x * ( x * ( x * 6 - 15 ) + 10 ); - - /// Applies trigonometric smoothing to the 0-1 interval. Similar to an EaseInOut operation - [MethodImpl( INLINE )] public static float SmoothCos01( float x ) => Cos( x * PI ) * -0.5f + 0.5f; - - /// Applies a gamma curve or something idk I've never used this function before but it was part of Unity's original Mathfs.cs and it's undocumented - public static float Gamma( float value, float absmax, float gamma ) { - bool negative = value < 0F; - float absval = Abs( value ); - if( absval > absmax ) - return negative ? -absval : absval; - - float result = Pow( absval / absmax, gamma ) * absmax; - return negative ? -result : result; - } - - #endregion - - #region Value & Vector interpolation - - /// Blends between a and b, based on the t-value. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly - /// The start value, when t is 0 - /// The start value, when t is 1 - /// The t-value from 0 to 1 representing position along the lerp - [MethodImpl( INLINE )] public static float Lerp( float a, float b, float t ) => ( 1f - t ) * a + t * b; - - /// Blends between a and b of each component, based on the t-value of each component in the t-vector. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly - /// The start value, when t is 0 - /// The start value, when t is 1 - /// The t-values from 0 to 1 representing position along the lerp - [MethodImpl( INLINE )] public static Vector2 Lerp( Vector2 a, Vector2 b, Vector2 t ) => new Vector2( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Lerp( Vector3 a, Vector3 b, Vector3 t ) => new Vector3( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ), Lerp( a.z, b.z, t.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Lerp( Vector4 a, Vector4 b, Vector4 t ) => new Vector4( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ), Lerp( a.z, b.z, t.z ), Lerp( a.w, b.w, t.w ) ); - - /// Linearly blends between two rectangles, moving and resizing from the center. Note: this lerp is unclamped - /// The start value, when t is 0 - /// The start value, when t is 1 - /// The t-values from 0 to 1 representing position along the lerp - public static Rect Lerp( Rect a, Rect b, float t ) { - Vector2 center = Vector2.LerpUnclamped( a.center, b.center, t ); - Vector2 size = Vector2.LerpUnclamped( a.size, b.size, t ); - return new Rect( default, size ) { center = center }; - } - - /// Blends between a and b, based on the t-value. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly - /// The start value, when t is 0 - /// The start value, when t is 1 - /// The t-value from 0 to 1 representing position along the lerp, clamped between 0 and 1 - [MethodImpl( INLINE )] public static float LerpClamped( float a, float b, float t ) => Lerp( a, b, Clamp01( t ) ); - - /// Lerps between a and b, applying cubic smoothing to the t-value - /// The start value, when t is 0 - /// The start value, when t is 1 - /// The t-value from 0 to 1 representing position along the lerp, clamped between 0 and 1 - [MethodImpl( INLINE )] public static float LerpSmooth( float a, float b, float t ) => Lerp( a, b, Smooth01( Clamp01( t ) ) ); - - /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1 - /// The start of the range, where it would return 0 - /// The end of the range, where it would return 1 - /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated - [MethodImpl( INLINE )] public static float InverseLerp( float a, float b, float value ) => ( value - a ) / ( b - a ); - - /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1. - /// This safe version returns 0 if a == b, instead of a division by zero - /// The start of the range, where it would return 0 - /// The end of the range, where it would return 1 - /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated - [MethodImpl( INLINE )] public static float InverseLerpSafe( float a, float b, float value ) { - float den = b - a; - if( den == 0 ) - return 0; - return ( value - a ) / den; - } - - /// Given values between a and b in each component, returns their normalized locations in the given ranges, as t-values (interpolants) from 0 to 1 - /// The start of the ranges, where it would return 0 - /// The end of the ranges, where it would return 1 - /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated - [MethodImpl( INLINE )] public static Vector2 InverseLerp( Vector2 a, Vector2 b, Vector2 v ) => new Vector2( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 InverseLerp( Vector3 a, Vector3 b, Vector3 v ) => new Vector3( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ), ( v.z - a.z ) / ( b.z - a.z ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 InverseLerp( Vector4 a, Vector4 b, Vector4 v ) => new Vector4( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ), ( v.z - a.z ) / ( b.z - a.z ), ( v.w - a.w ) / ( b.w - a.w ) ); - - /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) clamped between 0 and 1 - /// The start of the range, where it would return 0 - /// The end of the range, where it would return 1 - /// A value between a and b - [MethodImpl( INLINE )] public static float InverseLerpClamped( float a, float b, float value ) => Clamp01( ( value - a ) / ( b - a ) ); - - /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1, with cubic smoothing applied. - /// Equivalent to "smoothstep" in shader code - /// The start of the range, where it would return 0 - /// The end of the range, where it would return 1 - /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated - [MethodImpl( INLINE )] public static float InverseLerpSmooth( float a, float b, float value ) => Smooth01( Clamp01( ( value - a ) / ( b - a ) ) ); - - /// Remaps a value from the input range [iMin to iMax] into the output range [oMin to oMax]. - /// Equivalent to Lerp(oMin,oMax,InverseLerp(iMin,iMax,value)) - /// The start value of the input range - /// The end value of the input range - /// The start value of the output range - /// The end value of the output range - /// The value to remap - [MethodImpl( INLINE )] public static float Remap( float iMin, float iMax, float oMin, float oMax, float value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); - - /// - [MethodImpl( INLINE )] public static float Remap( float iMin, float iMax, float oMin, float oMax, int value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); - - /// Remaps values from the input range [iMin to iMax] into the output range [oMin to oMax] on a per-component basis. - /// Equivalent to Lerp(oMin,oMax,InverseLerp(iMin,iMax,value)) - /// The start values of the input ranges - /// The end values of the input ranges - /// The start values of the output ranges - /// The end values of the output ranges - /// The values to remap - [MethodImpl( INLINE )] public static Vector2 Remap( Vector2 iMin, Vector2 iMax, Vector2 oMin, Vector2 oMax, Vector2 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); - - /// - [MethodImpl( INLINE )] public static Vector3 Remap( Vector3 iMin, Vector3 iMax, Vector3 oMin, Vector3 oMax, Vector3 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); - - /// - [MethodImpl( INLINE )] public static Vector4 Remap( Vector4 iMin, Vector4 iMax, Vector4 oMin, Vector4 oMax, Vector4 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); - - /// Remaps a value from the input range [iMin to iMax] into the output range [oMin to oMax], clamping to make sure it does not extrapolate. - /// Equivalent to Lerp(oMin,oMax,InverseLerpClamped(iMin,iMax,value)) - /// The start value of the input range - /// The end value of the input range - /// The start value of the output range - /// The end value of the output range - /// The value to remap - [MethodImpl( INLINE )] public static float RemapClamped( float iMin, float iMax, float oMin, float oMax, float value ) => Lerp( oMin, oMax, InverseLerpClamped( iMin, iMax, value ) ); - - /// Remaps a value from the input Rect to the output Rect - /// The input Rect - /// The output Rect - /// The input position in the input Rect space - [MethodImpl( INLINE )] public static Vector2 Remap( Rect iRect, Rect oRect, Vector2 iPos ) => Remap( iRect.min, iRect.max, oRect.min, oRect.max, iPos ); - - /// Remaps a value from the input Bounds to the output Bounds - /// The input Bounds - /// The output Bounds - /// The input position in the input Bounds space - [MethodImpl( INLINE )] public static Vector3 Remap( Bounds iBounds, Bounds oBounds, Vector3 iPos ) => Remap( iBounds.min, iBounds.max, oBounds.min, oBounds.max, iPos ); - - /// Exponential interpolation, the multiplicative version of lerp, useful for values such as scaling or zooming - /// The start value - /// The end value - /// The t-value from 0 to 1 representing position along the eerp - [MethodImpl( INLINE )] public static float Eerp( float a, float b, float t ) => Mathf.Pow( a, 1 - t ) * Mathf.Pow( b, t ); - - /// Inverse exponential interpolation, the multiplicative version of InverseLerp, useful for values such as scaling or zooming - /// The start value - /// The end value - /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated - [MethodImpl( INLINE )] public static float InverseEerp( float a, float b, float v ) => Mathf.Log( a / v ) / Mathf.Log( a / b ); - - #endregion - - #region Movement helpers - - /// Moves a value current towards target - /// The current value - /// The value to move towards - /// The maximum change that should be applied to the value - public static float MoveTowards( float current, float target, float maxDelta ) { - if( Mathf.Abs( target - current ) <= maxDelta ) - return target; - return current + Mathf.Sign( target - current ) * maxDelta; - } - - /// Gradually changes a value towards a desired goal over time. - /// The value is smoothed by some spring-damper like function, which will never overshoot. - /// The function can be used to smooth any kind of value, positions, colors, scalars - /// The current position - /// The position we are trying to reach - /// The current velocity, this value is modified by the function every time you call it - /// Approximately the time it will take to reach the target. A smaller value will reach the target faster - /// Optionally allows you to clamp the maximum speed - public static float SmoothDamp( float current, float target, ref float currentVelocity, float smoothTime, float maxSpeed = Infinity ) { - float deltaTime = Time.deltaTime; - return SmoothDamp( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); - } - - /// Gradually changes a value towards a desired goal over time. - /// The value is smoothed by some spring-damper like function, which will never overshoot. - /// The function can be used to smooth any kind of value, positions, colors, scalars - /// The current position - /// The position we are trying to reach - /// The current velocity, this value is modified by the function every time you call it - /// Approximately the time it will take to reach the target. A smaller value will reach the target faster - /// Optionally allows you to clamp the maximum speed - /// The time since the last call to this function. By default Time.deltaTime - public static float SmoothDamp( float current, float target, ref float currentVelocity, float smoothTime, [Uei.DefaultValue( "Mathf.Infinity" )] float maxSpeed, [Uei.DefaultValue( "Time.deltaTime" )] float deltaTime ) { - // Based on Game Programming Gems 4 Chapter 1.10 - smoothTime = Mathf.Max( 0.0001F, smoothTime ); - float omega = 2F / smoothTime; - - float x = omega * deltaTime; - float exp = 1F / ( 1F + x + 0.48F * x * x + 0.235F * x * x * x ); - float change = current - target; - float originalTo = target; - - // Clamp maximum speed - float maxChange = maxSpeed * smoothTime; - change = Mathf.Clamp( change, -maxChange, maxChange ); - target = current - change; - - float temp = ( currentVelocity + omega * change ) * deltaTime; - currentVelocity = ( currentVelocity - omega * temp ) * exp; - float output = target + ( change + temp ) * exp; - - // Prevent overshooting - if( originalTo - current > 0.0F == output > originalTo ) { - output = originalTo; - currentVelocity = ( output - originalTo ) / deltaTime; - } - - return output; - } - - #endregion - - #region Weighted sums - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - [MethodImpl( INLINE )] public static float WeightedSum( Vector2 w, float a, float b ) => a * w.x + b * w.y; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - [MethodImpl( INLINE )] public static float WeightedSum( Vector3 w, float a, float b, float c ) => a * w.x + b * w.y + c * w.z; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - /// The fourth value, weighted by w.w - [MethodImpl( INLINE )] public static float WeightedSum( Vector4 w, float a, float b, float c, float d ) => a * w.x + b * w.y + c * w.z + d * w.w; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector2 w, Vector2 a, Vector2 b ) => a * w.x + b * w.y; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector3 w, Vector2 a, Vector2 b, Vector2 c ) => a * w.x + b * w.y + c * w.z; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - /// The fourth value, weighted by w.w - [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector4 w, Vector2 a, Vector2 b, Vector2 c, Vector2 d ) => a * w.x + b * w.y + c * w.z + d * w.w; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector3 w, Vector3 a, Vector3 b ) => a * w.x + b * w.y; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector3 w, Vector3 a, Vector3 b, Vector3 c ) => a * w.x + b * w.y + c * w.z; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - /// The fourth value, weighted by w.w - [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector4 w, Vector3 a, Vector3 b, Vector3 c, Vector3 d ) => a * w.x + b * w.y + c * w.z + d * w.w; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b ) => a * w.x + b * w.y; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b, Vector4 c ) => a * w.x + b * w.y + c * w.z; - - /// Multiplies each component of w by the input values, and returns their sum - /// The weights (per component) to apply to the rest of the values - /// The first value, weighted by w.x - /// The second value, weighted by w.y - /// The third value, weighted by w.z - /// The fourth value, weighted by w.w - [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b, Vector4 c, Vector4 d ) => a * w.x + b * w.y + c * w.z + d * w.w; - - #endregion - - #region Vector math - - /// The determinant is equivalent to the dot product, but with one vector rotated 90 degrees. - /// Note that det(a,b) != det(b,a). It's equivalent to a.x * b.y - a.y * b.x. - /// It is also known as the 2D Cross Product, Wedge Product, Outer Product and Perpendicular Dot Product - public static float Determinant /*or Cross*/( Vector2 a, Vector2 b ) => a.x * b.y - a.y * b.x; // 2D "cross product" - - /// Returns the direction and magnitude of the vector. Cheaper than calculating length and normalizing it separately - public static (Vector2 dir, float magnitude ) GetDirAndMagnitude( Vector2 v ) { - float magnitude = v.magnitude; - return ( v / magnitude, magnitude ); - } - - /// - public static (Vector3 dir, float magnitude ) GetDirAndMagnitude( Vector3 v ) { - float magnitude = v.magnitude; - return ( v / magnitude, magnitude ); - } - - /// Clamps the length of the vector between min and max - /// The vector to clamp - /// Minimum length - /// Maximum length - public static Vector2 ClampMagnitude( Vector2 v, float min, float max ) { - float mag = v.magnitude; - return mag < min ? ( v / mag ) * min : mag > max ? ( v / mag ) * max : v; - } - - /// - public static Vector3 ClampMagnitude( Vector3 v, float min, float max ) { - float mag = v.magnitude; - return mag < min ? ( v / mag ) * min : mag > max ? ( v / mag ) * max : v; - } - - /// Returns the average/center of the two input vectors - [MethodImpl( INLINE )] public static Vector2 Average( Vector2 a, Vector2 b ) => ( a + b ) / 2f; - - /// Returns the average/center of the two input vectors - [MethodImpl( INLINE )] public static Vector3 Average( Vector3 a, Vector3 b ) => ( a + b ) / 2f; - - /// Returns the average/halfway direction between the two input direction vectors. Note that this presumes both aDir and bDir have the same length - [MethodImpl( INLINE )] public static Vector2 AverageDir( Vector2 aDir, Vector2 bDir ) => ( aDir + bDir ).normalized; - - /// Returns the average/halfway direction between the two input direction vectors. Note that this presumes both aDir and bDir have the same length - [MethodImpl( INLINE )] public static Vector3 AverageDir( Vector3 aDir, Vector3 bDir ) => ( aDir + bDir ).normalized; - - /// Returns the squared distance between two points. - /// This is faster than the actual distance, and is useful when comparing distances where the absolute distance doesn't matter - [MethodImpl( INLINE )] public static float DistanceSquared( Vector2 a, Vector2 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square(); - - /// Returns the squared distance between two points. - /// This is faster than the actual distance, and is useful when comparing distances where the absolute distance doesn't matter - [MethodImpl( INLINE )] public static float DistanceSquared( Vector3 a, Vector3 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square() + ( a.z - b.z ).Square(); - - /// Returns the squared distance between two points. - /// This is faster than the actual distance, and is useful when comparing distances where the absolute distance doesn't matter - [MethodImpl( INLINE )] public static float DistanceSquared( Vector4 a, Vector4 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square() + ( a.z - b.z ).Square() + ( a.w - b.w ).Square(); - - #endregion - - #region Angles & Rotation - - /// Returns the direction of the input angle, as a normalized vector - /// The input angle, in radians - /// - [MethodImpl( INLINE )] public static Vector2 AngToDir( float aRad ) => new Vector2( Mathf.Cos( aRad ), Mathf.Sin( aRad ) ); - - /// Returns the angle of the input vector, in radians. You can also use myVector.Angle() - /// The vector to get the angle of. It does not have to be normalized - /// - [MethodImpl( INLINE )] public static float DirToAng( Vector2 vec ) => Mathf.Atan2( vec.y, vec.x ); - - /// Returns a 2D orientation from a vector, representing the X axis - /// The direction to create a 2D orientation from (does not have to be normalized) - [MethodImpl( INLINE )] public static Quaternion DirToOrientation( Vector2 v ) { - v.Normalize(); - v.x += 1; - v.Normalize(); - return new Quaternion( 0, 0, v.y, v.x ); - } - - /// Returns a 2D Pose from a point and a vector, representing the X axis - /// The location of the pose - /// The direction to create a 2D orientation from (does not have to be normalized) - [MethodImpl( INLINE )] public static Pose PointDirToPose( Vector2 pt, Vector2 v ) => new Pose( pt, DirToOrientation( v ) ); - - /// Linearly blends between two poses. The position will lerp, while the rotation will slerp. Note: this lerp is unclamped - /// Pose at t = 0 - /// Pose at t = 1 - /// The t-value to blend from a to b, from 0 to 1 (values outside will extrapolate) - public static Pose Lerp( Pose a, Pose b, float t ) => - new Pose( - Vector3.LerpUnclamped( a.position, b.position, t ), - Quaternion.SlerpUnclamped( a.rotation, b.rotation, t ) - ); - - /// Returns a matrix representing a 2D position and rotation - /// The location of the matrix - /// The direction of the X axis (has to be normalized) - [MethodImpl( INLINE )] public static Matrix4x4 GetMatrixFrom2DPointDir( Vector2 point, Vector2 tangent ) { - Vector2 N = tangent.Rotate90CCW(); - return new Matrix4x4( - new Vector4( tangent.x, tangent.y, 0, 0 ), - new Vector4( N.x, N.y, 0, 0 ), - new Vector4( 0, 0, 1, 0 ), - new Vector4( point.x, point.y, 0, 1 ) - ); - } - - /// Returns the signed curvature at a point in a curve, in radians per distance unit (equivalent to the reciprocal radius of the osculating circle) - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - [MethodImpl( INLINE )] public static float GetCurvature( Vector2 velocity, Vector2 acceleration ) { - float dMag = velocity.magnitude; - return Determinant( velocity, acceleration ) / ( dMag * dMag * dMag ); - } - - /// Returns a pseudovector of a point in a curve, where the magnitude is the curvature in radians per distance unit, and the direction is the axis of curvature - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - [MethodImpl( INLINE )] public static Vector3 GetCurvature( Vector3 velocity, Vector3 acceleration ) { - float dMag = velocity.magnitude; - return Vector3.Cross( velocity, acceleration ) / ( dMag * dMag * dMag ); - } - - /// Returns the torsion of a given point in a curve, in radians per distance unit - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - /// The third derivative of the point in the curve - [MethodImpl( INLINE )] public static float GetTorsion( Vector3 velocity, Vector3 acceleration, Vector3 jerk ) { - Vector3 cVector = Vector3.Cross( velocity, acceleration ); - return Vector3.Dot( cVector, jerk ) / cVector.sqrMagnitude; - } - - /// Returns the frenet-serret (curvature-based) normal direction at a given point in a curve - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - [MethodImpl( INLINE )] public static Vector3 GetArcNormal( Vector3 velocity, Vector3 acceleration ) => Vector3.Cross( velocity, Vector3.Cross( acceleration, velocity ) ).normalized; - - /// Returns the frenet-serret (curvature-based) binormal direction at a given point in a curve - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - [MethodImpl( INLINE )] public static Vector3 GetArcBinormal( Vector3 velocity, Vector3 acceleration ) => Vector3.Cross( velocity, acceleration ).normalized; - - /// Returns a normal direction given a reference up vector and a tangent direction - /// The tangent direction (does not have to be normalized) - /// The reference up vector. The normal will be perpendicular to both the supplied up vector and the curve - [MethodImpl( INLINE )] public static Vector3 GetNormalFromLookTangent( Vector3 tangent, Vector3 up ) => Vector3.Cross( up, tangent ).normalized; - - /// Returns the binormal from a vector, given a reference up vector. - /// The binormal will attempt to be as aligned with the reference vector as possible, - /// while still being perpendicular to the tangent - /// The tangent direction (does not have to be normalized) - /// The reference up vector. The normal will be perpendicular to both the supplied up vector and the tangent - [MethodImpl( INLINE )] public static Vector3 GetBinormalFromLookTangent( Vector3 tangent, Vector3 up ) { - Vector3 normal = Vector3.Cross( up, tangent ).normalized; - return Vector3.Cross( tangent.normalized, normal ); - } - - /// Returns the frenet-serret (curvature-based) orientation of a point in a curve with the given velocity and acceleration values, where the Z direction is tangent to the curve. - /// The X axis will point to the inner arc of the current curvature - /// The first derivative of the point in the curve - /// The second derivative of the point in the curve - [MethodImpl( INLINE )] public static Quaternion GetArcOrientation( Vector3 velocity, Vector3 acceleration ) { - Vector3 binormal = Vector3.Cross( velocity, acceleration ); - return Quaternion.LookRotation( velocity, binormal ); - } - - /// Returns the signed angle between a and b, in the range -tau/2 to tau/2 (-pi to pi) - [MethodImpl( INLINE )] public static float SignedAngle( Vector2 a, Vector2 b ) => AngleBetween( a, b ) * Mathf.Sign( Determinant( a, b ) ); // -tau/2 to tau/2 - - /// Returns the shortest angle between a and b, in the range 0 to tau/2 (0 to pi) - [MethodImpl( INLINE )] public static float AngleBetween( Vector2 a, Vector2 b ) => Mathf.Acos( Vector2.Dot( a.normalized, b.normalized ).ClampNeg1to1() ); - - /// - [MethodImpl( INLINE )] public static float AngleBetween( Vector3 a, Vector3 b ) => Mathf.Acos( Vector3.Dot( a.normalized, b.normalized ).ClampNeg1to1() ); - - /// Returns the clockwise angle between from and to, in the range 0 to tau (0 to 2*pi) - [MethodImpl( INLINE )] public static float AngleFromToCW( Vector2 from, Vector2 to ) => Determinant( from, to ) < 0 ? AngleBetween( from, to ) : TAU - AngleBetween( from, to ); - - /// Returns the counterclockwise angle between from and to, in the range 0 to tau (0 to 2*pi) - [MethodImpl( INLINE )] public static float AngleFromToCCW( Vector2 from, Vector2 to ) => Determinant( from, to ) > 0 ? AngleBetween( from, to ) : TAU - AngleBetween( from, to ); - - /// Blends between the aRad and bRad angles, based on the input t-value between 0 and 1 - /// The start value, in radians - /// The end value, in radians - /// The t-value between 0 and 1 - public static float LerpAngle( float aRad, float bRad, float t ) { - float delta = Repeat( ( bRad - aRad ), TAU ); - if( delta > PI ) - delta -= TAU; - return aRad + delta * Clamp01( t ); - } - - /// Returns the shortest angle between the two input angles, in radians - [MethodImpl( INLINE )] public static float DeltaAngle( float a, float b ) => ( b - a + PI ).Repeat( TAU ) - PI; - - /// Given an angle between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1 - /// The start angle of the range (in radians), where it would return 0 - /// The end angle of the range (in radians), where it would return 1 - /// An angle between a and b - public static float InverseLerpAngle( float a, float b, float v ) { - float angBetween = DeltaAngle( a, b ); - b = a + angBetween; // removes any a->b discontinuity - float h = a + angBetween * 0.5f; // halfway angle - v = h + DeltaAngle( h, v ); // get offset from h, and offset by h - return InverseLerpClamped( a, b, v ); - } - - #endregion - - #region Angular movement helpers - - /// Same as MoveTowards but makes sure the angles interpolate correctly when they wrap around a full turn. - /// Variables current and target are assumed to be in radians. - /// For optimization reasons, negative values of maxDelta are not supported and may cause oscillation. - /// To push current away from a target angle, add 180 to that angle instead. - /// The current angle - /// The angle to move towards - /// The maximum change that should be applied to the value - public static float MoveTowardsAngle( float current, float target, float maxDelta ) { - float deltaAngle = DeltaAngle( current, target ); - if( -maxDelta < deltaAngle && deltaAngle < maxDelta ) - return target; - target = current + deltaAngle; - return MoveTowards( current, target, maxDelta ); - } - - /// Gradually changes an angle given in radians towards a desired goal angle over time. - /// The value is smoothed by some spring-damper like function. - /// The function can be used to smooth any kind of value, positions, colors, scalars. The most common use is for smoothing a follow camera. - /// The current angle - /// The angle we are trying to reach - /// The current angular velocity, this value is modified by the function every time you call it - /// Approximately the time it will take to reach the target. A smaller value will reach the target faster - /// Optionally allows you to clamp the maximum speed - public static float SmoothDampAngle( float current, float target, ref float currentVelocity, float smoothTime, float maxSpeed = Infinity ) { - float deltaTime = Time.deltaTime; - return SmoothDampAngle( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); - } - - /// Gradually changes an angle given in radians towards a desired goal angle over time. - /// The value is smoothed by some spring-damper like function. - /// The function can be used to smooth any kind of value, positions, colors, scalars. The most common use is for smoothing a follow camera. - /// The current angle - /// The angle we are trying to reach - /// The current angular velocity, this value is modified by the function every time you call it - /// Approximately the time it will take to reach the target. A smaller value will reach the target faster - /// Optionally allows you to clamp the maximum speed - /// The time since the last call to this function. By default Time.deltaTime - public static float SmoothDampAngle( float current, float target, ref float currentVelocity, float smoothTime, [Uei.DefaultValue( "Mathf.Infinity" )] float maxSpeed, [Uei.DefaultValue( "Time.deltaTime" )] float deltaTime ) { - target = current + DeltaAngle( current, target ); - return SmoothDamp( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); - } - - #endregion - - #region Shape coordinate remapping - - /// Given a position within a -1 to 1 square, remaps it to the unit circle - /// The input position inside the square - public static Vector2 SquareToDisc( Vector2 c ) { - c.x = c.x.ClampNeg1to1(); - c.y = c.y.ClampNeg1to1(); - float u = c.x * Sqrt( 1 - ( c.y * c.y ) / 2 ); - float v = c.y * Sqrt( 1 - ( c.x * c.x ) / 2 ); - return new Vector2( u, v ); - } - - /// Given a position within the unit circle, remaps it to a square in the -1 to 1 range - /// The input position inside the circle - public static Vector2 DiscToSquare( Vector2 c ) { - c = c.ClampMagnitude( 0, 1 ); - float u2 = c.x * c.x; - float v2 = c.y * c.y; - Vector2 n = new Vector2( 1, -1 ); - Vector2 p = new Vector2( 2, 2 ) + n * ( u2 - v2 ); - Vector2 q = 2 * SQRT2 * c; - Vector2 smolVec = Vector2.one * 0.0001f; - return 0.5f * ( Vector2.Max( smolVec, p + q ).Sqrt() - Vector2.Max( smolVec, p - q ).Sqrt() ); - } - - #endregion - - } - +// Some of this code is similar to Unity's original Mathf source to match functionality. +// The original Mathf.cs source https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Mathf.cs +// ...and the trace amounts of it left in here is copyright (c) Unity Technologies with license: https://unity3d.com/legal/licenses/Unity_Reference_Only_License +// +// Collected and expanded upon to by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using UnityEngine; +using Uei = UnityEngine.Internal; +using System.Linq; // used for arbitrary count min/max functions, so it's safe and won't allocate garbage don't worry~ +using System.Runtime.CompilerServices; +using Unity.Mathematics; +using MidpointRounding = System.MidpointRounding; + +namespace Freya { + + /// The core math helper class. It has functions mostly for single values, but also vector helpers + public static class Mathfs { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + public static readonly bool[] bools = { false, true }; + + #region Constants + + /// The circle constant. Defined as the circumference of a circle divided by its radius. Equivalent to 2*pi + public const float TAU = 6.28318530717959f; + + /// An obscure circle constant. Defined as the circumference of a circle divided by its diameter. Equivalent to 0.5*tau + public const float PI = 3.14159265359f; + + /// Euler's number. The base of the natural logarithm. f(x)=e^x is equal to its own derivative + public const float E = 2.71828182846f; + + /// The golden ratio. It is the value of a/b where a/b = (a+b)/a. It's the positive root of x^2-x-1 + public const float GOLDEN_RATIO = 1.61803398875f; + + /// The square root of two. The length of the vector (1,1) + public const float SQRT2 = 1.41421356237f; + + /// The reciprocal of the square root of two. The components of a normalized (1,1) vector + public const float RSQRT2 = 1f / SQRT2; + + /// Multiply an angle in degrees by this, to convert it to radians + public const float Deg2Rad = TAU / 360f; + + /// Multiply an angle in radians by this, to convert it to degrees + public const float Rad2Deg = 360f / TAU; + + #endregion + + #region Math operations + + /// Returns the square root of the given value + [MethodImpl( INLINE )] public static float Sqrt( float value ) => MathF.Sqrt( value ); + + /// Returns the square root of each component + [MethodImpl( INLINE )] public static Vector2 Sqrt( Vector2 v ) => new Vector2( Sqrt( v.x ), Sqrt( v.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Sqrt( Vector3 v ) => new Vector3( Sqrt( v.x ), Sqrt( v.y ), Sqrt( v.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Sqrt( Vector4 v ) => new Vector4( Sqrt( v.x ), Sqrt( v.y ), Sqrt( v.z ), Sqrt( v.w ) ); + + /// Returns the cube root of the given value, properly handling negative values unlike Pow(v,1/3) + [MethodImpl( INLINE )] public static float Cbrt( float value ) => MathF.Cbrt( value ); + + /// Returns value raised to the power of exponent + [MethodImpl( INLINE )] public static float Pow( float value, float exponent ) => MathF.Pow( value, exponent ); + + /// Returns e to the power of the given value + [MethodImpl( INLINE )] public static float Exp( float power ) => MathF.Exp( power ); + + /// Returns 2 to the power of the given value + [MethodImpl( INLINE )] public static float Exp2( float power ) => MathF.Exp( power * 0.69314718056f ); + + /// Returns the logarithm of a value, with the given base + [MethodImpl( INLINE )] public static float Log( float value, float @base ) => MathF.Log( value, @base ); + + /// Returns the natural logarithm of the given value + [MethodImpl( INLINE )] public static float Log( float value ) => MathF.Log( value ); + + /// Returns the base 10 logarithm of the given value + [MethodImpl( INLINE )] public static float Log10( float value ) => MathF.Log10( value ); + + /// Returns the binomial coefficient n over k + public static ulong BinomialCoef( uint n, uint k ) { + // source: https://blog.plover.com/math/choose.html + ulong r = 1; + if( k > n ) return 0; + for( ulong d = 1; d <= k; d++ ) { + r *= n--; + r /= d; + } + + return r; + // mathematically clean but extremely prone to overflow + //return Factorial( n ) / ( Factorial( k ) * Factorial( n - k ) ); + } + + /// Returns the Factorial of a given value from 0 to 12 + /// A value between 0 and 12 (integers can't store the factorial of 13 or above) + [MethodImpl( INLINE )] public static int Factorial( uint value ) { + if( value <= 12 ) + return factorialInt[value]; + if( value <= 20 ) + throw new OverflowException( $"The Factorial of {value} is too big for integer representation, please use {nameof(FactorialLong)} instead" ); + throw new OverflowException( $"The Factorial of {value} is too big for integer representation" ); + } + + /// Returns the Factorial of a given value from 0 to 20 + /// A value between 0 and 20 (neither long nor ulong can store values large enough for the factorial of 21) + [MethodImpl( INLINE )] public static long FactorialLong( uint value ) { + if( value <= 20 ) + return factorialLong[value]; + throw new OverflowException( $"The Factorial of {value} is too big for integer representation, even unsigned longs, soooo, rip" ); + } + + static readonly long[] factorialLong = { + /*0*/ 1, + /*1*/ 1, + /*2*/ 2, + /*3*/ 6, + /*4*/ 24, + /*5*/ 120, + /*6*/ 720, + /*7*/ 5040, + /*8*/ 40320, + /*9*/ 362880, + /*10*/ 3628800, + /*11*/ 39916800, + /*12*/ 479001600, + /*13*/ 6227020800, + /*14*/ 87178291200, + /*15*/ 1307674368000, + /*16*/ 20922789888000, + /*17*/ 355687428096000, + /*18*/ 6402373705728000, + /*19*/ 121645100408832000, + /*20*/ 2432902008176640000 + }; + + static readonly int[] factorialInt = { + /*0*/ 1, + /*1*/ 1, + /*2*/ 2, + /*3*/ 6, + /*4*/ 24, + /*5*/ 120, + /*6*/ 720, + /*7*/ 5040, + /*8*/ 40320, + /*9*/ 362880, + /*10*/ 3628800, + /*11*/ 39916800, + /*12*/ 479001600 + }; + + #endregion + + #region Floating point shenanigans + + /// A very small value, used for various floating point inaccuracy thresholds + public static readonly float Epsilon = UnityEngineInternal.MathfInternal.IsFlushToZeroEnabled ? UnityEngineInternal.MathfInternal.FloatMinNormal : UnityEngineInternal.MathfInternal.FloatMinDenormal; + + /// float.PositiveInfinity + public const float Infinity = float.PositiveInfinity; + + /// float.NegativeInfinity + public const float NegativeInfinity = float.NegativeInfinity; + + /// Returns whether or not two values are approximately equal. + /// They are considered equal if they are within a Mathfs.Epsilon*8 or max(a,b)*0.000001f range of each other + /// The first value to compare + /// The second value to compare + [MethodImpl( INLINE )] public static bool Approximately( float a, float b ) => Abs( b - a ) < Max( 0.000001f * Max( Abs( a ), Abs( b ) ), Epsilon * 8 ); + + /// + [MethodImpl( INLINE )] public static bool Approximately( Vector2 a, Vector2 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ); + + /// + [MethodImpl( INLINE )] public static bool Approximately( Vector3 a, Vector3 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ) && Approximately( a.z, b.z ); + + /// + [MethodImpl( INLINE )] public static bool Approximately( Vector4 a, Vector4 b ) => Approximately( a.x, b.x ) && Approximately( a.y, b.y ) && Approximately( a.z, b.z ) && Approximately( a.w, b.w ); + + /// + [MethodImpl( INLINE )] public static bool Approximately( Color a, Color b ) => Approximately( a.r, b.r ) && Approximately( a.g, b.g ) && Approximately( a.b, b.b ) && Approximately( a.a, b.a ); + + #endregion + + #region Trigonometry + + /// Returns the cosine of the given angle. Equivalent to the x-component of a unit vector with the same angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Cos( float angRad ) => MathF.Cos( angRad ); + + /// Returns the sine of the given angle. Equivalent to the y-component of a unit vector with the same angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Sin( float angRad ) => MathF.Sin( angRad ); + + /// Returns the tangent of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Tan( float angRad ) => MathF.Tan( angRad ); + + /// Returns the arc cosine of the given value, in radians + /// A value between -1 and 1 + [MethodImpl( INLINE )] public static float Acos( float value ) => MathF.Acos( value ); + + /// Returns the arc sine of the given value, in radians + /// A value between -1 and 1 + [MethodImpl( INLINE )] public static float Asin( float value ) => MathF.Asin( value ); + + /// Returns the arc tangent of the given value, in radians + /// A value between -1 and 1 + [MethodImpl( INLINE )] public static float Atan( float value ) => MathF.Atan( value ); + + /// Returns the angle of a vector. I don't recommend using this function, it's confusing~ Use Mathfs.DirToAng instead + /// The y component of the vector. They're flipped yeah I know but this is how everyone implements if for some godforsaken reason + /// The x component of the vector. They're flipped yeah I know but this is how everyone implements if for some godforsaken reason + [MethodImpl( INLINE )] public static float Atan2( float y, float x ) => MathF.Atan2( y, x ); + + /// Returns the cosecant of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Csc( float angRad ) => 1f / MathF.Sin( angRad ); + + /// Returns the secant of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Sec( float angRad ) => 1f / MathF.Cos( angRad ); + + /// Returns the cotangent of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Cot( float angRad ) => 1f / MathF.Tan( angRad ); + + /// Returns the versine of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Ver( float angRad ) => 1 - MathF.Cos( angRad ); + + /// Returns the coversine of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Cvs( float angRad ) => 1 - MathF.Sin( angRad ); + + /// Returns the chord of the given angle + /// Angle in radians + [MethodImpl( INLINE )] public static float Crd( float angRad ) => 2 * MathF.Sin( angRad / 2 ); + + const double SINC_W = 0.01; + const double SINC_P_C2 = -1 / 6.0; + const double SINC_P_C4 = 1 / 120.0; + const double SINCRCP_P_C2 = 1 / 6.0; + const double SINCRCP_P_C4 = 7 / 360.0; + + /// The unnormalized sinc function sin(x)/x, properly handling the removable singularity around x = 0 + /// The input value for the Sinc function + public static float Sinc( float x ) => (float)Sinc( (double)x ); + + /// + public static double Sinc( double x ) { + x = Math.Abs( x ); // sinc is symmetric + if( x < SINC_W ) { + // approximate the singularity w. a polynomial + double x2 = x * x; + double x4 = x2 * x2; + return 1 + SINC_P_C2 * x2 + SINC_P_C4 * x4; + } + + return Math.Sin( x ) / x; + } + + /// The unnormalized cosinc or cosc function (1-cos(x))/x, properly handling the removable singularity around x = 0 + /// The input value for the Cosinc function + public static float Cosinc( float x ) => (float)Cosinc( (double)x ); + + /// + public static double Cosinc( double x ) { + if( Math.Abs( x ) < 0.01 ) + return x / 2 - ( x * x * x ) / 24; // approximate the singularity w. a polynomial, based on the taylor series expansion + return ( 1 - Math.Cos( x ) ) / x; + } + + /// The unnormalized reciprocal sinc function x/sin(x), properly handling the removable singularity around x = 0 + /// The input value for the reciprocal Sinc function + public static float SincRcp( float x ) => (float)SincRcp( (double)x ); + + /// + public static double SincRcp( double x ) { + x = Math.Abs( x ); // sinc is symmetric + if( x < SINC_W ) { + // approximate the singularity w. a polynomial + double x2 = x * x; + double x4 = x2 * x2; + return 1 + SINCRCP_P_C2 * x2 + SINCRCP_P_C4 * x4; + } + + return x / Math.Sin( x ); + } + + #endregion + + #region Hyperbolic Trigonometry + + /// Returns the hyperbolic cosine of the given hyperbolic angle + [MethodImpl( INLINE )] public static float Cosh( float x ) => MathF.Cosh( x ); + + /// Returns the hyperbolic sine of the given hyperbolic angle + [MethodImpl( INLINE )] public static float Sinh( float x ) => MathF.Sinh( x ); + + /// Returns the hyperbolic tangent of the given hyperbolic angle + [MethodImpl( INLINE )] public static float Tanh( float x ) => MathF.Tanh( x ); + + /// Returns the hyperbolic arc cosine of the given value + [MethodImpl( INLINE )] public static float Acosh( float x ) => MathF.Acosh( x ); + + /// Returns the hyperbolic arc sine of the given value + [MethodImpl( INLINE )] public static float Asinh( float x ) => MathF.Asinh( x ); + + /// Returns the hyperbolic arc tangent of the given value + [MethodImpl( INLINE )] public static float Atanh( float x ) => MathF.Atanh( x ); + + #endregion + + #region Geometric Algebra + + /// Returns the wedge product between two vectors + public static float Wedge( Vector2 a, Vector2 b ) => a.x * b.y - a.y * b.x; + + /// Returns the wedge product between two vectors + public static Bivector3 Wedge( Vector3 a, Vector3 b ) => + new Bivector3( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x + ); + + /// Returns the geometric product between two vectors + public static Rotor3 GeometricProduct( Vector3 a, Vector3 b ) => new Rotor3( Vector3.Dot( a, b ), Wedge( a, b ) ); + + #endregion + + #region Absolute Values + + /// Returns the absolute value. Basically makes negative numbers positive + [MethodImpl( INLINE )] public static float Abs( float value ) => Math.Abs( value ); + + /// + [MethodImpl( INLINE )] public static int Abs( int value ) => Math.Abs( value ); + + /// Returns the absolute value, per component. Basically makes negative numbers positive + [MethodImpl( INLINE )] public static Vector2 Abs( Vector2 v ) => new Vector2( Abs( v.x ), Abs( v.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Abs( Vector3 v ) => new Vector3( Abs( v.x ), Abs( v.y ), Abs( v.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Abs( Vector4 v ) => new Vector4( Abs( v.x ), Abs( v.y ), Abs( v.z ), Abs( v.w ) ); + + #endregion + + #region Clamping + + /// Returns the value clamped between min and max + /// The value to clamp + /// The minimum value + /// The maximum value + public static float Clamp( float value, float min, float max ) => value < min ? min : value > max ? max : value; + + /// Clamps each component between min and max + public static Vector2 Clamp( Vector2 v, Vector2 min, Vector2 max ) => + new Vector2( + v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, + v.y < min.y ? min.y : v.y > max.y ? max.y : v.y + ); + + /// + public static Vector3 Clamp( Vector3 v, Vector3 min, Vector3 max ) => + new Vector3( + v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, + v.y < min.y ? min.y : v.y > max.y ? max.y : v.y, + v.z < min.z ? min.z : v.z > max.z ? max.z : v.z + ); + + /// + public static Vector4 Clamp( Vector4 v, Vector4 min, Vector4 max ) => + new Vector4( + v.x < min.x ? min.x : v.x > max.x ? max.x : v.x, + v.y < min.y ? min.y : v.y > max.y ? max.y : v.y, + v.z < min.z ? min.z : v.z > max.z ? max.z : v.z, + v.w < min.w ? min.w : v.w > max.w ? max.w : v.w + ); + + /// + public static int Clamp( int value, int min, int max ) => value < min ? min : value > max ? max : value; + + /// Returns the value clamped between 0 and 1 + public static float Clamp01( float value ) => value < 0 ? 0 : value > 1 ? 1 : value; + + /// + public static double Clamp01( double value ) => value < 0 ? 0 : value > 1 ? 1 : value; + + /// Clamps each component between 0 and 1 + public static Vector2 Clamp01( Vector2 v ) => + new Vector2( + v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, + v.y < 0f ? 0f : v.y > 1f ? 1f : v.y + ); + + /// + public static Vector3 Clamp01( Vector3 v ) => + new Vector3( + v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, + v.y < 0f ? 0f : v.y > 1f ? 1f : v.y, + v.z < 0f ? 0f : v.z > 1f ? 1f : v.z + ); + + /// + public static Vector4 Clamp01( Vector4 v ) => + new Vector4( + v.x < 0f ? 0f : v.x > 1f ? 1f : v.x, + v.y < 0f ? 0f : v.y > 1f ? 1f : v.y, + v.z < 0f ? 0f : v.z > 1f ? 1f : v.z, + v.w < 0f ? 0f : v.w > 1f ? 1f : v.w + ); + + /// Clamps the value between -1 and 1 + public static double ClampNeg1to1( double value ) => value < -1.0 ? -1.0 : value > 1.0 ? 1.0 : value; + + /// Clamps the value between -1 and 1 + public static float ClampNeg1to1( float value ) => value < -1f ? -1f : value > 1f ? 1f : value; + + /// Clamps each component between -1 and 1 + public static Vector2 ClampNeg1to1( Vector2 v ) => + new Vector2( + v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, + v.y < -1f ? -1f : v.y > 1f ? 1f : v.y + ); + + /// Clamps each component between -1 and 1 + public static Vector3 ClampNeg1to1( Vector3 v ) => + new Vector3( + v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, + v.y < -1f ? -1f : v.y > 1f ? 1f : v.y, + v.z < -1f ? -1f : v.z > 1f ? 1f : v.z + ); + + /// Clamps each component between -1 and 1 + public static Vector4 ClampNeg1to1( Vector4 v ) => + new Vector4( + v.x < -1f ? -1f : v.x > 1f ? 1f : v.x, + v.y < -1f ? -1f : v.y > 1f ? 1f : v.y, + v.z < -1f ? -1f : v.z > 1f ? 1f : v.z, + v.w < -1f ? -1f : v.w > 1f ? 1f : v.w + ); + + #endregion + + #region Min & Max + + /// Returns the smallest of the two values + [MethodImpl( INLINE )] public static float Min( float a, float b ) => a < b ? a : b; + + /// Returns the smallest of the three values + [MethodImpl( INLINE )] public static float Min( float a, float b, float c ) => Min( Min( a, b ), c ); + + /// Returns the smallest of the four values + [MethodImpl( INLINE )] public static float Min( float a, float b, float c, float d ) => Min( Min( a, b ), Min( c, d ) ); + + /// Returns the largest of the two values + [MethodImpl( INLINE )] public static float Max( float a, float b ) => a > b ? a : b; + + /// Returns the largest of the three values + [MethodImpl( INLINE )] public static float Max( float a, float b, float c ) => Max( Max( a, b ), c ); + + /// Returns the largest of the four values + [MethodImpl( INLINE )] public static float Max( float a, float b, float c, float d ) => Max( Max( a, b ), Max( c, d ) ); + + /// Returns the smallest of the two values + [MethodImpl( INLINE )] public static int Min( int a, int b ) => a < b ? a : b; + + /// Returns the smallest of the three values + [MethodImpl( INLINE )] public static int Min( int a, int b, int c ) => Min( Min( a, b ), c ); + + /// Returns the smallest of the four values + [MethodImpl( INLINE )] public static int Min( int a, int b, int c, int d ) => Min( Min( a, b ), Min( c, d ) ); + + /// Returns the largest of the two values + [MethodImpl( INLINE )] public static int Max( int a, int b ) => a > b ? a : b; + + /// Returns the largest of the three values + [MethodImpl( INLINE )] public static int Max( int a, int b, int c ) => Max( Max( a, b ), c ); + + /// Returns the largest of the four values + [MethodImpl( INLINE )] public static int Max( int a, int b, int c, int d ) => Max( Max( a, b ), Max( c, d ) ); + + /// Returns the smallest of the given values + [MethodImpl( INLINE )] public static float Min( params float[] values ) => values.Min(); + + /// Returns the largest of the given values + [MethodImpl( INLINE )] public static float Max( params float[] values ) => values.Max(); + + /// Returns the smallest of the given values + [MethodImpl( INLINE )] public static int Min( params int[] values ) => values.Min(); + + /// Returns the largest of the given values + [MethodImpl( INLINE )] public static int Max( params int[] values ) => values.Max(); + + /// Returns the minimum value of all components in the vector + [MethodImpl( INLINE )] public static float Min( Vector2 v ) => Min( v.x, v.y ); + + /// + [MethodImpl( INLINE )] public static float Min( Vector3 v ) => Min( v.x, v.y, v.z ); + + /// + [MethodImpl( INLINE )] public static float Min( Vector4 v ) => Min( v.x, v.y, v.z, v.w ); + + /// Returns the maximum value of all components in the vector + [MethodImpl( INLINE )] public static float Max( Vector2 v ) => Max( v.x, v.y ); + + /// + [MethodImpl( INLINE )] public static float Max( Vector3 v ) => Max( v.x, v.y, v.z ); + + /// + [MethodImpl( INLINE )] public static float Max( Vector4 v ) => Max( v.x, v.y, v.z, v.w ); + + #endregion + + #region Signs & Rounding + + /// The sign of the value. Returns -1 if negative, returns 1 if greater than or equal to 0 + [MethodImpl( INLINE )] public static float Sign( float value ) => value >= 0f ? 1 : -1; + + /// The sign of each component. Returns -1 if negative, returns 1 if greater than or equal to 0 + [MethodImpl( INLINE )] public static Vector2 Sign( Vector2 value ) => new Vector2( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1 ); + + /// + [MethodImpl( INLINE )] public static Vector3 Sign( Vector3 value ) => new Vector3( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1, value.z >= 0f ? 1 : -1 ); + + /// + [MethodImpl( INLINE )] public static Vector4 Sign( Vector4 value ) => new Vector4( value.x >= 0f ? 1 : -1, value.y >= 0f ? 1 : -1, value.z >= 0f ? 1 : -1, value.w >= 0f ? 1 : -1 ); + + /// Returns the sign of the value, either -1 if negative, or 1 if positive or 0 + [MethodImpl( INLINE )] public static int Sign( int value ) => value >= 0 ? 1 : -1; + + /// The sign of the value as an integer. Returns -1 if negative, returns 1 if greater than or equal to 0 + [MethodImpl( INLINE )] public static int SignAsInt( float value ) => value >= 0f ? 1 : -1; + + /// The sign of the value. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive + [MethodImpl( INLINE )] public static float SignWithZero( float value, float zeroThreshold = 0.000001f ) => Abs( value ) < zeroThreshold ? 0 : Sign( value ); + + /// The sign of each component. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive + [MethodImpl( INLINE )] public static Vector2 SignWithZero( Vector2 value, float zeroThreshold = 0.000001f ) => + new Vector2( + Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), + Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ) + ); + + /// + [MethodImpl( INLINE )] public static Vector3 SignWithZero( Vector3 value, float zeroThreshold = 0.000001f ) => + new Vector3( + Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), + Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ), + Abs( value.z ) < zeroThreshold ? 0 : Sign( value.z ) + ); + + /// + [MethodImpl( INLINE )] public static Vector4 SignWithZero( Vector4 value, float zeroThreshold = 0.000001f ) => + new Vector4( + Abs( value.x ) < zeroThreshold ? 0 : Sign( value.x ), + Abs( value.y ) < zeroThreshold ? 0 : Sign( value.y ), + Abs( value.z ) < zeroThreshold ? 0 : Sign( value.z ), + Abs( value.w ) < zeroThreshold ? 0 : Sign( value.w ) + ); + + /// Returns the sign of the value, either -1 if negative, 0 if zero, 1 if positive + [MethodImpl( INLINE )] public static int SignWithZero( int value ) => value == 0 ? 0 : Sign( value ); + + /// The sign of the value. Returns -1 if negative, return 0 if zero (or within the given threshold), returns 1 if positive + [MethodImpl( INLINE )] public static int SignWithZeroAsInt( float value, float zeroThreshold = 0.000001f ) => Abs( value ) < zeroThreshold ? 0 : SignAsInt( value ); + + /// Rounds the value down to the nearest integer + [MethodImpl( INLINE )] public static float Floor( float value ) => MathF.Floor( value ); + + /// Rounds the vector components down to the nearest integer + [MethodImpl( INLINE )] public static Vector2 Floor( Vector2 value ) => new Vector2( MathF.Floor( value.x ), MathF.Floor( value.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Floor( Vector3 value ) => new Vector3( MathF.Floor( value.x ), MathF.Floor( value.y ), MathF.Floor( value.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Floor( Vector4 value ) => new Vector4( MathF.Floor( value.x ), MathF.Floor( value.y ), MathF.Floor( value.z ), MathF.Floor( value.w ) ); + + /// Rounds the value down to the nearest integer, returning an int value + [MethodImpl( INLINE )] public static int FloorToInt( float value ) => (int)Math.Floor( value ); + + /// Rounds the vector components down to the nearest integer, returning an integer vector + [MethodImpl( INLINE )] public static Vector2Int FloorToInt( Vector2 value ) => new Vector2Int( (int)Math.Floor( value.x ), (int)Math.Floor( value.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3Int FloorToInt( Vector3 value ) => new Vector3Int( (int)Math.Floor( value.x ), (int)Math.Floor( value.y ), (int)Math.Floor( value.z ) ); + + /// Rounds the value up to the nearest integer + [MethodImpl( INLINE )] public static float Ceil( float value ) => MathF.Ceiling( value ); + + /// Rounds the vector components up to the nearest integer + [MethodImpl( INLINE )] public static Vector2 Ceil( Vector2 value ) => new Vector2( MathF.Ceiling( value.x ), MathF.Ceiling( value.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Ceil( Vector3 value ) => new Vector3( MathF.Ceiling( value.x ), MathF.Ceiling( value.y ), MathF.Ceiling( value.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Ceil( Vector4 value ) => new Vector4( MathF.Ceiling( value.x ), MathF.Ceiling( value.y ), MathF.Ceiling( value.z ), MathF.Ceiling( value.w ) ); + + /// Rounds the value up to the nearest integer, returning an int value + [MethodImpl( INLINE )] public static int CeilToInt( float value ) => (int)Math.Ceiling( value ); + + /// Rounds the vector components up to the nearest integer, returning an integer vector + [MethodImpl( INLINE )] public static Vector2Int CeilToInt( Vector2 value ) => new Vector2Int( (int)Math.Ceiling( value.x ), (int)Math.Ceiling( value.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3Int CeilToInt( Vector3 value ) => new Vector3Int( (int)Math.Ceiling( value.x ), (int)Math.Ceiling( value.y ), (int)Math.Ceiling( value.z ) ); + + /// Rounds the value to the nearest integer + [MethodImpl( INLINE )] public static float Round( float value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => (float)MathF.Round( value, midpointRounding ); + + /// Rounds the vector components to the nearest integer + [MethodImpl( INLINE )] public static Vector2 Round( Vector2 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector2( MathF.Round( value.x, midpointRounding ), MathF.Round( value.y, midpointRounding ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Round( Vector3 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector3( MathF.Round( value.x, midpointRounding ), MathF.Round( value.y, midpointRounding ), MathF.Round( value.z, midpointRounding ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Round( Vector4 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector4( MathF.Round( value.x, midpointRounding ), MathF.Round( value.y, midpointRounding ), MathF.Round( value.z, midpointRounding ), MathF.Round( value.w, midpointRounding ) ); + + /// Rounds the value to the nearest value, snapped to the given interval size + [MethodImpl( INLINE )] public static float Round( float value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => MathF.Round( value / snapInterval, midpointRounding ) * snapInterval; + + /// Rounds the vector components to the nearest value, snapped to the given interval size + [MethodImpl( INLINE )] public static Vector2 Round( Vector2 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector2( Round( value.x, snapInterval, midpointRounding ), Round( value.y, snapInterval, midpointRounding ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Round( Vector3 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new(Round( value.x, snapInterval, midpointRounding ), Round( value.y, snapInterval, midpointRounding ), Round( value.z, snapInterval, midpointRounding )); + + /// + [MethodImpl( INLINE )] public static Vector4 Round( Vector4 value, float snapInterval, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector4( Round( value.x, snapInterval, midpointRounding ), Round( value.y, snapInterval, midpointRounding ), Round( value.z, snapInterval, midpointRounding ), Round( value.w, snapInterval, midpointRounding ) ); + + /// Rounds the value to the nearest integer, returning an int value + [MethodImpl( INLINE )] public static int RoundToInt( float value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => (int)Math.Round( value, midpointRounding ); + + /// Rounds the vector components to the nearest integer, returning an integer vector + [MethodImpl( INLINE )] public static Vector2Int RoundToInt( Vector2 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector2Int( (int)Math.Round( value.x, midpointRounding ), (int)Math.Round( value.y, midpointRounding ) ); + + /// + [MethodImpl( INLINE )] public static Vector3Int RoundToInt( Vector3 value, System.MidpointRounding midpointRounding = System.MidpointRounding.ToEven ) => new Vector3Int( (int)Math.Round( value.x, midpointRounding ), (int)Math.Round( value.y, midpointRounding ), (int)Math.Round( value.z, midpointRounding ) ); + + #endregion + + #region Range Repeating + + /// Returns the fractional part of the value. Equivalent to x - floor(x) + [MethodImpl( INLINE )] public static float Frac( float x ) => x - Floor( x ); + + /// Returns the fractional part of the value for each component. Equivalent to v - floor(v) + [MethodImpl( INLINE )] public static Vector2 Frac( Vector2 v ) => new Vector2( v.x - Floor( v.x ), v.y - Floor( v.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Frac( Vector3 v ) => new Vector3( v.x - Floor( v.x ), v.y - Floor( v.y ), v.z - Floor( v.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Frac( Vector4 v ) => new Vector4( v.x - Floor( v.x ), v.y - Floor( v.y ), v.z - Floor( v.z ), v.w - Floor( v.w ) ); + + /// Repeats the given value in the interval specified by length + [MethodImpl( INLINE )] public static float Repeat( float value, float length ) => Clamp( value - Floor( value / length ) * length, 0.0f, length ); + + /// Modulo, but, behaves the way you want with negative values, for stuff like array[(n+1)%length] etc. + [MethodImpl( INLINE )] public static int Mod( int value, int length ) => value >= 0 ? value % length : ( value % length + length ) % length; + + /// Repeats a value within a range, going back and forth + [MethodImpl( INLINE )] public static float PingPong( float t, float length ) => length - Abs( Repeat( t, length * 2f ) - length ); + + /// Returns the height of in a triangle wave at time t going from 0 to 1 and back to 0 within the the given period + [MethodImpl( INLINE )] public static float TriangleWave( float t, float period = 1f ) { + float x = t / period; + return 1f - Abs( 2 * ( x - Floor( x ) ) - 1 ); + } + + /// Returns the greatest common divisor of the two numbers + public static int Gcd( int a, int b ) { + // special case bc we can't negate int.MinValue + if( a == int.MinValue || b == int.MinValue ) { + if( a == int.MinValue && b == int.MinValue ) + return int.MinValue; // the only negative return value, bc we can't negate this number + int v = Max( a, b ).Abs(); + return v & -v; + } + + if( a == b ) + return a.Abs(); + ( a, b ) = ( Abs( a ), Abs( b ) ); + while( a != 0 && b != 0 ) + _ = a > b ? a %= b : b %= a; + return a | b; + } + + #endregion + + #region Smoothing & Easing Curves + + /// Applies cubic smoothing to the 0-1 interval, also known as the smoothstep function. Similar to an EaseInOut operation + [MethodImpl( INLINE )] public static float Smooth01( float x ) => x * x * ( 3 - 2 * x ); + + /// Applies quintic smoothing to the 0-1 interval, also known as the smootherstep function. Similar to an EaseInOut operation + [MethodImpl( INLINE )] public static float Smoother01( float x ) => x * x * x * ( x * ( x * 6 - 15 ) + 10 ); + + /// Applies trigonometric smoothing to the 0-1 interval. Similar to an EaseInOut operation + [MethodImpl( INLINE )] public static float SmoothCos01( float x ) => Cos( x * PI ) * -0.5f + 0.5f; + + /// Applies a gamma curve or something idk I've never used this function before but it was part of Unity's original Mathfs.cs and it's undocumented + public static float Gamma( float value, float absmax, float gamma ) { + bool negative = value < 0F; + float absval = Abs( value ); + if( absval > absmax ) + return negative ? -absval : absval; + + float result = Pow( absval / absmax, gamma ) * absmax; + return negative ? -result : result; + } + + #endregion + + #region Value & Vector interpolation + + /// Blends between a and b, based on the t-value. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly + /// The start value, when t is 0 + /// The start value, when t is 1 + /// The t-value from 0 to 1 representing position along the lerp + [MethodImpl( INLINE )] public static float Lerp( float a, float b, float t ) => ( 1f - t ) * a + t * b; + + /// Blends between a and b of each component, based on the t-value of each component in the t-vector. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly + /// The start value, when t is 0 + /// The start value, when t is 1 + /// The t-values from 0 to 1 representing position along the lerp + [MethodImpl( INLINE )] public static Vector2 Lerp( Vector2 a, Vector2 b, Vector2 t ) => new Vector2( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Lerp( Vector3 a, Vector3 b, Vector3 t ) => new Vector3( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ), Lerp( a.z, b.z, t.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Lerp( Vector4 a, Vector4 b, Vector4 t ) => new Vector4( Lerp( a.x, b.x, t.x ), Lerp( a.y, b.y, t.y ), Lerp( a.z, b.z, t.z ), Lerp( a.w, b.w, t.w ) ); + + /// Linearly blends between two rectangles, moving and resizing from the center. Note: this lerp is unclamped + /// The start value, when t is 0 + /// The start value, when t is 1 + /// The t-values from 0 to 1 representing position along the lerp + public static Rect Lerp( Rect a, Rect b, float t ) { + Vector2 center = Vector2.LerpUnclamped( a.center, b.center, t ); + Vector2 size = Vector2.LerpUnclamped( a.size, b.size, t ); + return new Rect( default, size ) { center = center }; + } + + /// Blends between a and b, based on the t-value. When t = 0 it returns a, when t = 1 it returns b, and any values between are blended linearly + /// The start value, when t is 0 + /// The start value, when t is 1 + /// The t-value from 0 to 1 representing position along the lerp, clamped between 0 and 1 + [MethodImpl( INLINE )] public static float LerpClamped( float a, float b, float t ) => Lerp( a, b, Clamp01( t ) ); + + /// Lerps between a and b, applying cubic smoothing to the t-value + /// The start value, when t is 0 + /// The start value, when t is 1 + /// The t-value from 0 to 1 representing position along the lerp, clamped between 0 and 1 + [MethodImpl( INLINE )] public static float LerpSmooth( float a, float b, float t ) => Lerp( a, b, Smooth01( Clamp01( t ) ) ); + + /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1 + /// The start of the range, where it would return 0 + /// The end of the range, where it would return 1 + /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated + [MethodImpl( INLINE )] public static float InverseLerp( float a, float b, float value ) => ( value - a ) / ( b - a ); + + /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1. + /// This safe version returns 0 if a == b, instead of a division by zero + /// The start of the range, where it would return 0 + /// The end of the range, where it would return 1 + /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated + [MethodImpl( INLINE )] public static float InverseLerpSafe( float a, float b, float value ) { + float den = b - a; + if( den == 0 ) + return 0; + return ( value - a ) / den; + } + + /// Given values between a and b in each component, returns their normalized locations in the given ranges, as t-values (interpolants) from 0 to 1 + /// The start of the ranges, where it would return 0 + /// The end of the ranges, where it would return 1 + /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated + [MethodImpl( INLINE )] public static Vector2 InverseLerp( Vector2 a, Vector2 b, Vector2 v ) => new Vector2( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 InverseLerp( Vector3 a, Vector3 b, Vector3 v ) => new Vector3( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ), ( v.z - a.z ) / ( b.z - a.z ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 InverseLerp( Vector4 a, Vector4 b, Vector4 v ) => new Vector4( ( v.x - a.x ) / ( b.x - a.x ), ( v.y - a.y ) / ( b.y - a.y ), ( v.z - a.z ) / ( b.z - a.z ), ( v.w - a.w ) / ( b.w - a.w ) ); + + /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) clamped between 0 and 1 + /// The start of the range, where it would return 0 + /// The end of the range, where it would return 1 + /// A value between a and b + [MethodImpl( INLINE )] public static float InverseLerpClamped( float a, float b, float value ) => Clamp01( ( value - a ) / ( b - a ) ); + + /// + [MethodImpl( INLINE )] public static double InverseLerpClamped( double a, double b, double value ) => Clamp01( ( value - a ) / ( b - a ) ); + + /// Given a value between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1, with cubic smoothing applied. + /// Equivalent to "smoothstep" in shader code + /// The start of the range, where it would return 0 + /// The end of the range, where it would return 1 + /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated + [MethodImpl( INLINE )] public static float InverseLerpSmooth( float a, float b, float value ) => Smooth01( Clamp01( ( value - a ) / ( b - a ) ) ); + + /// Remaps a value from the input range [iMin to iMax] into the output range [oMin to oMax]. + /// Equivalent to Lerp(oMin,oMax,InverseLerp(iMin,iMax,value)) + /// The start value of the input range + /// The end value of the input range + /// The start value of the output range + /// The end value of the output range + /// The value to remap + [MethodImpl( INLINE )] public static float Remap( float iMin, float iMax, float oMin, float oMax, float value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); + + /// + [MethodImpl( INLINE )] public static float Remap( float iMin, float iMax, float oMin, float oMax, int value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); + + /// Remaps values from the input range [iMin to iMax] into the output range [oMin to oMax] on a per-component basis. + /// Equivalent to Lerp(oMin,oMax,InverseLerp(iMin,iMax,value)) + /// The start values of the input ranges + /// The end values of the input ranges + /// The start values of the output ranges + /// The end values of the output ranges + /// The values to remap + [MethodImpl( INLINE )] public static Vector2 Remap( Vector2 iMin, Vector2 iMax, Vector2 oMin, Vector2 oMax, Vector2 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); + + /// + [MethodImpl( INLINE )] public static Vector3 Remap( Vector3 iMin, Vector3 iMax, Vector3 oMin, Vector3 oMax, Vector3 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); + + /// + [MethodImpl( INLINE )] public static Vector4 Remap( Vector4 iMin, Vector4 iMax, Vector4 oMin, Vector4 oMax, Vector4 value ) => Lerp( oMin, oMax, InverseLerp( iMin, iMax, value ) ); + + /// Remaps a value from the input range [iMin to iMax] into the output range [oMin to oMax], clamping to make sure it does not extrapolate. + /// Equivalent to Lerp(oMin,oMax,InverseLerpClamped(iMin,iMax,value)) + /// The start value of the input range + /// The end value of the input range + /// The start value of the output range + /// The end value of the output range + /// The value to remap + [MethodImpl( INLINE )] public static float RemapClamped( float iMin, float iMax, float oMin, float oMax, float value ) => Lerp( oMin, oMax, InverseLerpClamped( iMin, iMax, value ) ); + + /// Remaps a value from the input Rect to the output Rect + /// The input Rect + /// The output Rect + /// The input position in the input Rect space + [MethodImpl( INLINE )] public static Vector2 Remap( Rect iRect, Rect oRect, Vector2 iPos ) => Remap( iRect.min, iRect.max, oRect.min, oRect.max, iPos ); + + /// Remaps a value from the input Bounds to the output Bounds + /// The input Bounds + /// The output Bounds + /// The input position in the input Bounds space + [MethodImpl( INLINE )] public static Vector3 Remap( Bounds iBounds, Bounds oBounds, Vector3 iPos ) => Remap( iBounds.min, iBounds.max, oBounds.min, oBounds.max, iPos ); + + /// Remaps a value from the input range to the output range + /// The input range + /// The output range + /// The value to remap from the input range + [MethodImpl( INLINE )] public static float Remap( FloatRange inRange, FloatRange outRange, float value ) => Remap( inRange.a, inRange.b, outRange.a, outRange.b, value ); + + /// Remaps a value from the input range to the output range, clamping to make sure it does not extrapolate. + /// The input range + /// The output range + /// The value to remap from the input range + [MethodImpl( INLINE )] public static float RemapClamped( FloatRange inRange, FloatRange outRange, float value ) => RemapClamped( inRange.a, inRange.b, outRange.a, outRange.b, value ); + + /// Exponential interpolation, the multiplicative version of lerp, useful for values such as scaling or zooming + /// The start value + /// The end value + /// The t-value from 0 to 1 representing position along the eerp + [MethodImpl( INLINE )] public static float Eerp( float a, float b, float t ) => + t switch { + 0f => a, + 1f => b, + _ => a * MathF.Exp( MathF.Log( b / a ) * t ) // same as exp( lerp(ln a, ln b, t) ), but without numeric issues! + }; + + /// Harmonic interpolation + /// The start value + /// The end value + /// The t-value from 0 to 1 representing position along the eerp + [MethodImpl( INLINE )] public static float Herp( float a, float b, float t ) => + t switch { + 0f => a, + 1f => b, + _ => ( a * b ) / ( t * ( a - b ) + b ) + }; + + /// + [MethodImpl( INLINE )] public static Vector3 Eerp( Vector3 a, Vector3 b, float t ) => + t switch { + 0f => a, + 1f => b, + _ => new Vector3( + Eerp( a.x, b.x, t ), + Eerp( a.y, b.y, t ), + Eerp( a.z, b.z, t ) + ) + }; + + /// Inverse exponential interpolation, the multiplicative version of InverseLerp, useful for values such as scaling or zooming + /// The start value + /// The end value + /// A value between a and b. Note: values outside this range are still valid, and will be extrapolated + [MethodImpl( INLINE )] public static float InverseEerp( float a, float b, float v ) { + if( v == a ) + return 0f; + if( v == b ) + return 1f; + return MathF.Log( v / a ) / MathF.Log( b / a ); + } + + #endregion + + #region Movement helpers + + /// Moves a value current towards target + /// The current value + /// The value to move towards + /// The maximum change that should be applied to the value + public static float MoveTowards( float current, float target, float maxDelta ) { + if( MathF.Abs( target - current ) <= maxDelta ) + return target; + return current + MathF.Sign( target - current ) * maxDelta; + } + + /// Gradually changes a value towards a desired goal over time. + /// The value is smoothed by some spring-damper like function, which will never overshoot. + /// The function can be used to smooth any kind of value, positions, colors, scalars + /// The current position + /// The position we are trying to reach + /// The current velocity, this value is modified by the function every time you call it + /// Approximately the time it will take to reach the target. A smaller value will reach the target faster + /// Optionally allows you to clamp the maximum speed + public static float SmoothDamp( float current, float target, ref float currentVelocity, float smoothTime, float maxSpeed = Infinity ) { + float deltaTime = Time.deltaTime; + return SmoothDamp( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); + } + + /// Gradually changes a value towards a desired goal over time. + /// The value is smoothed by some spring-damper like function, which will never overshoot. + /// The function can be used to smooth any kind of value, positions, colors, scalars + /// The current position + /// The position we are trying to reach + /// The current velocity, this value is modified by the function every time you call it + /// Approximately the time it will take to reach the target. A smaller value will reach the target faster + /// Optionally allows you to clamp the maximum speed + /// The time since the last call to this function. By default Time.deltaTime + public static float SmoothDamp( float current, float target, ref float currentVelocity, float smoothTime, [Uei.DefaultValue( "Mathf.Infinity" )] float maxSpeed, [Uei.DefaultValue( "Time.deltaTime" )] float deltaTime ) { + // Based on Game Programming Gems 4 Chapter 1.10 + smoothTime = MathF.Max( 0.0001F, smoothTime ); + float omega = 2F / smoothTime; + + float x = omega * deltaTime; + float exp = 1F / ( 1F + x + 0.48F * x * x + 0.235F * x * x * x ); + float change = current - target; + float originalTo = target; + + // Clamp maximum speed + float maxChange = maxSpeed * smoothTime; + change = Clamp( change, -maxChange, maxChange ); + target = current - change; + + float temp = ( currentVelocity + omega * change ) * deltaTime; + currentVelocity = ( currentVelocity - omega * temp ) * exp; + float output = target + ( change + temp ) * exp; + + // Prevent overshooting + if( originalTo - current > 0.0F == output > originalTo ) { + output = originalTo; + currentVelocity = ( output - originalTo ) / deltaTime; + } + + return output; + } + + #endregion + + #region Weighted sums + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + [MethodImpl( INLINE )] public static float WeightedSum( Vector2 w, float a, float b ) => a * w.x + b * w.y; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + [MethodImpl( INLINE )] public static float WeightedSum( Vector3 w, float a, float b, float c ) => a * w.x + b * w.y + c * w.z; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + /// The fourth value, weighted by w.w + [MethodImpl( INLINE )] public static float WeightedSum( Vector4 w, float a, float b, float c, float d ) => a * w.x + b * w.y + c * w.z + d * w.w; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector2 w, Vector2 a, Vector2 b ) => a * w.x + b * w.y; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector3 w, Vector2 a, Vector2 b, Vector2 c ) => a * w.x + b * w.y + c * w.z; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + /// The fourth value, weighted by w.w + [MethodImpl( INLINE )] public static Vector2 WeightedSum( Vector4 w, Vector2 a, Vector2 b, Vector2 c, Vector2 d ) => a * w.x + b * w.y + c * w.z + d * w.w; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector3 w, Vector3 a, Vector3 b ) => a * w.x + b * w.y; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector3 w, Vector3 a, Vector3 b, Vector3 c ) => a * w.x + b * w.y + c * w.z; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + /// The fourth value, weighted by w.w + [MethodImpl( INLINE )] public static Vector3 WeightedSum( Vector4 w, Vector3 a, Vector3 b, Vector3 c, Vector3 d ) => a * w.x + b * w.y + c * w.z + d * w.w; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b ) => a * w.x + b * w.y; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b, Vector4 c ) => a * w.x + b * w.y + c * w.z; + + /// Multiplies each component of w by the input values, and returns their sum + /// The weights (per component) to apply to the rest of the values + /// The first value, weighted by w.x + /// The second value, weighted by w.y + /// The third value, weighted by w.z + /// The fourth value, weighted by w.w + [MethodImpl( INLINE )] public static Vector4 WeightedSum( Vector4 w, Vector4 a, Vector4 b, Vector4 c, Vector4 d ) => a * w.x + b * w.y + c * w.z + d * w.w; + + #endregion + + #region Vector math + + /// The determinant is equivalent to the dot product, but with one vector rotated 90 degrees. + /// Note that det(a,b) != det(b,a). It's equivalent to a.x * b.y - a.y * b.x. + /// It is also known as the 2D Cross Product, Wedge Product, Outer Product and Perpendicular Dot Product + public static float Determinant /*or Cross*/( Vector2 a, Vector2 b ) => a.x * b.y - a.y * b.x; // 2D "cross product" + + /// Returns the direction and magnitude of the vector. Cheaper than calculating length and normalizing it separately + public static (Vector2 dir, float magnitude ) GetDirAndMagnitude( Vector2 v ) { + float magnitude = v.magnitude; + return ( v / magnitude, magnitude ); + } + + /// + public static (Vector3 dir, float magnitude ) GetDirAndMagnitude( Vector3 v ) { + float magnitude = v.magnitude; + return ( v / magnitude, magnitude ); + } + + /// Clamps the length of the vector between min and max + /// The vector to clamp + /// Minimum length + /// Maximum length + public static Vector2 ClampMagnitude( Vector2 v, float min, float max ) { + float mag = v.magnitude; + return mag < min ? ( v / mag ) * min : mag > max ? ( v / mag ) * max : v; + } + + /// + public static Vector3 ClampMagnitude( Vector3 v, float min, float max ) { + float mag = v.magnitude; + return mag < min ? ( v / mag ) * min : mag > max ? ( v / mag ) * max : v; + } + + /// Returns the chebyshev distance between the two vectors + [MethodImpl( INLINE )] public static float ChebyshevDistance( Vector3 a, Vector3 b ) => Max( Abs( a.x - b.x ), Abs( a.y - b.y ), Abs( a.z - b.z ) ); + + /// Returns the taxicab/rectilinear distance between the two vectors + [MethodImpl( INLINE )] public static float TaxicabDistance( Vector3 a, Vector3 b ) => Abs( a.x - b.x ) + Abs( a.y - b.y ) + Abs( a.z - b.z ); + + /// + [MethodImpl( INLINE )] public static float ChebyshevDistance( Vector2 a, Vector2 b ) => Max( Abs( a.x - b.x ), Abs( a.y - b.y ) ); + + /// + [MethodImpl( INLINE )] public static float TaxicabDistance( Vector2 a, Vector2 b ) => Abs( a.x - b.x ) + Abs( a.y - b.y ); + + /// Returns the average/center of the two input vectors + [MethodImpl( INLINE )] public static Vector2 Average( Vector2 a, Vector2 b ) => ( a + b ) / 2f; + + /// + [MethodImpl( INLINE )] public static Vector3 Average( Vector3 a, Vector3 b ) => ( a + b ) / 2f; + + /// Returns the average/halfway direction between the two input direction vectors. Note that this presumes both aDir and bDir have the same length + [MethodImpl( INLINE )] public static Vector2 AverageDir( Vector2 aDir, Vector2 bDir ) => ( aDir + bDir ).normalized; + + /// + [MethodImpl( INLINE )] public static Vector3 AverageDir( Vector3 aDir, Vector3 bDir ) => ( aDir + bDir ).normalized; + + /// Returns the squared distance between two points. + /// This is faster than the actual distance, and is useful when comparing distances where the absolute distance doesn't matter + [MethodImpl( INLINE )] public static float DistanceSquared( Vector2 a, Vector2 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square(); + + /// + [MethodImpl( INLINE )] public static float DistanceSquared( Vector3 a, Vector3 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square() + ( a.z - b.z ).Square(); + + /// + [MethodImpl( INLINE )] public static float DistanceSquared( Vector4 a, Vector4 b ) => ( a.x - b.x ).Square() + ( a.y - b.y ).Square() + ( a.z - b.z ).Square() + ( a.w - b.w ).Square(); + + /// The t-value (fraction) where a projected along b would be + /// The vector to project + /// The vector to project onto + public static float ProjectionTValue( Vector3 a, Vector3 b ) => Vector3.Dot( a, b ) / Vector3.Dot( b, b ); + + /// + public static float ProjectionTValue( Vector2 a, Vector2 b ) => Vector2.Dot( a, b ) / Vector2.Dot( b, b ); + + /// Calculates a rotation minimizing normal direction, given start and end conditions. This is usually used when evaluating rotation minimizing frames on curves. + /// The start position + /// The start tangent direction + /// The start normal direction + /// The end position + /// The end tangent direction + public static Vector3 GetRotationMinimizingNormal( Vector3 posA, Vector3 tangentA, Vector3 normalA, Vector3 posB, Vector3 tangentB ) { + // source: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf + Vector3 v1 = posB - posA; + float v1_dot_v1_half = Vector3.Dot( v1, v1 ) / 2; + float r1 = Vector3.Dot( v1, normalA ) / v1_dot_v1_half; + float r2 = Vector3.Dot( v1, tangentA ) / v1_dot_v1_half; + Vector3 nL = normalA - r1 * v1; + Vector3 tL = tangentA - r2 * v1; + Vector3 v2 = tangentB - tL; + float r3 = Vector3.Dot( v2, nL ) / Vector3.Dot( v2, v2 ); + return ( nL - 2 * r3 * v2 ).normalized; + } + + #endregion + + #region Angles & Rotation + + /// Returns the direction of the input angle, as a normalized vector + /// The input angle, in radians + /// + [MethodImpl( INLINE )] public static float2 AngToDir( float aRad ) => new float2( MathF.Cos( aRad ), MathF.Sin( aRad ) ); + + /// Returns the angle of the input vector, in radians. You can also use myVector.Angle() + /// The vector to get the angle of. It does not have to be normalized + /// + [MethodImpl( INLINE )] public static float DirToAng( float2 vec ) => MathF.Atan2( vec.y, vec.x ); + + /// Returns a 2D orientation from a vector, representing the X axis + /// The direction to create a 2D orientation from (does not have to be normalized) + [MethodImpl( INLINE )] public static Quaternion DirToOrientation( Vector2 v ) { + v.Normalize(); + v.x += 1; + v.Normalize(); + return new Quaternion( 0, 0, v.y, v.x ); + } + + /// The angle between two quaternions, in radians + public static float Angle( Quaternion a, Quaternion b ) { + float num = Mathf.Min( Mathf.Abs( Quaternion.Dot( a, b ) ), 1f ); + return num > 0.999998986721039 ? 0.0f : (float)( MathF.Acos( num ) * 2.0 ); + } + + /// Returns a 2D Pose from a point and a vector, representing the X axis + /// The location of the pose + /// The direction to create a 2D orientation from (does not have to be normalized) + [MethodImpl( INLINE )] public static Pose PointDirToPose( Vector2 pt, Vector2 v ) => new Pose( pt, DirToOrientation( v ) ); + + /// Linearly blends between two poses. The position will lerp, while the rotation will slerp. Note: this lerp is unclamped + /// Pose at t = 0 + /// Pose at t = 1 + /// The t-value to blend from a to b, from 0 to 1 (values outside will extrapolate) + public static Pose Lerp( Pose a, Pose b, float t ) => + new Pose( + Vector3.LerpUnclamped( a.position, b.position, t ), + Quaternion.SlerpUnclamped( a.rotation, b.rotation, t ) + ); + + /// Returns a matrix representing a 2D position and rotation + /// The location of the matrix + /// The direction of the X axis (has to be normalized) + [MethodImpl( INLINE )] public static Matrix4x4 GetMatrixFrom2DPointDir( Vector2 point, Vector2 tangent ) { + Vector2 N = tangent.Rotate90CCW(); + return new Matrix4x4( + new Vector4( tangent.x, tangent.y, 0, 0 ), + new Vector4( N.x, N.y, 0, 0 ), + new Vector4( 0, 0, 1, 0 ), + new Vector4( point.x, point.y, 0, 1 ) + ); + } + + /// Returns the signed curvature at a point in a curve, in radians per distance unit (equivalent to the reciprocal radius of the osculating circle) + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static float GetCurvature( Vector2 velocity, Vector2 acceleration ) { + float dMag = velocity.magnitude; + return Determinant( velocity, acceleration ) / ( dMag * dMag * dMag ); + } + + /// Returns the curvature of a point in a 3D curve, as a Bivector. + /// The magnitude is the curvature in radians per distance unit, + /// casting it to a Vector3 gives you the axis of curvature + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static Bivector3 GetCurvature( Vector3 velocity, Vector3 acceleration ) { + float dMag = velocity.magnitude; + return Wedge( velocity, acceleration ) / ( dMag * dMag * dMag ); + } + + /// Returns the torsion of a given point in a curve, in radians per distance unit + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + /// The third derivative of the point in the curve + [MethodImpl( INLINE )] public static float GetTorsion( Vector3 velocity, Vector3 acceleration, Vector3 jerk ) { + Vector3 cVector = Vector3.Cross( velocity, acceleration ); + return Vector3.Dot( cVector, jerk ) / cVector.sqrMagnitude; + } + + /// Returns the frenet-serret (curvature-based) normal direction at a given point in a curve + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static Vector3 GetArcNormal( Vector3 velocity, Vector3 acceleration ) => Vector3.Cross( Vector3.Cross( velocity, acceleration ).normalized, velocity.normalized ); + + /// Returns the frenet-serret (curvature-based) binormal direction at a given point in a curve + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static Vector3 GetArcBinormal( Vector3 velocity, Vector3 acceleration ) => Vector3.Cross( velocity, acceleration ).normalized; + + /// Returns a normal direction given a reference up vector and a tangent direction + /// The tangent direction (does not have to be normalized) + /// The reference up vector. The normal will be perpendicular to both the supplied up vector and the curve + [MethodImpl( INLINE )] public static Vector3 GetNormalFromLookTangent( Vector3 tangent, Vector3 up ) => Vector3.Cross( up, tangent ).normalized; + + /// Returns the binormal from a vector, given a reference up vector. + /// The binormal will attempt to be as aligned with the reference vector as possible, + /// while still being perpendicular to the tangent + /// The tangent direction (does not have to be normalized) + /// The reference up vector. The normal will be perpendicular to both the supplied up vector and the tangent + [MethodImpl( INLINE )] public static Vector3 GetBinormalFromLookTangent( Vector3 tangent, Vector3 up ) { + Vector3 normal = Vector3.Cross( up, tangent ).normalized; + return Vector3.Cross( tangent.normalized, normal ); + } + + /// Returns the frenet-serret (curvature-based) orientation of a point in a curve with the given velocity and acceleration values, where the Z direction is tangent to the curve. + /// The X axis will point to the inner arc of the current curvature, while Y is the axis of rotation + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static Quaternion GetArcOrientation( Vector3 velocity, Vector3 acceleration ) { + Vector3 binormal = Vector3.Cross( velocity, acceleration ); + return Quaternion.LookRotation( velocity, binormal ); + } + + /// + [MethodImpl( INLINE )] public static Quaternion GetArcOrientation( Vector2 velocity, Vector2 acceleration ) { + Vector3 binormal = new Vector3( 0, 0, Sign( Determinant( velocity, acceleration ) ) ); + return Quaternion.LookRotation( velocity, binormal ); + } + + /// Returns the frenet-serret (curvature-based) orientation of a point in a curve with the given velocity and acceleration values, where the X direction is tangent to the curve. + /// The Y axis (the normal) will point to the inner arc of the current curvature, while Z is the axis of rotation + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + [MethodImpl( INLINE )] public static Quaternion GetFrenetSerretOrientation( Vector3 velocity, Vector3 acceleration ) { + GetCurvatureOrientationAxes( velocity, acceleration, out _, out Vector3 N, out Vector3 B ); + return Quaternion.LookRotation( B, N ); + } + + /// + [MethodImpl( INLINE )] public static Quaternion GetFrenetSerretOrientation( Vector2 velocity, Vector2 acceleration ) { + GetCurvatureOrientationAxes( velocity, acceleration, out _, out Vector3 N, out Vector3 B ); + return Quaternion.LookRotation( B, N ); + } + + /// Returns the frenet-serret (curvature-based) orientation axes of a point in a curve with the given velocity and acceleration values + /// The first derivative of the point in the curve + /// The second derivative of the point in the curve + /// The axis pointing along the curve + /// The axis pointing to the inside of the curve + /// The axis of rotation of the curve + [MethodImpl( INLINE )] public static void GetCurvatureOrientationAxes( Vector3 velocity, Vector3 acceleration, out Vector3 tangent, out Vector3 normal, out Vector3 binormal ) { + tangent = velocity.normalized; + binormal = Vector3.Cross( velocity, acceleration ).normalized; + normal = Vector3.Cross( binormal, tangent ); + } + + /// + [MethodImpl( INLINE )] public static void GetCurvatureOrientationAxes( Vector2 velocity, Vector2 acceleration, out Vector3 tangent, out Vector3 normal, out Vector3 binormal ) { + tangent = velocity.normalized; + float sign = Sign( Determinant( velocity, acceleration ) ); + binormal = new Vector3( 0, 0, sign ); + normal = new Vector3( -sign * tangent.y, sign * tangent.x, 0 ); + } + + /// Returns a 2D look-orientation (X forward), ensuring the returned Y axis is upright with regards to the up vector + /// The forward direction of the rotation (X axis) + /// The reference up direction of the rotation to align to, usually pointing along world up + [MethodImpl( INLINE )] public static Quaternion GetLookRotation2D( Vector2 forward, Vector2 up ) { + int sign = Determinant( forward, up ) >= 0 ? 1 : -1; + Vector2 Y = new(-sign * forward.y, sign * forward.x); + Vector3 Z = new(0, 0, sign); + return Quaternion.LookRotation( Z, Y ); + } + + /// + [MethodImpl( INLINE )] public static Quaternion GetLookRotation2D( Vector2 forward ) => GetLookRotation2D( forward, Vector2.up ); + + /// Returns the signed angle between a and b, in the range -tau/2 to tau/2 (-pi to pi) + [MethodImpl( INLINE )] public static float SignedAngle( Vector2 a, Vector2 b ) => AngleBetween( a, b ) * MathF.Sign( Determinant( a, b ) ); // -tau/2 to tau/2 + + /// Returns the shortest angle between a and b, in the range 0 to tau/2 (0 to pi) + [MethodImpl( INLINE )] public static float AngleBetween( Vector2 a, Vector2 b ) => MathF.Acos( Vector2.Dot( a.normalized, b.normalized ).ClampNeg1to1() ); + + /// Returns the shortest angle between two normalized vectors a and b, in the range 0 to tau/2 (0 to pi) + [MethodImpl( INLINE )] public static float AngleBetweenPreNormalized( Vector2 a, Vector2 b ) => MathF.Acos( Vector2.Dot( a, b ).ClampNeg1to1() ); + + /// + [MethodImpl( INLINE )] public static float AngleBetween( Vector3 a, Vector3 b ) => MathF.Acos( Vector3.Dot( a.normalized, b.normalized ).ClampNeg1to1() ); + + /// + [MethodImpl( INLINE )] public static float AngleBetweenPreNormalized( Vector3 a, Vector3 b ) => MathF.Acos( Vector3.Dot( a, b ).ClampNeg1to1() ); + + /// Returns the clockwise angle between from and to, in the range 0 to tau (0 to 2*pi) + [MethodImpl( INLINE )] public static float AngleFromToCW( Vector2 from, Vector2 to ) => Determinant( from, to ) < 0 ? AngleBetween( from, to ) : TAU - AngleBetween( from, to ); + + /// Returns the counterclockwise angle between from and to, in the range 0 to tau (0 to 2*pi) + [MethodImpl( INLINE )] public static float AngleFromToCCW( Vector2 from, Vector2 to ) => Determinant( from, to ) > 0 ? AngleBetween( from, to ) : TAU - AngleBetween( from, to ); + + /// Blends between the aRad and bRad angles, based on the input t-value between 0 and 1 + /// The start value, in radians + /// The end value, in radians + /// The t-value between 0 and 1 + public static float LerpAngle( float aRad, float bRad, float t ) { + float delta = Repeat( ( bRad - aRad ), TAU ); + if( delta > PI ) + delta -= TAU; + return aRad + delta * Clamp01( t ); + } + + /// Returns the shortest angle between the two input angles, in radians + [MethodImpl( INLINE )] public static float DeltaAngle( float a, float b ) => ( b - a + PI ).Repeat( TAU ) - PI; + + /// Given an angle between a and b, returns its normalized location in that range, as a t-value (interpolant) from 0 to 1 + /// The start angle of the range (in radians), where it would return 0 + /// The end angle of the range (in radians), where it would return 1 + /// An angle between a and b + public static float InverseLerpAngle( float a, float b, float v ) { + float angBetween = DeltaAngle( a, b ); + b = a + angBetween; // removes any a->b discontinuity + float h = a + angBetween * 0.5f; // halfway angle + v = h + DeltaAngle( h, v ); // get offset from h, and offset by h + return InverseLerpClamped( a, b, v ); + } + + /// An enumerable sequence of count number of vectors + /// on a circle with the given radius, starting from the X axis + /// The number of vectors to arrange on the circle. + /// A negative count will enumerate in the negative direction + /// The radius of the circle + public static IEnumerable PointsInCircle( int count, float radius = 1, float startAngle = 0f ) { + if( count == 0 ) + yield break; + int absCount = Math.Abs( count ); + for( int i = 0; i < absCount; i++ ) { + float angle = ( TAU * i ) / count + startAngle; + yield return new Vector2( + MathF.Cos( angle ) * radius, + MathF.Sin( angle ) * radius + ); + } + } + + /// + public static IEnumerable<(Vector2 p, int i)> PointsInCircleIdx( int count, float radius = 1, int startOffset = 0 ) { + if( count == 0 ) + yield break; + int absCount = Math.Abs( count ); + for( int i = 0; i < absCount; i++ ) { + float angle = ( TAU * ( i + startOffset ) ) / count; + yield return ( new Vector2( + MathF.Cos( angle ) * radius, + MathF.Sin( angle ) * radius + ), i ); + } + } + + /// Converts a yaw/pitch pair in radians, into a direction vector + /// A pitch of 0 points along the horizon. An angle of tau/4 points directly up + /// A yaw of 0 points along the axis after your up axis. If Y is up, a yaw of 0 points along Z. If Z is up, a yaw of 0 points along X + /// The axis considered up + public static float3 PitchYawToDirection( float pitch, float yaw, Axis upAxis = Axis.Y ) { + float2 a = AngToDir( yaw ); + float2 b = AngToDir( pitch ); + float3 v = default; + int u = (int)upAxis; + v[u] = b.y; + v[( u + 1 ) % 3] = a.x * b.x; + v[( u + 2 ) % 3] = a.y * b.x; + return v; + } + + #endregion + + #region Angular movement helpers + + /// Same as MoveTowards but makes sure the angles interpolate correctly when they wrap around a full turn. + /// Variables current and target are assumed to be in radians. + /// For optimization reasons, negative values of maxDelta are not supported and may cause oscillation. + /// To push current away from a target angle, add 180 to that angle instead. + /// The current angle + /// The angle to move towards + /// The maximum change that should be applied to the value + public static float MoveTowardsAngle( float current, float target, float maxDelta ) { + float deltaAngle = DeltaAngle( current, target ); + if( -maxDelta < deltaAngle && deltaAngle < maxDelta ) + return target; + target = current + deltaAngle; + return MoveTowards( current, target, maxDelta ); + } + + /// Gradually changes an angle given in radians towards a desired goal angle over time. + /// The value is smoothed by some spring-damper like function. + /// The function can be used to smooth any kind of value, positions, colors, scalars. The most common use is for smoothing a follow camera. + /// The current angle + /// The angle we are trying to reach + /// The current angular velocity, this value is modified by the function every time you call it + /// Approximately the time it will take to reach the target. A smaller value will reach the target faster + /// Optionally allows you to clamp the maximum speed + public static float SmoothDampAngle( float current, float target, ref float currentVelocity, float smoothTime, float maxSpeed = Infinity ) { + float deltaTime = Time.deltaTime; + return SmoothDampAngle( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); + } + + /// Gradually changes an angle given in radians towards a desired goal angle over time. + /// The value is smoothed by some spring-damper like function. + /// The function can be used to smooth any kind of value, positions, colors, scalars. The most common use is for smoothing a follow camera. + /// The current angle + /// The angle we are trying to reach + /// The current angular velocity, this value is modified by the function every time you call it + /// Approximately the time it will take to reach the target. A smaller value will reach the target faster + /// Optionally allows you to clamp the maximum speed + /// The time since the last call to this function. By default Time.deltaTime + public static float SmoothDampAngle( float current, float target, ref float currentVelocity, float smoothTime, [Uei.DefaultValue( "Mathf.Infinity" )] float maxSpeed, [Uei.DefaultValue( "Time.deltaTime" )] float deltaTime ) { + target = current + DeltaAngle( current, target ); + return SmoothDamp( current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime ); + } + + #endregion + + #region Shape coordinate remapping + + /// Given a position within a -1 to 1 square, remaps it to the unit circle + /// The input position inside the square + public static Vector2 SquareToDisc( Vector2 c ) { + c.x = c.x.ClampNeg1to1(); + c.y = c.y.ClampNeg1to1(); + float u = c.x * Sqrt( 1 - ( c.y * c.y ) / 2 ); + float v = c.y * Sqrt( 1 - ( c.x * c.x ) / 2 ); + return new Vector2( u, v ); + } + + /// Given a position within the unit circle, remaps it to a square in the -1 to 1 range + /// The input position inside the circle + public static Vector2 DiscToSquare( Vector2 c ) { + c = c.ClampMagnitude( 0, 1 ); + float u2 = c.x * c.x; + float v2 = c.y * c.y; + Vector2 n = new Vector2( 1, -1 ); + Vector2 p = new Vector2( 2, 2 ) + n * ( u2 - v2 ); + Vector2 q = 2 * SQRT2 * c; + Vector2 smolVec = Vector2.one * 0.0001f; + return 0.5f * ( Vector2.Max( smolVec, p + q ).Sqrt() - Vector2.Max( smolVec, p - q ).Sqrt() ); + } + + #endregion + + } + } \ No newline at end of file diff --git a/Runtime/Mathfs.cs.meta b/Runtime/Mathfs.cs.meta new file mode 100644 index 0000000..9373f7e --- /dev/null +++ b/Runtime/Mathfs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2069a5561e158411e9e5f4a6e688b7e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics.meta b/Runtime/Numerics.meta new file mode 100644 index 0000000..83f5893 --- /dev/null +++ b/Runtime/Numerics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 00f54cc78acf34c319c7a4961ce108c7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/DualQuaternion.cs b/Runtime/Numerics/DualQuaternion.cs new file mode 100644 index 0000000..4c8916c --- /dev/null +++ b/Runtime/Numerics/DualQuaternion.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Freya { + + [Serializable] + public struct DualQuaternion { + + float r; + float i, j, k; + float e; + float ei, ej, ek; + + public DualQuaternion( float r, float i, float j, float k, float e, float ei, float ej, float ek ) { + this.r = r; + this.i = i; + this.j = j; + this.k = k; + this.e = e; + this.ei = ei; + this.ej = ej; + this.ek = ek; + } + + public static DualQuaternion operator *( DualQuaternion a, DualQuaternion b ) { + return new DualQuaternion( + r: a.r * b.r - a.i * b.i - a.j * b.j - a.k * b.k, + i: a.r * b.i + a.i * b.r + a.j * b.k - a.k * b.j, + j: a.r * b.j - a.i * b.k + a.j * b.r + a.k * b.i, + k: a.r * b.k + a.i * b.j - a.j * b.i + a.k * b.r, + e: a.r * b.e - a.i * b.ei - a.j * b.ej - a.k * b.ek + a.e * b.r - a.ei * b.i - a.ej * b.j - a.ek * b.k, + ei: a.r * b.ei + a.i * b.e + a.j * b.ek - a.k * b.ej + a.e * b.i + a.ei * b.r + a.ej * b.k - a.ek * b.j, + ej: a.r * b.ej - a.i * b.ek + a.j * b.e + a.k * b.ei + a.e * b.j - a.ei * b.k + a.ej * b.r + a.ek * b.i, + ek: a.r * b.ek + a.i * b.ej - a.j * b.ei + a.k * b.e + a.e + b.k + a.ei * b.j - a.ej * b.i + a.ek * b.r + ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/DualQuaternion.cs.meta b/Runtime/Numerics/DualQuaternion.cs.meta new file mode 100644 index 0000000..334be28 --- /dev/null +++ b/Runtime/Numerics/DualQuaternion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc3197f5010eddc4fa722b4c2a152812 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/EnumerationExtensions.cs b/Runtime/Numerics/EnumerationExtensions.cs new file mode 100644 index 0000000..8d5eea7 --- /dev/null +++ b/Runtime/Numerics/EnumerationExtensions.cs @@ -0,0 +1,62 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Freya { + + public static class EnumerationExtensions { + + + /// Enumerates each item pair as tuples + /// The items to enumerate + /// Whether to include a last pair formed by the last element and the first element + /// Given items [a,b,c,d], this returns: + ///
    + ///
  • [(a,b),(b,c),(c,d)] if cyclic == false
  • + ///
  • [(a,b),(b,c),(c,d),(d,a)] if cyclic == true
  • + ///
+ public static IEnumerable<(T a, T b)> Pairs( this IEnumerable items, bool cyclic ) { + bool hasFoundFirst = false; + T first = default; + T prev = default; + foreach( T item in items ) { + if( hasFoundFirst == false ) { + hasFoundFirst = true; + first = item; + } else { + yield return ( prev, item ); + } + prev = item; + } + if( cyclic && hasFoundFirst ) + yield return ( prev, first ); + } + + /// A shorthand for selecting the out parameters of bool functions returning true + /// The items to enumerate + /// Predicate selecting tuples with the boolean return value, and the out parameter value + /// items.SelectOutParamsWhereTrue( x => (x.TryThing(out y), y) ) + public static IEnumerable SelectOutParamsWhereTrue( this IEnumerable items, Func predicate ) { + foreach( T item in items ) { + ( bool valid, O value ) = predicate( item ); + if( valid ) + yield return value; + } + } + + /// Tries to select the minimum value. Returns false with a minimum value of int.MaxValue if there are no items + /// The items to enumerate + /// The selector for the minimum value + /// The minimum value found + public static bool TryMin( this IEnumerable items, Func predicate, out int minimum ) => items.Select( predicate ).TryMin( out minimum ); + + /// Tries to select the minimum value. Returns false with a minimum value of int.MaxValue if there are no items + /// The items to enumerate + /// The minimum value found + public static bool TryMin( this IEnumerable items, out int minimum ) => ( minimum = items.Aggregate( int.MaxValue, Math.Min ) ) != int.MaxValue; + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/EnumerationExtensions.cs.meta b/Runtime/Numerics/EnumerationExtensions.cs.meta new file mode 100644 index 0000000..adadfdc --- /dev/null +++ b/Runtime/Numerics/EnumerationExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 61cb471c2d994254a13184e13221a399 +timeCreated: 1775338660 \ No newline at end of file diff --git a/Runtime/Numerics/FloatRange.cs b/Runtime/Numerics/FloatRange.cs new file mode 100644 index 0000000..5e2afaa --- /dev/null +++ b/Runtime/Numerics/FloatRange.cs @@ -0,0 +1,251 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + /// A value range between two values a and b + [Serializable] public struct FloatRange : IEquatable { + + /// The unit interval of 0 to 1 + public static readonly FloatRange unit = new FloatRange( 0, 1 ); + + /// The start of this range + public float a; + + /// The end of this range + public float b; + + /// Creates a new value range + /// The start of the range + /// The end of the range + public FloatRange( float a, float b ) => ( this.a, this.b ) = ( a, b ); + + /// The value at the center of this value range + public float Center => ( a + b ) / 2; + + /// The length/span of this value range + public float Length => MathF.Abs( b - a ); + + /// The minimum value of this range + public float Min => MathF.Min( a, b ); + + /// The maximum value of this range + public float Max => MathF.Max( a, b ); + + /// The direction of this value range. Returns -1 if b is greater than a, otherwise returns 1 + public int Direction => b > a ? 1 : -1; + + /// Interpolates a value from a to b, based on a parameter t + /// The normalized interpolant from a to b. A value of 0 returns a, a value of 1 returns b + public readonly float Lerp( float t ) => Mathfs.Lerp( a, b, t ); + + /// Returns the normalized position of the input value v within this range + /// The value to get the normalized position of + public readonly float InverseLerp( float v ) => Mathfs.InverseLerp( a, b, v ); + + /// Returns whether or not this range contains the value v (inclusive) + /// The value to see if it's inside + public readonly bool Contains( float v ) => v >= MathF.Min( a, b ) && v <= MathF.Max( a, b ); + + /// Returns whether or not this range contains the range r + /// The range to see if it's inside + public bool Contains( FloatRange r ) => r.Min >= Min && r.Max <= Max; + + /// Remaps the input value from the input range to the output range + /// The value to remap + /// The input range + /// The output range + public static float Remap( float value, FloatRange input, FloatRange output ) => output.Lerp( input.InverseLerp( value ) ); + + /// Remaps the input value from the input range to the output range, and contains it within + /// The value to remap + /// The input range + /// The output range + public static float RemapClamped( float value, FloatRange input, FloatRange output ) => output.Lerp( input.InverseLerp( value ).Clamp01() ); + + /// Remaps a range from the input range to the output range + /// The range to remap + /// The input range + /// The output range + public static FloatRange Remap( FloatRange value, FloatRange input, FloatRange output ) => new(Remap( value.a, input, output ), Remap( value.b, input, output )); + + /// Returns whether or not this range overlaps another range + /// The other range to test overlap with + public bool Overlaps( FloatRange other ) { + float separation = MathF.Abs( other.Center - Center ); + float rTotal = ( other.Length + Length ) / 2; + return separation < rTotal; + } + + /// Wraps/repeats the input value to stay within this range + /// The value to wrap/repeat in this interval + public float Wrap( float value ) { + if( value >= a && value <= b ) + return value; + return a + Mathfs.Repeat( value - a, b - a ); + } + + /// Clamps the input value to this range + /// The value to clamp to this interval + public float Clamp( float value ) => Mathfs.Clamp( value, Min, Max ); + + /// Expands the minimum or maximum value to contain the given value + /// The value to include + public FloatRange Encapsulate( float value ) => + Direction switch { + 1 => ( Mathfs.Min( a, value ), Mathfs.Max( b, value ) ), // forward - a is min, b is max + _ => ( Mathfs.Min( b, value ), Mathfs.Max( a, value ) ) // reversed - b is min, a is max + }; + + /// Expands the minimum or maximum value to contain the given range + /// The value range to include + public FloatRange Encapsulate( FloatRange range ) => + Direction switch { + 1 => ( Mathfs.Min( a, range.a ), Mathfs.Max( b, range.b ) ), // forward - a is min, b is max + _ => ( Mathfs.Min( b, range.b ), Mathfs.Max( a, range.a ) ) // reversed - b is min, a is max + }; + + /// Combines overlapping ranges and enumerates the results + public static IEnumerable Union( IEnumerable ranges ) { + int i = 0; + FloatRange range = default; + foreach( FloatRange r in ranges.OrderBy( r => r.a ) ) { + if( i == 0 ) { + range = r; + } else { + if( range.b < r.a ) { // r.a is guaranteed to be >= range.a + // the next range r is outside the current range, commit current range + yield return range; + // switch to next range + range = r; + } else { + range = range.Encapsulate( r ); // the next range starts inside the current one, combine them + } + } + i++; + } + if( i > 0 ) // if i == 0 there were no items provided + yield return range; + } + + /// Enumerates the remaining float ranges after this range has range b taken out of it. May return either 0, 1 or 2 ranges + /// The range to subtract with + public IEnumerable Difference( FloatRange remove ) { + if( this.Overlaps( remove ) == false ) { + yield return this; // nothing is subtracted + } else if( remove.a <= this.a && remove.b >= this.b ) { + // everything has been subtracted away + } else { + // now we know it's a partial cut, which leads to either one or two results + if( remove.a < this.b && remove.a > this.a ) + yield return new FloatRange( this.a, remove.a ); + if( remove.b < this.b && remove.b > this.a ) + yield return new FloatRange( remove.b, this.b ); + } + } + + /// Returns the range shared by this range and other, which is either one range, or no ranges at all + /// The range to intersect with + public IEnumerable Intersection( FloatRange other ) { + if( this.TryIntersect( other, out FloatRange intersection ) ) + yield return intersection; + } + + /// Tries to get the range shared by this range and other + /// The range to intersect with + /// The range common to both ranges + /// Returns true if there is a range shared by both + public bool TryIntersect( FloatRange other, out FloatRange intersection ) { + if( other.b <= this.a || other.a >= this.b ) { + intersection = default; // no overlap, return nothing + return false; + } + intersection = new FloatRange( math.max( other.a, this.a ), math.min( other.b, this.b ) ); + return true; + } + + /// Enumerates the remaining float ranges after range a has range b taken out of it + /// The ranges to subtract from + /// The range to subtract with + public static IEnumerable Difference( IEnumerable solid, FloatRange remove ) { + return solid.SelectMany( x => x.Difference( remove ) ); + } + + /// Splits the range if it crosses a modulo boundary. For example, the range [-20,10] mod 360 will return [0,10] and [340,360] + /// The value at the discontinuity. For example, if the ranges represent degrees, mod should be 360 + public IEnumerable ModuloSplit( float mod ) { + float len = Length; + if( len >= mod ) { // this means the range necessarily covers the entire range + yield return new FloatRange( 0, mod ); + } else { + float aCanon = Mathfs.Repeat( a, mod ); + + float spaceBeforeDiscontinuity = mod - aCanon; + if( len <= spaceBeforeDiscontinuity ) { + // this means it's not crossing the discontinuity + yield return new FloatRange( aCanon, aCanon + len ); + } else { + // this means it needs to be split up into two around the discontinuity + yield return new FloatRange( aCanon, mod ); + yield return new FloatRange( 0, len - spaceBeforeDiscontinuity ); + } + } + } + + /// Combines overlapping ranges with a specified modulo + /// The ranges to combine + /// The value at the discontinuity. For example, if the ranges represent degrees, mod should be 360 + public static IEnumerable UnionModulo( IEnumerable ranges, float mod ) { + return Union( ranges.SelectMany( r => r.ModuloSplit( mod ) ) ); + } + + /// Returns a version of this range, scaled around its start value + /// The value to scale the range by + public FloatRange ScaleFromStart( float scale ) => new FloatRange( a, a + scale * ( b - a ) ); + + /// Returns this range mirrored around a given value + /// The value to mirror around + public FloatRange MirrorAround( float pivot ) => new FloatRange( 2 * pivot - a, 2 * pivot - b ); + + /// Returns a reversed version of this range, where a and b is swapped + public FloatRange Reverse() => ( b, a ); + + /// Returns the rectangle encapsulating the region defined by a range per axis. Note: The direction of each range is ignored + /// The range of the X axis + /// The range of the Y axis + public static Rect ToRect( FloatRange rangeX, FloatRange rangeY ) => new Rect( rangeX.Min, rangeY.Min, rangeX.Length, rangeY.Length ); + + /// Returns the bounding box encapsulating the region defined by a range per axis. Note: The direction of each range is ignored + /// The range of the X axis + /// The range of the Y axis + /// The range of the Z axis + public static Bounds ToBounds( FloatRange rangeX, FloatRange rangeY, FloatRange rangeZ ) { + Vector3 center = new(rangeX.Center, rangeY.Center, rangeZ.Center); + Vector3 size = new(rangeX.Length, rangeY.Length, rangeZ.Length); + return new Bounds( center, size ); + } + + public static FloatRange operator -( FloatRange range, float v ) => new(range.a - v, range.b - v); + public static FloatRange operator +( FloatRange range, float v ) => new(range.a + v, range.b + v); + public static FloatRange operator /( FloatRange range, int v ) => new(range.a / v, range.b / v); + public static FloatRange operator /( FloatRange range, float v ) => new(range.a / v, range.b / v); + public static FloatRange operator *( FloatRange range, int v ) => new(range.a * v, range.b * v); + public static FloatRange operator *( FloatRange range, float v ) => new(range.a * v, range.b * v); + + public static implicit operator FloatRange( (float a, float b) tuple ) => new FloatRange( tuple.a, tuple.b ); + public static bool operator ==( FloatRange a, FloatRange b ) => a.a == b.a && a.b == b.b; + public static bool operator !=( FloatRange a, FloatRange b ) => a.a != b.a || a.b != b.b; + public bool Equals( FloatRange other ) => a.Equals( other.a ) && b.Equals( other.b ); + public override bool Equals( object obj ) => obj is FloatRange other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( a, b ); + + public override string ToString() => $"[{a},{b}]"; + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/FloatRange.cs.meta b/Runtime/Numerics/FloatRange.cs.meta new file mode 100644 index 0000000..f64ad87 --- /dev/null +++ b/Runtime/Numerics/FloatRange.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b99f89e16c9e64dc59e6b11cbf47e2d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/IntRange.cs b/Runtime/Numerics/IntRange.cs new file mode 100644 index 0000000..12a4629 --- /dev/null +++ b/Runtime/Numerics/IntRange.cs @@ -0,0 +1,71 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Text; + +namespace Freya { + + /// An integer range + [Serializable] public struct IntRange { + + public static IntRange empty = new IntRange( 0, 0 ); + + public int start; + public int count; + + public int this[ int i ] => start + i; + + /// The last integer in the range + public int Last => start + count - 1; + + /// The distance from first to last integer. Equivalent to count-1 + public int Distance => count - 1; + + /// Creates a new integer range, given a start integer and how many integers to include in total + /// The first integer + /// How many integers to include in the full range + public IntRange( int start, int count ) { + this.start = start; + this.count = count; + } + + /// Whether or not this range contains a given value (inclusive) + /// The value to check if it's inside, or equal to the start or end + public bool Contains( int value ) => value >= start && value <= Last; + + /// Returns a copy of this range, without the last element (ie: count is reduced by 1) + public IntRange WithoutLast() => new(start, count - 1); + + /// Create an integer range from start to end (inclusive) + /// The first integer + /// The last integer + public static IntRange FirstToLast( int first, int last ) => new IntRange( first, last - first + 1 ); + + static StringBuilder toStrBuilder = new StringBuilder(); + + public override string ToString() { + toStrBuilder.Clear(); + toStrBuilder.Append( "{ " ); + int last = Last; + for( int i = start; i <= last; i++ ) { + toStrBuilder.Append( i ); + if( i != last ) + toStrBuilder.Append( ", " ); + } + toStrBuilder.Append( " }" ); + return toStrBuilder.ToString(); + } + + public IntRangeEnumerator GetEnumerator() => new IntRangeEnumerator( this ); + + public struct IntRangeEnumerator /*: IEnumerator*/ { + IntRange intRange; + int currValue; + public IntRangeEnumerator( IntRange range ) => ( this.intRange, currValue ) = ( range, range.start - 1 ); + public bool MoveNext() => ++currValue <= intRange.Last; + public int Current => currValue; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/IntRange.cs.meta b/Runtime/Numerics/IntRange.cs.meta new file mode 100644 index 0000000..844ddc0 --- /dev/null +++ b/Runtime/Numerics/IntRange.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f48d1234c10c4a84cb575073effd06be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Interfaces.meta b/Runtime/Numerics/Interfaces.meta new file mode 100644 index 0000000..6b0209c --- /dev/null +++ b/Runtime/Numerics/Interfaces.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0381adb3a8a820d4e9eed12a0a1d9c1f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs b/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs new file mode 100644 index 0000000..e3cdba2 --- /dev/null +++ b/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Freya { + + [AttributeUsage( AttributeTargets.Method | AttributeTargets.Property )] + public class BinaryOpAttribute : Attribute {} + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs.meta b/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs.meta new file mode 100644 index 0000000..e14f391 --- /dev/null +++ b/Runtime/Numerics/Interfaces/BinaryOpAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a29244bda02e460aa637a7c0616ba8dd +timeCreated: 1776112169 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IComplex.cs b/Runtime/Numerics/Interfaces/IComplex.cs new file mode 100644 index 0000000..609917a --- /dev/null +++ b/Runtime/Numerics/Interfaces/IComplex.cs @@ -0,0 +1,15 @@ +using Unity.Mathematics; + +namespace Freya { + + /// Objects that can be treated like complex numbers + public interface IComplex { + /// Multiplies as if they were complex numbers. The resulting vector is "rotated" by the other, and scaled by its magnitude. + /// Note that this operation does not use any trigonometry or square roots, it's very cheap to use! + [BinaryOp] public M complexMul( V other ); + + /// The complex conjugate of this vector, if treated as a complex number. Which, in english, just means it negates the y component + public V complexConj { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IComplex.cs.meta b/Runtime/Numerics/Interfaces/IComplex.cs.meta new file mode 100644 index 0000000..c3e03b7 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IComplex.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: de4c219a2a3e4371855d4d0082686edc +timeCreated: 1775599900 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IDotProduct.cs b/Runtime/Numerics/Interfaces/IDotProduct.cs new file mode 100644 index 0000000..8cbb893 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IDotProduct.cs @@ -0,0 +1,17 @@ +using Unity.Mathematics; + +namespace Freya { + + /// Objects that implement a dot product + public interface IDotProduct : ISqrMag { + /// The dot product between two vectors. This is the sum of the product of each respective component + [BinaryOp] public D dot( B other ); + } + + public static partial class mathfs { + // todo: special case + /// + public static rat dot( int2 a, rat2 b ) => b.dot( b ); + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IDotProduct.cs.meta b/Runtime/Numerics/Interfaces/IDotProduct.cs.meta new file mode 100644 index 0000000..983b1d8 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IDotProduct.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a2af38b25b54b9faa05712669f67d69 +timeCreated: 1775599886 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IHalfNumber.cs b/Runtime/Numerics/Interfaces/IHalfNumber.cs new file mode 100644 index 0000000..e3cabf8 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IHalfNumber.cs @@ -0,0 +1,8 @@ +namespace Freya { + + public interface IHalfNumber { + /// Multiplies this by 2 and returns an integer value + public F times2 { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IHalfNumber.cs.meta b/Runtime/Numerics/Interfaces/IHalfNumber.cs.meta new file mode 100644 index 0000000..bc89876 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IHalfNumber.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e4a005c3ebe444fa84eec0e7f9c25c65 +timeCreated: 1775597892 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/INumberBase.cs b/Runtime/Numerics/Interfaces/INumberBase.cs new file mode 100644 index 0000000..d74114c --- /dev/null +++ b/Runtime/Numerics/Interfaces/INumberBase.cs @@ -0,0 +1,41 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + public interface INumberBase { + /// Returns whether this number is an integer + public bool isInteger { get; } + + /// Returns whether this vector is the zero vector + public bool isZero { get; } + + /// Returns whether this is zero or lies along a single axis + public bool isOrthogonal { get; } + + } + + public interface INumber : INumberBase { + /// Returns the absolute value of the number. Makes negative values positive + public N abs { get; } + + /// Returns the minimum of two numbers + [BinaryOp] public N min( N other ); + + /// Returns the maximum of two numbers + [BinaryOp] public N max( N other ); + + /// The vector from this point to the target. Equivalent to target - this + public N to( N target ); + + // I can't do this bc Unity uses older versions of C#: + // public static abstract R zero { get; } + // public static abstract R one { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/INumberBase.cs.meta b/Runtime/Numerics/Interfaces/INumberBase.cs.meta new file mode 100644 index 0000000..a458364 --- /dev/null +++ b/Runtime/Numerics/Interfaces/INumberBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a60a1c63dc754d8b98dbec145cf5ed44 +timeCreated: 1774975260 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IQuadrant2D.cs b/Runtime/Numerics/Interfaces/IQuadrant2D.cs new file mode 100644 index 0000000..b76253a --- /dev/null +++ b/Runtime/Numerics/Interfaces/IQuadrant2D.cs @@ -0,0 +1,41 @@ +using Unity.Mathematics; + +namespace Freya { + + /// Objects that reside within four quadrants in 2D + public interface IQuadrant2D { + /// The index of the quadrant containing this position, indexed from 0 to 3, starting from 0 in the top right, + /// increasing in the positive rotation direction/counter-clockwise. + /// Ambiguous positions pick the quadrant in the positive rotation direction.

+ /// Quadrant layout: + /// + /// 1 + /// 0 + /// + /// + /// 2 + /// 3 + ///
+ public int quadrant { get; } + /// The signed of the quadrant containing this position, indexed from 0 to 3, starting from 0 in the top right, + /// increasing in the positive rotation direction/counter-clockwise. + /// Ambiguous positions pick the quadrant in the positive rotation direction.

+ /// Quadrant layout: + /// + /// 1 + /// 0 + /// + /// + /// -2 + /// -1 + ///
+ public int signedQuadrant { get; } + /// The X-axis of the basis within the current quadrant. + /// Ambiguous positions pick the quadrant in the positive rotation direction. Zero-vectors return (1,0) + public int2 quadrantBasisX { get; } + /// Returns the two basis vectors of the quadrant that contains this position. + /// Ambiguous positions pick the quadrant in the positive rotation direction + public (int2 x, int2 y) quadrantBasis { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IQuadrant2D.cs.meta b/Runtime/Numerics/Interfaces/IQuadrant2D.cs.meta new file mode 100644 index 0000000..ee467a6 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IQuadrant2D.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9adcc573f2d54e61bc55661ce9250038 +timeCreated: 1775599895 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IRoundable.cs b/Runtime/Numerics/Interfaces/IRoundable.cs new file mode 100644 index 0000000..1188320 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IRoundable.cs @@ -0,0 +1,41 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; + +namespace Freya { + + /// Objects that can be rounded to nearby values + public interface IRoundable { + + /// Rounds to the nearest integer + /// The rounding method to use + public R round( RoundingDirection rounding = RoundingDirection.ToEven ); + + /// Rounds to the nearest integer towards 0 + public R floorToward0 { get; } + + /// Rounds to the nearest integer away from 0 + public R ceilAwayFrom0 { get; } + + /// Rounds down to the nearest integer + public R floor { get; } + + /// Rounds up to the nearest integer + public R ceil { get; } + } + + public static partial class mathfs { + /// + public static R round( V v, RoundingDirection rounding = RoundingDirection.ToEven ) where V : IRoundable => v.round(); + } + + /// Basically the same as C#'s , but older .net versions don't have all the options + public enum RoundingDirection { + ToEven = 0, + AwayFromZero = 1, + ToZero = 2, + ToNegativeInfinity = 3, + ToPositiveInfinity = 4, + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IRoundable.cs.meta b/Runtime/Numerics/Interfaces/IRoundable.cs.meta new file mode 100644 index 0000000..8960a9c --- /dev/null +++ b/Runtime/Numerics/Interfaces/IRoundable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74aac72b52ce4bf68533409de752b6f8 +timeCreated: 1774975320 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/ISignedNumber.cs b/Runtime/Numerics/Interfaces/ISignedNumber.cs new file mode 100644 index 0000000..e522bc5 --- /dev/null +++ b/Runtime/Numerics/Interfaces/ISignedNumber.cs @@ -0,0 +1,16 @@ +using System; +using Unity.Mathematics; + +namespace Freya { + + public interface ISignedNumber : INumberBase { + /// Returns the sign of this number. Either -1, 0, or 1 + public R sign { get; } + } + + public static partial class mathfs { + /// + public static T sign( T v ) where T : ISignedNumber => v.sign; + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/ISignedNumber.cs.meta b/Runtime/Numerics/Interfaces/ISignedNumber.cs.meta new file mode 100644 index 0000000..fa71514 --- /dev/null +++ b/Runtime/Numerics/Interfaces/ISignedNumber.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c90009622ba8461a8ee07d285dbfe90c +timeCreated: 1775597835 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/ISqrMag.cs b/Runtime/Numerics/Interfaces/ISqrMag.cs new file mode 100644 index 0000000..11ba365 --- /dev/null +++ b/Runtime/Numerics/Interfaces/ISqrMag.cs @@ -0,0 +1,8 @@ +namespace Freya { + + public interface ISqrMag { + /// The squared magnitude of this vector + public D magSq { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/ISqrMag.cs.meta b/Runtime/Numerics/Interfaces/ISqrMag.cs.meta new file mode 100644 index 0000000..aeb7076 --- /dev/null +++ b/Runtime/Numerics/Interfaces/ISqrMag.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be0ef07e4c8844ed9bd026a0c11fe408 +timeCreated: 1776111941 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec.cs b/Runtime/Numerics/Interfaces/IVec.cs new file mode 100644 index 0000000..4208bc2 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec.cs @@ -0,0 +1,31 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using Unity.Mathematics; + +namespace Freya { + + public interface IVec : INumber, IDotProduct, IVecComponents { + + /// The chebyshev magnitude of this vector. + /// In chebyshev distance, diagonal distances are treated the same as orthogonal distances. + /// This means the magnitude of (1,1) is 1, the magnitude of (2,2) is 2 + public C magChebyshev { get; } + /// The taxicab magnitude of this vector. + /// In taxicab distance, diagonal distances are treated as if you can only measure orthogonally. + /// This means the magnitude of (1,1) is 2, the magnitude of (2,2) is 4 + public C magTaxicab { get; } + + /// Returns whether this point is in front of or behind a plane. + ///
    + ///
  • returns +1 when in front of the plane
  • + ///
  • returns 0 when inside the plane
  • + ///
  • returns -1 when behind the plane
  • + ///
+ ///
+ /// A point inside the plane + /// The normal direction of the plane + public int pointSideOfPlane( V planePos, V planeNormal ); + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec.cs.meta b/Runtime/Numerics/Interfaces/IVec.cs.meta new file mode 100644 index 0000000..d4de972 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 98afe4bf8b2b4756a3b7b17448d5f670 +timeCreated: 1775585084 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec2.cs b/Runtime/Numerics/Interfaces/IVec2.cs new file mode 100644 index 0000000..90c5cd8 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec2.cs @@ -0,0 +1,15 @@ +using Unity.Mathematics; + +namespace Freya { + + /// Operations that are unique to 2D vectors + public interface IVec2 : IVec2Base, IVecProjections, IWedgeProduct, IQuadrant2D, IComplex { + /// Rotates this vector in the positive rotation direction by 90 degrees. This is usually a counter-clockwise/left turn + public V rot90 { get; } + /// Rotates this vector in the negative rotation direction by 90 degrees. This is usually a clockwise/right turn + public V rotNeg90 { get; } + /// Rotates this vector by 180 degrees. Equivalent to negating this vector + public V rot180 { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec2.cs.meta b/Runtime/Numerics/Interfaces/IVec2.cs.meta new file mode 100644 index 0000000..b7c988b --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec2.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1e2bcbef65e14075ad5e39a1d176b2ed +timeCreated: 1775605827 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec3.cs b/Runtime/Numerics/Interfaces/IVec3.cs new file mode 100644 index 0000000..89e2afe --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec3.cs @@ -0,0 +1,6 @@ +namespace Freya { + + /// Operations that are unique to 3D vectors + public interface IVec3 : IVec3Base, IWedgeProduct {} + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec3.cs.meta b/Runtime/Numerics/Interfaces/IVec3.cs.meta new file mode 100644 index 0000000..dbc9224 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec3.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 971c31cdc8c9452c9c3648519ded3e49 +timeCreated: 1776039209 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec4.cs b/Runtime/Numerics/Interfaces/IVec4.cs new file mode 100644 index 0000000..f009b66 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec4.cs @@ -0,0 +1,6 @@ +namespace Freya { + + /// Operations that are unique to 4D vectors + public interface IVec4 : IVec4Base /*todo: , IWedgeProduct*/ {} + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVec4.cs.meta b/Runtime/Numerics/Interfaces/IVec4.cs.meta new file mode 100644 index 0000000..d136d5c --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVec4.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b527c2f521a545239e46984ef6ce1f7b +timeCreated: 1776039214 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecBase.cs b/Runtime/Numerics/Interfaces/IVecBase.cs new file mode 100644 index 0000000..d0f6d76 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecBase.cs @@ -0,0 +1,42 @@ +namespace Freya { + + public interface IVec1Base : IVec { + /// The X component of this vector + public C X { get; } + /// This vector with a reversed X component + public V flipX { get; } + /// This vector with a zeroed-out X component + public V zeroX { get; } + } + + /// Operations for vectors 2D and above + public interface IVec2Base : IVec1Base { + /// The Y component of this vector + public C Y { get; } + /// This vector with a reversed Y component + public V flipY { get; } + /// This vector with a zeroed-out Y component + public V zeroY { get; } + } + + /// Operations for vectors 3D and above + public interface IVec3Base : IVec2Base { + /// The Z component of this vector + public C Z { get; } + /// This vector with a reversed Z component + public V flipZ { get; } + /// This vector with a zeroed-out Z component + public V zeroZ { get; } + } + + /// Operations for vectors 4D and above + public interface IVec4Base : IVec3Base { + /// The W component of this vector + public C W { get; } + /// This vector with a reversed W component + public V flipW { get; } + /// This vector with a zeroed-out W component + public V zeroW { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecBase.cs.meta b/Runtime/Numerics/Interfaces/IVecBase.cs.meta new file mode 100644 index 0000000..dc47dfa --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9bc3a385d7214b5195b24771d33ac1d5 +timeCreated: 1776111377 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecComponents.cs b/Runtime/Numerics/Interfaces/IVecComponents.cs new file mode 100644 index 0000000..570f26f --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecComponents.cs @@ -0,0 +1,17 @@ +using Unity.Mathematics; + +namespace Freya { + + public interface IVecComponents { + // todo: coooould make a C Component( V elem, int iAxis ) + /// Returns a component of this vector by index + public C this[ int i ] { get; } + /// The minimum of the components of this vector + public C cmin { get; } + /// The maximum of the components of this vector + public C cmax { get; } + /// The sum of the components of this vector + public C csum { get; } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecComponents.cs.meta b/Runtime/Numerics/Interfaces/IVecComponents.cs.meta new file mode 100644 index 0000000..37d7d80 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecComponents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 57a82d6700e243a48effc9a2786a4620 +timeCreated: 1775613169 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecProjections.cs b/Runtime/Numerics/Interfaces/IVecProjections.cs new file mode 100644 index 0000000..383f6ab --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecProjections.cs @@ -0,0 +1,9 @@ +namespace Freya { + + public interface IVecProjections { + /// Projects the vector onto a second vector, as a scalar t-value along the second vector. + /// The second vector does not need to be normalized, but if it is, the t-value is also a distance along that vector + public Proj projTValue( V n ); // => math.dot( v, n ) / math.dot( n, n ); + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVecProjections.cs.meta b/Runtime/Numerics/Interfaces/IVecProjections.cs.meta new file mode 100644 index 0000000..f2117fb --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVecProjections.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f99cf60bb61f4a33883593f524db0dc5 +timeCreated: 1776130282 \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVectorMath.cs b/Runtime/Numerics/Interfaces/IVectorMath.cs new file mode 100644 index 0000000..7bdb990 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVectorMath.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + public static class VectorMathExt { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [MethodImpl( INLINE )] public static float SqMag( this VM vm, V v ) where VM : struct, IVectorMath => vm.Dot( v, v ); + [MethodImpl( INLINE )] public static float SqDist( this VM vm, V a, V b ) where VM : struct, IVectorMath => vm.SqMag( vm.Sub( b, a ) ); + [MethodImpl( INLINE )] public static V VecProject( this VM vm, V p, V to ) where VM : struct, IVectorMath => vm.Mul( to, vm.BasisProject( p, to ) ); + [MethodImpl( INLINE )] public static V VecReject( this VM vm, V p, V to ) where VM : struct, IVectorMath => vm.Sub( p, vm.VecProject( p, to ) ); + [MethodImpl( INLINE )] public static float BasisProject( this VM vm, V p, V to ) where VM : struct, IVectorMath => vm.Dot( p, to ) / vm.Dot( to, to ); + [MethodImpl( INLINE )] public static V VecFromLineToPoint( this VM vm, V o, V n, V p ) where VM : struct, IVectorMath => vm.VecReject( vm.Sub( p, o ), n ); + [MethodImpl( INLINE )] public static float SqDistFromPointToLine( this VM vm, V o, V n, V p ) where VM : struct, IVectorMath => vm.SqMag( vm.VecFromLineToPoint( o, n, p ) ); + [MethodImpl( INLINE )] public static V GetPointAlongLine( this VM vm, V o, V n, float t ) where VM : struct, IVectorMath => vm.Add( o, vm.Mul( n, t ) ); + [MethodImpl( INLINE )] public static float ProjPointToLineSegmentTValue( this VM vm, V a, V b, V p ) where VM : struct, IVectorMath => Mathf.Clamp01( vm.ProjPointToLineTValue( a, vm.Sub( b, a ), p ) ); + [MethodImpl( INLINE )] public static float ProjPointToLineTValue( this VM vm, V o, V n, V p ) where VM : struct, IVectorMath => vm.BasisProject( vm.Sub( p, o ), n ); + [MethodImpl( INLINE )] public static V ProjPointToLine( this VM vm, V o, V n, V p ) where VM : struct, IVectorMath => vm.Add( o, vm.VecProject( vm.Sub( p, o ), n ) ); + + public static (float tA, float tB) ClosestPointBetweenLinesTValues( this VM vm, V aOrigin, V aDir, V bOrigin, V bDir ) where VM : struct, IVectorMath { + // source: https://math.stackexchange.com/questions/2213165/find-shortest-distance-between-lines-in-3d + V e = vm.Sub( aOrigin, bOrigin ); + float be = vm.Dot( aDir, e ); + float de = vm.Dot( bDir, e ); + float bd = vm.Dot( aDir, bDir ); + float b2 = vm.Dot( aDir, aDir ); + float d2 = vm.Dot( bDir, bDir ); + float A = -b2 * d2 + bd * bd; + float s = ( -b2 * de + be * bd ) / A; + float t = ( d2 * be - de * bd ) / A; + return ( t, s ); + } + + public static bool TryIntersectSphereAtOrigin( this VM vm, V o, V n, float r, out (float tMin, float tMax) tValues ) where VM : struct, IVectorMath { + float nn = vm.Dot( n, n ); + if( nn <= 0f ) { // vector has zero length, there's no direction + tValues = default; + return false; + } + float oo = vm.Dot( o, o ); + float on = vm.Dot( o, n ); + + // quadratic terms + double A = nn; + double B = 2 * on; + double C = oo - r * r; + + // try root solving + double discriminant = B * B - 4 * A * C; + if( discriminant < 0 ) { // no root, line is outside the circle + tValues = default; + return false; + } + int sign = B < 0 ? -1 : 1; + double u = -B - sign * Math.Sqrt( discriminant ); + + float tA = (float)( u / ( 2 * A ) ); + float tB = (float)( 2 * C / u ); + tValues = tA < tB ? ( tA, tB ) : ( tB, tA ); + return true; + } + + } + + public interface IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] V Zero(); + [MethodImpl( INLINE )] V Add( V a, V b ); + [MethodImpl( INLINE )] V Sub( V a, V b ); + [MethodImpl( INLINE )] V Mul( V v, float c ); + [MethodImpl( INLINE )] V Mul( float c, V v ); + [MethodImpl( INLINE )] V Div( V v, float c ); + [MethodImpl( INLINE )] float Dot( V a, V b ); + [MethodImpl( INLINE )] float Mag( V v ); + [MethodImpl( INLINE )] float Dist( V a, V b ); + [MethodImpl( INLINE )] V Normalize( V v ); + [MethodImpl( INLINE )] V Lerp( V a, V b, float t ); + } + + public struct VectorMath1D : IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] public float Zero() => 0; + [MethodImpl( INLINE )] public float Add( float a, float b ) => a + b; + [MethodImpl( INLINE )] public float Sub( float a, float b ) => a - b; + [MethodImpl( INLINE )] public float Mul( float v, float c ) => v * c; + [MethodImpl( INLINE )] public float Div( float v, float c ) => v / c; + [MethodImpl( INLINE )] public float Dot( float a, float b ) => a * b; + [MethodImpl( INLINE )] public float Mag( float v ) => MathF.Abs( v ); + [MethodImpl( INLINE )] public float Dist( float a, float b ) => MathF.Abs( b - a ); + [MethodImpl( INLINE )] public float Normalize( float v ) => v < 0 ? -1 : 1; + + // shared implementations + [MethodImpl( INLINE )] public float Lerp( float a, float b, float t ) => ( 1f - t ) * a + t * b; + } + + public struct VectorMath2D : IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] public Vector2 Zero() => new(0, 0); + [MethodImpl( INLINE )] public Vector2 Add( Vector2 a, Vector2 b ) => new(a.x + b.x, a.y + b.y); + [MethodImpl( INLINE )] public Vector2 Sub( Vector2 a, Vector2 b ) => new(a.x - b.x, a.y - b.y); + [MethodImpl( INLINE )] public Vector2 Mul( Vector2 v, float c ) => new(v.x * c, v.y * c); + [MethodImpl( INLINE )] public Vector2 Mul( float c, Vector2 v ) => new(v.x * c, v.y * c); + [MethodImpl( INLINE )] public Vector2 Div( Vector2 v, float c ) => new(v.x / c, v.y / c); + [MethodImpl( INLINE )] public float Dot( Vector2 a, Vector2 b ) => a.x * b.x + a.y * b.y; + [MethodImpl( INLINE )] public float Mag( Vector2 v ) => MathF.Sqrt( Dot( v, v ) ); + [MethodImpl( INLINE )] public float Dist( Vector2 a, Vector2 b ) => Mag( Sub( b, a ) ); + [MethodImpl( INLINE )] public Vector2 Normalize( Vector2 v ) => Div( v, Mag( v ) ); + + // shared implementations + [MethodImpl( INLINE )] public Vector2 Lerp( Vector2 a, Vector2 b, float t ) { + float omt = 1f - t; + return new Vector2( omt * a.x + t * b.x, omt * a.y + t * b.y ); + } + + } + + public struct VectorMath3D : IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] public Vector3 Zero() => new(0, 0, 0); + [MethodImpl( INLINE )] public Vector3 Add( Vector3 a, Vector3 b ) => new(a.x + b.x, a.y + b.y, a.z + b.z); + [MethodImpl( INLINE )] public Vector3 Sub( Vector3 a, Vector3 b ) => new(a.x - b.x, a.y - b.y, a.z - b.z); + [MethodImpl( INLINE )] public Vector3 Mul( Vector3 v, float c ) => new(v.x * c, v.y * c, v.z * c); + [MethodImpl( INLINE )] public Vector3 Mul( float c, Vector3 v ) => new(v.x * c, v.y * c, v.z * c); + [MethodImpl( INLINE )] public Vector3 Div( Vector3 v, float c ) => new(v.x / c, v.y / c, v.z / c); + [MethodImpl( INLINE )] public float Dot( Vector3 a, Vector3 b ) => a.x * b.x + a.y * b.y + a.z * b.z; + [MethodImpl( INLINE )] public float Mag( Vector3 v ) => MathF.Sqrt( Dot( v, v ) ); + [MethodImpl( INLINE )] public float Dist( Vector3 a, Vector3 b ) => Mag( Sub( b, a ) ); + [MethodImpl( INLINE )] public Vector3 Normalize( Vector3 v ) => Div( v, Mag( v ) ); + + // shared implementations + [MethodImpl( INLINE )] public Vector3 Lerp( Vector3 a, Vector3 b, float t ) { + float omt = 1f - t; + return new Vector3( omt * a.x + t * b.x, omt * a.y + t * b.y, omt * a.z + t * b.z ); + } + } + + public struct VectorMath4D : IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] public Vector4 Zero() => new(0, 0, 0, 0); + [MethodImpl( INLINE )] public Vector4 Add( Vector4 a, Vector4 b ) => new(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + [MethodImpl( INLINE )] public Vector4 Sub( Vector4 a, Vector4 b ) => new(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + [MethodImpl( INLINE )] public Vector4 Mul( Vector4 v, float c ) => new(v.x * c, v.y * c, v.z * c, v.w * c); + [MethodImpl( INLINE )] public Vector4 Mul( float c, Vector4 v ) => new(v.x * c, v.y * c, v.z * c, v.w * c); + [MethodImpl( INLINE )] public Vector4 Div( Vector4 v, float c ) => new(v.x / c, v.y / c, v.z / c, v.w / c); + [MethodImpl( INLINE )] public float Dot( Vector4 a, Vector4 b ) => a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; + [MethodImpl( INLINE )] public float Mag( Vector4 v ) => MathF.Sqrt( Dot( v, v ) ); + [MethodImpl( INLINE )] public float Dist( Vector4 a, Vector4 b ) => Mag( Sub( b, a ) ); + [MethodImpl( INLINE )] public Vector4 Normalize( Vector4 v ) => Div( v, Mag( v ) ); + + // shared implementations + [MethodImpl( INLINE )] public Vector4 Lerp( Vector4 a, Vector4 b, float t ) { + float omt = 1f - t; + return new Vector4( omt * a.x + t * b.x, omt * a.y + t * b.y, omt * a.z + t * b.z, omt * a.w + t * b.w ); + } + } + + // todo: quaternions, as a treat. this is untested and unported basically lol + public struct VectorMathQuat : IVectorMath { + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + [MethodImpl( INLINE )] public Quaternion Zero() => new(0, 0, 0, 0); + [MethodImpl( INLINE )] public Quaternion Add( Quaternion a, Quaternion b ) => new(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + [MethodImpl( INLINE )] public Quaternion Sub( Quaternion a, Quaternion b ) => new(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + [MethodImpl( INLINE )] public Quaternion Mul( Quaternion v, float c ) => new(v.x * c, v.y * c, v.z * c, v.w * c); + [MethodImpl( INLINE )] public Quaternion Mul( float c, Quaternion v ) => new(v.x * c, v.y * c, v.z * c, v.w * c); + [MethodImpl( INLINE )] public Quaternion Div( Quaternion v, float c ) => new(v.x / c, v.y / c, v.z / c, v.w / c); + [MethodImpl( INLINE )] public float Dot( Quaternion a, Quaternion b ) => a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; + [MethodImpl( INLINE )] public float Mag( Quaternion v ) => MathF.Sqrt( Dot( v, v ) ); + [MethodImpl( INLINE )] public float Dist( Quaternion a, Quaternion b ) => Mathfs.Angle( a, b ); + [MethodImpl( INLINE )] public Quaternion Normalize( Quaternion v ) => Div( v, Mag( v ) ); + + // shared implementations + [MethodImpl( INLINE )] public Quaternion Lerp( Quaternion a, Quaternion b, float t ) { + float omt = 1f - t; + return new Quaternion( omt * a.x + t * b.x, omt * a.y + t * b.y, omt * a.z + t * b.z, omt * a.w + t * b.w ); + } + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IVectorMath.cs.meta b/Runtime/Numerics/Interfaces/IVectorMath.cs.meta new file mode 100644 index 0000000..699a8c2 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IVectorMath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 106146a67b5f75647bef03ff84a853bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Interfaces/IWedgeProduct.cs b/Runtime/Numerics/Interfaces/IWedgeProduct.cs new file mode 100644 index 0000000..4fabc14 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IWedgeProduct.cs @@ -0,0 +1,20 @@ +using Unity.Mathematics; + +namespace Freya { + + /// Objects that implement the wedge product + public interface IWedgeProduct { + /// The wedge product between two vectors. This is a generalized form of the cross product. + ///
  • In 2D, this returns a scalar, and is sometimes called the perpendicular dot product.
  • + ///
  • In 3D, this returns a vector, and is effectively the same as the cross product + /// (technically it's a bivector but whatever)
  • + ///
+ [BinaryOp] public W wedge( V other ); + } + + public static partial class mathfs { + /// + public static W wedge( V a, V b ) where V : IWedgeProduct => a.wedge( b ); + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Interfaces/IWedgeProduct.cs.meta b/Runtime/Numerics/Interfaces/IWedgeProduct.cs.meta new file mode 100644 index 0000000..d992f57 --- /dev/null +++ b/Runtime/Numerics/Interfaces/IWedgeProduct.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37b54846b53c434ca8491a5454ce6eaf +timeCreated: 1775599891 \ No newline at end of file diff --git a/Runtime/Numerics/Matrix3x1.cs b/Runtime/Numerics/Matrix3x1.cs new file mode 100644 index 0000000..67b1b56 --- /dev/null +++ b/Runtime/Numerics/Matrix3x1.cs @@ -0,0 +1,29 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +namespace Freya { + /// A 3x1 column matrix with float values + [Serializable] public struct Matrix3x1 { + public float m0, m1, m2; + public Matrix3x1(float m0, float m1, float m2) => (this.m0, this.m1, this.m2) = (m0, m1, m2); + public float this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" ); + } + } + } + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Matrix3x1 Lerp( Matrix3x1 a, Matrix3x1 b, float t ) => new Matrix3x1(Mathfs.Lerp( a.m0, b.m0, t ), Mathfs.Lerp( a.m1, b.m1, t ), Mathfs.Lerp( a.m2, b.m2, t )); + public static bool operator ==( Matrix3x1 a, Matrix3x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2; + public static bool operator !=( Matrix3x1 a, Matrix3x1 b ) => !( a == b ); + public bool Equals( Matrix3x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ); + public override bool Equals( object obj ) => obj is Matrix3x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]"; + } +} diff --git a/Runtime/Numerics/Matrix3x1.cs.meta b/Runtime/Numerics/Matrix3x1.cs.meta new file mode 100644 index 0000000..72c0ac0 --- /dev/null +++ b/Runtime/Numerics/Matrix3x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46eb5269471cf45fe92c8d3c3f846794 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Matrix3x3.cs b/Runtime/Numerics/Matrix3x3.cs new file mode 100644 index 0000000..f36e6cf --- /dev/null +++ b/Runtime/Numerics/Matrix3x3.cs @@ -0,0 +1,227 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Globalization; +using UnityEngine; + +namespace Freya { + + /// A 3x3 matrix + public readonly struct Matrix3x3 { + + public static readonly Matrix3x3 Identity = new Matrix3x3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ); + public static readonly Matrix3x3 Zero = new Matrix3x3( 0, 0, 0, 0, 0, 0, 0, 0, 0 ); + + public readonly float m00, m01, m02; + public readonly float m10, m11, m12; + public readonly float m20, m21, m22; + + public Matrix3x3( float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22 ) { + ( this.m00, this.m01, this.m02 ) = ( m00, m01, m02 ); + ( this.m10, this.m11, this.m12 ) = ( m10, m11, m12 ); + ( this.m20, this.m21, this.m22 ) = ( m20, m21, m22 ); + } + + public Matrix3x3( Vector3 col0, Vector3 col1, Vector3 col2 ) { + m00 = col0.x; + m10 = col0.y; + m20 = col0.z; + m01 = col1.x; + m11 = col1.y; + m21 = col1.z; + m02 = col2.x; + m12 = col2.y; + m22 = col2.z; + } + + public Matrix3x3( Matrix4x4 m ) { + ( this.m00, this.m01, this.m02 ) = ( m.m00, m.m01, m.m02 ); + ( this.m10, this.m11, this.m12 ) = ( m.m10, m.m11, m.m12 ); + ( this.m20, this.m21, this.m22 ) = ( m.m20, m.m21, m.m22 ); + } + + public Matrix3x3( Quaternion q ) { + Matrix4x4 m = Matrix4x4.Rotate( q ); + ( this.m00, this.m01, this.m02 ) = ( m.m00, m.m01, m.m02 ); + ( this.m10, this.m11, this.m12 ) = ( m.m10, m.m11, m.m12 ); + ( this.m20, this.m21, this.m22 ) = ( m.m20, m.m21, m.m22 ); + } + + public static Matrix3x3 Scale( Vector3 s ) { + return new Matrix3x3( + s.x, 0, 0, + 0, s.y, 0, + 0, 0, s.z + ); + } + + public Matrix3x3 NormalizeColumns() { + double lenX = Math.Sqrt( m00 * m00 + m10 * m10 + m20 * m20 ); + double lenY = Math.Sqrt( m01 * m01 + m11 * m11 + m21 * m21 ); + double lenZ = Math.Sqrt( m02 * m02 + m12 * m12 + m22 * m22 ); + return new( + (float)( m00 / lenX ), (float)( m01 / lenY ), (float)( m02 / lenZ ), + (float)( m10 / lenX ), (float)( m11 / lenY ), (float)( m12 / lenZ ), + (float)( m20 / lenX ), (float)( m21 / lenY ), (float)( m22 / lenZ )); + } + + public float this[ int row, int column ] { + get { + return ( row, column ) switch { + (0, 0) => m00, + (0, 1) => m01, + (0, 2) => m02, + (1, 0) => m10, + (1, 1) => m11, + (1, 2) => m12, + (2, 0) => m20, + (2, 1) => m21, + (2, 2) => m22, + _ => throw new IndexOutOfRangeException( $"Matrix row/column indices have to be from 0 to 2, got: ({row},{column})" ) + }; + } + } + + /// Returns the inverse of this matrix. Throws a division by zero exception if it's not invertible + public Matrix3x3 Inverse { + get { + float A1212 = m11 * m22 - m12 * m21; + float A0212 = m10 * m22 - m12 * m20; + float A0112 = m10 * m21 - m11 * m20; + float det = m00 * A1212 - m01 * A0212 + m02 * A0112; + + if( det == 0 ) + throw new DivideByZeroException( "The matrix is not invertible - its determinant is 0" ); + + return new Matrix3x3( + A1212, m02 * m21 - m01 * m22, m01 * m12 - m02 * m11, + -A0212, m00 * m22 - m02 * m20, m10 * m02 - m00 * m12, + A0112, m20 * m01 - m00 * m21, m00 * m11 - m10 * m01 + ) / det; + } + } + + public float Minor( int r, int c ) { + // forming a 2x2 by deleting row r and column c + int r0 = r == 0 ? 1 : 0; + int r1 = r == 2 ? 1 : 2; + int c0 = c == 0 ? 1 : 0; + int c1 = c == 2 ? 1 : 2; + float mn00 = this[r0, c0]; + float mn01 = this[r0, c1]; + float mn10 = this[r1, c0]; + float mn11 = this[r1, c1]; + return mn00 * mn11 - mn01 * mn10; // 2x2 determinant + } + + public float Cofactor( int r, int c ) { + // https://en.wikipedia.org/wiki/Minor_(linear_algebra) + int sign = ( r + c ) % 2 == 0 ? 1 : -1; // (-1)^(r+c) + return sign * Minor( r, c ); + } + + public Matrix3x3 CofactorMatrix => + new( + Cofactor( 0, 0 ), Cofactor( 0, 1 ), Cofactor( 0, 2 ), + Cofactor( 1, 0 ), Cofactor( 1, 1 ), Cofactor( 1, 2 ), + Cofactor( 2, 0 ), Cofactor( 2, 1 ), Cofactor( 2, 2 ) + ); + + /// Returns the determinant of this matrix + public float Determinant { + get { + float A1212 = m11 * m22 - m12 * m21; + float A0212 = m10 * m22 - m12 * m20; + float A0112 = m10 * m21 - m11 * m20; + return m00 * A1212 - m01 * A0212 + m02 * A0112; + } + } + public Matrix3x3 Transpose => + new(m00, m10, m20, + m01, m11, m21, + m02, m12, m22); + + public override string ToString() => ToStringMatrix().ToValueTableString(); + + public string[,] ToStringMatrix() { + return new[,] { + { m00.ToString( CultureInfo.InvariantCulture ), m01.ToString( CultureInfo.InvariantCulture ), m02.ToString( CultureInfo.InvariantCulture ) }, + { m10.ToString( CultureInfo.InvariantCulture ), m11.ToString( CultureInfo.InvariantCulture ), m12.ToString( CultureInfo.InvariantCulture ) }, + { m20.ToString( CultureInfo.InvariantCulture ), m21.ToString( CultureInfo.InvariantCulture ), m22.ToString( CultureInfo.InvariantCulture ) } + }; + } + + public Matrix4x4 ToMatrix4x4( Vector3 pos, float m33 = 1 ) { + Matrix4x4 m4 = (Matrix4x4)this; + m4.SetColumn( 3, new Vector4( pos.x, pos.y, pos.z, m33 ) ); + return m4; + } + + public static explicit operator Matrix3x3( Matrix4x4 m ) => new(m.m00, m.m01, m.m02, m.m10, m.m11, m.m12, m.m20, m.m21, m.m22); + + public static explicit operator Matrix4x4( Matrix3x3 m ) => + new() { + m00 = m.m00, + m01 = m.m01, + m02 = m.m02, + m10 = m.m10, + m11 = m.m11, + m12 = m.m12, + m20 = m.m20, + m21 = m.m21, + m22 = m.m22, + m33 = 1 // to match identity matrix + }; + + public static Matrix3x3 operator *( Matrix3x3 c, float v ) => + new(c.m00 * v, c.m01 * v, c.m02 * v, + c.m10 * v, c.m11 * v, c.m12 * v, + c.m20 * v, c.m21 * v, c.m22 * v); + + public static Matrix3x3 operator /( Matrix3x3 c, float v ) => c * ( 1f / v ); + + public static Matrix3x3 operator *( Matrix3x3 a, Matrix3x3 b ) { + float GetEntry( int r, int c ) => + a[r, 0] * b[0, c] + + a[r, 1] * b[1, c] + + a[r, 2] * b[2, c]; + + return new Matrix3x3( + GetEntry( 0, 0 ), GetEntry( 0, 1 ), GetEntry( 0, 2 ), + GetEntry( 1, 0 ), GetEntry( 1, 1 ), GetEntry( 1, 2 ), + GetEntry( 2, 0 ), GetEntry( 2, 1 ), GetEntry( 2, 2 ) + ); + } + + public static Matrix3x1 operator *( Matrix3x3 c, Matrix3x1 m ) => + new(m.m0 * c.m00 + m.m1 * c.m01 + m.m2 * c.m02, + m.m0 * c.m10 + m.m1 * c.m11 + m.m2 * c.m12, + m.m0 * c.m20 + m.m1 * c.m21 + m.m2 * c.m22); + + public static Vector3 operator *( Matrix3x3 c, Vector3 v ) => + new(v.x * c.m00 + v.y * c.m01 + v.z * c.m02, + v.x * c.m10 + v.y * c.m11 + v.z * c.m12, + v.x * c.m20 + v.y * c.m21 + v.z * c.m22); + + public static Bivector3 operator *( Matrix3x3 c, Bivector3 v ) => + new(v.yz * c.m00 + v.zx * c.m01 + v.xy * c.m02, + v.yz * c.m10 + v.zx * c.m11 + v.xy * c.m12, + v.yz * c.m20 + v.zx * c.m21 + v.xy * c.m22); + + public static Vector2Matrix3x1 operator *( Matrix3x3 c, Vector2Matrix3x1 m ) => new(c * m.X, c * m.Y); + + public static Vector3Matrix3x1 operator *( Matrix3x3 c, Vector3Matrix3x1 m ) => new(c * m.X, c * m.Y, c * m.Z); + + public static Vector4Matrix3x1 operator *( Matrix3x3 c, Vector4Matrix3x1 m ) => new(c * m.X, c * m.Y, c * m.Z, c * m.W); + + public float AverageScale() { + return ( + MathF.Sqrt( m00 * m00 + m10 * m10 + m20 * m20 ) + + MathF.Sqrt( m01 * m01 + m11 * m11 + m21 * m21 ) + + MathF.Sqrt( m02 * m02 + m12 * m12 + m22 * m22 ) + ) / 3; + } + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Matrix3x3.cs.meta b/Runtime/Numerics/Matrix3x3.cs.meta new file mode 100644 index 0000000..b1e5470 --- /dev/null +++ b/Runtime/Numerics/Matrix3x3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d17543dcc01204c4fa46d00e24181170 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Matrix4x1.cs b/Runtime/Numerics/Matrix4x1.cs new file mode 100644 index 0000000..9d5bd8b --- /dev/null +++ b/Runtime/Numerics/Matrix4x1.cs @@ -0,0 +1,29 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +namespace Freya { + /// A 4x1 column matrix with float values + [Serializable] public struct Matrix4x1 { + public float m0, m1, m2, m3; + public Matrix4x1(float m0, float m1, float m2, float m3) => (this.m0, this.m1, this.m2, this.m3) = (m0, m1, m2, m3); + public float this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, 3 => m3, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; case 3: m3 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" ); + } + } + } + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Matrix4x1 Lerp( Matrix4x1 a, Matrix4x1 b, float t ) => new Matrix4x1(Mathfs.Lerp( a.m0, b.m0, t ), Mathfs.Lerp( a.m1, b.m1, t ), Mathfs.Lerp( a.m2, b.m2, t ), Mathfs.Lerp( a.m3, b.m3, t )); + public static bool operator ==( Matrix4x1 a, Matrix4x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2 && a.m3 == b.m3; + public static bool operator !=( Matrix4x1 a, Matrix4x1 b ) => !( a == b ); + public bool Equals( Matrix4x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ) && m3.Equals( other.m3 ); + public override bool Equals( object obj ) => obj is Matrix4x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2, m3 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]\n[{m3}]"; + } +} diff --git a/Runtime/Numerics/Matrix4x1.cs.meta b/Runtime/Numerics/Matrix4x1.cs.meta new file mode 100644 index 0000000..4cd0a1f --- /dev/null +++ b/Runtime/Numerics/Matrix4x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62662aa0799434a159869af6ad28e0f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Probability.cs b/Runtime/Numerics/Probability.cs new file mode 100644 index 0000000..d8b4a7d --- /dev/null +++ b/Runtime/Numerics/Probability.cs @@ -0,0 +1,53 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; + +namespace Freya { + + /// A struct representing a probability (as a rational number) + [Serializable] public struct Probability : IComparable { + + public static readonly rat Zero = new(0, 1); + public static readonly rat One = new(1, 1); + + /// The value of this probability + public rat value; + + /// Creates a representation of probability using a rational number + /// /// The probability value + public Probability( rat value ) => this.value = value; + + /// Creates a representation of probability using a rational number + /// The numerator of this probability + /// The denominator of this probability + public Probability( int num, int den ) : this( new rat( num, den ) ) { + } + + /// Randomly samples this probability, returning either true or false + public bool Sample => Random.Range( 0, value.d ) < value.n; + + public override string ToString() => $"{value} ({(float)value * 100:#.#####}%)"; + + // statics + public static Probability operator &( Probability a, Probability b ) => new(a.value * b.value); + public static Probability operator |( Probability a, Probability b ) => !( !a & !b ); + public static Probability operator +( Probability a, Probability b ) => new(a.value + b.value); + public static Probability operator !( Probability p ) => new(1 - p.value); + + // comparison operators + public static bool operator ==( Probability a, Probability b ) => a.value == b.value; + public static bool operator !=( Probability a, Probability b ) => a.value != b.value; + public static bool operator <( Probability a, Probability b ) => a.value < b.value; + public static bool operator >( Probability a, Probability b ) => a.value > b.value; + public static bool operator <=( Probability a, Probability b ) => a.value <= b.value; + public static bool operator >=( Probability a, Probability b ) => a.value >= b.value; + + // comparison functions + public int CompareTo( Probability other ) => value.CompareTo( other.value ); + public bool Equals( Probability other ) => value.Equals( other.value ); + public override bool Equals( object obj ) => obj is Probability other && Equals( other ); + public override int GetHashCode() => value.GetHashCode(); + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/Probability.cs.meta b/Runtime/Numerics/Probability.cs.meta new file mode 100644 index 0000000..c7e42ba --- /dev/null +++ b/Runtime/Numerics/Probability.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c81aa65965d057d488f9b20e1ae6aa95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/QuaternionMatrix4x1.cs b/Runtime/Numerics/QuaternionMatrix4x1.cs new file mode 100644 index 0000000..3ec3bed --- /dev/null +++ b/Runtime/Numerics/QuaternionMatrix4x1.cs @@ -0,0 +1,30 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 4x1 column matrix with Quaternion values + [Serializable] public struct QuaternionMatrix4x1 { + public Quaternion m0, m1, m2, m3; + public QuaternionMatrix4x1(Quaternion m0, Quaternion m1, Quaternion m2, Quaternion m3) => (this.m0, this.m1, this.m2, this.m3) = (m0, m1, m2, m3); + public Quaternion this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, 3 => m3, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; case 3: m3 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" ); + } + } + } + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static QuaternionMatrix4x1 Slerp( QuaternionMatrix4x1 a, QuaternionMatrix4x1 b, float t ) => new QuaternionMatrix4x1(Quaternion.SlerpUnclamped( a.m0, b.m0, t ), Quaternion.SlerpUnclamped( a.m1, b.m1, t ), Quaternion.SlerpUnclamped( a.m2, b.m2, t ), Quaternion.SlerpUnclamped( a.m3, b.m3, t )); + public static bool operator ==( QuaternionMatrix4x1 a, QuaternionMatrix4x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2 && a.m3 == b.m3; + public static bool operator !=( QuaternionMatrix4x1 a, QuaternionMatrix4x1 b ) => !( a == b ); + public bool Equals( QuaternionMatrix4x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ) && m3.Equals( other.m3 ); + public override bool Equals( object obj ) => obj is QuaternionMatrix4x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2, m3 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]\n[{m3}]"; + } +} diff --git a/Runtime/Numerics/QuaternionMatrix4x1.cs.meta b/Runtime/Numerics/QuaternionMatrix4x1.cs.meta new file mode 100644 index 0000000..7ace23a --- /dev/null +++ b/Runtime/Numerics/QuaternionMatrix4x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 526976a4e61ad714696fce96490bcd19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/RationalMatrix3x3.cs b/Runtime/Numerics/RationalMatrix3x3.cs new file mode 100644 index 0000000..9e9c164 --- /dev/null +++ b/Runtime/Numerics/RationalMatrix3x3.cs @@ -0,0 +1,114 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine; + +namespace Freya { + + /// A 3x3 matrix using exact rational number representation + public readonly struct RationalMatrix3x3 { + + public static readonly RationalMatrix3x3 Identity = new RationalMatrix3x3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ); + public static readonly RationalMatrix3x3 Zero = new RationalMatrix3x3( 0, 0, 0, 0, 0, 0, 0, 0, 0 ); + + public readonly rat m00, m01, m02; + public readonly rat m10, m11, m12; + public readonly rat m20, m21, m22; + + public RationalMatrix3x3( rat m00, rat m01, rat m02, rat m10, rat m11, rat m12, rat m20, rat m21, rat m22 ) { + ( this.m00, this.m01, this.m02 ) = ( m00, m01, m02 ); + ( this.m10, this.m11, this.m12 ) = ( m10, m11, m12 ); + ( this.m20, this.m21, this.m22 ) = ( m20, m21, m22 ); + } + + public rat this[ int row, int column ] { + get { + return ( row, column ) switch { + (0, 0) => m00, + (0, 1) => m01, + (0, 2) => m02, + (1, 0) => m10, + (1, 1) => m11, + (1, 2) => m12, + (2, 0) => m20, + (2, 1) => m21, + (2, 2) => m22, + _ => throw new IndexOutOfRangeException( $"Matrix row/column indices have to be from 0 to 3, got: ({row},{column})" ) + }; + } + } + + /// Returns the inverse of this matrix. Throws a division by zero exception if it's not invertible + public RationalMatrix3x3 Inverse { + get { + rat A1212 = m11 * m22 - m12 * m21; + rat A0212 = m10 * m22 - m12 * m20; + rat A0112 = m10 * m21 - m11 * m20; + rat det = m00 * A1212 - m01 * A0212 + m02 * A0112; + + if( det == rat.zero ) + throw new DivideByZeroException( "The matrix is not invertible - its determinant is 0" ); + + return new RationalMatrix3x3( + A1212, m02 * m21 - m01 * m22, m01 * m12 - m02 * m11, + -A0212, m00 * m22 - m02 * m20, m10 * m02 - m00 * m12, + A0112, m20 * m01 - m00 * m21, m00 * m11 - m10 * m01 + ) / det; + } + } + + /// Returns the determinant of this matrix + public rat Determinant { + get { + rat A1212 = m11 * m22 - m12 * m21; + rat A0212 = m10 * m22 - m12 * m20; + rat A0112 = m10 * m21 - m11 * m20; + return m00 * A1212 - m01 * A0212 + m02 * A0112; + } + } + + public override string ToString() => ToStringMatrix().ToValueTableString(); + + public string[,] ToStringMatrix() { + return new[,] { + { m00.ToString(), m01.ToString(), m02.ToString() }, + { m10.ToString(), m11.ToString(), m12.ToString() }, + { m20.ToString(), m21.ToString(), m22.ToString() } + }; + } + + public static RationalMatrix3x3 operator *( RationalMatrix3x3 c, rat v ) => + new(c.m00 * v, c.m01 * v, c.m02 * v, + c.m10 * v, c.m11 * v, c.m12 * v, + c.m20 * v, c.m21 * v, c.m22 * v); + + public static RationalMatrix3x3 operator /( RationalMatrix3x3 c, rat v ) => c * v.Reciprocal; + + public static RationalMatrix3x3 operator *( RationalMatrix3x3 a, RationalMatrix3x3 b ) { + rat GetEntry( int r, int c ) => a[r, 0] * b[0, c] + a[r, 1] * b[1, c] + a[r, 2] * b[2, c] + a[r, 3] * b[3, c]; + + return new RationalMatrix3x3( + GetEntry( 0, 0 ), GetEntry( 0, 1 ), GetEntry( 0, 2 ), + GetEntry( 1, 0 ), GetEntry( 1, 1 ), GetEntry( 1, 2 ), + GetEntry( 2, 0 ), GetEntry( 2, 1 ), GetEntry( 2, 2 ) + ); + } + + /// + public static Matrix3x1 operator *( RationalMatrix3x3 c, Matrix3x1 m ) => + new(m.m0 * c.m00 + m.m1 * c.m01 + m.m2 * c.m02, + m.m0 * c.m10 + m.m1 * c.m11 + m.m2 * c.m12, + m.m0 * c.m20 + m.m1 * c.m21 + m.m2 * c.m22); + + /// + public static Vector2Matrix3x1 operator *( RationalMatrix3x3 c, Vector2Matrix3x1 m ) => new(c * m.X, c * m.Y); + + /// + public static Vector3Matrix3x1 operator *( RationalMatrix3x3 c, Vector3Matrix3x1 m ) => new(c * m.X, c * m.Y, c * m.Z); + + /// + public static Vector4Matrix3x1 operator *( RationalMatrix3x3 c, Vector4Matrix3x1 m ) => new(c * m.X, c * m.Y, c * m.Z, c * m.W); + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/RationalMatrix3x3.cs.meta b/Runtime/Numerics/RationalMatrix3x3.cs.meta new file mode 100644 index 0000000..7ca6c4f --- /dev/null +++ b/Runtime/Numerics/RationalMatrix3x3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c3f12f91d06c450ea67ecad997e7621 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/RationalMatrix4x4.cs b/Runtime/Numerics/RationalMatrix4x4.cs new file mode 100644 index 0000000..59426e8 --- /dev/null +++ b/Runtime/Numerics/RationalMatrix4x4.cs @@ -0,0 +1,173 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine; + +namespace Freya { + + /// A 4x4 matrix using exact rational number representation + public readonly struct RationalMatrix4x4 { + + public static readonly RationalMatrix4x4 Identity = new RationalMatrix4x4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); + public static readonly RationalMatrix4x4 Zero = new RationalMatrix4x4( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ); + + public readonly rat m00, m01, m02, m03; + public readonly rat m10, m11, m12, m13; + public readonly rat m20, m21, m22, m23; + public readonly rat m30, m31, m32, m33; + + public RationalMatrix4x4( rat m00, rat m01, rat m02, rat m03, rat m10, rat m11, rat m12, rat m13, rat m20, rat m21, rat m22, rat m23, rat m30, rat m31, rat m32, rat m33 ) { + ( this.m00, this.m01, this.m02, this.m03 ) = ( m00, m01, m02, m03 ); + ( this.m10, this.m11, this.m12, this.m13 ) = ( m10, m11, m12, m13 ); + ( this.m20, this.m21, this.m22, this.m23 ) = ( m20, m21, m22, m23 ); + ( this.m30, this.m31, this.m32, this.m33 ) = ( m30, m31, m32, m33 ); + } + + public rat this[ int row, int column ] { + get { + return ( row, column ) switch { + (0, 0) => m00, + (0, 1) => m01, + (0, 2) => m02, + (0, 3) => m03, + (1, 0) => m10, + (1, 1) => m11, + (1, 2) => m12, + (1, 3) => m13, + (2, 0) => m20, + (2, 1) => m21, + (2, 2) => m22, + (2, 3) => m23, + (3, 0) => m30, + (3, 1) => m31, + (3, 2) => m32, + (3, 3) => m33, + _ => throw new IndexOutOfRangeException( $"Matrix row/column indices have to be from 0 to 3, got: ({row},{column})" ) + }; + } + } + + /// Returns the inverse of this matrix. Throws a division by zero exception if it's not invertible + public RationalMatrix4x4 Inverse { + get { + // source: https://stackoverflow.com/questions/1148309/inverting-a-4x4-matrix + rat A2323 = m22 * m33 - m23 * m32; + rat A1323 = m21 * m33 - m23 * m31; + rat A1223 = m21 * m32 - m22 * m31; + rat A0323 = m20 * m33 - m23 * m30; + rat A0223 = m20 * m32 - m22 * m30; + rat A0123 = m20 * m31 - m21 * m30; + rat det = m00 * ( m11 * A2323 - m12 * A1323 + m13 * A1223 ) + - m01 * ( m10 * A2323 - m12 * A0323 + m13 * A0223 ) + + m02 * ( m10 * A1323 - m11 * A0323 + m13 * A0123 ) + - m03 * ( m10 * A1223 - m11 * A0223 + m12 * A0123 ); + + if( det == rat.zero ) + throw new DivideByZeroException( "The matrix is not invertible - its determinant is 0" ); + + rat A2313 = m12 * m33 - m13 * m32; + rat A1313 = m11 * m33 - m13 * m31; + rat A1213 = m11 * m32 - m12 * m31; + rat A2312 = m12 * m23 - m13 * m22; + rat A1312 = m11 * m23 - m13 * m21; + rat A1212 = m11 * m22 - m12 * m21; + rat A0313 = m10 * m33 - m13 * m30; + rat A0213 = m10 * m32 - m12 * m30; + rat A0312 = m10 * m23 - m13 * m20; + rat A0212 = m10 * m22 - m12 * m20; + rat A0113 = m10 * m31 - m11 * m30; + rat A0112 = m10 * m21 - m11 * m20; + + return new RationalMatrix4x4( + ( m11 * A2323 - m12 * A1323 + m13 * A1223 ), -( m01 * A2323 - m02 * A1323 + m03 * A1223 ), ( m01 * A2313 - m02 * A1313 + m03 * A1213 ), -( m01 * A2312 - m02 * A1312 + m03 * A1212 ), + -( m10 * A2323 - m12 * A0323 + m13 * A0223 ), ( m00 * A2323 - m02 * A0323 + m03 * A0223 ), -( m00 * A2313 - m02 * A0313 + m03 * A0213 ), ( m00 * A2312 - m02 * A0312 + m03 * A0212 ), + ( m10 * A1323 - m11 * A0323 + m13 * A0123 ), -( m00 * A1323 - m01 * A0323 + m03 * A0123 ), ( m00 * A1313 - m01 * A0313 + m03 * A0113 ), -( m00 * A1312 - m01 * A0312 + m03 * A0112 ), + -( m10 * A1223 - m11 * A0223 + m12 * A0123 ), ( m00 * A1223 - m01 * A0223 + m02 * A0123 ), -( m00 * A1213 - m01 * A0213 + m02 * A0113 ), ( m00 * A1212 - m01 * A0212 + m02 * A0112 ) + ) / det; + } + } + + /// Returns the determinant of this matrix + public rat Determinant { + get { + // source: https://stackoverflow.com/questions/1148309/inverting-a-4x4-matrix + rat A2323 = m22 * m33 - m23 * m32; + rat A1323 = m21 * m33 - m23 * m31; + rat A1223 = m21 * m32 - m22 * m31; + rat A0323 = m20 * m33 - m23 * m30; + rat A0223 = m20 * m32 - m22 * m30; + rat A0123 = m20 * m31 - m21 * m30; + return m00 * ( m11 * A2323 - m12 * A1323 + m13 * A1223 ) + - m01 * ( m10 * A2323 - m12 * A0323 + m13 * A0223 ) + + m02 * ( m10 * A1323 - m11 * A0323 + m13 * A0123 ) + - m03 * ( m10 * A1223 - m11 * A0223 + m12 * A0123 ); + } + } + + public override string ToString() => ToStringMatrix().ToValueTableString(); + + public string[,] ToStringMatrix() { + return new[,] { + { m00.ToString(), m01.ToString(), m02.ToString(), m03.ToString() }, + { m10.ToString(), m11.ToString(), m12.ToString(), m13.ToString() }, + { m20.ToString(), m21.ToString(), m22.ToString(), m23.ToString() }, + { m30.ToString(), m31.ToString(), m32.ToString(), m33.ToString() } + }; + } + + public static explicit operator Matrix4x4( RationalMatrix4x4 c ) { + return CharMatrix.Create( + (float)c.m00, (float)c.m01, (float)c.m02, (float)c.m03, + (float)c.m10, (float)c.m11, (float)c.m12, (float)c.m13, + (float)c.m20, (float)c.m21, (float)c.m22, (float)c.m23, + (float)c.m30, (float)c.m31, (float)c.m32, (float)c.m33 + ); + } + + public static RationalMatrix4x4 operator *( RationalMatrix4x4 c, rat v ) => + new(c.m00 * v, c.m01 * v, c.m02 * v, c.m03 * v, + c.m10 * v, c.m11 * v, c.m12 * v, c.m13 * v, + c.m20 * v, c.m21 * v, c.m22 * v, c.m23 * v, + c.m30 * v, c.m31 * v, c.m32 * v, c.m33 * v); + + public static explicit operator RationalMatrix4x4( RationalMatrix3x3 c ) => + new(c.m00, c.m01, c.m02, 0, + c.m10, c.m11, c.m12, 0, + c.m20, c.m21, c.m22, 0, + 0, 0, 0, 1); + + + public static RationalMatrix4x4 operator /( RationalMatrix4x4 c, rat v ) => c * v.Reciprocal; + + public static RationalMatrix4x4 operator *( RationalMatrix4x4 a, RationalMatrix4x4 b ) { + rat GetEntry( int r, int c ) => a[r, 0] * b[0, c] + a[r, 1] * b[1, c] + a[r, 2] * b[2, c] + a[r, 3] * b[3, c]; + + return new RationalMatrix4x4( + GetEntry( 0, 0 ), GetEntry( 0, 1 ), GetEntry( 0, 2 ), GetEntry( 0, 3 ), + GetEntry( 1, 0 ), GetEntry( 1, 1 ), GetEntry( 1, 2 ), GetEntry( 1, 3 ), + GetEntry( 2, 0 ), GetEntry( 2, 1 ), GetEntry( 2, 2 ), GetEntry( 2, 3 ), + GetEntry( 3, 0 ), GetEntry( 3, 1 ), GetEntry( 3, 2 ), GetEntry( 3, 3 ) + ); + } + + /// Multiplies this matrix C by a column matrix M + /// The left hand side 4x4 matrix + /// The right hand side 4x1 matrix + public static Matrix4x1 operator *( RationalMatrix4x4 c, Matrix4x1 m ) => + new(m.m0 * c.m00 + m.m1 * c.m01 + m.m2 * c.m02 + m.m3 * c.m03, + m.m0 * c.m10 + m.m1 * c.m11 + m.m2 * c.m12 + m.m3 * c.m13, + m.m0 * c.m20 + m.m1 * c.m21 + m.m2 * c.m22 + m.m3 * c.m23, + m.m0 * c.m30 + m.m1 * c.m31 + m.m2 * c.m32 + m.m3 * c.m33); + + /// + public static Vector2Matrix4x1 operator *( RationalMatrix4x4 c, Vector2Matrix4x1 m ) => new(c * m.X, c * m.Y); + + /// + public static Vector3Matrix4x1 operator *( RationalMatrix4x4 c, Vector3Matrix4x1 m ) => new(c * m.X, c * m.Y, c * m.Z); + + /// + public static Vector4Matrix4x1 operator *( RationalMatrix4x4 c, Vector4Matrix4x1 m ) => new(c * m.X, c * m.Y, c * m.Z, c * m.W); + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/RationalMatrix4x4.cs.meta b/Runtime/Numerics/RationalMatrix4x4.cs.meta new file mode 100644 index 0000000..81c25f9 --- /dev/null +++ b/Runtime/Numerics/RationalMatrix4x4.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e23eb42353ba44267aad2d4f107fb5f0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector2Matrix3x1.cs b/Runtime/Numerics/Vector2Matrix3x1.cs new file mode 100644 index 0000000..51c602e --- /dev/null +++ b/Runtime/Numerics/Vector2Matrix3x1.cs @@ -0,0 +1,33 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 3x1 column matrix with Vector2 values + [Serializable] public struct Vector2Matrix3x1 { + public Vector2 m0, m1, m2; + public Vector2Matrix3x1(Vector2 m0, Vector2 m1, Vector2 m2) => (this.m0, this.m1, this.m2) = (m0, m1, m2); + public Vector2Matrix3x1(Matrix3x1 x, Matrix3x1 y) => (m0, m1, m2) = (new Vector2(x.m0, y.m0), new Vector2(x.m1, y.m1), new Vector2(x.m2, y.m2)); + public Vector2 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" ); + } + } + } + public Matrix3x1 X => new(m0.x, m1.x, m2.x); + public Matrix3x1 Y => new(m0.y, m1.y, m2.y); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector2Matrix3x1 Lerp( Vector2Matrix3x1 a, Vector2Matrix3x1 b, float t ) => new Vector2Matrix3x1(Vector2.LerpUnclamped( a.m0, b.m0, t ), Vector2.LerpUnclamped( a.m1, b.m1, t ), Vector2.LerpUnclamped( a.m2, b.m2, t )); + public static bool operator ==( Vector2Matrix3x1 a, Vector2Matrix3x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2; + public static bool operator !=( Vector2Matrix3x1 a, Vector2Matrix3x1 b ) => !( a == b ); + public bool Equals( Vector2Matrix3x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ); + public override bool Equals( object obj ) => obj is Vector2Matrix3x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]"; + } +} diff --git a/Runtime/Numerics/Vector2Matrix3x1.cs.meta b/Runtime/Numerics/Vector2Matrix3x1.cs.meta new file mode 100644 index 0000000..09179a7 --- /dev/null +++ b/Runtime/Numerics/Vector2Matrix3x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4896fd8a91aeb4827a2f369b30b5c846 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector2Matrix4x1.cs b/Runtime/Numerics/Vector2Matrix4x1.cs new file mode 100644 index 0000000..a558041 --- /dev/null +++ b/Runtime/Numerics/Vector2Matrix4x1.cs @@ -0,0 +1,33 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 4x1 column matrix with Vector2 values + [Serializable] public struct Vector2Matrix4x1 { + public Vector2 m0, m1, m2, m3; + public Vector2Matrix4x1(Vector2 m0, Vector2 m1, Vector2 m2, Vector2 m3) => (this.m0, this.m1, this.m2, this.m3) = (m0, m1, m2, m3); + public Vector2Matrix4x1(Matrix4x1 x, Matrix4x1 y) => (m0, m1, m2, m3) = (new Vector2(x.m0, y.m0), new Vector2(x.m1, y.m1), new Vector2(x.m2, y.m2), new Vector2(x.m3, y.m3)); + public Vector2 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, 3 => m3, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; case 3: m3 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" ); + } + } + } + public Matrix4x1 X => new(m0.x, m1.x, m2.x, m3.x); + public Matrix4x1 Y => new(m0.y, m1.y, m2.y, m3.y); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector2Matrix4x1 Lerp( Vector2Matrix4x1 a, Vector2Matrix4x1 b, float t ) => new Vector2Matrix4x1(Vector2.LerpUnclamped( a.m0, b.m0, t ), Vector2.LerpUnclamped( a.m1, b.m1, t ), Vector2.LerpUnclamped( a.m2, b.m2, t ), Vector2.LerpUnclamped( a.m3, b.m3, t )); + public static bool operator ==( Vector2Matrix4x1 a, Vector2Matrix4x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2 && a.m3 == b.m3; + public static bool operator !=( Vector2Matrix4x1 a, Vector2Matrix4x1 b ) => !( a == b ); + public bool Equals( Vector2Matrix4x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ) && m3.Equals( other.m3 ); + public override bool Equals( object obj ) => obj is Vector2Matrix4x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2, m3 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]\n[{m3}]"; + } +} diff --git a/Runtime/Numerics/Vector2Matrix4x1.cs.meta b/Runtime/Numerics/Vector2Matrix4x1.cs.meta new file mode 100644 index 0000000..8eaa31f --- /dev/null +++ b/Runtime/Numerics/Vector2Matrix4x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c4f1faa6ec0d4a49af803a0d4893ac9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector3Matrix3x1.cs b/Runtime/Numerics/Vector3Matrix3x1.cs new file mode 100644 index 0000000..6e6dc20 --- /dev/null +++ b/Runtime/Numerics/Vector3Matrix3x1.cs @@ -0,0 +1,34 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 3x1 column matrix with Vector3 values + [Serializable] public struct Vector3Matrix3x1 { + public Vector3 m0, m1, m2; + public Vector3Matrix3x1(Vector3 m0, Vector3 m1, Vector3 m2) => (this.m0, this.m1, this.m2) = (m0, m1, m2); + public Vector3Matrix3x1(Matrix3x1 x, Matrix3x1 y, Matrix3x1 z) => (m0, m1, m2) = (new Vector3(x.m0, y.m0, z.m0), new Vector3(x.m1, y.m1, z.m1), new Vector3(x.m2, y.m2, z.m2)); + public Vector3 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" ); + } + } + } + public Matrix3x1 X => new(m0.x, m1.x, m2.x); + public Matrix3x1 Y => new(m0.y, m1.y, m2.y); + public Matrix3x1 Z => new(m0.z, m1.z, m2.z); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector3Matrix3x1 Lerp( Vector3Matrix3x1 a, Vector3Matrix3x1 b, float t ) => new Vector3Matrix3x1(Vector3.LerpUnclamped( a.m0, b.m0, t ), Vector3.LerpUnclamped( a.m1, b.m1, t ), Vector3.LerpUnclamped( a.m2, b.m2, t )); + public static bool operator ==( Vector3Matrix3x1 a, Vector3Matrix3x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2; + public static bool operator !=( Vector3Matrix3x1 a, Vector3Matrix3x1 b ) => !( a == b ); + public bool Equals( Vector3Matrix3x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ); + public override bool Equals( object obj ) => obj is Vector3Matrix3x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]"; + } +} diff --git a/Runtime/Numerics/Vector3Matrix3x1.cs.meta b/Runtime/Numerics/Vector3Matrix3x1.cs.meta new file mode 100644 index 0000000..302d6ea --- /dev/null +++ b/Runtime/Numerics/Vector3Matrix3x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2d4626d12e4443a7813a4345b9efc21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector3Matrix4x1.cs b/Runtime/Numerics/Vector3Matrix4x1.cs new file mode 100644 index 0000000..bf9943f --- /dev/null +++ b/Runtime/Numerics/Vector3Matrix4x1.cs @@ -0,0 +1,34 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 4x1 column matrix with Vector3 values + [Serializable] public struct Vector3Matrix4x1 { + public Vector3 m0, m1, m2, m3; + public Vector3Matrix4x1(Vector3 m0, Vector3 m1, Vector3 m2, Vector3 m3) => (this.m0, this.m1, this.m2, this.m3) = (m0, m1, m2, m3); + public Vector3Matrix4x1(Matrix4x1 x, Matrix4x1 y, Matrix4x1 z) => (m0, m1, m2, m3) = (new Vector3(x.m0, y.m0, z.m0), new Vector3(x.m1, y.m1, z.m1), new Vector3(x.m2, y.m2, z.m2), new Vector3(x.m3, y.m3, z.m3)); + public Vector3 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, 3 => m3, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; case 3: m3 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" ); + } + } + } + public Matrix4x1 X => new(m0.x, m1.x, m2.x, m3.x); + public Matrix4x1 Y => new(m0.y, m1.y, m2.y, m3.y); + public Matrix4x1 Z => new(m0.z, m1.z, m2.z, m3.z); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector3Matrix4x1 Lerp( Vector3Matrix4x1 a, Vector3Matrix4x1 b, float t ) => new Vector3Matrix4x1(Vector3.LerpUnclamped( a.m0, b.m0, t ), Vector3.LerpUnclamped( a.m1, b.m1, t ), Vector3.LerpUnclamped( a.m2, b.m2, t ), Vector3.LerpUnclamped( a.m3, b.m3, t )); + public static bool operator ==( Vector3Matrix4x1 a, Vector3Matrix4x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2 && a.m3 == b.m3; + public static bool operator !=( Vector3Matrix4x1 a, Vector3Matrix4x1 b ) => !( a == b ); + public bool Equals( Vector3Matrix4x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ) && m3.Equals( other.m3 ); + public override bool Equals( object obj ) => obj is Vector3Matrix4x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2, m3 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]\n[{m3}]"; + } +} diff --git a/Runtime/Numerics/Vector3Matrix4x1.cs.meta b/Runtime/Numerics/Vector3Matrix4x1.cs.meta new file mode 100644 index 0000000..0cfa3e4 --- /dev/null +++ b/Runtime/Numerics/Vector3Matrix4x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f670854c9791746b08f464357e34ac53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector4Matrix3x1.cs b/Runtime/Numerics/Vector4Matrix3x1.cs new file mode 100644 index 0000000..e8f48b7 --- /dev/null +++ b/Runtime/Numerics/Vector4Matrix3x1.cs @@ -0,0 +1,35 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 3x1 column matrix with Vector4 values + [Serializable] public struct Vector4Matrix3x1 { + public Vector4 m0, m1, m2; + public Vector4Matrix3x1(Vector4 m0, Vector4 m1, Vector4 m2) => (this.m0, this.m1, this.m2) = (m0, m1, m2); + public Vector4Matrix3x1(Matrix3x1 x, Matrix3x1 y, Matrix3x1 z, Matrix3x1 w) => (m0, m1, m2) = (new Vector4(x.m0, y.m0, z.m0, w.m0), new Vector4(x.m1, y.m1, z.m1, w.m1), new Vector4(x.m2, y.m2, z.m2, w.m2)); + public Vector4 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 2, got: {row}" ); + } + } + } + public Matrix3x1 X => new(m0.x, m1.x, m2.x); + public Matrix3x1 Y => new(m0.y, m1.y, m2.y); + public Matrix3x1 Z => new(m0.z, m1.z, m2.z); + public Matrix3x1 W => new(m0.w, m1.w, m2.w); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector4Matrix3x1 Lerp( Vector4Matrix3x1 a, Vector4Matrix3x1 b, float t ) => new Vector4Matrix3x1(Vector4.LerpUnclamped( a.m0, b.m0, t ), Vector4.LerpUnclamped( a.m1, b.m1, t ), Vector4.LerpUnclamped( a.m2, b.m2, t )); + public static bool operator ==( Vector4Matrix3x1 a, Vector4Matrix3x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2; + public static bool operator !=( Vector4Matrix3x1 a, Vector4Matrix3x1 b ) => !( a == b ); + public bool Equals( Vector4Matrix3x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ); + public override bool Equals( object obj ) => obj is Vector4Matrix3x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]"; + } +} diff --git a/Runtime/Numerics/Vector4Matrix3x1.cs.meta b/Runtime/Numerics/Vector4Matrix3x1.cs.meta new file mode 100644 index 0000000..48f6885 --- /dev/null +++ b/Runtime/Numerics/Vector4Matrix3x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 067c7fbbdf8b242f099b313b03dc26f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/Vector4Matrix4x1.cs b/Runtime/Numerics/Vector4Matrix4x1.cs new file mode 100644 index 0000000..c7f6ced --- /dev/null +++ b/Runtime/Numerics/Vector4Matrix4x1.cs @@ -0,0 +1,35 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using UnityEngine; +namespace Freya { + /// A 4x1 column matrix with Vector4 values + [Serializable] public struct Vector4Matrix4x1 { + public Vector4 m0, m1, m2, m3; + public Vector4Matrix4x1(Vector4 m0, Vector4 m1, Vector4 m2, Vector4 m3) => (this.m0, this.m1, this.m2, this.m3) = (m0, m1, m2, m3); + public Vector4Matrix4x1(Matrix4x1 x, Matrix4x1 y, Matrix4x1 z, Matrix4x1 w) => (m0, m1, m2, m3) = (new Vector4(x.m0, y.m0, z.m0, w.m0), new Vector4(x.m1, y.m1, z.m1, w.m1), new Vector4(x.m2, y.m2, z.m2, w.m2), new Vector4(x.m3, y.m3, z.m3, w.m3)); + public Vector4 this[int row] { + get => row switch{0 => m0, 1 => m1, 2 => m2, 3 => m3, _ => throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" )}; + set { + switch(row) { + case 0: m0 = value; break; case 1: m1 = value; break; case 2: m2 = value; break; case 3: m3 = value; break; + default: throw new IndexOutOfRangeException( $"Matrix row index has to be from 0 to 3, got: {row}" ); + } + } + } + public Matrix4x1 X => new(m0.x, m1.x, m2.x, m3.x); + public Matrix4x1 Y => new(m0.y, m1.y, m2.y, m3.y); + public Matrix4x1 Z => new(m0.z, m1.z, m2.z, m3.z); + public Matrix4x1 W => new(m0.w, m1.w, m2.w, m3.w); + /// Linearly interpolates between two matrices, based on a value t + /// The value to blend by + public static Vector4Matrix4x1 Lerp( Vector4Matrix4x1 a, Vector4Matrix4x1 b, float t ) => new Vector4Matrix4x1(Vector4.LerpUnclamped( a.m0, b.m0, t ), Vector4.LerpUnclamped( a.m1, b.m1, t ), Vector4.LerpUnclamped( a.m2, b.m2, t ), Vector4.LerpUnclamped( a.m3, b.m3, t )); + public static bool operator ==( Vector4Matrix4x1 a, Vector4Matrix4x1 b ) => a.m0 == b.m0 && a.m1 == b.m1 && a.m2 == b.m2 && a.m3 == b.m3; + public static bool operator !=( Vector4Matrix4x1 a, Vector4Matrix4x1 b ) => !( a == b ); + public bool Equals( Vector4Matrix4x1 other ) => m0.Equals( other.m0 ) && m1.Equals( other.m1 ) && m2.Equals( other.m2 ) && m3.Equals( other.m3 ); + public override bool Equals( object obj ) => obj is Vector4Matrix4x1 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( m0, m1, m2, m3 ); + public override string ToString() => $"[{m0}]\n[{m1}]\n[{m2}]\n[{m3}]"; + } +} diff --git a/Runtime/Numerics/Vector4Matrix4x1.cs.meta b/Runtime/Numerics/Vector4Matrix4x1.cs.meta new file mode 100644 index 0000000..58c2ee6 --- /dev/null +++ b/Runtime/Numerics/Vector4Matrix4x1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 123ba71fee2e44862b0c1409d27df3ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/inth.cs b/Runtime/Numerics/inth.cs new file mode 100644 index 0000000..f3a7ee2 --- /dev/null +++ b/Runtime/Numerics/inth.cs @@ -0,0 +1,95 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine; + +namespace Freya {} + +namespace Freya { + + /// A fixed precision data type for half-integers, using a single backing interger. For numbers like: 0, 0.5, 1, 1.5, 2, etc. + [Serializable] public struct inth : + IComparable, + IEquatable, + INumber, + ISignedNumber, + IHalfNumber, + IRoundable { + /// The number of halves + [SerializeField] public int h; + + // public intHalf( int halves ) => this.h = halves; + public inth fromInt( int intValue ) => this.h = intValue * 2; + + public bool isInteger => h % 2 == 0; + public bool isZero => h == 0; + public bool isOrthogonal => true; + public int sign => Math.Sign( h ); + public inth abs => new() { h = Math.Abs( h ) }; + public inth max( inth other ) => this > other ? this : other; + public inth min( inth other ) => this < other ? this : other; + public inth to( inth target ) => target - this; + public static int zero => 0; + public static inth half => new() { h = 1 }; + public static int one => 1; + public int times2 => h; + + public static inth fromFloat( float v, RoundingDirection rounding = RoundingDirection.ToEven ) => new() { h = mathfs.round( v * 2, rounding ) }; + + public int round( RoundingDirection rounding = RoundingDirection.ToEven ) { + if( isInteger ) + return h / 2; + int rUp = ( h + 1 ) / 2; + return rounding switch { + RoundingDirection.AwayFromZero => h > 0 ? rUp : rUp - 1, + RoundingDirection.ToZero => h < 0 ? rUp : rUp - 1, + RoundingDirection.ToNegativeInfinity => rUp - 1, // floor(x) + RoundingDirection.ToPositiveInfinity => rUp, // ceil(x) + RoundingDirection.ToEven or _ => rUp % 2 == 0 ? rUp : rUp - 1, + }; + } + + public int floorToward0 => round( RoundingDirection.ToZero ); + public int ceilAwayFrom0 => round( RoundingDirection.AwayFromZero ); + public int floor => round( RoundingDirection.ToNegativeInfinity ); + public int ceil => round( RoundingDirection.ToPositiveInfinity ); + + public override string ToString() => isInteger ? $"{h / 2}" : $"{h / 2}.5"; + + // public static intHalf Lerp( intHalf a, intHalf b, intHalf t ) => a + t * ( b - a ); + // public static intHalf InverseLerp( intHalf a, intHalf b, intHalf v ) => ( v - a ) / ( b - a ); + + public static implicit operator rat( inth ih ) => new(ih.h, 2); + public static implicit operator inth( int i ) => new() { h = i * 2 }; + public static explicit operator float( inth i ) => i.h / 2f; // todo: this is a little wasteful prolly + + // unary operations + public static inth operator -( inth ih ) => new() { h = -ih.h }; + public static inth operator +( inth ih ) => ih; + + // binary operations + public static inth operator +( inth a, inth b ) => new() { h = a.h + b.h }; + public static inth operator +( inth a, int b ) => new() { h = a.h + b * 2 }; + public static inth operator +( int a, inth b ) => new() { h = a * 2 + b.h }; + public static inth operator -( inth a, inth b ) => new() { h = a.h - b.h }; + public static inth operator -( inth a, int b ) => new() { h = a.h - b * 2 }; + public static inth operator -( int a, inth b ) => new() { h = a * 2 - b.h }; + public static inth operator *( inth a, int b ) => new() { h = a.h * b }; + public static inth operator *( int a, inth b ) => new() { h = a * b.h }; + + // comparison operators + public static bool operator ==( inth a, inth b ) => a.CompareTo( b ) == 0; + public static bool operator !=( inth a, inth b ) => a.CompareTo( b ) != 0; + public static bool operator <( inth a, inth b ) => a.CompareTo( b ) < 0; + public static bool operator >( inth a, inth b ) => a.CompareTo( b ) > 0; + public static bool operator <=( inth a, inth b ) => a.CompareTo( b ) <= 0; + public static bool operator >=( inth a, inth b ) => a.CompareTo( b ) >= 0; + + // comparison functions + public int CompareTo( inth other ) => h.CompareTo( other.h ); + public bool Equals( inth other ) => h == other.h; + public override bool Equals( object obj ) => obj is inth other && Equals( other ); + public override int GetHashCode() => h.GetHashCode(); + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/inth.cs.meta b/Runtime/Numerics/inth.cs.meta new file mode 100644 index 0000000..8969ddd --- /dev/null +++ b/Runtime/Numerics/inth.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ad7fd47f9cf845ca991eb4563b706893 +timeCreated: 1774974007 \ No newline at end of file diff --git a/Runtime/Numerics/inth2.cs b/Runtime/Numerics/inth2.cs new file mode 100644 index 0000000..e9ef8cf --- /dev/null +++ b/Runtime/Numerics/inth2.cs @@ -0,0 +1,125 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using Unity.Mathematics; +using UnityEngine; +using static Freya.mathfs; + +namespace Freya { + + /// A fixed precision data type for half-integers, using a single backing interger. For numbers like: 0, 0.5, 1, 1.5, 2, etc. + [Serializable] public struct inth2 : + IEquatable, + IVec2, + INumber, + ISignedNumber, + IHalfNumber, + IRoundable { + + /// The number of halves in the x axis + [SerializeField] public inth x; + /// The number of halves in the y axis + [SerializeField] public inth y; + + public inth X => x; + public inth Y => y; + public inth2 zeroX => new(0, y); + public inth2 zeroY => new(x, 0); + public inth2 flipX => new(-x, y); + public inth2 flipY => new(x, -y); + public inth this[ int i ] => i switch { 0 => x, 1 => y, _ => throw new IndexOutOfRangeException( i.ToString() ) }; + public bool isOrthogonal => ( ceilAwayFrom0.abs() > 0 ).csum() <= 1; + public bool isZero => x == 0 && y == 0; + + public inth2( inth x, inth y ) => ( this.x, this.y ) = ( x, y ); + + public bool isInteger => x.isInteger && y.isInteger; + + public inth2 rot90 => new(-y, x); + public inth2 rotNeg90 => new(y, -x); + public inth2 rot180 => -this; + + public inth2 FromVector2( Vector2 v, RoundingDirection rounding = RoundingDirection.ToEven ) => + new() { + x = inth.fromFloat( v.x, rounding ), + y = inth.fromFloat( v.y, rounding ) + }; + + public int2 times2 => new(x.times2, y.times2); + public inth cmin => x.min( y ); + public inth cmax => x.max( y ); + public inth csum => x + y; + public rat dot( inth2 other ) => ( (rat2)this ).dot( other ); + public rat wedge( inth2 other ) => ( (rat2)this ).wedge( other ); + public inth2 to( inth2 target ) => target - this; + public rat magSq => this.dot( this ); + public inth magChebyshev => this.abs.cmax; + public inth magTaxicab => this.abs.csum; + public inth2 abs => new(x.abs, y.abs); + public inth2 max( inth2 other ) => new(x.max( other.x ), y.max( other.y )); + public inth2 min( inth2 other ) => new(x.min( other.x ), y.min( other.y )); + public int2 sign => new(x.sign, y.sign); + public int2 round( RoundingDirection rounding = RoundingDirection.ToEven ) => new(x.round( rounding ), y.round( rounding )); + public int2 floorToward0 => new(x.floorToward0, y.floorToward0); + public int2 ceilAwayFrom0 => new(x.ceilAwayFrom0, y.ceilAwayFrom0); + public int2 floor => new(x.floor, y.floor); + public int2 ceil => new(x.ceil, y.ceil); + public int quadrant => ceilAwayFrom0.quadrant(); + public int signedQuadrant => ceilAwayFrom0.signedQuadrant(); + public int2 quadrantBasisX => ceilAwayFrom0.quadrantBasisX(); + public (int2 x, int2 y) quadrantBasis => ceilAwayFrom0.quadrantBasis(); + public int pointSideOfPlane( inth2 planePos, inth2 planeNormal ) => this.times2.pointSideOfPlane( planePos.times2, planeNormal.times2 ); + public rat projTValue( inth2 n ) => this.dot( n ) / n.dot( n ); + + public rat2 complexMul( inth2 other ) => ( (rat2)times2.complexMul( other.times2 ) ) / 4; + public inth2 complexConj => new(x, -y); + + public override string ToString() => $"( {x}, {y} )"; + + public static implicit operator rat2( inth2 ih ) => new(ih.x, ih.y); + public static implicit operator inth2( int2 i ) => new(i.x, i.y); + + public static explicit operator int2( inth2 ih ) => new((int)ih.x, (int)ih.y); + public static explicit operator float2( inth2 ih ) => new((float)ih.x, (float)ih.y); + public static explicit operator Vector2( inth2 ih ) => new((float)ih.x, (float)ih.y); + public static explicit operator double2( inth2 ih ) => new((double)ih.x, (double)ih.y); + public static explicit operator Vector3( inth2 ih ) => new((float)ih.x, (float)ih.y, 0); + public static explicit operator double3( inth2 ih ) => new((double)ih.x, (double)ih.y, 0); + + public static int2 zero => new(0, 0); + public static inth2 half => new(inth.half, inth.half); + public static int2 one => new(1, 1); + + // unary operations + public static inth2 operator -( inth2 ih ) => new(-ih.x, -ih.y); + public static inth2 operator +( inth2 ih ) => ih; + + // binary operations + public static inth2 operator +( inth2 a, inth2 b ) => new(a.x + b.x, a.y + b.y); + public static inth2 operator +( inth2 a, int2 b ) => new(a.x + b.x, a.y + b.y); + public static inth2 operator +( int2 a, inth2 b ) => new(a.x + b.x, a.y + b.y); + public static inth2 operator -( inth2 a, inth2 b ) => new(a.x - b.x, a.y - b.y); + public static inth2 operator -( inth2 a, int2 b ) => new(a.x - b.x, a.y - b.y); + public static inth2 operator -( int2 a, inth2 b ) => new(a.x - b.x, a.y - b.y); + public static inth2 operator *( inth2 a, int2 b ) => new(a.x * b.x, a.y * b.y); + public static inth2 operator *( int2 a, inth2 b ) => new(a.x * b.x, a.y * b.y); + public static rat2 operator *( inth2 a, rat b ) => new(a.x * b, a.y * b); + public static rat2 operator *( rat a, inth2 b ) => new(a * b.x, a * b.y); + + // comparison operators + public static bool2 operator ==( inth2 a, inth2 b ) => new(a.x == b.x, a.y == b.y); + public static bool2 operator !=( inth2 a, inth2 b ) => new(a.x != b.x, a.y != b.y); + public static bool2 operator <( inth2 a, inth2 b ) => new(a.x < b.x, a.y < b.y); + public static bool2 operator >( inth2 a, inth2 b ) => new(a.x > b.x, a.y > b.y); + public static bool2 operator <=( inth2 a, inth2 b ) => new(a.x <= b.x, a.y <= b.y); + public static bool2 operator >=( inth2 a, inth2 b ) => new(a.x >= b.x, a.y >= b.y); + + // comparison functions + public bool Equals( inth2 other ) => x == other.x && y == other.y; + public override bool Equals( object obj ) => obj is inth2 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( x, y ); + + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/inth2.cs.meta b/Runtime/Numerics/inth2.cs.meta new file mode 100644 index 0000000..25a8397 --- /dev/null +++ b/Runtime/Numerics/inth2.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4e0cbee7d7ff47279e6435584568257b +timeCreated: 1775060027 \ No newline at end of file diff --git a/Runtime/Numerics/mathfs.cs b/Runtime/Numerics/mathfs.cs new file mode 100644 index 0000000..605fe51 --- /dev/null +++ b/Runtime/Numerics/mathfs.cs @@ -0,0 +1,193 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + + /// Static functions appropriate for using static + public static partial class mathfs { + + // todo: this file is under construction + + // global functions? + public static int2 quadrantToBasisX( int i ) => + i switch { + 1 => new int2( 00, +1 ), + 2 => new int2( -1, 00 ), + 3 => new int2( 00, -1 ), + _ => new int2( +1, 00 ) + }; + + public static int quadrantToSignedQuadrant( int i ) => i switch { 1 => 1, 2 => -2, 3 => -1, _ => 0 }; + + public static (int2 x, int2 y) quadrantToBasis( int i ) => + i switch { + 1 => ( new int2( 00, +1 ), new int2( -1, 00 ) ), + 2 => ( new int2( -1, 00 ), new int2( 00, -1 ) ), + 3 => ( new int2( 00, -1 ), new int2( +1, 00 ) ), + _ => ( new int2( +1, 00 ), new int2( 00, +1 ) ) + }; + + // todo: sort these: + public static rat round( rat r, rat interval, RoundingDirection rounding = RoundingDirection.ToEven ) => ( r / interval ).round( rounding ) * interval; + public static rat2 round( rat2 r, rat interval, RoundingDirection rounding = RoundingDirection.ToEven ) => new(round( r.x, interval ), round( r.y, interval )); + public static rat2 round( rat2 r, rat2 intervals, RoundingDirection rounding = RoundingDirection.ToEven ) => new(round( r.x, intervals.x ), round( r.y, intervals.y )); + + // todo: these should probably move to codegen + public static int csum( this bool b ) => b ? 1 : 0; + public static int csum( this bool2 b ) => math.csum( (int2)b ); + public static int csum( this bool3 b ) => math.csum( (int3)b ); + public static int csum( this bool4 b ) => math.csum( (int4)b ); + + public static Vector3 asVec3( this int2 v, float z = 0f ) => new(v.x, v.y, z); + public static float3 asFloat3( this int2 v, float z = 0f ) => new(v.x, v.y, z); + public static Vector4 asVec4( this int3 v, float w = 0f ) => new(v.x, v.y, v.z, w); + public static float4 asFloat4( this int3 v, float w = 0f ) => new(v.x, v.y, v.z, w); + + // UNSORTED: + public static Rect expandFromCenter( this Rect r, float expansionPerSide ) { + rat2 g = default; + r.xMin -= expansionPerSide; + r.yMin -= expansionPerSide; + r.xMax += expansionPerSide; + r.yMax += expansionPerSide; + return r; + } + + /// Unsigned shortest delta between a and b under modulo mod + public static int modDelta( int a, int b, int mod ) { + a = a.Mod( mod ); + b = b.Mod( mod ); + int delta = math.max( a, b ) - math.min( a, b ); // delta is guaranteed to be positive here + // find shortest direction: + return Mathf.Min( mod - delta, delta ); + } + + /// The signed number of quadrants travered/rotated through in going from a and b + public static int quadrantDelta( int2 a, int2 b ) => a.wedge( b ).sign() * modDelta( a.quadrant(), b.quadrant(), 4 ); + + /// + public static int quadrantDelta( inth2 a, inth2 b ) => a.wedge( b ).sign * modDelta( a.quadrant, b.quadrant, 4 ); + + /// + public static int quadrantDelta( rat2 a, rat2 b ) => a.wedge( b ).sign * modDelta( a.quadrant, b.quadrant, 4 ); + + /// Integer powers of floats, using repeated multiplication. Falls back to standard pow() beyond a power of 16 + public static float pow( this float x, int exp ) { + switch( exp ) { + case 0: return 1f; + case 1: return x; + case 2: return x * x; + case 3: { + return x * x * x; + } + case 4: { + float x2 = x * x; + return x2 * x2; + } + case 5: { + float x2 = x * x; + float x4 = x2 * x2; + return x4 * x; + } + case 6: { + float x2 = x * x; + float x4 = x2 * x2; + return x4 * x2; + } + case 7: { + float x2 = x * x; + float x4 = x2 * x2; + return x4 * x2 * x; + } + case 8: { + float x2 = x * x; + float x4 = x2 * x2; + return x4 * x4; + } + case 9: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x; + } + case 10: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x2; + } + case 11: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x2 * x; + } + case 12: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x4; + } + case 13: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x4 * x; + } + case 14: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x4 * x2; + } + case 15: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x4 * x2 * x; + } + case 16: { + float x2 = x * x; + float x4 = x2 * x2; + float x8 = x4 * x4; + return x8 * x8; + } + default: return MathF.Pow( x, exp ); + } + } + + /// Returns 1 when even, -1 when odd + public static int esign( this int x ) => x % 2 == 0 ? 1 : -1; + + public static inth divideBy2( this int p ) => new() { h = p }; + public static inth2 divideBy2( this int2 p ) => new(p.x.divideBy2(), p.y.divideBy2()); + + public static int2 rot45chebyshev( this int2 v ) { + int m = v.magChebyshev(); + return math.clamp( v + v.rot90(), new int2( -m, -m ), new int2( m, m ) ); + } + + public static float projectionTValue( float2 v, float2 n ) => math.dot( v, n ) / math.dot( n, n ); + public static rat projectionTValue( rat2 v, rat2 n ) => dot( v, n ) / dot( n, n ); + public static rat projectionTValue( rat2 v, int2 n ) => dot( v, n ) / dot( n, n ); + + public static rat projectionTValuePerp( rat2 v, rat2 n ) => dot( v, v ) / dot( v, n ); + + public static float2 projectToNormal( float2 v, float2 n ) => n * projectionTValue( v, n ); + public static rat2 projectToNormal( rat2 v, rat2 n ) => n * projectionTValue( v, n ); + public static rat2 projectToNormal( rat2 v, int2 n ) => n * projectionTValue( v, n ); + + public static rat2 projectToNormalPerp( rat2 v, rat2 n ) => n * projectionTValuePerp( v, n ); + + public static rat2 lerp( rat2 a, rat2 b, rat2 t ) => new(lerp( a.x, b.x, t.x ), lerp( a.y, b.y, t.y )); + public static rat2 lerp( rat2 a, rat2 b, rat t ) => a + t * ( b - a ); + public static rat lerp( rat a, rat b, rat t ) => a + t * ( b - a ); + + public static rat inverseLerp( rat a, rat b, rat v ) => ( v - a ) / ( b - a ); + + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/mathfs.cs.meta b/Runtime/Numerics/mathfs.cs.meta new file mode 100644 index 0000000..8227ede --- /dev/null +++ b/Runtime/Numerics/mathfs.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 378e711f01ba400d8850de03d5e43de3 +timeCreated: 1775075911 \ No newline at end of file diff --git a/Runtime/Numerics/rat.cs b/Runtime/Numerics/rat.cs new file mode 100644 index 0000000..037e7e8 --- /dev/null +++ b/Runtime/Numerics/rat.cs @@ -0,0 +1,178 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace Freya { + + /// A data type representing a rational number n/d, using a pair of integers. + [Serializable] public struct rat : + IComparable, + IEquatable, + INumber, + ISignedNumber, + IRoundable { + /// The numerator of this number. Note: Directly modifying this value will not reduce the fraction + [SerializeField] public int n; + + /// The denominator of this number. Note: Directly modifying this value will not reduce the fraction + [SerializeField] [NonZeroInteger] public int d; + + public static readonly rat zero = new(0, 1); + public static readonly rat one = new(1, 1); + public static readonly rat half = new(1, 2); + public static readonly rat MaxValue = new(int.MaxValue, 1); + public static readonly rat MinValue = new(int.MinValue, 1); + + /// Creates an exact representation of a rational number + /// The numerator of this number + /// The denominator of this number + public rat( int num, int den ) { + switch( den ) { + case -1: + ( n, d ) = ( -num, -den ); + break; + case 0: throw new DivideByZeroException( "The denominator can't be 0" ); + case 1: + ( n, d ) = ( num, den ); + break; + default: + if( num == 0 ) { + ( n, d ) = ( 0, 1 ); + break; + } + + // ensure only the numerator carries the sign + int sign = Mathfs.Sign( den ); + n = sign * num; + d = sign * den; + + if( n is -1 or 1 ) + break; // no reduction needed + + // in this case, we have to try simplifying the expression + int gcd = Mathfs.Gcd( num, den ); + n /= gcd; + d /= gcd; + break; + } + } + + public static rat FromFloat( float v, int snapStepsPerUnit = 2 ) => new(Mathf.RoundToInt( v * snapStepsPerUnit ), snapStepsPerUnit); + + /// Returns the reciprocal of this number + public rat Reciprocal => new(d, n); + + public bool isInteger => d == 1; + public bool isZero => n == 0; + public bool isOrthogonal => true; + + + /// Returns this number to the power of another integer pow + /// The power to raise this number by + public rat Pow( int pow ) => + pow switch { + <= -2 => Reciprocal.Pow( -pow ), + -1 => Reciprocal, + 0 => 1, + 1 => this, + >= 2 => new rat( n.Pow( pow ), d.Pow( pow ) ) + }; + + public bool TryCastToIntHalf( out inth ih ) { + switch( d ) { + case 1: + ih = n; + return true; + case 2: + ih = new inth { h = n }; + return true; + default: + ih = default; + return false; + } + } + + public override string ToString() => d == 1 ? n.ToString() : $"{n}/{d}"; + + // statics + public rat abs => new(n.Abs(), d); + public rat max( rat other ) => this > other ? this : other; + public rat min( rat other ) => this < other ? this : other; + public rat to( rat target ) => target - this; + public int sign => MathF.Sign( n ); + public int round( RoundingDirection rounding = RoundingDirection.ToEven ) => ( n < 0 == d < 0 ? ( n + d / 2 ) / d : ( n - d / 2 ) / d ); // todo: work out which rounding method this is + public int floorToward0 => n < 0 ? ceil : floor; + public int ceilAwayFrom0 => n < 0 ? floor : ceil; + public int floor => ( n < 0 ? n - d + 1 : n ) / d; + public int ceil => ( n > 0 ? n + d - 1 : n ) / d; + + // type casting + public static implicit operator rat( int n ) => new(n, 1); + public static explicit operator int( rat r ) => r.isInteger ? r.n : throw new ArithmeticException( $"Rational value {r} can't be cast to an integer" ); + + public static explicit operator inth( rat r ) => + r.d switch { + 1 => r.n, + 2 => new inth { h = r.n * 2 }, + _ => throw new ArithmeticException( $"Rational value {r} can't be cast to a half-step integer" ) + }; + + public static explicit operator float( rat r ) => (float)r.n / r.d; + public static explicit operator double( rat r ) => (double)r.n / r.d; + + // unary operations + public static rat operator -( rat r ) => checked( new(-r.n, r.d) ); + public static rat operator +( rat r ) => r; + + // binary operations + public static rat operator +( rat a, rat b ) => checked( new(a.n * b.d + a.d * b.n, a.d * b.d) ); + public static rat operator -( rat a, rat b ) => checked( new(a.n * b.d - a.d * b.n, a.d * b.d) ); + public static rat operator *( rat a, rat b ) => checked( new(a.n * b.n, a.d * b.d) ); + public static rat operator /( rat a, rat b ) => checked( new(a.n * b.d, a.d * b.n) ); + + // additional addition operations + public static rat operator +( rat a, int b ) => checked( new(a.n + a.d * b, a.d) ); + public static rat operator +( int a, rat b ) => checked( new(a * b.d + b.n, b.d) ); + + // additional subtraction operations + public static rat operator -( rat a, int b ) => checked( new(a.n - a.d * b, a.d) ); + public static rat operator -( int a, rat b ) => checked( new(a * b.d - b.n, b.d) ); + + // additional multiplication operations + public static rat operator *( rat a, int b ) => checked( new(a.n * b, a.d) ); + public static rat operator *( int a, rat b ) => checked( new(b.n * a, b.d) ); + public static float operator *( rat a, float b ) => ( a.n * b ) / a.d; + public static float operator *( float a, rat b ) => ( b.n * a ) / b.d; + public static double operator *( rat a, double b ) => ( a.n * b ) / a.d; + public static double operator *( double a, rat b ) => ( b.n * a ) / b.d; + public static rat2 operator *( int2 a, rat b ) => new(a.x * b, a.y * b); + public static rat2 operator *( rat a, int2 b ) => new(a * b.x, a * b.y); + + // additional division operations + public static rat operator /( rat a, int b ) => checked( new(a.n, a.d * b) ); + public static rat operator /( int a, rat b ) => checked( new(a * b.d, b.n) ); + public static float operator /( rat a, float b ) => a.n / ( a.d * b ); + public static float operator /( float a, rat b ) => ( a * b.d ) / b.n; + public static double operator /( rat a, double b ) => a.n / ( a.d * b ); + public static double operator /( double a, rat b ) => ( a * b.d ) / b.n; + + // comparison operators + public static bool operator ==( rat a, rat b ) => a.CompareTo( b ) == 0; + public static bool operator !=( rat a, rat b ) => a.CompareTo( b ) != 0; + public static bool operator <( rat a, rat b ) => a.CompareTo( b ) < 0; + public static bool operator >( rat a, rat b ) => a.CompareTo( b ) > 0; + public static bool operator <=( rat a, rat b ) => a.CompareTo( b ) <= 0; + public static bool operator >=( rat a, rat b ) => a.CompareTo( b ) >= 0; + + // comparison functions + public int CompareTo( rat other ) => checked( ( n * other.d ).CompareTo( d * other.n ) ); + public bool Equals( rat other ) => n == other.n && d == other.d; + public override bool Equals( object obj ) => obj is rat other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( n, d ); + + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/rat.cs.meta b/Runtime/Numerics/rat.cs.meta new file mode 100644 index 0000000..86624c2 --- /dev/null +++ b/Runtime/Numerics/rat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32002ef5527d54148944ed8a754cc7e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Numerics/rat2.cs b/Runtime/Numerics/rat2.cs new file mode 100644 index 0000000..c13e966 --- /dev/null +++ b/Runtime/Numerics/rat2.cs @@ -0,0 +1,211 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Assertions; +using static Freya.mathfs; + +namespace Freya { + + /// A 2D vector with rational components (ℚ² instead of ℝ²) + [Serializable] public struct rat2 : IEquatable, + IVec2, + ISignedNumber, + IDotProduct, + IRoundable { + [SerializeField] public rat x; + [SerializeField] public rat y; + public rat this[ int i ] => i switch { 0 => x, 1 => y, _ => throw new IndexOutOfRangeException( i.ToString() ) }; + public rat X => x; + public rat Y => y; + public rat2 zeroX => new(0, y); + public rat2 zeroY => new(x, 0); + public rat2 flipX => new(-x, y); + public rat2 flipY => new(x, -y); + // public Rational2 rot45chebyshev => throw new NotImplementedException(); + public static readonly rat2 zero = new(rat.zero, rat.zero); + public static readonly rat2 half = new(rat.half, rat.half); + public static readonly rat2 one = new(rat.one, rat.one); + /// The numerator, as an int2 vector + public int2 N => new(x.n, y.n); + /// The denominator, as an int2 vector + public int2 D => new(x.d, y.d); + public rat2( rat x, rat y ) => ( this.x, this.y ) = ( x, y ); + public rat2( int2 v ) => ( this.x, this.y ) = ( v.x, v.y ); + + public static rat2 FromVector2( Vector2 v, int snapStepsPerUnit = 2 ) { + return new rat2( rat.FromFloat( v.x, snapStepsPerUnit ), rat.FromFloat( v.y, snapStepsPerUnit ) ); + } + + public bool isZero => math.all( N == new int2( 0, 0 ) ); + public bool isInteger => math.all( D == new int2( 1, 1 ) ); + public bool isOrthogonal => ( ceilAwayFrom0.abs() > 0 ).csum() <= 1; + public bool IsDiagonal => x.abs == y.abs; + + // Chebyshev distances + public rat2 to( rat2 target ) => target - this; + public rat magSq => this.dot( this ); + public rat magChebyshev => this.abs.cmax; + public rat magTaxicab => this.abs.csum; + + public rat2 normalizedChebyshev => this / magChebyshev; + public static rat distChebyshev( rat2 a, rat2 b ) => ( b - a ).magChebyshev; + + public int quadrant => ceilAwayFrom0.quadrant(); + public int signedQuadrant => ceilAwayFrom0.signedQuadrant(); + public int2 quadrantBasisX => ceilAwayFrom0.quadrantBasisX(); + public (int2 x, int2 y) quadrantBasis => ceilAwayFrom0.quadrantBasis(); + public rat2 complexMul( rat2 other ) => new(x * other.x - y * other.y, x * other.y + y * other.x); + public rat2 complexConj => new(x, -y); + public int pointSideOfPlane( rat2 planePos, rat2 planeNormal ) => ( this - planePos ).dot( planeNormal ).sign; + public rat projTValue( rat2 n ) => this.dot( n ) / n.dot( n ); + + public rat2 normalizedTaxicab => this / magTaxicab; + + public static rat sqDist( rat2 a, rat2 b ) => ( b - a ).magSq; + public int2 normalized { + get { + Assert.IsTrue( isOrthogonal, $"Non-orthogonal {nameof(rat2)} vectors can't be normalized" ); + return this.sign; + } + } + /// Returns an int2 snapped to the nearest orthogonal normal. + /// Ambiguities along diagonals are resolved in the positive rotation direction + public int2 orthonormalized { + get { + Assert.IsFalse( isZero, "Can't orthonormalize a zero vector" ); + rat2 a = this.abs; + rat2 g = this / a.cmax; + if( IsDiagonal ) + return (int2)( ( g + g.rot90 ) / 2 ); + return a.x > a.y ? new int2( (int)g.x, 0 ) : new int2( 0, (int)g.y ); + } + } + /// Similar to orthonormalized, but also provides the "other" axis. + /// Note that this is the other axis in the same direction, it does not strictly follow handedness + public (int2 main, int2 secondary, rat mainLen, rat secondaryLen) decomposeOrthonormal { + get { + int2 m = orthonormalized; + int2 s = m.rot90(); + if( isOrthogonal ) + s *= wedge( m ).sign; + return ( m, s, projectionTValue( this, m ), projectionTValue( this, s ) ); + } + } + public rat2 rot90 => new(-y, x); + public rat2 rotNeg90 => new(y, -x); + public rat2 rot180 => -this; + public rat cmin => x.min( y ); + public rat cmax => x.max( y ); + public rat csum => x + y; + + public rat dot( rat2 other ) => x * other.x + y * other.y; + public rat dot( int2 other ) => x * other.x + y * other.y; + public rat wedge( rat2 other ) => x * other.y - y * other.x; + public rat wedge( int2 other ) => x * other.y - y * other.x; + + public rat2 abs => new(x.abs, y.abs); + public rat2 max( rat2 other ) => new(x.max( other.x ), y.max( other.y )); + public rat2 min( rat2 other ) => new(x.min( other.x ), y.min( other.y )); + public int2 sign => new(x.sign, y.sign); + public int2 round( RoundingDirection rounding = RoundingDirection.ToEven ) => new(x.round( rounding ), y.round( rounding )); + public int2 floorToward0 => new(x.floorToward0, y.floorToward0); + public int2 ceilAwayFrom0 => new(x.ceilAwayFrom0, y.ceilAwayFrom0); + public int2 floor => new(x.floor, y.floor); + public int2 ceil => new(x.ceil, y.ceil); + + public bool TryCastToIntHalf2( out inth2 ih ) { + if( x.TryCastToIntHalf( out inth ihx ) && y.TryCastToIntHalf( out inth ihy ) ) { + ih = new inth2( ihx, ihy ); + return true; + } + ih = default; + return false; + } + + public override string ToString() => $"( {x}, {y} )"; + + /// Returns the signed "distance" from the plane to a point + public static rat PointScaledDistFromPlane( rat2 pt, rat2 planePt, rat2 planeTangent ) => mathfs.wedge( pt - planePt, planeTangent ); + + public static rat LineIntersectionTValueAlongA( rat2 aOrigin, rat2 aDir, rat2 bOrigin, rat2 bDir ) { + rat2 beta = bDir.rot90; + rat2 c = projectToNormal( bOrigin - aOrigin, beta ); + return projectionTValuePerp( c, aDir ); + } + + public static rat2 IntersectLines( rat2 aOrigin, rat2 aDir, rat2 bOrigin, rat2 bDir ) { + if( mathfs.wedge( aDir, bDir ) == 0 ) // check if parallel + throw new Exception( "Cannot intersect parallel lines" ); + rat t = LineIntersectionTValueAlongA( aOrigin, aDir, bOrigin, bDir ); + return aOrigin + aDir * t; + // bOrigin -= aOrigin; + // Rational2 beta = bDir.rot90; + // Rational2 c = projectToNormal( bOrigin, beta ); + // Rational2 I = projectToNormalPerp( c, aDir ); + // return I + aOrigin; + } + + // type casting + public static implicit operator rat2( int2 n ) => new(n.x, n.y); + public static explicit operator int2( rat2 r ) => r.isInteger ? r.N : throw new ArithmeticException( $"Rational value {r} can't be cast to an integer" ); + public static explicit operator inth2( rat2 r ) => new((inth)r.x, (inth)r.y); + public static explicit operator float2( rat2 r ) => new((float)r.x, (float)r.y); + public static explicit operator double2( rat2 r ) => new((double)r.x, (double)r.y); + public static explicit operator Vector2( rat2 r ) => new((float)r.x, (float)r.y); + public static explicit operator Vector3( rat2 r ) => new((float)r.x, (float)r.y, 0); + public static explicit operator double3( rat2 r ) => new((double)r.x, (double)r.y, 0); + + // unary operations + public static rat2 operator -( rat2 r ) => new(-r.x, -r.y); + public static rat2 operator +( rat2 r ) => r; + + // addition + public static rat2 operator +( rat2 a, rat2 b ) => new(a.x + b.x, a.y + b.y); + public static rat2 operator +( rat2 a, int2 b ) => new(a.x + b.x, a.y + b.y); + public static rat2 operator +( int2 a, rat2 b ) => new(a.x + b.x, a.y + b.y); + + // subtraction + public static rat2 operator -( rat2 a, rat2 b ) => new(a.x - b.x, a.y - b.y); + public static rat2 operator -( rat2 a, int2 b ) => new(a.x - b.x, a.y - b.y); + public static rat2 operator -( int2 a, rat2 b ) => new(a.x - b.x, a.y - b.y); + + // multiplication + // public static Rational2 operator *( Rational2 a, Rational2 b ) => new(a.x * b.x, a.y * b.y); + public static rat2 operator *( rat2 a, rat b ) => new(a.x * b, a.y * b); + public static rat2 operator *( rat a, rat2 b ) => new(a * b.x, a * b.y); + public static rat2 operator *( rat2 a, int b ) => new(a.x * b, a.y * b); + public static rat2 operator *( int a, rat2 b ) => new(a * b.x, a * b.y); + public static float2 operator *( rat2 a, float b ) => new(a.x * b, a.y * b); + public static float2 operator *( float a, rat2 b ) => new(a * b.x, a * b.y); + public static double2 operator *( rat2 a, double b ) => new(a.x * b, a.y * b); + public static double2 operator *( double a, rat2 b ) => new(a * b.x, a * b.y); + // public static Rational2 operator *( Rational2 a, Rational2 b ) => checked( new(a.n * b.n, a.d * b.d) ); + + // division + public static rat2 operator /( rat2 a, rat b ) => new(a.x / b, a.y / b); + public static rat2 operator /( rat2 a, int b ) => new(a.x / b, a.y / b); + public static rat2 operator /( int a, rat2 b ) => new(a / b.x, a / b.y); + public static float2 operator /( rat2 a, float b ) => new(a.x / b, a.y / b); + public static double2 operator /( rat2 a, double b ) => new(a.x / b, a.y / b); + + // public static Rational2 operator /( Rational2 a, Rational2 b ) => checked( new(a.n * b.d, a.d * b.n) ); + // public static float operator /( float a, Rational2 b ) => ( a * b.d ) / b.n; + // public static double operator /( double a, Rational2 b ) => ( a * b.d ) / b.n; + + // comparison operators + public static bool2 operator ==( rat2 a, rat2 b ) => new(a.x == b.x, a.y == b.y); + public static bool2 operator !=( rat2 a, rat2 b ) => new(a.x != b.x, a.y != b.y); + public static bool2 operator <( rat2 a, rat2 b ) => new(a.x < b.x, a.y < b.y); + public static bool2 operator >( rat2 a, rat2 b ) => new(a.x > b.x, a.y > b.y); + public static bool2 operator <=( rat2 a, rat2 b ) => new(a.x <= b.x, a.y <= b.y); + public static bool2 operator >=( rat2 a, rat2 b ) => new(a.x >= b.x, a.y >= b.y); + public bool Equals( rat2 other ) => x.Equals( other.x ) && y.Equals( other.y ); + public override bool Equals( object obj ) => obj is rat2 other && Equals( other ); + public override int GetHashCode() => HashCode.Combine( x, y ); + + + } + +} \ No newline at end of file diff --git a/Runtime/Numerics/rat2.cs.meta b/Runtime/Numerics/rat2.cs.meta new file mode 100644 index 0000000..9567369 --- /dev/null +++ b/Runtime/Numerics/rat2.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e437079d92a54cdabe992f3570e08ec6 +timeCreated: 1774753386 \ No newline at end of file diff --git a/Runtime/Property Drawers.meta b/Runtime/Property Drawers.meta new file mode 100644 index 0000000..9c4e903 --- /dev/null +++ b/Runtime/Property Drawers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15ddb36d8f130b147b402770205352f6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Property Drawers/ClampedIntegerAttributes.cs b/Runtime/Property Drawers/ClampedIntegerAttributes.cs new file mode 100644 index 0000000..0c987ba --- /dev/null +++ b/Runtime/Property Drawers/ClampedIntegerAttributes.cs @@ -0,0 +1,20 @@ +namespace Freya { + + using UnityEngine; + + /// Restricts this integer to a range of 1 and above + public class PositiveIntegerAttribute : PropertyAttribute {} + + /// Restricts this integer to a range of -1 and below + public class NegativeIntegerAttribute : PropertyAttribute {} + + /// Restricts this integer to a range of 0 and above + public class NonNegativeIntegerAttribute : PropertyAttribute {} + + /// Restricts this integer to a range of 0 and below + public class NonPositiveIntegerAttribute : PropertyAttribute {} + + /// Restricts this integer to be non-zero + public class NonZeroIntegerAttribute : PropertyAttribute {} + +} \ No newline at end of file diff --git a/Runtime/Property Drawers/ClampedIntegerAttributes.cs.meta b/Runtime/Property Drawers/ClampedIntegerAttributes.cs.meta new file mode 100644 index 0000000..6768c19 --- /dev/null +++ b/Runtime/Property Drawers/ClampedIntegerAttributes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 846a20ede34f7a448b88c3220ddd45d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Random.cs b/Runtime/Random.cs similarity index 51% rename from Random.cs rename to Runtime/Random.cs index 93368a9..870018e 100644 --- a/Random.cs +++ b/Runtime/Random.cs @@ -1,5 +1,6 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +using System; using UnityEngine; using static Freya.Mathfs; using UnityRandom = UnityEngine.Random; @@ -20,10 +21,15 @@ public static class Random { public static float Direction1D => Sign; /// Randomly returns a value between min [inclusive] and max [inclusive] - /// The minimum value - /// The maximum value + /// The minimum value [inclusive] + /// The maximum value [inclusive] public static float Range( float min, float max ) => UnityRandom.Range( min, max ); + /// Randomly returns a value between min [inclusive] and max [exclusive] + /// The minimum value [inclusive] + /// The maximum value [exclusive] + public static int Range( int min, int max ) => UnityRandom.Range( min, max ); + // 2D /// Returns a random point on the unit circle public static Vector2 OnUnitCircle => AngToDir( Value * TAU ); @@ -57,6 +63,37 @@ public static class Random { // 3D Orientation /// Returns a random uniformly distributed rotation public static Quaternion Rotation => UnityRandom.rotationUniform; + + // Normal distributions + /// Returns random numbers with a gaussian distribution + /// The distance away from the mean that will contain 68.27% of all values + /// The mean (average) value of the distribution + public static float Gaussian( float variance = 1, float mean = 0 ) { + float u1 = UnityEngine.Random.value; + float u2 = UnityEngine.Random.value; + return MathF.Sqrt( -2 * MathF.Log( u1 ) ) * MathF.Cos( TAU * u2 ) * variance + mean; + } + + /// Samples a gaussian with rejection sampling, retrying when samples end up outside the given range + /// The distance away from the mean that will contain 68.27% of all values + /// The mean (average) value of the distribution + /// The minimum value of the range + /// The maximum value of the range + /// The maximum number of times to attempt to get a value within the range, before giving up and clamping instead + public static float GaussianInRange( float variance = 1, float mean = 0, float min = -1, float max = 1, int samplingMaxAttempts = 3 ) { + float x; + int attempts = 0; + do { + x = Gaussian( variance, mean ); + attempts++; + } while( attempts < samplingMaxAttempts && x.Within( min, max ) == false ); + return x.Clamp( min, max ); + } + + /// Returns a value between -1 and 1 with a variance of 1/4 + public static float GaussianNeg1to1 => GaussianInRange( 1 / 4f ); + /// Returns a value between 0 and 1 with a variance of 1/8 + public static float Gaussian01 => GaussianInRange( 1 / 8f, 1 / 2f, 0, 1 ); } } \ No newline at end of file diff --git a/Runtime/Random.cs.meta b/Runtime/Random.cs.meta new file mode 100644 index 0000000..f496c1d --- /dev/null +++ b/Runtime/Random.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8210027925ef4bb5807a0e22c44e047 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ResultsMax4FloatExt.cs b/Runtime/ResultsMax4FloatExt.cs new file mode 100644 index 0000000..db089a5 --- /dev/null +++ b/Runtime/ResultsMax4FloatExt.cs @@ -0,0 +1,49 @@ +using System; + +namespace Freya { + + public static class ResultsMax4FloatExt { + public static ResultsMax4 InsertSorted( this ResultsMax4 r, float value ) { + switch( r.count ) { + case 0: return new ResultsMax4( value ); + case 1: + if( value < r.a ) + return new ResultsMax4( value, r.a ); + return new ResultsMax4( r.a, value ); + case 2: + if( value < r.a ) + return new ResultsMax4( value, r.a, r.b ); + if( value < r.b ) + return new ResultsMax4( r.a, value, r.b ); + return new ResultsMax4( r.a, r.b, value ); + case 3: + if( value < r.a ) + return new ResultsMax4( value, r.a, r.b, r.c ); + if( value < r.b ) + return new ResultsMax4( r.a, value, r.b, r.c ); + if( value < r.c ) + return new ResultsMax4( r.a, r.b, value, r.c ); + return new ResultsMax4( r.a, r.b, r.c, value ); + default: throw new IndexOutOfRangeException( "Can't add more than four values to ResultsMax4" ); + } + } + + public static ResultsMax3 InsertSorted( this ResultsMax3 r, float value ) { + switch( r.count ) { + case 0: return new ResultsMax3( value ); + case 1: + if( value < r.a ) + return new ResultsMax3( value, r.a ); + return new ResultsMax3( r.a, value ); + case 2: + if( value < r.a ) + return new ResultsMax3( value, r.a, r.b ); + if( value < r.b ) + return new ResultsMax3( r.a, value, r.b ); + return new ResultsMax3( r.a, r.b, value ); + default: throw new IndexOutOfRangeException( "Can't add more than three values to ResultsMax3" ); + } + } + } + +} \ No newline at end of file diff --git a/Runtime/ResultsMax4FloatExt.cs.meta b/Runtime/ResultsMax4FloatExt.cs.meta new file mode 100644 index 0000000..3c390ce --- /dev/null +++ b/Runtime/ResultsMax4FloatExt.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8fcbd682de6943409948584ed60e7af0 +timeCreated: 1785454763 \ No newline at end of file diff --git a/Runtime/Splines.meta b/Runtime/Splines.meta new file mode 100644 index 0000000..dba3feb --- /dev/null +++ b/Runtime/Splines.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2fd831045bfb24ee9adeb289a9cee378 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/CatRomType.cs b/Runtime/Splines/CatRomType.cs similarity index 90% rename from Curves/CatRomType.cs rename to Runtime/Splines/CatRomType.cs index 32a407b..351ce13 100644 --- a/Curves/CatRomType.cs +++ b/Runtime/Splines/CatRomType.cs @@ -1,5 +1,4 @@ // by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) -// a lot of stuff here made possible by this excellent writeup on bezier curves: https://pomax.github.io/bezierinfo/ namespace Freya { diff --git a/Runtime/Splines/CatRomType.cs.meta b/Runtime/Splines/CatRomType.cs.meta new file mode 100644 index 0000000..0e983bc --- /dev/null +++ b/Runtime/Splines/CatRomType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8d1051627b6a43a49fa2fd6f564d0b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/CharMatrix.cs b/Runtime/Splines/CharMatrix.cs new file mode 100644 index 0000000..5e4fca2 --- /dev/null +++ b/Runtime/Splines/CharMatrix.cs @@ -0,0 +1,133 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine; + +namespace Freya { + + public static class CharMatrix { + + /// The characteristic matrix of a quadratic bézier curve + public static readonly RationalMatrix3x3 quadraticBezier = new( + 1, 0, 0, + -2, 2, 0, + 1, -2, 1 + ); + + /// The characteristic matrix of a cubic bézier curve + public static readonly RationalMatrix4x4 cubicBezier = new( + 1, 0, 0, 0, + -3, 3, 0, 0, + 3, -6, 3, 0, + -1, 3, -3, 1 + ); + + /// The characteristic matrix of a uniform cubic hermite curve + public static readonly RationalMatrix4x4 cubicHermite = new( + 1, 0, 0, 0, + 0, 1, 0, 0, + -3, -2, 3, -1, + 2, 1, -2, 1 + ); + public static readonly Polynomial[] cubicHermitePositionBasisFunctions = { + GetBasisFunction( cubicHermite, 0 ), + GetBasisFunction( cubicHermite, 2 ) + }; + public static readonly Polynomial[] cubicHermiteVelocityBasisFunctions = { + GetBasisFunction( cubicHermite, 1 ), + GetBasisFunction( cubicHermite, 3 ) + }; + + /// The characteristic matrix of a uniform cubic catmull-rom curve + public static readonly RationalMatrix4x4 cubicCatmullRom = new RationalMatrix4x4( + 0, 2, 0, 0, + -1, 0, 1, 0, + 2, -5, 4, -1, + -1, 3, -3, 1 + ) / 2; + + public static readonly Polynomial[] cubicCatmullRomBasisFunctions = { + GetBasisFunction( cubicCatmullRom, 0 ), + GetBasisFunction( cubicCatmullRom, 1 ), + GetBasisFunction( cubicCatmullRom, 2 ), + GetBasisFunction( cubicCatmullRom, 3 ) + }; + + /// The characteristic matrix of a uniform cubic B-spline curve + public static readonly RationalMatrix4x4 cubicUniformBspline = new RationalMatrix4x4( + 1, 4, 1, 0, + -3, 0, 3, 0, + 3, -6, 3, 0, + -1, 3, -3, 1 + ) / 6; + + /// The inverse characteristic matrix of a quadratic bézier curve + public static readonly RationalMatrix3x3 quadraticBezierInverse = quadraticBezier.Inverse; + + /// The inverse characteristic matrix of a cubic bézier curve + public static readonly RationalMatrix4x4 cubicBezierInverse = cubicBezier.Inverse; + + /// The characteristic matrix of a uniform cubic hermite curve + public static readonly RationalMatrix4x4 cubicHermiteInverse = cubicHermite.Inverse; + + /// The characteristic matrix of a uniform cubic catmull-rom curve + public static readonly RationalMatrix4x4 cubicCatmullRomInverse = cubicCatmullRom.Inverse; + + /// The characteristic matrix of a uniform cubic B-spline curve + public static readonly RationalMatrix4x4 cubicUniformBsplineInverse = cubicUniformBspline.Inverse; + + /// Returns the matrix to convert control points from one cubic spline to another, keeping the same curve intact + /// The characteristic matrix of the spline to convert from + /// The characteristic matrix of the spline to convert from + public static RationalMatrix4x4 GetConversionMatrix( RationalMatrix4x4 from, RationalMatrix4x4 to ) => to.Inverse * from; + + public static Matrix4x4 Create( float m00, float m01, float m02, float m03, float m10, float m11, float m12, float m13, float m20, float m21, float m22, float m23, float m30, float m31, float m32, float m33 ) { + Matrix4x4 m; + m.m00 = m00; + m.m10 = m10; + m.m20 = m20; + m.m30 = m30; + m.m01 = m01; + m.m11 = m11; + m.m21 = m21; + m.m31 = m31; + m.m02 = m02; + m.m12 = m12; + m.m22 = m22; + m.m32 = m32; + m.m03 = m03; + m.m13 = m13; + m.m23 = m23; + m.m33 = m33; + return m; + } + + /// Returns the basis function (weight) for the given spline points by index i, + /// equal to the t-matrix multiplied by the characteristic matrix + /// The characteristic matrix to get the basis functions of + /// The point index to get the basis function of + public static Polynomial GetBasisFunction( RationalMatrix4x4 c, int i ) { + return i switch { + 0 => new Polynomial( (float)c.m00, (float)c.m10, (float)c.m20, (float)c.m30 ), + 1 => new Polynomial( (float)c.m01, (float)c.m11, (float)c.m21, (float)c.m31 ), + 2 => new Polynomial( (float)c.m02, (float)c.m12, (float)c.m22, (float)c.m32 ), + 3 => new Polynomial( (float)c.m03, (float)c.m13, (float)c.m23, (float)c.m33 ), + _ => throw new IndexOutOfRangeException( "Basis index needs to be between 0 and 3" ) + }; + } + + /// + public static Polynomial GetBasisFunction( Matrix4x4 c, int i ) { + return i switch { + 0 => new Polynomial( c.m00, c.m10, c.m20, c.m30 ), + 1 => new Polynomial( c.m01, c.m11, c.m21, c.m31 ), + 2 => new Polynomial( c.m02, c.m12, c.m22, c.m32 ), + 3 => new Polynomial( c.m03, c.m13, c.m23, c.m33 ), + _ => throw new IndexOutOfRangeException( "Basis index needs to be between 0 and 3" ) + }; + } + + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/CharMatrix.cs.meta b/Runtime/Splines/CharMatrix.cs.meta new file mode 100644 index 0000000..44f592c --- /dev/null +++ b/Runtime/Splines/CharMatrix.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77b97aac785974286bde838188365fd8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Multi-Segment Splines.meta b/Runtime/Splines/Multi-Segment Splines.meta new file mode 100644 index 0000000..f0185b8 --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 621407d19775a485d938187df7c297f9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/BSpline2D.cs b/Runtime/Splines/Multi-Segment Splines/BSpline2D.cs similarity index 96% rename from Curves/BSpline2D.cs rename to Runtime/Splines/Multi-Segment Splines/BSpline2D.cs index 65b8d2d..f579a25 100644 --- a/Curves/BSpline2D.cs +++ b/Runtime/Splines/Multi-Segment Splines/BSpline2D.cs @@ -24,8 +24,9 @@ public BSpline2D( Vector2[] points, float[] knots, int degree = 3 ) { this.knots = knots; this.degree = degree; this.evalBuffer = new Vector2[degree + 1]; - if( knots.Length != SplineUtils.BSplineKnotCount( this.points.Length, this.degree ) ) - throw new ArgumentException( $"The knots array has to be of length (degree+pointCount+1). Got an array of {knots.Length} knots, expected ${KnotCount}", nameof(knots) ); + int expectedKnotCount = SplineUtils.BSplineKnotCount( this.points.Length, this.degree ); + if( knots.Length != expectedKnotCount ) + throw new ArgumentException( $"The knots array has to be of length (degree+pointCount+1). Got an array of {knots.Length} knots, expected {expectedKnotCount}", nameof(knots) ); } /// Creates a uniform B-spline of the given degree, automatically configuring the knot vector to be uniform @@ -84,14 +85,12 @@ public bool Open { #endregion - #region Derivative + #region Differentiation /// Returns the derivative of this B-spline, which is a B-spline in and of itself - public BSpline2D GetDerivative() { - // knots are the same except we remove the two outermost ones - float[] dKnots = new float[KnotCount - 2]; - for( int i = 0; i < dKnots.Length; i++ ) - dKnots[i] = knots[i + 1]; + public BSpline2D Differentiate() { + float[] dKnots = new float[KnotCount]; + knots.CopyTo( dKnots, 0 ); // one point less Vector2[] dPts = new Vector2[PointCount - 1]; diff --git a/Runtime/Splines/Multi-Segment Splines/BSpline2D.cs.meta b/Runtime/Splines/Multi-Segment Splines/BSpline2D.cs.meta new file mode 100644 index 0000000..596f7e0 --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/BSpline2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6efd4bd1062de4b5ab2559215cd3d3c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs b/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs new file mode 100644 index 0000000..8b5383c --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + [Serializable] + public class CatRom2DSpline { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + public enum EndpointMode { + None, + Extrapolate, + Collapse + } + + public struct Node { + public Vector2 pos; + public float knot; + public Polynomial2D curve; + + public Vector2 EvalPoint( float u ) { + u -= knot; // transform to local knot value + return curve.Eval( u ); + } + + public Vector2 EvalDerivative( float u ) { + u -= knot; // transform to local knot value + return curve.EvalDerivative( u ); + } + + public Vector2 EvalSecondDerivative( float u ) { + u -= knot; // transform to local knot value + return curve.EvalSecondDerivative( u ); + } + + public Vector2 EvalThirdDerivative() => curve.EvalThirdDerivative(); + } + + public List nodes; + [SerializeField] [Range( 0, 1 )] float alpha; + [SerializeField] bool autoCalculateKnots; + [SerializeField] public EndpointMode endpointMode; + [NonSerialized] bool isDirty; + + #region Properties + + bool IncludeEndpoints { + [MethodImpl( INLINE )] get => endpointMode != EndpointMode.None; + } + + /// + public float Alpha { + [MethodImpl( INLINE )] get => alpha; + [MethodImpl( INLINE )] set { + isDirty = true; + alpha = value; + } + } + + /// Whether or not to calculate knots based on the alpha value + public bool AutoCalculateKnots { + [MethodImpl( INLINE )] get => autoCalculateKnots; + [MethodImpl( INLINE )] set { + isDirty = true; + autoCalculateKnots = value; + } + } + + int IndexSplineStart { + [MethodImpl( INLINE )] get => IncludeEndpoints ? 0 : 1; + } + int IndexSplineEnd { + [MethodImpl( INLINE )] get => ControlPointCount - ( IncludeEndpoints ? 1 : 2 ); + } + + /// The number of control points in this spline + public int ControlPointCount { + [MethodImpl( INLINE )] get => nodes.Count; + } + + /// The number of curves in this spline + public int CurveCount { + [MethodImpl( INLINE )] get => ControlPointCount - ( IncludeEndpoints ? 1 : 2 ); + } + + /// The knot value at the start of the spline + public float KnotStart { + [MethodImpl( INLINE )] get => GetKnot( IndexSplineStart ); + } + + /// The knot value at the end of the spline + public float KnotEnd { + [MethodImpl( INLINE )] get => GetKnot( IndexSplineEnd ); + } + /// The knot range of the spline from start to end + public float KnotRange { + [MethodImpl( INLINE )] get => KnotEnd - KnotStart; + } + + /// The starting point of this spline + public Vector2 StartPoint { + [MethodImpl( INLINE )] get => nodes[IndexSplineStart].pos; + } + + /// The endpoint of this spline + public Vector2 EndPoint { + [MethodImpl( INLINE )] get => nodes[IndexSplineEnd].pos; + } + + #endregion + + #region Constructors + + /// Creates a cubic catmull-rom spline, given a set of control points + /// The control points of the spline + /// The knot values of the spline + /// Whether or not the spline should reach the endpoints, and how + public CatRom2DSpline( IReadOnlyCollection points, IReadOnlyCollection knots, EndpointMode endpointMode = EndpointMode.None ) { + if( points == null || knots == null ) + throw new NullReferenceException( $"{GetType().Name} requires non-null inputs" ); + if( points.Count != knots.Count ) + throw new Exception( $"{GetType().Name} points[{points.Count}] and knots[{knots.Count}] have to have the same count" ); + if( points.Count < 2 ) + throw new Exception( $"{GetType().Name} requires at least 2 points" ); + nodes = points.Zip( knots, ( p, k ) => new Node { pos = p, knot = k } ).ToList(); + isDirty = true; + autoCalculateKnots = false; + this.endpointMode = endpointMode; + } + + /// Creates a cubic catmull-rom spline, given a set of control points + /// The control points of the spline + /// The alpha parameter controls how much the length of each segment should influence the shape of the curve. + /// A value of 0 is called a uniform catrom, and is fast to evaluate but has a tendency to overshoot. + /// A value of 0.5 is a centripetal catrom, which follows points very tightly, and prevents cusps and loops. + /// A value of 1 is a chordal catrom, which follows the points very smoothly with wide arcs + /// Whether or not the spline should reach the endpoints, and how + public CatRom2DSpline( IReadOnlyCollection points, float alpha, EndpointMode endpointMode = EndpointMode.None ) { + if( points == null ) + throw new NullReferenceException( $"{GetType().Name} requires non-null points" ); + if( points.Count < 2 ) + throw new Exception( $"{GetType().Name} requires at least 2 points" ); + nodes = points.Select( p => new Node { pos = p } ).ToList(); + this.alpha = alpha; + this.isDirty = true; + this.autoCalculateKnots = true; + this.endpointMode = endpointMode; + } + + /// Creates a cubic catmull-rom spline, given a set of control points + /// The control points of the spline + /// The type of catrom curve to use. This will internally determine the value of the alpha parameter + /// Whether or not the spline should reach the endpoints, and how + public CatRom2DSpline( IReadOnlyCollection points, CatRomType type, EndpointMode endpointMode = EndpointMode.None ) { + if( points == null ) + throw new NullReferenceException( $"{GetType().Name} requires non-null points" ); + if( points.Count < 2 ) + throw new Exception( $"{GetType().Name} requires at least 2 points" ); + nodes = points.Select( p => new Node { pos = p } ).ToList(); + this.alpha = type.AlphaValue(); + this.isDirty = true; + this.autoCalculateKnots = true; + this.endpointMode = endpointMode; + } + + #endregion + + #region Points & Derivatives + + /// Returns the point at parameter value u + /// The parameter space position to sample the point at + [MethodImpl( INLINE )] public Vector2 GetPoint( float u ) { + u = ReadyAndClampU( u ); + return GetPointInternal( GetIntervalIndexForKnotValue( u ), u ); + } + + /// Returns the derivative with respect to u at the input parameter value + /// The parameter space position to sample the derivative at + [MethodImpl( INLINE )] public Vector2 GetDerivative( float u ) { + u = ReadyAndClampU( u ); + return GetDerivativeInternal( GetIntervalIndexForKnotValue( u ), u ); + } + + /// Returns the second derivative with respect to u at the input parameter value + /// The parameter space position to sample the second derivative at + [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( float u ) { + u = ReadyAndClampU( u ); + return GetSecondDerivativeInternal( GetIntervalIndexForKnotValue( u ), u ); + } + + /// Returns the third derivative with respect to u at the input parameter value + /// The parameter space position to sample the third derivative at + [MethodImpl( INLINE )] public Vector2 GetThirdDerivative( float u ) { + return GetThirdDerivativeInternal( GetIntervalIndexForKnotValue( ReadyAndClampU( u ) ) ); + } + + [MethodImpl( INLINE )] float ReadyAndClampU( float u ) { + ReadyKnotsAndCoefficients(); + return ClampToKnotRange( u ); + } + + #endregion + + #region Recalculations + + /// Ensures the knot vector is ready (if autoCalculateKnots is on) and the coefficients are up to date + public void ReadyKnotsAndCoefficients() { + if( isDirty ) { + isDirty = false; + if( autoCalculateKnots ) + RecalculateKnots(); + for( int i = 0; i < ControlPointCount - 1; i++ ) { + Node n = nodes[i]; + // knot parameters are local to each spline's main (second) knot + Vector2Matrix4x1 pts = new(GetControlPoint( i - 1 ), n.pos, GetControlPoint( i + 1 ), GetControlPoint( i + 2 )); + Matrix4x1 knots = new(GetKnot( i - 1 ) - n.knot, 0, GetKnot( i + 1 ) - n.knot, GetKnot( i + 2 ) - n.knot); + n.curve = SplineUtils.CalculateCatRomCurve( pts, knots ); + nodes[i] = n; + } + } + } + + /// Recalculates the knot vector based on alpha and the point distances + public void RecalculateKnots() { + if( alpha == 0 ) { // uniform catrom + for( int i = 0; i < ControlPointCount; i++ ) + SetKnotInternal( i, i ); + } else { // non-uniform + SetKnotInternal( 0, 0 ); // first knot is 0 + // todo: it's possible to cache and optimize these distance checks + // todo: by caching distances and only recalculating the necessary ones + for( int i = 1; i < ControlPointCount; i++ ) { + float sqDist = Vector2.SqrMagnitude( nodes[i - 1].pos - nodes[i].pos ); + SetKnotInternal( i, SplineUtils.CalcCatRomKnot( nodes[i - 1].knot, sqDist, alpha, true ) ); + } + } + } + + #endregion + + #region Point/Knot getter/setters + + /// Get the position of a control point by index + /// The index of the point + public Vector2 GetControlPoint( int index ) { + if( endpointMode == EndpointMode.Collapse ) + index = index.Clamp( 0, ControlPointCount - 1 ); + + if( index == -1 ) // extrapolate at the ends + return Vector2.LerpUnclamped( nodes[1].pos, nodes[0].pos, 2 ); + if( index == ControlPointCount ) + return Vector2.LerpUnclamped( nodes[ControlPointCount - 2].pos, nodes[ControlPointCount - 1].pos, 2 ); + + return nodes[index].pos; + } + + /// Set the position of a control point by index + /// The index of the knot + /// The position to assign to the control point + public void SetControlPoint( int index, Vector2 position ) { + isDirty = true; + Node n = nodes[index]; + n.pos = position; + nodes[index] = n; + } + + /// Get the value of knot by index + /// The index of the knot + public float GetKnot( int index ) { + ReadyKnotsAndCoefficients(); + if( index == -1 ) // extrapolate at the ends + return Mathfs.Lerp( nodes[1].knot, nodes[0].knot, 2 ); + if( index == ControlPointCount ) + return Mathfs.Lerp( nodes[ControlPointCount - 2].knot, nodes[ControlPointCount - 1].knot, 2 ); + return nodes[index].knot; + } + + /// Get the knot value at a given t-value along the whole spline + /// The percentage along the spline from 0 to 1 + public float GetKnotValue( float t ) => Mathfs.LerpClamped( KnotStart, KnotEnd, t ); + + /// Sets the given knot to a specific value + /// The index of the knot to edit + /// The value to assign to the knot + [MethodImpl( INLINE )] public void SetKnot( int index, float value ) { + isDirty = true; + SetKnotInternal( index, value ); + } + + [MethodImpl( INLINE )] void SetKnotInternal( int index, float value ) { + Node n = nodes[index]; + n.knot = value; + nodes[index] = n; + } + + #endregion + + #region Get point/derivative by curve index + + /// Returns a point along a curve by index + /// The index of the curve to sample + /// The fraction along this segment from 0 to 1 + [MethodImpl( INLINE )] public Vector2 GetPoint( int curve, float t ) => GetPointInternal( curve, RangeCheckAndGetU( curve, t ) ); + + /// Returns the derivative with respect to u along a curve by index + /// The index of the curve to sample + /// The fraction along this segment from 0 to 1 + [MethodImpl( INLINE )] public Vector2 GetDerivative( int curve, float t ) => GetDerivativeInternal( curve, RangeCheckAndGetU( curve, t ) ); + + /// Returns the second derivative with respect to u along a curve by index + /// The index of the curve to sample + /// The fraction along this segment from 0 to 1 + [MethodImpl( INLINE )] public Vector2 GetSecondDerivative( int curve, float t ) => GetSecondDerivativeInternal( curve, RangeCheckAndGetU( curve, t ) ); + + /// Returns the third derivative with respect to u of a curve by index + /// The index of the curve to sample + [MethodImpl( INLINE )] public Vector2 GetThirdDerivative( int curve ) => GetThirdDerivativeInternal( curve ); + + float RangeCheckAndGetU( int curve, float t ) { + if( curve < 0 || curve >= CurveCount ) + throw new IndexOutOfRangeException( $"Curve index {curve} is out of the range 0 to {CurveCount - 1}" ); + ReadyKnotsAndCoefficients(); + return Mathfs.Lerp( nodes[curve].knot, nodes[curve + 1].knot, t ); + } + + [MethodImpl( INLINE )] Vector2 GetPointInternal( int curve, float u ) => nodes[curve].EvalPoint( u ); + [MethodImpl( INLINE )] Vector2 GetDerivativeInternal( int curve, float u ) => nodes[curve].EvalDerivative( u ); + [MethodImpl( INLINE )] Vector2 GetSecondDerivativeInternal( int curve, float u ) => nodes[curve].EvalSecondDerivative( u ); + [MethodImpl( INLINE )] Vector2 GetThirdDerivativeInternal( int curve ) => nodes[curve].EvalThirdDerivative(); + + #endregion + + #region Knot Utilities + + /// Clamps the input value u to the range of this spline + /// The parameter space value to clamp + public float ClampToKnotRange( float u ) => u.Clamp( KnotStart, KnotEnd ); + + /// Returns the index of the curve containing knot value u + /// The knot value to get the curve of + public int GetIntervalIndexForKnotValue( float u ) { + ReadyKnotsAndCoefficients(); + if( u <= KnotStart ) + return 0; + if( u >= GetKnot( ControlPointCount - 2 ) ) + return ControlPointCount - 2; // very last knot is never used as an interval (fencepost issue |-|-|) + // todo: linear search, but, might want to use binary search if more than ~30 nodes + for( int i = 0; i < ControlPointCount - 1; i++ ) { + if( u < GetKnot( i + 1 ) ) + return i; + } + + throw new Exception( $"Failed to get spline interval for knot value {u} in the range {KnotStart} to {KnotEnd}" ); + } + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs.meta b/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs.meta new file mode 100644 index 0000000..2db78cd --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/CatRom2DSpline.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db0f72c5f03f3e74fb2ad9649a885e3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs b/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs new file mode 100644 index 0000000..85ac965 --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs @@ -0,0 +1,113 @@ +using System; +using Freya; + +public class ElevatorEase { + + public enum Mode { + FastStartPolynomial, + Trigonometric + } + + public readonly Mode mode; + public readonly float maxDistance; + public readonly float contractSpeed; + public readonly float accDuration; + + public readonly float t1; + public readonly float t2; + public readonly float tEnd; + public readonly float v; + + // linear section + float g_c0, g_c1; + + // polynomial coefficients used by Mode.FastStartPolynomial + float f_c3, f_c4, f_c5; // ease in + float h_c4, h_c5; // ease out (relative) + + // constants used by Mode.Trigonometric + float c_xsq, c_trigScale, c_trigInner; + + public ElevatorEase( Mode mode, float maxDistance, float contractSpeed, float accDuration ) { + this.mode = mode; + this.maxDistance = maxDistance; + this.contractSpeed = contractSpeed; + this.accDuration = accDuration; + + // derived things + t1 = accDuration; + t2 = maxDistance / contractSpeed; + tEnd = t1 + t2; + v = contractSpeed; + float tHalf = tEnd / 2; + if( accDuration >= tHalf ) { + // no linear section - adjust accordingly + t1 = t2 = tHalf; + v = 2 * ( maxDistance / tEnd ); + } + + // calculate coefficients + g_c1 = v; + if( mode == Mode.FastStartPolynomial ) { + g_c0 = -2 * v * t1 / 5; + float t1_2 = t1 * t1; + float t1_3 = t1_2 * t1; + float t1_4 = t1_2 * t1_2; + f_c3 = 2 * v / t1_2; + f_c4 = 2 * v / -t1_3; + f_c5 = 3 * v / ( 5 * t1_4 ); + h_c4 = v / -t1_3; + h_c5 = -3 * v / ( 5 * t1_4 ); + } else if( mode == Mode.Trigonometric ) { + g_c0 = -v * t1 / 2; + c_xsq = v / ( 2 * t1 ); + c_trigScale = v * t1 / ( Mathfs.TAU * Mathfs.TAU ); + c_trigInner = Mathfs.TAU / t1; + } + } + + float EaseIn( float t ) { + float x2 = t * t; + + if( mode == Mode.FastStartPolynomial ) { + float x3 = x2 * t; + float x4 = x2 * x2; + float x5 = x3 * x2; + return f_c3 * x3 + f_c4 * x4 + f_c5 * x5; + } else if( mode == Mode.Trigonometric ) { + return c_xsq * x2 + c_trigScale * ( MathF.Cos( c_trigInner * t ) - 1 ); + } + throw new NotImplementedException(); + } + + float Linear( float t ) => g_c0 + g_c1 * t; + + float EaseOut( float t ) { + if( mode == Mode.FastStartPolynomial ) { + // this one is asymmetric, and needs special handling + float x = t - tEnd; // remap to relative + float x2 = x * x; + float x4 = x2 * x2; + float x5 = x4 * x; + return h_c4 * x4 + h_c5 * x5 + maxDistance; + } + + // same as ease-in but reversed + return maxDistance - EaseIn( tEnd - t ); + } + + public float Eval( float t ) { + // check extremes: + if( t <= 0 ) + return 0; + if( t >= tEnd ) + return maxDistance; + // check functions: + if( t < t1 ) + return EaseIn( t ); + if( t > t2 ) + return EaseOut( t ); + return Linear( t ); + } + +} \ No newline at end of file diff --git a/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs.meta b/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs.meta new file mode 100644 index 0000000..94acd15 --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/ElevatorEase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba96d62d0c3156b4fb6c1e76c2c5a3b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/Nurbs2D.cs b/Runtime/Splines/Multi-Segment Splines/NURBS2D.cs similarity index 90% rename from Curves/Nurbs2D.cs rename to Runtime/Splines/Multi-Segment Splines/NURBS2D.cs index 697b1fa..c984c15 100644 --- a/Curves/Nurbs2D.cs +++ b/Runtime/Splines/Multi-Segment Splines/NURBS2D.cs @@ -3,7 +3,7 @@ namespace Freya { - public class Nurbs2D { + public class NURBS2D { public Vector2[] points; public float[] knots; @@ -17,10 +17,10 @@ public class Nurbs2D { public int KnotCount => degree + PointCount + 1; public int SegmentCount => KnotCount - degree * 2 - 1; - public static Nurbs2D GetUniformBSpline( Vector2[] points, int degree = 3, bool open = true ) { + public static NURBS2D GetUniformBSpline( Vector2[] points, int degree = 3, bool open = true ) { int ptCount = points.Length; float[] knots = SplineUtils.GenerateUniformKnots( degree, ptCount, open ); - return new Nurbs2D( points, knots, null, degree ); + return new NURBS2D( points, knots, null, degree ); } public static float[] GetUnweightedWeights( int count ) { @@ -31,7 +31,7 @@ public static float[] GetUnweightedWeights( int count ) { } - public Nurbs2D( Vector2[] points, float[] knots, float[] weights, int degree = 3 ) { + public NURBS2D( Vector2[] points, float[] knots, float[] weights, int degree = 3 ) { this.points = points; this.knots = knots; this.degree = degree; @@ -48,7 +48,7 @@ public Vector2 GetPointByKnotValue( float t ) { Vector2 sum = default; float norm = 0; - for( int i = 0; i < PointCount; i++ ) { + for( int i = 0; i < PointCount; i++ ) { // todo: unnecessary, don't do all points float basis = Basis( i, Order, t ); if( weighted ) norm += ( basis *= weights[i] ); sum += points[i] * basis; diff --git a/Runtime/Splines/Multi-Segment Splines/NURBS2D.cs.meta b/Runtime/Splines/Multi-Segment Splines/NURBS2D.cs.meta new file mode 100644 index 0000000..558ab8c --- /dev/null +++ b/Runtime/Splines/Multi-Segment Splines/NURBS2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca7fc048128b44d20a057f6436283f8c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Non-Uniform Spline Segments.meta b/Runtime/Splines/Non-Uniform Spline Segments.meta new file mode 100644 index 0000000..05973ec --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b66eb786294404d91b8f267343232c7b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs new file mode 100644 index 0000000..7e215b5 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs @@ -0,0 +1,204 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// A non-uniform cubic catmull-rom 2D curve + [Serializable] public struct NUCatRomCubic2D : IParamSplineSegment { + + public enum KnotCalcMode { + Manual, + Auto, + AutoUnitInterval + } + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + #region Constructors + + /// Creates a cubic catmull-rom curve, from 4 control points and their corresponding knot values + /// The control points of the curve + /// The knot vector of the curve + public NUCatRomCubic2D( Vector2Matrix4x1 pointMatrix, Matrix4x1 knotVector ) { + this.pointMatrix = pointMatrix; + this.knotVector = knotVector; + validCoefficients = false; + curve = default; + knotCalcMode = KnotCalcMode.Manual; + alpha = default; // unused when using manual knots + } + + /// Creates a cubic catmull-rom curve, from 4 control points and their corresponding knot values + /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catrom curve + /// The third control point, and the end of the catrom curve + /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The first knot value + /// The second knot value + /// The third knot value + /// The fourth knot value + public NUCatRomCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float k0, float k1, float k2, float k3 ) + : this( new Vector2Matrix4x1( p0, p1, p2, p3 ), new Matrix4x1( k0, k1, k2, k3 ) ) { + } + + /// Creates a uniform cubic catmull-rom curve, from 4 control points + /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catrom curve + /// The third control point, and the end of the catrom curve + /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + public NUCatRomCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) : this( p0, p1, p2, p3, -1, 0, 1, 2 ) { + } + + /// Creates a cubic catmull-rom curve, from 4 control points with explicit type for auto-generating its knot values + /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catrom curve + /// The third control point, and the end of the catrom curve + /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The type of catrom curve to use. This will internally determine the value of the alpha parameter + /// If true, the knot generation will ensure k1 = 0 and k2 = 1, + /// making it span the unit interval of 0 to 1, instead of using the raw knot values generated by the alpha parameterization + public NUCatRomCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, CatRomType type, bool parameterizeToUnitInterval = true ) + : this( p0, p1, p2, p3, type.AlphaValue(), parameterizeToUnitInterval ) { + } + + /// Creates a cubic catmull-rom curve, from 4 control points with explicit alpha parameter to define its type + /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catrom curve + /// The third control point, and the end of the catrom curve + /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The alpha parameter controls how much the length of each segment should influence the knot values, which in turn influence the shape of the curve. + /// A value of 0 is called a uniform catrom, and is fast to evaluate but has a tendency to overshoot. + /// A value of 0.5 is a centripetal catrom, which follows points very tightly, and prevents cusps and loops. + /// A value of 1 is a chordal catrom, which follows the points very smoothly with wide arcs + /// If true, the knot generation will ensure k1 = 0 and k2 = 1, + /// making it span the unit interval of 0 to 1 instead of using the raw knot values generated by the alpha parameterization + public NUCatRomCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float alpha, bool parameterizeToUnitInterval = true ) { + pointMatrix = new Vector2Matrix4x1( p0, p1, p2, p3 ); + knotVector = default; + validCoefficients = false; + curve = default; + knotCalcMode = parameterizeToUnitInterval ? KnotCalcMode.AutoUnitInterval : KnotCalcMode.Auto; + this.alpha = alpha; + } + + #endregion + + // serialized data + [SerializeField] Vector2Matrix4x1 pointMatrix; + public Vector2Matrix4x1 PointMatrix { + get => pointMatrix; + set => _ = ( pointMatrix = value, validCoefficients = false ); + } + [SerializeField] Matrix4x1 knotVector; + public Matrix4x1 KnotVector { + get { + if( knotCalcMode != KnotCalcMode.Manual ) + ReadyCoefficients(); + return knotVector; + } + set => _ = ( knotVector = value, validCoefficients = false ); + } + + // knot auto-calculation fields + [SerializeField] KnotCalcMode knotCalcMode; // knot recalculation mode + [SerializeField] float alpha; // alpha parameterization + + Polynomial2D curve; + public Polynomial2D Curve { + get { + ReadyCoefficients(); + return curve; + } + } + + #region Properties + + /// The first control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector2 P0 { + [MethodImpl( INLINE )] get => pointMatrix.m0; + [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); + } + /// The second control point, and the start of the catrom curve + public Vector2 P1 { + [MethodImpl( INLINE )] get => pointMatrix.m1; + [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); + } + /// The third control point, and the end of the catrom curve + public Vector2 P2 { + [MethodImpl( INLINE )] get => pointMatrix.m2; + [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); + } + /// The last control point of the catrom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector2 P3 { + [MethodImpl( INLINE )] get => pointMatrix.m3; + [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); + } + + /// The knot value of the first control point of the catrom curve + public float K0 { + [MethodImpl( INLINE )] get => KnotVector.m0; + [MethodImpl( INLINE )] set => _ = ( knotVector.m0 = value, validCoefficients = false ); + } + /// The knot value of the second control point, and the start of the catrom curve + public float K1 { + [MethodImpl( INLINE )] get => KnotVector.m1; + [MethodImpl( INLINE )] set => _ = ( knotVector.m1 = value, validCoefficients = false ); + } + /// The knot value of the third control point, and the end of the catrom curve + public float K2 { + [MethodImpl( INLINE )] get => KnotVector.m2; + [MethodImpl( INLINE )] set => _ = ( knotVector.m2 = value, validCoefficients = false ); + } + /// The knot value of the last control point of the catrom curve + public float K3 { + [MethodImpl( INLINE )] get => KnotVector.m3; + [MethodImpl( INLINE )] set => _ = ( knotVector.m3 = value, validCoefficients = false ); + } + + /// The alpha parameter, which controls how much the length of each segment should influence the shape of the curve. + /// A value of 0 is called a uniform catrom, and is fast to evaluate but has a tendency to overshoot. + /// A value of 0.5 is a centripetal catrom, which follows points very tightly, and prevents cusps and loops. + /// A value of 1 is a chordal catrom, which follows the points very smoothly with wide arcs + public float Alpha { + [MethodImpl( INLINE )] get => alpha; + set => _ = ( alpha = value, validCoefficients = false ); + } + + #endregion + + // cached data to accelerate calculations + [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) + + [MethodImpl( INLINE )] void ReadyCoefficients() { + if( validCoefficients ) + return; // no need to update + validCoefficients = true; + if( knotCalcMode != KnotCalcMode.Manual ) + KnotVector = SplineUtils.CalcCatRomKnots( pointMatrix, alpha, knotCalcMode == KnotCalcMode.AutoUnitInterval ); + curve = SplineUtils.CalculateCatRomCurve( pointMatrix, knotVector ); + } + + /// Returns the weight of the given control point at the given parameter value + /// The point to get the weight of + /// The parameter value at which to sample the weight + public float GetPointWeightAtKnotValue( int i, float u ) { + float a = Mathfs.InverseLerp( K0, K1, u ); + float b = Mathfs.InverseLerp( K1, K2, u ); + float c = Mathfs.InverseLerp( K2, K3, u ); + float d = Mathfs.InverseLerp( K0, K2, u ); + float g = Mathfs.InverseLerp( K1, K3, u ); + switch( i ) { + case 0: return ( 1 - a ) * ( 1 - b ) * ( 1 - d ); + case 1: return ( 1 - b ) * ( a * ( 1 - d ) + b * ( 1 - d - g ) + d ); + case 2: return -b * ( b * ( d + g - 1 ) + g * ( c - 1 ) - d ); + case 3: return b * c * g; + default: throw new IndexOutOfRangeException( $"Catrom point has to be either 0, 1, 2 or 3. Got: {i}" ); + } + } + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs.meta b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs.meta new file mode 100644 index 0000000..bd1fbc8 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d8e47e3fa3e54659b61527bf0f41842 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs new file mode 100644 index 0000000..f8dac96 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs @@ -0,0 +1,168 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// A non-uniform cubic catmull-rom 3D curve + [Serializable] public struct NUCatRomCubic3D : IParamSplineSegment { + + public enum KnotCalcMode { + Manual, + Auto, + AutoUnitInterval + } + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + #region Constructors + + /// + public NUCatRomCubic3D( Vector3Matrix4x1 pointMatrix, Matrix4x1 knotVector ) { + this.pointMatrix = pointMatrix; + this.knotVector = knotVector; + validCoefficients = false; + curve = default; + knotCalcMode = KnotCalcMode.Manual; + alpha = default; // unused when using manual knots + } + + /// + public NUCatRomCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float k0, float k1, float k2, float k3 ) + : this( new Vector3Matrix4x1( p0, p1, p2, p3 ), new Matrix4x1( k0, k1, k2, k3 ) ) { + } + + /// + public NUCatRomCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) : this( p0, p1, p2, p3, -1, 0, 1, 2 ) { + } + + /// + public NUCatRomCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, CatRomType type, bool parameterizeToUnitInterval = true ) + : this( p0, p1, p2, p3, type.AlphaValue(), parameterizeToUnitInterval ) { + } + + /// + public NUCatRomCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha, bool parameterizeToUnitInterval = true ) { + pointMatrix = new Vector3Matrix4x1( p0, p1, p2, p3 ); + validCoefficients = false; + curve = default; + knotVector = default; + knotCalcMode = parameterizeToUnitInterval ? KnotCalcMode.AutoUnitInterval : KnotCalcMode.Auto; + this.alpha = alpha; + } + + #endregion + + // serialized data + [SerializeField] Vector3Matrix4x1 pointMatrix; + public Vector3Matrix4x1 PointMatrix { + get => pointMatrix; + set => _ = ( pointMatrix = value, validCoefficients = false ); + } + [SerializeField] Matrix4x1 knotVector; + public Matrix4x1 KnotVector { + get { + if( knotCalcMode != KnotCalcMode.Manual ) + ReadyCoefficients(); + return knotVector; + } + set => _ = ( knotVector = value, validCoefficients = false ); + } + + // knot auto-calculation fields + [SerializeField] KnotCalcMode knotCalcMode; // knot recalculation mode + [SerializeField] float alpha; // alpha parameterization + + Polynomial3D curve; + public Polynomial3D Curve { + get { + ReadyCoefficients(); + return curve; + } + } + + #region Properties + + /// + public Vector3 P0 { + [MethodImpl( INLINE )] get => pointMatrix.m0; + set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); + } + /// + public Vector3 P1 { + [MethodImpl( INLINE )] get => pointMatrix.m1; + set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); + } + /// + public Vector3 P2 { + [MethodImpl( INLINE )] get => pointMatrix.m2; + set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); + } + /// + public Vector3 P3 { + [MethodImpl( INLINE )] get => pointMatrix.m3; + set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); + } + + /// + public float K0 { + [MethodImpl( INLINE )] get => KnotVector.m0; + [MethodImpl( INLINE )] set => _ = ( knotVector.m0 = value, validCoefficients = false ); + } + /// + public float K1 { + [MethodImpl( INLINE )] get => KnotVector.m1; + [MethodImpl( INLINE )] set => _ = ( knotVector.m1 = value, validCoefficients = false ); + } + /// + public float K2 { + [MethodImpl( INLINE )] get => KnotVector.m2; + [MethodImpl( INLINE )] set => _ = ( knotVector.m2 = value, validCoefficients = false ); + } + /// + public float K3 { + [MethodImpl( INLINE )] get => KnotVector.m3; + [MethodImpl( INLINE )] set => _ = ( knotVector.m3 = value, validCoefficients = false ); + } + + /// + public float Alpha { + [MethodImpl( INLINE )] get => alpha; + set => _ = ( alpha = value, validCoefficients = false ); + } + + #endregion + + // cached data to accelerate calculations + [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) + + [MethodImpl( INLINE )] void ReadyCoefficients() { + if( validCoefficients ) + return; // no need to update + validCoefficients = true; + if( knotCalcMode != KnotCalcMode.Manual ) + KnotVector = SplineUtils.CalcCatRomKnots( pointMatrix, alpha, knotCalcMode == KnotCalcMode.AutoUnitInterval ); + curve = SplineUtils.CalculateCatRomCurve( pointMatrix, knotVector ); + } + + /// + public float GetPointWeightAtKnotValue( int i, float u ) { + float a = Mathfs.InverseLerp( K0, K1, u ); + float b = Mathfs.InverseLerp( K1, K2, u ); + float c = Mathfs.InverseLerp( K2, K3, u ); + float d = Mathfs.InverseLerp( K0, K2, u ); + float g = Mathfs.InverseLerp( K1, K3, u ); + switch( i ) { + case 0: return ( 1 - a ) * ( 1 - b ) * ( 1 - d ); + case 1: return ( b - 1 ) * ( a * d - a + b * ( d + g - 1 ) - d ); + case 2: return -b * ( b * ( d + g - 1 ) + g * ( c - 1 ) - d ); + case 3: return b * c * g; + default: throw new IndexOutOfRangeException( $"Catrom point has to be either 0, 1, 2 or 3. Got: {i}" ); + } + } + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs.meta b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs.meta new file mode 100644 index 0000000..a3064b0 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUCatRomCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8fd379bcd8a540d39bfe61760455451 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs b/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs new file mode 100644 index 0000000..b90b922 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + public class NUHermiteCubic3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + /// + public NUHermiteCubic3D( Vector3Matrix4x1 pointMatrix, (float k0, float k1) knotVector ) { + this.pointMatrix = pointMatrix; + this.KnotVector = knotVector; + validCoefficients = false; + curve = default; + } + + // serialized data + [SerializeField] Vector3Matrix4x1 pointMatrix; + public Vector3Matrix4x1 PointMatrix { + get => pointMatrix; + set => _ = ( pointMatrix = value, validCoefficients = false ); + } + [SerializeField] float k0, k1; + public (float k0, float k1) KnotVector { + get => ( k0, k1 ); + set => _ = ( ( k0, k1 ) = value, validCoefficients = false ); + } + + Polynomial3D curve; + public Polynomial3D Curve { + get { + ReadyCoefficients(); + return curve; + } + } + + // cached data to accelerate calculations + [NonSerialized] bool validCoefficients; // inverted isDirty flag (can't default to true in structs) + + [MethodImpl( INLINE )] void ReadyCoefficients() { + if( validCoefficients ) + return; // no need to update + validCoefficients = true; + curve = SplineUtils.CalculateHermiteCurve( pointMatrix, k0, k1 ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs.meta b/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs.meta new file mode 100644 index 0000000..59b05f7 --- /dev/null +++ b/Runtime/Splines/Non-Uniform Spline Segments/NUHermiteCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96c8df53b570b63439afac85966a0084 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/SplineUtils.cs b/Runtime/Splines/SplineUtils.cs new file mode 100644 index 0000000..3af6256 --- /dev/null +++ b/Runtime/Splines/SplineUtils.cs @@ -0,0 +1,297 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using System; +using UnityEngine; + +namespace Freya { + + /// Various utility functions for splines + public static class SplineUtils { + + /// Samples a bernstein polynomial bézier basis function + /// The degree of the bézier curve + /// The basis function index + /// The value to sample at + public static float SampleBernsteinBasisFunction( int degree, int i, float t ) { + ulong bc = Mathfs.BinomialCoef( (uint)degree, (uint)i ); + double scale = Math.Pow( 1f - t, degree - i ) * Math.Pow( t, i ); + return (float)( bc * scale ); + } + + public static float[] GenerateUniformKnots( int degree, int pCount, bool open ) { + int kCount = degree + pCount + 1; + float[] knots = new float[kCount]; + // open: 0 0[0 1 2 3 4]4 4 + // closed: [0 1 2 3 4 5 6 7 8] + for( int i = 0; i < kCount; i++ ) + knots[i] = open == false ? i : Mathf.Clamp( i - degree, 0, kCount - 2 * degree - 1 ); + return knots; + } + + internal static int BSplineKnotCount( int pointCount, int degree ) => degree + pointCount + 1; + + public static float CalcCatRomKnot( float kPrev, float sqDist, float alpha, bool isSquaredDist ) { + return kPrev + CalcCatRomKnot( sqDist, alpha, isSquaredDist ).AtLeast( 0.00001f ); // ensure there are no duplicate knots + } + + public static float CalcCatRomKnot( float dist, float alpha, bool isSquaredDist ) => + alpha switch { + 0 => 1, // uniform + 0.5f => isSquaredDist ? dist.Pow( 0.25f ) : Mathf.Sqrt( dist ), // centripetal + 1 => isSquaredDist ? Mathf.Sqrt( dist ) : dist, // chordal + _ => isSquaredDist ? dist.Pow( 0.5f * alpha ) : dist.Pow( alpha ) + }; + + static readonly Matrix4x1 knotsUniformUnit = new(-1, 0, 1, 2); + static readonly Matrix4x1 knotsUniform = new(0, 1, 2, 3); + + static Matrix4x1 GetUniformKnots( bool unitInterval ) => unitInterval ? knotsUniformUnit : knotsUniform; + + public static Matrix4x1 CalcCatRomKnots( Vector2Matrix4x1 m, float alpha, bool unitInterval ) { + if( alpha == 0 ) // uniform catrom + return GetUniformKnots( unitInterval ); + float sqMag01 = Vector2.SqrMagnitude( m.m0 - m.m1 ); + float sqMag12 = Vector2.SqrMagnitude( m.m1 - m.m2 ); + float sqMag23 = Vector2.SqrMagnitude( m.m2 - m.m3 ); + return CalcCatRomKnots( sqMag01, sqMag12, sqMag23, alpha, unitInterval, isSquaredDist:true ); + } + + public static Matrix4x1 CalcCatRomKnots( Vector3Matrix4x1 m, float alpha, bool unitInterval ) { + if( alpha == 0 ) // uniform catrom + return GetUniformKnots( unitInterval ); + float sqMag01 = Vector3.SqrMagnitude( m.m0 - m.m1 ); + float sqMag12 = Vector3.SqrMagnitude( m.m1 - m.m2 ); + float sqMag23 = Vector3.SqrMagnitude( m.m2 - m.m3 ); + return CalcCatRomKnots( sqMag01, sqMag12, sqMag23, alpha, unitInterval, isSquaredDist:true ); + } + + static Matrix4x1 CalcCatRomKnots( float dist01, float dist12, float dist23, float alpha, bool unitInterval, bool isSquaredDist ) { + float i01 = CalcCatRomKnot( dist01, alpha, isSquaredDist ); + float i12 = CalcCatRomKnot( dist12, alpha, isSquaredDist ); + float i23 = CalcCatRomKnot( dist23, alpha, isSquaredDist ); + float k0, k1, k2, k3; + if( unitInterval ) { + return new(-i01 / i12, 0, 1, 1 + i23 / i12); + } else { + k0 = 0; + k1 = k0 + i01; + k2 = k1 + i12; + k3 = k2 + i23; + } + + return new(k0, k1, k2, k3); + } + + public static Matrix4x4 GetNUCatRomCharMatrix( Matrix4x1 knots ) { + float k0 = knots.m0; + float k1 = knots.m1; + float k2 = knots.m2; + float k3 = knots.m3; + if( k1 == 0f && k2 == 1f ) + return GetNUCatRomCharMatrixUnitInterval( k0, k3 ); + float k1k1 = k1 * k1; + float k2k2 = k2 * k2; + float k0k1 = k0 * k1; + float _2k0k1 = 2 * k0k1; + float k0k2 = k0 * k2; + float k1k2 = k1 * k2; + float _2k1k2 = 2 * k1k2; + float k1k3 = k1 * k3; + float k2k3 = k2 * k3; + float _2k2k3 = 2 * k2k3; + float k0k1k2 = k0k1 * k2; + float k0k1k3 = k0k1 * k3; + float k1k1k2 = k1k1 * k2; + float k0k2k3 = k0k2 * k3; + float k1k1k3 = k1k1 * k3; + float k1k2k2 = k1k2 * k2; + float k1k2k3 = k1k2 * k3; + float k0k1k1 = k0k1 * k1; + float k0k2k2 = k0k2 * k2; + + float common = _2k0k1 + k0k2 - k1k3 - _2k2k3; + float common2 = k0k1k3 + k0k2k2 - k0k2k3; + float common3 = k1k1k2 - k1k2k2; + float common4 = k0 - k3; + + // CHAR matrix COLUMN 0: + float p0u0 = -k1k2k2; + float p0u1 = _2k1k2 + k2k2; + float p0u2 = -k1 - 2 * k2; + const float p0u3 = 1; + // CHAR matrix COLUMN 1: + float p1u0 = k2 * ( k0k1k2 + k0k1k3 - k0k2k3 - k1k1k3 ); + float p1u1 = -3 * k0k1k2 - k0k1k3 + k0k2k3 + k1k1k3 + common3 + k1k2k3 + k2k2 * k3; + float p1u2 = common - k1k1 + k1k2; + float p1u3 = -common4; + // CHAR matrix COLUMN 2: + float p2u0 = -k1 * ( common2 - k1k2k3 ); + float p2u1 = k0k1k1 + k0k1k2 + common2 - common3 - 3 * k1k2k3; + float p2u2 = -common - k2k2 + k1k2; + float p2u3 = common4; + // CHAR matrix COLUMN 3: + float p3u0 = k1k1k2; + float p3u1 = -k1k1 - _2k1k2; + float p3u2 = 2 * k1 + k2; + const float p3u3 = -1; + + float i01 = k0 - k1; + float i02 = k0 - k2; + float i12 = k1 - k2; + float i12sq = i12 * i12; + float i13 = k1 - k3; + float i23 = k2 - k3; + float p0sc = ( i01 * i02 * i12 ); + float p1sc = ( i01 * i12sq * i13 ); + float p2sc = ( i02 * i12sq * i23 ); + float p3sc = ( i12 * i13 * i23 ); + return CharMatrix.Create( + p0u0 / p0sc, p1u0 / p1sc, p2u0 / p2sc, p3u0 / p3sc, + p0u1 / p0sc, p1u1 / p1sc, p2u1 / p2sc, p3u1 / p3sc, + p0u2 / p0sc, p1u2 / p1sc, p2u2 / p2sc, p3u2 / p3sc, + p0u3 / p0sc, p1u3 / p1sc, p2u3 / p2sc, p3u3 / p3sc + ); + } + + public static Vector3 GetNUCatRomCharMatrixC2End( Matrix4x1 knots, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) { + float k0 = knots.m0; + float k1 = knots.m1; + float k2 = knots.m2; + float k3 = knots.m3; + + float k1k1 = k1 * k1; + float k2k2 = k2 * k2; + float k0k1 = k0 * k1; + float _2k0k1 = 2 * k0k1; + float k0k2 = k0 * k2; + float k1k2 = k1 * k2; + float k1k3 = k1 * k3; + float k2k3 = k2 * k3; + float _2k2k3 = 2 * k2k3; + + float common = _2k0k1 + k0k2 - k1k3 - _2k2k3; + + // CHAR matrix COLUMN 0: + // CHAR matrix COLUMN 1: + // CHAR matrix COLUMN 2: + // CHAR matrix COLUMN 3: + + float i01 = k0 - k1; + float i02 = k0 - k2; + float i12 = k1 - k2; + float i12sq = i12 * i12; + float i13 = k1 - k3; + float i23 = k2 - k3; + float p0sc = ( i01 * i02 * i12 ); + float p1sc = ( i01 * i12sq * i13 ); + float p2sc = ( i02 * i12sq * i23 ); + float p3sc = ( i12 * i13 * i23 ); + + float m20 = ( -k1 - 2 * k2 ) / p0sc; + float m21 = ( common - k1k1 + k1k2 ) / p1sc; + float m22 = ( -common - k2k2 + k1k2 ) / p2sc; + float m23 = ( 2 * k1 + k2 ) / p3sc; + float m30 = 1f / p0sc; + float m31 = ( k3 - k0 ) / p1sc; + float m32 = ( k0 - k3 ) / p2sc; + float m33 = -1f / p3sc; + + return p0 * ( ( m20 + 3 * m30 ) / m23 ) + + p1 * ( ( m21 + 3 * m31 - m20 ) / m23 ) + + p2 * ( ( m22 + 3 * m32 - m21 ) / m23 ) + + p3 * ( 1 + ( 3 * m33 - m22 ) / m23 ); + } + + static Matrix4x4 GetNUCatRomCharMatrixUnitInterval( float k0, float k3 ) { + float k0mk3 = k0 - k3; + float k0m2k3 = k0mk3 - k3; + float k0k3 = k0 * k3; + + // CHAR matrix COLUMN 1: + float p1u1 = k0k3 + k3; + float p1u2 = k0m2k3; + float p1u3 = -k0mk3; + // CHAR matrix COLUMN 2: + float p2u1 = k0 - k0k3; + float p2u2 = -k0m2k3 - 1; + float p2u3 = k0mk3; + + float i02 = k0 - 1; + float i23 = 1 - k3; + float p0sc = 1f / ( -k0 * i02 ); + float p1sc = 1f / ( -k0k3 ); + float p2sc = 1f / ( i02 * i23 ); + float p3sc = 1f / ( k3 * i23 ); + + return CharMatrix.Create( + 0, 1, 0, 0, + p0sc, -p1u1 / k0k3, p2sc * p2u1, 0, + p0sc * -2, -p1u2 / k0k3, p2sc * p2u2, p3sc, + p0sc, -p1u3 / k0k3, p2sc * p2u3, -p3sc + ); + } + + internal static Polynomial2D CalculateCatRomCurve( Vector2Matrix4x1 m, Matrix4x1 knots ) { + return new Polynomial2D( GetNUCatRomCharMatrix( knots ).MultiplyColumnVector( m ) ); + } + + internal static Polynomial3D CalculateCatRomCurve( Vector3Matrix4x1 m, Matrix4x1 knots ) { + return new Polynomial3D( GetNUCatRomCharMatrix( knots ).MultiplyColumnVector( m ) ); + } + + internal static Polynomial3D CalculateHermiteCurve( Vector3Matrix4x1 m, float k0, float k1 ) { + return new Polynomial3D( GetNUHermiteCharMatrix( k0, k1 ).MultiplyColumnVector( m ) ); + } + + internal static Polynomial2D CalculateHermiteCurve( Vector2Matrix4x1 m, float k0, float k1 ) { + return new Polynomial2D( GetNUHermiteCharMatrix( k0, k1 ).MultiplyColumnVector( m ) ); + } + + static Matrix4x4 GetNUHermiteCharMatrix( float k0, float k1 ) { + float d = k1 - k0; + float d2 = d * d; + float d3 = d * d * d; + float k0_2 = k0 * k0; + float k0_3 = k0 * k0 * k0; + + // row 0 + float m02 = ( 3 * k0_2 ) / d2 + ( 2 * k0_3 ) / d3; + float m00 = 1 - m02; + float _k02d = k0_2 / d; + float _k03d2 = k0_3 / d2; + float m03 = -_k02d - _k03d2; + float m01 = -k0 - 2 * _k02d - _k03d2; + + // row 1 + float _2k0d = 2 * k0 / d; + float _3k02d2 = 3 * k0_2 / d2; + float m13 = _2k0d + _3k02d2; + float m11 = 1 + 2 * _2k0d + _3k02d2; + float m10 = 6 * ( k0 / d2 + k0_2 / d3 ); + float m12 = -m10; + + // row 2 + float m22 = 3 / d2 + ( 6 * k0 ) / d3; + float m20 = -m22; + float _dRcp = 1 / d; + float _3k0d2 = 3 * k0 / d2; + float m23 = -_dRcp - _3k0d2; + float m21 = m23 - _dRcp; + + // row 3 + float m30 = 2 / d3; + float m31 = 1 / d2; + float m32 = -m30; + float m33 = m31; + + return CharMatrix.Create( + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33 + ); + } + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/SplineUtils.cs.meta b/Runtime/Splines/SplineUtils.cs.meta new file mode 100644 index 0000000..4b69f67 --- /dev/null +++ b/Runtime/Splines/SplineUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0c94e230385c4b4888e548c3fd650b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments.meta b/Runtime/Splines/Uniform Spline Segments.meta new file mode 100644 index 0000000..a45ee06 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b1febd520dc5434c8e4bf061f9b6908 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/Bezier2D.cs b/Runtime/Splines/Uniform Spline Segments/Bezier2D.cs similarity index 72% rename from Curves/Bezier2D.cs rename to Runtime/Splines/Uniform Spline Segments/Bezier2D.cs index 7b541a2..a88d147 100644 --- a/Curves/Bezier2D.cs +++ b/Runtime/Splines/Uniform Spline Segments/Bezier2D.cs @@ -17,6 +17,11 @@ namespace Freya { readonly Vector2[] ptEvalBuffer; + /// The number of control points in this curve + public int Count { + [MethodImpl( INLINE )] get => points.Length; + } + /// Creates a general bezier curve, from any number of control points /// The control points of this curve public Bezier2D( params Vector2[] points ) { @@ -34,19 +39,12 @@ public Vector2 this[ int i ] { #region Core IParamCurve Implementations - public Vector2 GetStartPoint() => points[0]; - public Vector2 GetEndPoint() => points[Count - 1]; - - public int Count { - [MethodImpl( INLINE )] get => points.Length; - } - /// The degree of the curve, equal to the number of control points minus 1. 2 points = degree 1 (linear), 3 points = degree 2 (quadratic), 4 points = degree 3 (cubic) public int Degree { [MethodImpl( INLINE )] get => points.Length - 1; } - public Vector2 GetPoint( float t ) { + public Vector2 Eval( float t ) { float n = Count - 1; for( int i = 0; i < n; i++ ) ptEvalBuffer[i] = Vector2.LerpUnclamped( points[i], points[i + 1], t ); @@ -57,27 +55,12 @@ public Vector2 GetPoint( float t ) { } return ptEvalBuffer[0]; - /* // pretty but slow recursive implementation: - return B( Degree, 0 ); - Vector2 B( int k, int i ) { - if( k == 0 ) return points[i]; - return Vector2.LerpUnclamped( B( k - 1, i ), B( k - 1, i + 1 ), t ); - }*/ } #endregion - /// Calculates the weight (influence) of a given point at the given t-value - /// The point to get the weight of - /// The t-value where you want sample the weight value - public float GetPointWeight( int i, float t ) { - if(i < 0 || i >= Count) - throw new IndexOutOfRangeException($"GetPointWeight index {i} is out of range. Valid range is 0 to {Count-1}"); - return SplineUtils.SampleBasisFunction( Degree, i, t ); - } - /// Returns the derivative bezier curve if possible, otherwise returns null - public Bezier2D GetDerivative() { + public Bezier2D Differentiate() { int n = Count - 1; if( n == 0 ) return null; // no derivative diff --git a/Runtime/Splines/Uniform Spline Segments/Bezier2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/Bezier2D.cs.meta new file mode 100644 index 0000000..edd05c3 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/Bezier2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a62110a194614ce0b365b9d24ee54e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Curves/Bezier3D.cs b/Runtime/Splines/Uniform Spline Segments/Bezier3D.cs similarity index 63% rename from Curves/Bezier3D.cs rename to Runtime/Splines/Uniform Spline Segments/Bezier3D.cs index 8db6c89..90c1f73 100644 --- a/Curves/Bezier3D.cs +++ b/Runtime/Splines/Uniform Spline Segments/Bezier3D.cs @@ -15,8 +15,20 @@ namespace Freya { /// public readonly Vector3[] points; + readonly Vector3[] ptEvalBuffer; + + /// + public int Count { + [MethodImpl( INLINE )] get => points.Length; + } + /// - public Bezier3D( Vector3[] points ) => this.points = points; + public Bezier3D( Vector3[] points ) { + this.points = points; + if( points == null || points.Length <= 1 ) + throw new ArgumentException( "Bézier curves require at least two points" ); + ptEvalBuffer = new Vector3[points.Length - 1]; + } /// public Vector3 this[ int i ] { @@ -26,31 +38,28 @@ public Vector3 this[ int i ] { #region Core IParamCurve Implementations - public Vector3 GetStartPoint() => points[0]; - public Vector3 GetEndPoint() => points[Count - 1]; - - public int Count { - [MethodImpl( INLINE )] get => points.Length; - } - - /// The degree of the curve, equal to the number of control points minus 1. 2 points = degree 1 (linear), 3 points = degree 2 (quadratic), 4 points = degree 3 (cubic) + /// public int Degree { [MethodImpl( INLINE )] get => points.Length - 1; } - public Vector3 GetPoint( float t ) { - return B( Degree, 0 ); - - Vector3 B( int k, int i ) { - if( k == 0 ) return points[i]; - return Vector3.LerpUnclamped( B( k - 1, i ), B( k - 1, i + 1 ), t ); // todo: optimize + public Vector3 Eval( float t ) { + float n = Count - 1; + for( int i = 0; i < n; i++ ) + ptEvalBuffer[i] = Vector3.LerpUnclamped( points[i], points[i + 1], t ); + while( n > 1 ) { + n--; + for( int i = 0; i < n; i++ ) + ptEvalBuffer[i] = Vector3.LerpUnclamped( ptEvalBuffer[i], ptEvalBuffer[i + 1], t ); } + + return ptEvalBuffer[0]; } #endregion - /// - public Bezier3D GetDerivative() { + /// + public Bezier3D Differentiate() { int n = Count - 1; if( n == 0 ) return null; // no derivative diff --git a/Runtime/Splines/Uniform Spline Segments/Bezier3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/Bezier3D.cs.meta new file mode 100644 index 0000000..c762531 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/Bezier3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b696f42e5a5c467ebd6ccd85bc70d91 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs b/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs new file mode 100644 index 0000000..97f6959 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs @@ -0,0 +1,107 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 1D Cubic bézier segment, with 4 control points + [Serializable] public struct BezierCubic1D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Matrix4x1 pointMatrix; + [NonSerialized] Polynomial curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 1D Cubic bézier segment, from 4 control points + /// The starting point of the curve + /// The second control point of the curve, sometimes called the start tangent point + /// The third control point of the curve, sometimes called the end tangent point + /// The end point of the curve + public BezierCubic1D( float p0, float p1, float p2, float p3 ) : this(new Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 1D Cubic bézier segment, from 4 control points + /// The matrix containing the control points of this spline + public BezierCubic1D( Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial( + P0, + 3*(-P0+P1), + 3*P0-6*P1+3*P2, + -P0+3*P1-3*P2+P3 + ); + } + } + public Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public float P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point of the curve, sometimes called the start tangent point + public float P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point of the curve, sometimes called the end tangent point + public float P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The end point of the curve + public float P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public float this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierCubic1D a, BezierCubic1D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierCubic1D a, BezierCubic1D b ) => !( a == b ); + public bool Equals( BezierCubic1D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is BezierCubic1D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator HermiteCubic1D( BezierCubic1D s ) => + new HermiteCubic1D( + s.P0, + 3*(-s.P0+s.P1), + s.P3, + 3*(-s.P2+s.P3) + ); + public static explicit operator CatRomCubic1D( BezierCubic1D s ) => + new CatRomCubic1D( + 6*s.P0-6*s.P1+s.P3, + s.P0, + s.P3, + s.P0-6*s.P2+6*s.P3 + ); + public static explicit operator UBSCubic1D( BezierCubic1D s ) => + new UBSCubic1D( + 6*s.P0-7*s.P1+2*s.P2, + 2*s.P1-s.P2, + -s.P1+2*s.P2, + 2*s.P1-7*s.P2+6*s.P3 + ); + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic1D Lerp( BezierCubic1D a, BezierCubic1D b, float t ) => + new( + Mathfs.Lerp( a.P0, b.P0, t ), + Mathfs.Lerp( a.P1, b.P1, t ), + Mathfs.Lerp( a.P2, b.P2, t ), + Mathfs.Lerp( a.P3, b.P3, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierCubic1D pre, BezierCubic1D post) Split( float t ) { + float a = P0 + ( P1 - P0 ) * t; + float b = P1 + ( P2 - P1 ) * t; + float c = P2 + ( P3 - P2 ) * t; + float d = a + ( b - a ) * t; + float e = b + ( c - b ) * t; + float p = d + ( e - d ) * t; + return ( new BezierCubic1D( P0, a, d, p ), new BezierCubic1D( p, e, c, P3 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs.meta new file mode 100644 index 0000000..e825429 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic1D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d4c99da6f5ba4f8180ad18ea3b40a53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs b/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs new file mode 100644 index 0000000..116f842 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs @@ -0,0 +1,137 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 2D Cubic bézier segment, with 4 control points + [Serializable] public struct BezierCubic2D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector2Matrix4x1 pointMatrix; + [NonSerialized] Polynomial2D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 2D Cubic bézier segment, from 4 control points + /// The starting point of the curve + /// The second control point of the curve, sometimes called the start tangent point + /// The third control point of the curve, sometimes called the end tangent point + /// The end point of the curve + public BezierCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) : this(new Vector2Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 2D Cubic bézier segment, from 4 control points + /// The matrix containing the control points of this spline + public BezierCubic2D( Vector2Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial2D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial2D( + P0, + 3*(-P0+P1), + 3*P0-6*P1+3*P2, + -P0+3*P1-3*P2+P3 + ); + } + } + public Vector2Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector2 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point of the curve, sometimes called the start tangent point + public Vector2 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point of the curve, sometimes called the end tangent point + public Vector2 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector2 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector2 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierCubic2D a, BezierCubic2D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierCubic2D a, BezierCubic2D b ) => !( a == b ); + public bool Equals( BezierCubic2D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is BezierCubic2D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this spline segment in 3D, where z = 0 + /// The 2D curve to cast to 3D + public static explicit operator BezierCubic3D( BezierCubic2D curve2D ) => new BezierCubic3D( curve2D.P0, curve2D.P1, curve2D.P2, curve2D.P3 ); + public static explicit operator HermiteCubic2D( BezierCubic2D s ) => + new HermiteCubic2D( + s.P0, + 3*(-s.P0+s.P1), + s.P3, + 3*(-s.P2+s.P3) + ); + public static explicit operator CatRomCubic2D( BezierCubic2D s ) => + new CatRomCubic2D( + 6*s.P0-6*s.P1+s.P3, + s.P0, + s.P3, + s.P0-6*s.P2+6*s.P3 + ); + public static explicit operator UBSCubic2D( BezierCubic2D s ) => + new UBSCubic2D( + 6*s.P0-7*s.P1+2*s.P2, + 2*s.P1-s.P2, + -s.P1+2*s.P2, + 2*s.P1-7*s.P2+6*s.P3 + ); + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic2D Lerp( BezierCubic2D a, BezierCubic2D b, float t ) => + new( + Vector2.LerpUnclamped( a.P0, b.P0, t ), + Vector2.LerpUnclamped( a.P1, b.P1, t ), + Vector2.LerpUnclamped( a.P2, b.P2, t ), + Vector2.LerpUnclamped( a.P3, b.P3, t ) + ); + + /// Returns a linear blend between two bézier curves, where the tangent directions are spherically interpolated + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic2D Slerp( BezierCubic2D a, BezierCubic2D b, float t ) { + Vector2 P0 = Vector2.LerpUnclamped( a.P0, b.P0, t ); + Vector2 P3 = Vector2.LerpUnclamped( a.P3, b.P3, t ); + return new BezierCubic2D( + P0, + P0 + (Vector2)Vector3.SlerpUnclamped( a.P1 - a.P0, b.P1 - b.P0, t ), + P3 + (Vector2)Vector3.SlerpUnclamped( a.P2 - a.P3, b.P2 - b.P3, t ), + P3 + ); + } + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierCubic2D pre, BezierCubic2D post) Split( float t ) { + Vector2 a = new Vector2( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t ); + Vector2 b = new Vector2( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t ); + Vector2 c = new Vector2( + P2.x + ( P3.x - P2.x ) * t, + P2.y + ( P3.y - P2.y ) * t ); + Vector2 d = new Vector2( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t ); + Vector2 e = new Vector2( + b.x + ( c.x - b.x ) * t, + b.y + ( c.y - b.y ) * t ); + Vector2 p = new Vector2( + d.x + ( e.x - d.x ) * t, + d.y + ( e.y - d.y ) * t ); + return ( new BezierCubic2D( P0, a, d, p ), new BezierCubic2D( p, e, c, P3 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs.meta new file mode 100644 index 0000000..fba95e4 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7082194004e35467fab3dce11f04f195 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs b/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs new file mode 100644 index 0000000..8ec0cd1 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs @@ -0,0 +1,143 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 3D Cubic bézier segment, with 4 control points + [Serializable] public struct BezierCubic3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector3Matrix4x1 pointMatrix; + [NonSerialized] Polynomial3D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 3D Cubic bézier segment, from 4 control points + /// The starting point of the curve + /// The second control point of the curve, sometimes called the start tangent point + /// The third control point of the curve, sometimes called the end tangent point + /// The end point of the curve + public BezierCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) : this(new Vector3Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 3D Cubic bézier segment, from 4 control points + /// The matrix containing the control points of this spline + public BezierCubic3D( Vector3Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial3D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial3D( + P0, + 3*(-P0+P1), + 3*P0-6*P1+3*P2, + -P0+3*P1-3*P2+P3 + ); + } + } + public Vector3Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector3 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point of the curve, sometimes called the start tangent point + public Vector3 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point of the curve, sometimes called the end tangent point + public Vector3 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector3 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector3 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierCubic3D a, BezierCubic3D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierCubic3D a, BezierCubic3D b ) => !( a == b ); + public bool Equals( BezierCubic3D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is BezierCubic3D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this curve flattened to 2D. Effectively setting z = 0 + /// The 3D curve to flatten to the Z plane + public static explicit operator BezierCubic2D( BezierCubic3D curve3D ) => new BezierCubic2D( curve3D.P0, curve3D.P1, curve3D.P2, curve3D.P3 ); + public static explicit operator HermiteCubic3D( BezierCubic3D s ) => + new HermiteCubic3D( + s.P0, + 3*(-s.P0+s.P1), + s.P3, + 3*(-s.P2+s.P3) + ); + public static explicit operator CatRomCubic3D( BezierCubic3D s ) => + new CatRomCubic3D( + 6*s.P0-6*s.P1+s.P3, + s.P0, + s.P3, + s.P0-6*s.P2+6*s.P3 + ); + public static explicit operator UBSCubic3D( BezierCubic3D s ) => + new UBSCubic3D( + 6*s.P0-7*s.P1+2*s.P2, + 2*s.P1-s.P2, + -s.P1+2*s.P2, + 2*s.P1-7*s.P2+6*s.P3 + ); + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic3D Lerp( BezierCubic3D a, BezierCubic3D b, float t ) => + new( + Vector3.LerpUnclamped( a.P0, b.P0, t ), + Vector3.LerpUnclamped( a.P1, b.P1, t ), + Vector3.LerpUnclamped( a.P2, b.P2, t ), + Vector3.LerpUnclamped( a.P3, b.P3, t ) + ); + + /// Returns a linear blend between two bézier curves, where the tangent directions are spherically interpolated + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic3D Slerp( BezierCubic3D a, BezierCubic3D b, float t ) { + Vector3 P0 = Vector3.LerpUnclamped( a.P0, b.P0, t ); + Vector3 P3 = Vector3.LerpUnclamped( a.P3, b.P3, t ); + return new BezierCubic3D( + P0, + P0 + Vector3.SlerpUnclamped( a.P1 - a.P0, b.P1 - b.P0, t ), + P3 + Vector3.SlerpUnclamped( a.P2 - a.P3, b.P2 - b.P3, t ), + P3 + ); + } + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierCubic3D pre, BezierCubic3D post) Split( float t ) { + Vector3 a = new Vector3( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t, + P0.z + ( P1.z - P0.z ) * t ); + Vector3 b = new Vector3( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t, + P1.z + ( P2.z - P1.z ) * t ); + Vector3 c = new Vector3( + P2.x + ( P3.x - P2.x ) * t, + P2.y + ( P3.y - P2.y ) * t, + P2.z + ( P3.z - P2.z ) * t ); + Vector3 d = new Vector3( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t, + a.z + ( b.z - a.z ) * t ); + Vector3 e = new Vector3( + b.x + ( c.x - b.x ) * t, + b.y + ( c.y - b.y ) * t, + b.z + ( c.z - b.z ) * t ); + Vector3 p = new Vector3( + d.x + ( e.x - d.x ) * t, + d.y + ( e.y - d.y ) * t, + d.z + ( e.z - d.z ) * t ); + return ( new BezierCubic3D( P0, a, d, p ), new BezierCubic3D( p, e, c, P3 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs.meta new file mode 100644 index 0000000..636c325 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ed432da1ee6545b09b08bfcf670a3b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs b/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs new file mode 100644 index 0000000..bfafe42 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs @@ -0,0 +1,131 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 4D Cubic bézier segment, with 4 control points + [Serializable] public struct BezierCubic4D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector4Matrix4x1 pointMatrix; + [NonSerialized] Polynomial4D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 4D Cubic bézier segment, from 4 control points + /// The starting point of the curve + /// The second control point of the curve, sometimes called the start tangent point + /// The third control point of the curve, sometimes called the end tangent point + /// The end point of the curve + public BezierCubic4D( Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3 ) : this(new Vector4Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 4D Cubic bézier segment, from 4 control points + /// The matrix containing the control points of this spline + public BezierCubic4D( Vector4Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial4D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial4D( + P0, + 3*(-P0+P1), + 3*P0-6*P1+3*P2, + -P0+3*P1-3*P2+P3 + ); + } + } + public Vector4Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector4 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point of the curve, sometimes called the start tangent point + public Vector4 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point of the curve, sometimes called the end tangent point + public Vector4 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector4 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector4 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierCubic4D a, BezierCubic4D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierCubic4D a, BezierCubic4D b ) => !( a == b ); + public bool Equals( BezierCubic4D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is BezierCubic4D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator HermiteCubic4D( BezierCubic4D s ) => + new HermiteCubic4D( + s.P0, + 3*(-s.P0+s.P1), + s.P3, + 3*(-s.P2+s.P3) + ); + public static explicit operator CatRomCubic4D( BezierCubic4D s ) => + new CatRomCubic4D( + 6*s.P0-6*s.P1+s.P3, + s.P0, + s.P3, + s.P0-6*s.P2+6*s.P3 + ); + public static explicit operator UBSCubic4D( BezierCubic4D s ) => + new UBSCubic4D( + 6*s.P0-7*s.P1+2*s.P2, + 2*s.P1-s.P2, + -s.P1+2*s.P2, + 2*s.P1-7*s.P2+6*s.P3 + ); + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierCubic4D Lerp( BezierCubic4D a, BezierCubic4D b, float t ) => + new( + Vector4.LerpUnclamped( a.P0, b.P0, t ), + Vector4.LerpUnclamped( a.P1, b.P1, t ), + Vector4.LerpUnclamped( a.P2, b.P2, t ), + Vector4.LerpUnclamped( a.P3, b.P3, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierCubic4D pre, BezierCubic4D post) Split( float t ) { + Vector4 a = new Vector4( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t, + P0.z + ( P1.z - P0.z ) * t, + P0.w + ( P1.w - P0.w ) * t ); + Vector4 b = new Vector4( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t, + P1.z + ( P2.z - P1.z ) * t, + P1.w + ( P2.w - P1.w ) * t ); + Vector4 c = new Vector4( + P2.x + ( P3.x - P2.x ) * t, + P2.y + ( P3.y - P2.y ) * t, + P2.z + ( P3.z - P2.z ) * t, + P2.w + ( P3.w - P2.w ) * t ); + Vector4 d = new Vector4( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t, + a.z + ( b.z - a.z ) * t, + a.w + ( b.w - a.w ) * t ); + Vector4 e = new Vector4( + b.x + ( c.x - b.x ) * t, + b.y + ( c.y - b.y ) * t, + b.z + ( c.z - b.z ) * t, + b.w + ( c.w - b.w ) * t ); + Vector4 p = new Vector4( + d.x + ( e.x - d.x ) * t, + d.y + ( e.y - d.y ) * t, + d.z + ( e.z - d.z ) * t, + d.w + ( e.w - d.w ) * t ); + return ( new BezierCubic4D( P0, a, d, p ), new BezierCubic4D( p, e, c, P3 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs.meta new file mode 100644 index 0000000..83ba311 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierCubic4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba539f1adb9aa4d5981b5adec2f3f8c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs b/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs new file mode 100644 index 0000000..9e8ca9e --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs @@ -0,0 +1,78 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 1D Quadratic bézier segment, with 3 control points + [Serializable] public struct BezierQuad1D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Matrix3x1 pointMatrix; + [NonSerialized] Polynomial curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 1D Quadratic bézier segment, from 3 control points + /// The starting point of the curve + /// The middle control point of the curve, sometimes called a tangent point + /// The end point of the curve + public BezierQuad1D( float p0, float p1, float p2 ) : this(new Matrix3x1(p0, p1, p2)){} + /// Creates a uniform 1D Quadratic bézier segment, from 3 control points + /// The matrix containing the control points of this spline + public BezierQuad1D( Matrix3x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial( + P0, + 2*(-P0+P1), + P0-2*P1+P2 + ); + } + } + public Matrix3x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public float P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The middle control point of the curve, sometimes called a tangent point + public float P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public float P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 2 + public float this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierQuad1D a, BezierQuad1D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierQuad1D a, BezierQuad1D b ) => !( a == b ); + public bool Equals( BezierQuad1D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ); + public override bool Equals( object obj ) => obj is BezierQuad1D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2})"; + + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierQuad1D Lerp( BezierQuad1D a, BezierQuad1D b, float t ) => + new( + Mathfs.Lerp( a.P0, b.P0, t ), + Mathfs.Lerp( a.P1, b.P1, t ), + Mathfs.Lerp( a.P2, b.P2, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierQuad1D pre, BezierQuad1D post) Split( float t ) { + float a = P0 + ( P1 - P0 ) * t; + float b = P1 + ( P2 - P1 ) * t; + float p = a + ( b - a ) * t; + return ( new BezierQuad1D( P0, a, p ), new BezierQuad1D( p, b, P2 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs.meta new file mode 100644 index 0000000..af6e269 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad1D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a8260881189248708f453a096bd0b65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs b/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs new file mode 100644 index 0000000..b844b65 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs @@ -0,0 +1,84 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 2D Quadratic bézier segment, with 3 control points + [Serializable] public struct BezierQuad2D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector2Matrix3x1 pointMatrix; + [NonSerialized] Polynomial2D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 2D Quadratic bézier segment, from 3 control points + /// The starting point of the curve + /// The middle control point of the curve, sometimes called a tangent point + /// The end point of the curve + public BezierQuad2D( Vector2 p0, Vector2 p1, Vector2 p2 ) : this(new Vector2Matrix3x1(p0, p1, p2)){} + /// Creates a uniform 2D Quadratic bézier segment, from 3 control points + /// The matrix containing the control points of this spline + public BezierQuad2D( Vector2Matrix3x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial2D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial2D( + P0, + 2*(-P0+P1), + P0-2*P1+P2 + ); + } + } + public Vector2Matrix3x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector2 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The middle control point of the curve, sometimes called a tangent point + public Vector2 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector2 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 2 + public Vector2 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierQuad2D a, BezierQuad2D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierQuad2D a, BezierQuad2D b ) => !( a == b ); + public bool Equals( BezierQuad2D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ); + public override bool Equals( object obj ) => obj is BezierQuad2D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2})"; + + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierQuad2D Lerp( BezierQuad2D a, BezierQuad2D b, float t ) => + new( + Vector2.LerpUnclamped( a.P0, b.P0, t ), + Vector2.LerpUnclamped( a.P1, b.P1, t ), + Vector2.LerpUnclamped( a.P2, b.P2, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierQuad2D pre, BezierQuad2D post) Split( float t ) { + Vector2 a = new Vector2( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t ); + Vector2 b = new Vector2( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t ); + Vector2 p = new Vector2( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t ); + return ( new BezierQuad2D( P0, a, p ), new BezierQuad2D( p, b, P2 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs.meta new file mode 100644 index 0000000..f7f8587 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6eb524f1bc4cd445ba5106f72fb87364 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs b/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs new file mode 100644 index 0000000..f617a09 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs @@ -0,0 +1,87 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 3D Quadratic bézier segment, with 3 control points + [Serializable] public struct BezierQuad3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector3Matrix3x1 pointMatrix; + [NonSerialized] Polynomial3D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 3D Quadratic bézier segment, from 3 control points + /// The starting point of the curve + /// The middle control point of the curve, sometimes called a tangent point + /// The end point of the curve + public BezierQuad3D( Vector3 p0, Vector3 p1, Vector3 p2 ) : this(new Vector3Matrix3x1(p0, p1, p2)){} + /// Creates a uniform 3D Quadratic bézier segment, from 3 control points + /// The matrix containing the control points of this spline + public BezierQuad3D( Vector3Matrix3x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial3D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial3D( + P0, + 2*(-P0+P1), + P0-2*P1+P2 + ); + } + } + public Vector3Matrix3x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector3 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The middle control point of the curve, sometimes called a tangent point + public Vector3 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector3 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 2 + public Vector3 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierQuad3D a, BezierQuad3D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierQuad3D a, BezierQuad3D b ) => !( a == b ); + public bool Equals( BezierQuad3D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ); + public override bool Equals( object obj ) => obj is BezierQuad3D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2})"; + + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierQuad3D Lerp( BezierQuad3D a, BezierQuad3D b, float t ) => + new( + Vector3.LerpUnclamped( a.P0, b.P0, t ), + Vector3.LerpUnclamped( a.P1, b.P1, t ), + Vector3.LerpUnclamped( a.P2, b.P2, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierQuad3D pre, BezierQuad3D post) Split( float t ) { + Vector3 a = new Vector3( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t, + P0.z + ( P1.z - P0.z ) * t ); + Vector3 b = new Vector3( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t, + P1.z + ( P2.z - P1.z ) * t ); + Vector3 p = new Vector3( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t, + a.z + ( b.z - a.z ) * t ); + return ( new BezierQuad3D( P0, a, p ), new BezierQuad3D( p, b, P2 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs.meta new file mode 100644 index 0000000..52b3c29 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50fefeb9ca6674a69aa14218c38ee6ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs b/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs new file mode 100644 index 0000000..e36e998 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs @@ -0,0 +1,90 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 4D Quadratic bézier segment, with 3 control points + [Serializable] public struct BezierQuad4D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector4Matrix3x1 pointMatrix; + [NonSerialized] Polynomial4D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 4D Quadratic bézier segment, from 3 control points + /// The starting point of the curve + /// The middle control point of the curve, sometimes called a tangent point + /// The end point of the curve + public BezierQuad4D( Vector4 p0, Vector4 p1, Vector4 p2 ) : this(new Vector4Matrix3x1(p0, p1, p2)){} + /// Creates a uniform 4D Quadratic bézier segment, from 3 control points + /// The matrix containing the control points of this spline + public BezierQuad4D( Vector4Matrix3x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial4D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial4D( + P0, + 2*(-P0+P1), + P0-2*P1+P2 + ); + } + } + public Vector4Matrix3x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector4 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The middle control point of the curve, sometimes called a tangent point + public Vector4 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector4 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 2 + public Vector4 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 2 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( BezierQuad4D a, BezierQuad4D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( BezierQuad4D a, BezierQuad4D b ) => !( a == b ); + public bool Equals( BezierQuad4D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ); + public override bool Equals( object obj ) => obj is BezierQuad4D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2})"; + + /// Returns a linear blend between two bézier curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static BezierQuad4D Lerp( BezierQuad4D a, BezierQuad4D b, float t ) => + new( + Vector4.LerpUnclamped( a.P0, b.P0, t ), + Vector4.LerpUnclamped( a.P1, b.P1, t ), + Vector4.LerpUnclamped( a.P2, b.P2, t ) + ); + /// Splits this curve at the given t-value, into two curves that together form the exact same shape + /// The t-value to split at + public (BezierQuad4D pre, BezierQuad4D post) Split( float t ) { + Vector4 a = new Vector4( + P0.x + ( P1.x - P0.x ) * t, + P0.y + ( P1.y - P0.y ) * t, + P0.z + ( P1.z - P0.z ) * t, + P0.w + ( P1.w - P0.w ) * t ); + Vector4 b = new Vector4( + P1.x + ( P2.x - P1.x ) * t, + P1.y + ( P2.y - P1.y ) * t, + P1.z + ( P2.z - P1.z ) * t, + P1.w + ( P2.w - P1.w ) * t ); + Vector4 p = new Vector4( + a.x + ( b.x - a.x ) * t, + a.y + ( b.y - a.y ) * t, + a.z + ( b.z - a.z ) * t, + a.w + ( b.w - a.w ) * t ); + return ( new BezierQuad4D( P0, a, p ), new BezierQuad4D( p, b, P2 ) ); + } + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs.meta b/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs.meta new file mode 100644 index 0000000..e29e23e --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/BezierQuad4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95c73aefe92d749e0962fd79ccd9b59c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs b/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs new file mode 100644 index 0000000..0935785 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 1D Cubic catmull-rom segment, with 4 control points + [Serializable] public struct CatRomCubic1D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Matrix4x1 pointMatrix; + [NonSerialized] Polynomial curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 1D Cubic catmull-rom segment, from 4 control points + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catmull-rom curve + /// The third control point, and the end of the catmull-rom curve + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public CatRomCubic1D( float p0, float p1, float p2, float p3 ) : this(new Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 1D Cubic catmull-rom segment, from 4 control points + /// The matrix containing the control points of this spline + public CatRomCubic1D( Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial( + P1, + (-P0+P2)/2, + P0-(5/2f)*P1+2*P2-(1/2f)*P3, + -(1/2f)*P0+(3/2f)*P1-(3/2f)*P2+(1/2f)*P3 + ); + } + } + public Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public float P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point, and the start of the catmull-rom curve + public float P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point, and the end of the catmull-rom curve + public float P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public float P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public float this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( CatRomCubic1D a, CatRomCubic1D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( CatRomCubic1D a, CatRomCubic1D b ) => !( a == b ); + public bool Equals( CatRomCubic1D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is CatRomCubic1D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic1D( CatRomCubic1D s ) => + new BezierCubic1D( + s.P1, + -(1/6f)*s.P0+s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+s.P2-(1/6f)*s.P3, + s.P2 + ); + public static explicit operator HermiteCubic1D( CatRomCubic1D s ) => + new HermiteCubic1D( + s.P1, + (-s.P0+s.P2)/2, + s.P2, + (-s.P1+s.P3)/2 + ); + public static explicit operator UBSCubic1D( CatRomCubic1D s ) => + new UBSCubic1D( + (7/6f)*s.P0-(2/3f)*s.P1+(5/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(11/6f)*s.P1-(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(2/3f)*s.P1+(11/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(5/6f)*s.P1-(2/3f)*s.P2+(7/6f)*s.P3 + ); + /// Returns a linear blend between two catmull-rom curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static CatRomCubic1D Lerp( CatRomCubic1D a, CatRomCubic1D b, float t ) => + new( + Mathfs.Lerp( a.P0, b.P0, t ), + Mathfs.Lerp( a.P1, b.P1, t ), + Mathfs.Lerp( a.P2, b.P2, t ), + Mathfs.Lerp( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs.meta b/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs.meta new file mode 100644 index 0000000..91007bc --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic1D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17d55b0e3881c4be18542f651d39605b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs b/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs new file mode 100644 index 0000000..cfbfc49 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 2D Cubic catmull-rom segment, with 4 control points + [Serializable] public struct CatRomCubic2D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector2Matrix4x1 pointMatrix; + [NonSerialized] Polynomial2D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 2D Cubic catmull-rom segment, from 4 control points + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catmull-rom curve + /// The third control point, and the end of the catmull-rom curve + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public CatRomCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) : this(new Vector2Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 2D Cubic catmull-rom segment, from 4 control points + /// The matrix containing the control points of this spline + public CatRomCubic2D( Vector2Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial2D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial2D( + P1, + (-P0+P2)/2, + P0-(5/2f)*P1+2*P2-(1/2f)*P3, + -(1/2f)*P0+(3/2f)*P1-(3/2f)*P2+(1/2f)*P3 + ); + } + } + public Vector2Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector2 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point, and the start of the catmull-rom curve + public Vector2 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point, and the end of the catmull-rom curve + public Vector2 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector2 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector2 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( CatRomCubic2D a, CatRomCubic2D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( CatRomCubic2D a, CatRomCubic2D b ) => !( a == b ); + public bool Equals( CatRomCubic2D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is CatRomCubic2D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this spline segment in 3D, where z = 0 + /// The 2D curve to cast to 3D + public static explicit operator CatRomCubic3D( CatRomCubic2D curve2D ) => new CatRomCubic3D( curve2D.P0, curve2D.P1, curve2D.P2, curve2D.P3 ); + public static explicit operator BezierCubic2D( CatRomCubic2D s ) => + new BezierCubic2D( + s.P1, + -(1/6f)*s.P0+s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+s.P2-(1/6f)*s.P3, + s.P2 + ); + public static explicit operator HermiteCubic2D( CatRomCubic2D s ) => + new HermiteCubic2D( + s.P1, + (-s.P0+s.P2)/2, + s.P2, + (-s.P1+s.P3)/2 + ); + public static explicit operator UBSCubic2D( CatRomCubic2D s ) => + new UBSCubic2D( + (7/6f)*s.P0-(2/3f)*s.P1+(5/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(11/6f)*s.P1-(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(2/3f)*s.P1+(11/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(5/6f)*s.P1-(2/3f)*s.P2+(7/6f)*s.P3 + ); + /// Returns a linear blend between two catmull-rom curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static CatRomCubic2D Lerp( CatRomCubic2D a, CatRomCubic2D b, float t ) => + new( + Vector2.LerpUnclamped( a.P0, b.P0, t ), + Vector2.LerpUnclamped( a.P1, b.P1, t ), + Vector2.LerpUnclamped( a.P2, b.P2, t ), + Vector2.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs.meta new file mode 100644 index 0000000..b791225 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b0c539b74c9a44d54b5675954d44ff7c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs b/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs new file mode 100644 index 0000000..3d9d724 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 3D Cubic catmull-rom segment, with 4 control points + [Serializable] public struct CatRomCubic3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector3Matrix4x1 pointMatrix; + [NonSerialized] Polynomial3D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 3D Cubic catmull-rom segment, from 4 control points + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catmull-rom curve + /// The third control point, and the end of the catmull-rom curve + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public CatRomCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) : this(new Vector3Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 3D Cubic catmull-rom segment, from 4 control points + /// The matrix containing the control points of this spline + public CatRomCubic3D( Vector3Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial3D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial3D( + P1, + (-P0+P2)/2, + P0-(5/2f)*P1+2*P2-(1/2f)*P3, + -(1/2f)*P0+(3/2f)*P1-(3/2f)*P2+(1/2f)*P3 + ); + } + } + public Vector3Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector3 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point, and the start of the catmull-rom curve + public Vector3 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point, and the end of the catmull-rom curve + public Vector3 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector3 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector3 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( CatRomCubic3D a, CatRomCubic3D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( CatRomCubic3D a, CatRomCubic3D b ) => !( a == b ); + public bool Equals( CatRomCubic3D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is CatRomCubic3D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this curve flattened to 2D. Effectively setting z = 0 + /// The 3D curve to flatten to the Z plane + public static explicit operator CatRomCubic2D( CatRomCubic3D curve3D ) => new CatRomCubic2D( curve3D.P0, curve3D.P1, curve3D.P2, curve3D.P3 ); + public static explicit operator BezierCubic3D( CatRomCubic3D s ) => + new BezierCubic3D( + s.P1, + -(1/6f)*s.P0+s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+s.P2-(1/6f)*s.P3, + s.P2 + ); + public static explicit operator HermiteCubic3D( CatRomCubic3D s ) => + new HermiteCubic3D( + s.P1, + (-s.P0+s.P2)/2, + s.P2, + (-s.P1+s.P3)/2 + ); + public static explicit operator UBSCubic3D( CatRomCubic3D s ) => + new UBSCubic3D( + (7/6f)*s.P0-(2/3f)*s.P1+(5/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(11/6f)*s.P1-(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(2/3f)*s.P1+(11/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(5/6f)*s.P1-(2/3f)*s.P2+(7/6f)*s.P3 + ); + /// Returns a linear blend between two catmull-rom curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static CatRomCubic3D Lerp( CatRomCubic3D a, CatRomCubic3D b, float t ) => + new( + Vector3.LerpUnclamped( a.P0, b.P0, t ), + Vector3.LerpUnclamped( a.P1, b.P1, t ), + Vector3.LerpUnclamped( a.P2, b.P2, t ), + Vector3.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs.meta new file mode 100644 index 0000000..3043b8b --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abce92a8a033c4b0da7f8fbb1828ffcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs b/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs new file mode 100644 index 0000000..0b768dd --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 4D Cubic catmull-rom segment, with 4 control points + [Serializable] public struct CatRomCubic4D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector4Matrix4x1 pointMatrix; + [NonSerialized] Polynomial4D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 4D Cubic catmull-rom segment, from 4 control points + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + /// The second control point, and the start of the catmull-rom curve + /// The third control point, and the end of the catmull-rom curve + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public CatRomCubic4D( Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3 ) : this(new Vector4Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 4D Cubic catmull-rom segment, from 4 control points + /// The matrix containing the control points of this spline + public CatRomCubic4D( Vector4Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial4D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial4D( + P1, + (-P0+P2)/2, + P0-(5/2f)*P1+2*P2-(1/2f)*P3, + -(1/2f)*P0+(3/2f)*P1-(3/2f)*P2+(1/2f)*P3 + ); + } + } + public Vector4Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector4 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second control point, and the start of the catmull-rom curve + public Vector4 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third control point, and the end of the catmull-rom curve + public Vector4 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The last control point of the catmull-rom curve. Note that this point is not included in the curve itself, and only helps to shape it + public Vector4 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector4 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( CatRomCubic4D a, CatRomCubic4D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( CatRomCubic4D a, CatRomCubic4D b ) => !( a == b ); + public bool Equals( CatRomCubic4D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is CatRomCubic4D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic4D( CatRomCubic4D s ) => + new BezierCubic4D( + s.P1, + -(1/6f)*s.P0+s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+s.P2-(1/6f)*s.P3, + s.P2 + ); + public static explicit operator HermiteCubic4D( CatRomCubic4D s ) => + new HermiteCubic4D( + s.P1, + (-s.P0+s.P2)/2, + s.P2, + (-s.P1+s.P3)/2 + ); + public static explicit operator UBSCubic4D( CatRomCubic4D s ) => + new UBSCubic4D( + (7/6f)*s.P0-(2/3f)*s.P1+(5/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(11/6f)*s.P1-(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(2/3f)*s.P1+(11/6f)*s.P2-(1/3f)*s.P3, + -(1/3f)*s.P0+(5/6f)*s.P1-(2/3f)*s.P2+(7/6f)*s.P3 + ); + /// Returns a linear blend between two catmull-rom curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static CatRomCubic4D Lerp( CatRomCubic4D a, CatRomCubic4D b, float t ) => + new( + Vector4.LerpUnclamped( a.P0, b.P0, t ), + Vector4.LerpUnclamped( a.P1, b.P1, t ), + Vector4.LerpUnclamped( a.P2, b.P2, t ), + Vector4.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs.meta b/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs.meta new file mode 100644 index 0000000..20237c8 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/CatRomCubic4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdd4e8e6d088547a195955dd98e51e27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs b/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs new file mode 100644 index 0000000..59be8b1 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 1D Cubic hermite segment, with 4 control points + [Serializable] public struct HermiteCubic1D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Matrix4x1 pointMatrix; + [NonSerialized] Polynomial curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 1D Cubic hermite segment, from 4 control points + /// The starting point of the curve + /// The rate of change (velocity) at the start of the curve + /// The end point of the curve + /// The rate of change (velocity) at the end of the curve + public HermiteCubic1D( float p0, float v0, float p1, float v1 ) : this(new Matrix4x1(p0, v0, p1, v1)){} + /// Creates a uniform 1D Cubic hermite segment, from 4 control points + /// The matrix containing the control points of this spline + public HermiteCubic1D( Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial( + P0, + V0, + -3*P0-2*V0+3*P1-V1, + 2*P0+V0-2*P1+V1 + ); + } + } + public Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public float P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the start of the curve + public float V0{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public float P1{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the end of the curve + public float V1{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public float this[ int i ] { + get => i switch { 0 => P0, 1 => V0, 2 => P1, 3 => V1, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: V0 = value; break; case 2: P1 = value; break; case 3: V1 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( HermiteCubic1D a, HermiteCubic1D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( HermiteCubic1D a, HermiteCubic1D b ) => !( a == b ); + public bool Equals( HermiteCubic1D other ) => P0.Equals( other.P0 ) && V0.Equals( other.V0 ) && P1.Equals( other.P1 ) && V1.Equals( other.V1 ); + public override bool Equals( object obj ) => obj is HermiteCubic1D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic1D( HermiteCubic1D s ) => + new BezierCubic1D( + s.P0, + s.P0+(1/3f)*s.V0, + s.P1-(1/3f)*s.V1, + s.P1 + ); + public static explicit operator CatRomCubic1D( HermiteCubic1D s ) => + new CatRomCubic1D( + -2*s.V0+s.P1, + s.P0, + s.P1, + s.P0+2*s.V1 + ); + public static explicit operator UBSCubic1D( HermiteCubic1D s ) => + new UBSCubic1D( + -s.P0-(7/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(1/3f)*s.V1, + -s.P0-(1/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(7/3f)*s.V1 + ); + /// Returns a linear blend between two hermite curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static HermiteCubic1D Lerp( HermiteCubic1D a, HermiteCubic1D b, float t ) => + new( + Mathfs.Lerp( a.P0, b.P0, t ), + Mathfs.Lerp( a.V0, b.V0, t ), + Mathfs.Lerp( a.P1, b.P1, t ), + Mathfs.Lerp( a.V1, b.V1, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs.meta b/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs.meta new file mode 100644 index 0000000..f074e16 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic1D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f5df525ea1f3407992f8a28de6dd2c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs b/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs new file mode 100644 index 0000000..4f7e15a --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 2D Cubic hermite segment, with 4 control points + [Serializable] public struct HermiteCubic2D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector2Matrix4x1 pointMatrix; + [NonSerialized] Polynomial2D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 2D Cubic hermite segment, from 4 control points + /// The starting point of the curve + /// The rate of change (velocity) at the start of the curve + /// The end point of the curve + /// The rate of change (velocity) at the end of the curve + public HermiteCubic2D( Vector2 p0, Vector2 v0, Vector2 p1, Vector2 v1 ) : this(new Vector2Matrix4x1(p0, v0, p1, v1)){} + /// Creates a uniform 2D Cubic hermite segment, from 4 control points + /// The matrix containing the control points of this spline + public HermiteCubic2D( Vector2Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial2D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial2D( + P0, + V0, + -3*P0-2*V0+3*P1-V1, + 2*P0+V0-2*P1+V1 + ); + } + } + public Vector2Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector2 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the start of the curve + public Vector2 V0{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector2 P1{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the end of the curve + public Vector2 V1{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector2 this[ int i ] { + get => i switch { 0 => P0, 1 => V0, 2 => P1, 3 => V1, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: V0 = value; break; case 2: P1 = value; break; case 3: V1 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( HermiteCubic2D a, HermiteCubic2D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( HermiteCubic2D a, HermiteCubic2D b ) => !( a == b ); + public bool Equals( HermiteCubic2D other ) => P0.Equals( other.P0 ) && V0.Equals( other.V0 ) && P1.Equals( other.P1 ) && V1.Equals( other.V1 ); + public override bool Equals( object obj ) => obj is HermiteCubic2D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this spline segment in 3D, where z = 0 + /// The 2D curve to cast to 3D + public static explicit operator HermiteCubic3D( HermiteCubic2D curve2D ) => new HermiteCubic3D( curve2D.P0, curve2D.V0, curve2D.P1, curve2D.V1 ); + public static explicit operator BezierCubic2D( HermiteCubic2D s ) => + new BezierCubic2D( + s.P0, + s.P0+(1/3f)*s.V0, + s.P1-(1/3f)*s.V1, + s.P1 + ); + public static explicit operator CatRomCubic2D( HermiteCubic2D s ) => + new CatRomCubic2D( + -2*s.V0+s.P1, + s.P0, + s.P1, + s.P0+2*s.V1 + ); + public static explicit operator UBSCubic2D( HermiteCubic2D s ) => + new UBSCubic2D( + -s.P0-(7/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(1/3f)*s.V1, + -s.P0-(1/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(7/3f)*s.V1 + ); + /// Returns a linear blend between two hermite curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static HermiteCubic2D Lerp( HermiteCubic2D a, HermiteCubic2D b, float t ) => + new( + Vector2.LerpUnclamped( a.P0, b.P0, t ), + Vector2.LerpUnclamped( a.V0, b.V0, t ), + Vector2.LerpUnclamped( a.P1, b.P1, t ), + Vector2.LerpUnclamped( a.V1, b.V1, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs.meta new file mode 100644 index 0000000..26b43a0 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c66bda3f5beff458998ca6127a3f911d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs b/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs new file mode 100644 index 0000000..c703060 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 3D Cubic hermite segment, with 4 control points + [Serializable] public struct HermiteCubic3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector3Matrix4x1 pointMatrix; + [NonSerialized] Polynomial3D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 3D Cubic hermite segment, from 4 control points + /// The starting point of the curve + /// The rate of change (velocity) at the start of the curve + /// The end point of the curve + /// The rate of change (velocity) at the end of the curve + public HermiteCubic3D( Vector3 p0, Vector3 v0, Vector3 p1, Vector3 v1 ) : this(new Vector3Matrix4x1(p0, v0, p1, v1)){} + /// Creates a uniform 3D Cubic hermite segment, from 4 control points + /// The matrix containing the control points of this spline + public HermiteCubic3D( Vector3Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial3D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial3D( + P0, + V0, + -3*P0-2*V0+3*P1-V1, + 2*P0+V0-2*P1+V1 + ); + } + } + public Vector3Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector3 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the start of the curve + public Vector3 V0{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector3 P1{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the end of the curve + public Vector3 V1{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector3 this[ int i ] { + get => i switch { 0 => P0, 1 => V0, 2 => P1, 3 => V1, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: V0 = value; break; case 2: P1 = value; break; case 3: V1 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( HermiteCubic3D a, HermiteCubic3D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( HermiteCubic3D a, HermiteCubic3D b ) => !( a == b ); + public bool Equals( HermiteCubic3D other ) => P0.Equals( other.P0 ) && V0.Equals( other.V0 ) && P1.Equals( other.P1 ) && V1.Equals( other.V1 ); + public override bool Equals( object obj ) => obj is HermiteCubic3D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this curve flattened to 2D. Effectively setting z = 0 + /// The 3D curve to flatten to the Z plane + public static explicit operator HermiteCubic2D( HermiteCubic3D curve3D ) => new HermiteCubic2D( curve3D.P0, curve3D.V0, curve3D.P1, curve3D.V1 ); + public static explicit operator BezierCubic3D( HermiteCubic3D s ) => + new BezierCubic3D( + s.P0, + s.P0+(1/3f)*s.V0, + s.P1-(1/3f)*s.V1, + s.P1 + ); + public static explicit operator CatRomCubic3D( HermiteCubic3D s ) => + new CatRomCubic3D( + -2*s.V0+s.P1, + s.P0, + s.P1, + s.P0+2*s.V1 + ); + public static explicit operator UBSCubic3D( HermiteCubic3D s ) => + new UBSCubic3D( + -s.P0-(7/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(1/3f)*s.V1, + -s.P0-(1/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(7/3f)*s.V1 + ); + /// Returns a linear blend between two hermite curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static HermiteCubic3D Lerp( HermiteCubic3D a, HermiteCubic3D b, float t ) => + new( + Vector3.LerpUnclamped( a.P0, b.P0, t ), + Vector3.LerpUnclamped( a.V0, b.V0, t ), + Vector3.LerpUnclamped( a.P1, b.P1, t ), + Vector3.LerpUnclamped( a.V1, b.V1, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs.meta new file mode 100644 index 0000000..18ec0b8 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 723633b56770946fd95419f8b49ce803 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs b/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs new file mode 100644 index 0000000..3db1280 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 4D Cubic hermite segment, with 4 control points + [Serializable] public struct HermiteCubic4D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector4Matrix4x1 pointMatrix; + [NonSerialized] Polynomial4D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 4D Cubic hermite segment, from 4 control points + /// The starting point of the curve + /// The rate of change (velocity) at the start of the curve + /// The end point of the curve + /// The rate of change (velocity) at the end of the curve + public HermiteCubic4D( Vector4 p0, Vector4 v0, Vector4 p1, Vector4 v1 ) : this(new Vector4Matrix4x1(p0, v0, p1, v1)){} + /// Creates a uniform 4D Cubic hermite segment, from 4 control points + /// The matrix containing the control points of this spline + public HermiteCubic4D( Vector4Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial4D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial4D( + P0, + V0, + -3*P0-2*V0+3*P1-V1, + 2*P0+V0-2*P1+V1 + ); + } + } + public Vector4Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The starting point of the curve + public Vector4 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the start of the curve + public Vector4 V0{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The end point of the curve + public Vector4 P1{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The rate of change (velocity) at the end of the curve + public Vector4 V1{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector4 this[ int i ] { + get => i switch { 0 => P0, 1 => V0, 2 => P1, 3 => V1, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: V0 = value; break; case 2: P1 = value; break; case 3: V1 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( HermiteCubic4D a, HermiteCubic4D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( HermiteCubic4D a, HermiteCubic4D b ) => !( a == b ); + public bool Equals( HermiteCubic4D other ) => P0.Equals( other.P0 ) && V0.Equals( other.V0 ) && P1.Equals( other.P1 ) && V1.Equals( other.V1 ); + public override bool Equals( object obj ) => obj is HermiteCubic4D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic4D( HermiteCubic4D s ) => + new BezierCubic4D( + s.P0, + s.P0+(1/3f)*s.V0, + s.P1-(1/3f)*s.V1, + s.P1 + ); + public static explicit operator CatRomCubic4D( HermiteCubic4D s ) => + new CatRomCubic4D( + -2*s.V0+s.P1, + s.P0, + s.P1, + s.P0+2*s.V1 + ); + public static explicit operator UBSCubic4D( HermiteCubic4D s ) => + new UBSCubic4D( + -s.P0-(7/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(1/3f)*s.V1, + -s.P0-(1/3f)*s.V0+2*s.P1-(2/3f)*s.V1, + 2*s.P0+(2/3f)*s.V0-s.P1+(7/3f)*s.V1 + ); + /// Returns a linear blend between two hermite curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static HermiteCubic4D Lerp( HermiteCubic4D a, HermiteCubic4D b, float t ) => + new( + Vector4.LerpUnclamped( a.P0, b.P0, t ), + Vector4.LerpUnclamped( a.V0, b.V0, t ), + Vector4.LerpUnclamped( a.P1, b.P1, t ), + Vector4.LerpUnclamped( a.V1, b.V1, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs.meta b/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs.meta new file mode 100644 index 0000000..e64a753 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/HermiteCubic4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b9289807d1c84363900e4adc25ab188 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs b/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs new file mode 100644 index 0000000..53f3e1a --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 1D Cubic b-spline segment, with 4 control points + [Serializable] public struct UBSCubic1D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Matrix4x1 pointMatrix; + [NonSerialized] Polynomial curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 1D Cubic b-spline segment, from 4 control points + /// The first point of the B-spline hull + /// The second point of the B-spline hull + /// The third point of the B-spline hull + /// The fourth point of the B-spline hull + public UBSCubic1D( float p0, float p1, float p2, float p3 ) : this(new Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 1D Cubic b-spline segment, from 4 control points + /// The matrix containing the control points of this spline + public UBSCubic1D( Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial( + (1/6f)*P0+(2/3f)*P1+(1/6f)*P2, + (-P0+P2)/2, + (1/2f)*P0-P1+(1/2f)*P2, + -(1/6f)*P0+(1/2f)*P1-(1/2f)*P2+(1/6f)*P3 + ); + } + } + public Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first point of the B-spline hull + public float P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second point of the B-spline hull + public float P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third point of the B-spline hull + public float P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The fourth point of the B-spline hull + public float P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public float this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( UBSCubic1D a, UBSCubic1D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( UBSCubic1D a, UBSCubic1D b ) => !( a == b ); + public bool Equals( UBSCubic1D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is UBSCubic1D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic1D( UBSCubic1D s ) => + new BezierCubic1D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (2/3f)*s.P1+(1/3f)*s.P2, + (1/3f)*s.P1+(2/3f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3 + ); + public static explicit operator HermiteCubic1D( UBSCubic1D s ) => + new HermiteCubic1D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (-s.P0+s.P2)/2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (-s.P1+s.P3)/2 + ); + public static explicit operator CatRomCubic1D( UBSCubic1D s ) => + new CatRomCubic1D( + s.P0+(1/6f)*s.P1-(1/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(1/3f)*s.P1+(1/6f)*s.P2+s.P3 + ); + /// Returns a linear blend between two b-spline curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static UBSCubic1D Lerp( UBSCubic1D a, UBSCubic1D b, float t ) => + new( + Mathfs.Lerp( a.P0, b.P0, t ), + Mathfs.Lerp( a.P1, b.P1, t ), + Mathfs.Lerp( a.P2, b.P2, t ), + Mathfs.Lerp( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs.meta b/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs.meta new file mode 100644 index 0000000..60ba3c8 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic1D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc7e1a18c41064f9d905f02be89a10e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs b/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs new file mode 100644 index 0000000..22583c7 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 2D Cubic b-spline segment, with 4 control points + [Serializable] public struct UBSCubic2D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector2Matrix4x1 pointMatrix; + [NonSerialized] Polynomial2D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 2D Cubic b-spline segment, from 4 control points + /// The first point of the B-spline hull + /// The second point of the B-spline hull + /// The third point of the B-spline hull + /// The fourth point of the B-spline hull + public UBSCubic2D( Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3 ) : this(new Vector2Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 2D Cubic b-spline segment, from 4 control points + /// The matrix containing the control points of this spline + public UBSCubic2D( Vector2Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial2D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial2D( + (1/6f)*P0+(2/3f)*P1+(1/6f)*P2, + (-P0+P2)/2, + (1/2f)*P0-P1+(1/2f)*P2, + -(1/6f)*P0+(1/2f)*P1-(1/2f)*P2+(1/6f)*P3 + ); + } + } + public Vector2Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first point of the B-spline hull + public Vector2 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second point of the B-spline hull + public Vector2 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third point of the B-spline hull + public Vector2 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The fourth point of the B-spline hull + public Vector2 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector2 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( UBSCubic2D a, UBSCubic2D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( UBSCubic2D a, UBSCubic2D b ) => !( a == b ); + public bool Equals( UBSCubic2D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is UBSCubic2D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this spline segment in 3D, where z = 0 + /// The 2D curve to cast to 3D + public static explicit operator UBSCubic3D( UBSCubic2D curve2D ) => new UBSCubic3D( curve2D.P0, curve2D.P1, curve2D.P2, curve2D.P3 ); + public static explicit operator BezierCubic2D( UBSCubic2D s ) => + new BezierCubic2D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (2/3f)*s.P1+(1/3f)*s.P2, + (1/3f)*s.P1+(2/3f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3 + ); + public static explicit operator HermiteCubic2D( UBSCubic2D s ) => + new HermiteCubic2D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (-s.P0+s.P2)/2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (-s.P1+s.P3)/2 + ); + public static explicit operator CatRomCubic2D( UBSCubic2D s ) => + new CatRomCubic2D( + s.P0+(1/6f)*s.P1-(1/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(1/3f)*s.P1+(1/6f)*s.P2+s.P3 + ); + /// Returns a linear blend between two b-spline curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static UBSCubic2D Lerp( UBSCubic2D a, UBSCubic2D b, float t ) => + new( + Vector2.LerpUnclamped( a.P0, b.P0, t ), + Vector2.LerpUnclamped( a.P1, b.P1, t ), + Vector2.LerpUnclamped( a.P2, b.P2, t ), + Vector2.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs.meta b/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs.meta new file mode 100644 index 0000000..97f1904 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e4308106ac0144929902c1245834058c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs b/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs new file mode 100644 index 0000000..c653996 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs @@ -0,0 +1,99 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 3D Cubic b-spline segment, with 4 control points + [Serializable] public struct UBSCubic3D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector3Matrix4x1 pointMatrix; + [NonSerialized] Polynomial3D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 3D Cubic b-spline segment, from 4 control points + /// The first point of the B-spline hull + /// The second point of the B-spline hull + /// The third point of the B-spline hull + /// The fourth point of the B-spline hull + public UBSCubic3D( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 ) : this(new Vector3Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 3D Cubic b-spline segment, from 4 control points + /// The matrix containing the control points of this spline + public UBSCubic3D( Vector3Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial3D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial3D( + (1/6f)*P0+(2/3f)*P1+(1/6f)*P2, + (-P0+P2)/2, + (1/2f)*P0-P1+(1/2f)*P2, + -(1/6f)*P0+(1/2f)*P1-(1/2f)*P2+(1/6f)*P3 + ); + } + } + public Vector3Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first point of the B-spline hull + public Vector3 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second point of the B-spline hull + public Vector3 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third point of the B-spline hull + public Vector3 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The fourth point of the B-spline hull + public Vector3 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector3 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( UBSCubic3D a, UBSCubic3D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( UBSCubic3D a, UBSCubic3D b ) => !( a == b ); + public bool Equals( UBSCubic3D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is UBSCubic3D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + /// Returns this curve flattened to 2D. Effectively setting z = 0 + /// The 3D curve to flatten to the Z plane + public static explicit operator UBSCubic2D( UBSCubic3D curve3D ) => new UBSCubic2D( curve3D.P0, curve3D.P1, curve3D.P2, curve3D.P3 ); + public static explicit operator BezierCubic3D( UBSCubic3D s ) => + new BezierCubic3D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (2/3f)*s.P1+(1/3f)*s.P2, + (1/3f)*s.P1+(2/3f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3 + ); + public static explicit operator HermiteCubic3D( UBSCubic3D s ) => + new HermiteCubic3D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (-s.P0+s.P2)/2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (-s.P1+s.P3)/2 + ); + public static explicit operator CatRomCubic3D( UBSCubic3D s ) => + new CatRomCubic3D( + s.P0+(1/6f)*s.P1-(1/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(1/3f)*s.P1+(1/6f)*s.P2+s.P3 + ); + /// Returns a linear blend between two b-spline curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static UBSCubic3D Lerp( UBSCubic3D a, UBSCubic3D b, float t ) => + new( + Vector3.LerpUnclamped( a.P0, b.P0, t ), + Vector3.LerpUnclamped( a.P1, b.P1, t ), + Vector3.LerpUnclamped( a.P2, b.P2, t ), + Vector3.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs.meta b/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs.meta new file mode 100644 index 0000000..2cb951a --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdb3a3a315c444d9b95c59bb5ccf692f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs b/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs new file mode 100644 index 0000000..3dc3ea5 --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs @@ -0,0 +1,96 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) +// Do not manually edit - this file is generated by MathfsCodegen.cs + +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Freya { + + /// An optimized uniform 4D Cubic b-spline segment, with 4 control points + [Serializable] public struct UBSCubic4D : IParamSplineSegment { + + const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining; + + [SerializeField] Vector4Matrix4x1 pointMatrix; + [NonSerialized] Polynomial4D curve; + [NonSerialized] bool validCoefficients; + + /// Creates a uniform 4D Cubic b-spline segment, from 4 control points + /// The first point of the B-spline hull + /// The second point of the B-spline hull + /// The third point of the B-spline hull + /// The fourth point of the B-spline hull + public UBSCubic4D( Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3 ) : this(new Vector4Matrix4x1(p0, p1, p2, p3)){} + /// Creates a uniform 4D Cubic b-spline segment, from 4 control points + /// The matrix containing the control points of this spline + public UBSCubic4D( Vector4Matrix4x1 pointMatrix ) => (this.pointMatrix,curve,validCoefficients) = (pointMatrix,default,false); + + public Polynomial4D Curve { + get { + if( validCoefficients ) + return curve; // no need to update + validCoefficients = true; + return curve = new Polynomial4D( + (1/6f)*P0+(2/3f)*P1+(1/6f)*P2, + (-P0+P2)/2, + (1/2f)*P0-P1+(1/2f)*P2, + -(1/6f)*P0+(1/2f)*P1-(1/2f)*P2+(1/6f)*P3 + ); + } + } + public Vector4Matrix4x1 PointMatrix {[MethodImpl( INLINE )] get => pointMatrix; [MethodImpl( INLINE )] set => _ = ( pointMatrix = value, validCoefficients = false ); } + /// The first point of the B-spline hull + public Vector4 P0{ [MethodImpl( INLINE )] get => pointMatrix.m0; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m0 = value, validCoefficients = false ); } + /// The second point of the B-spline hull + public Vector4 P1{ [MethodImpl( INLINE )] get => pointMatrix.m1; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m1 = value, validCoefficients = false ); } + /// The third point of the B-spline hull + public Vector4 P2{ [MethodImpl( INLINE )] get => pointMatrix.m2; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m2 = value, validCoefficients = false ); } + /// The fourth point of the B-spline hull + public Vector4 P3{ [MethodImpl( INLINE )] get => pointMatrix.m3; [MethodImpl( INLINE )] set => _ = ( pointMatrix.m3 = value, validCoefficients = false ); } + /// Get or set a control point position by index. Valid indices from 0 to 3 + public Vector4 this[ int i ] { + get => i switch { 0 => P0, 1 => P1, 2 => P2, 3 => P3, _ => throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ) }; + set { switch( i ){ case 0: P0 = value; break; case 1: P1 = value; break; case 2: P2 = value; break; case 3: P3 = value; break; default: throw new ArgumentOutOfRangeException( nameof(i), $"Index has to be in the 0 to 3 range, and I think {i} is outside that range you know" ); }} + } + public static bool operator ==( UBSCubic4D a, UBSCubic4D b ) => a.pointMatrix == b.pointMatrix; + public static bool operator !=( UBSCubic4D a, UBSCubic4D b ) => !( a == b ); + public bool Equals( UBSCubic4D other ) => P0.Equals( other.P0 ) && P1.Equals( other.P1 ) && P2.Equals( other.P2 ) && P3.Equals( other.P3 ); + public override bool Equals( object obj ) => obj is UBSCubic4D other && pointMatrix.Equals( other.pointMatrix ); + public override int GetHashCode() => pointMatrix.GetHashCode(); + public override string ToString() => $"({pointMatrix.m0}, {pointMatrix.m1}, {pointMatrix.m2}, {pointMatrix.m3})"; + + public static explicit operator BezierCubic4D( UBSCubic4D s ) => + new BezierCubic4D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (2/3f)*s.P1+(1/3f)*s.P2, + (1/3f)*s.P1+(2/3f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3 + ); + public static explicit operator HermiteCubic4D( UBSCubic4D s ) => + new HermiteCubic4D( + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (-s.P0+s.P2)/2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (-s.P1+s.P3)/2 + ); + public static explicit operator CatRomCubic4D( UBSCubic4D s ) => + new CatRomCubic4D( + s.P0+(1/6f)*s.P1-(1/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0+(2/3f)*s.P1+(1/6f)*s.P2, + (1/6f)*s.P1+(2/3f)*s.P2+(1/6f)*s.P3, + (1/6f)*s.P0-(1/3f)*s.P1+(1/6f)*s.P2+s.P3 + ); + /// Returns a linear blend between two b-spline curves + /// The first spline segment + /// The second spline segment + /// A value from 0 to 1 to blend between a and b + public static UBSCubic4D Lerp( UBSCubic4D a, UBSCubic4D b, float t ) => + new( + Vector4.LerpUnclamped( a.P0, b.P0, t ), + Vector4.LerpUnclamped( a.P1, b.P1, t ), + Vector4.LerpUnclamped( a.P2, b.P2, t ), + Vector4.LerpUnclamped( a.P3, b.P3, t ) + ); + } +} diff --git a/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs.meta b/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs.meta new file mode 100644 index 0000000..d07d94c --- /dev/null +++ b/Runtime/Splines/Uniform Spline Segments/UBSCubic4D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8323b54d3931942118674a6807c3aa40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Splines/UniformCurveSampler.cs b/Runtime/Splines/UniformCurveSampler.cs new file mode 100644 index 0000000..71ca064 --- /dev/null +++ b/Runtime/Splines/UniformCurveSampler.cs @@ -0,0 +1,134 @@ +// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs) + +using UnityEngine; + +namespace Freya { + + /// A helper class to let you sample a curve by uniform parameter values, t-values or by distance + public class UniformCurveSampler { + + /// The number of distance samples used when calculating the cumulative distance table + public readonly int resolution; + + /// Cumulative distance samples, where the first element is 0 and the last element is the total length of the curve + public readonly float[] cumulativeDistances; + + /// Returns the approximate length of this curve. This property doesn't have to recalculate length, since it's already stored in the cumulative distances array + public float CurveIntervalLength => cumulativeDistances[resolution - 1]; + + FloatRange paramInterval; + + /// The t-value range in which we've calculated the uniform sampler for + public FloatRange ParamInterval => paramInterval; + + // The way the point recalculation works right now is pretty naive, and doesn't handle extreme acceleration very well. + // Right now you need around 30 samples to properly sample, which, is a lot + + #region Constructors & Point recalc + + /// Creates a sampler that can be used to sample a curve by distance or by uniform t-values. + /// You'll need to call sampler.Recalculate(curve) to recalculate if the curve changes shape after this points. + /// Recommended resolution for animation: [8-16] + /// Recommended resolution for even point spacing: [16-50] + /// The curve to use when sampling + /// The interval you want to uniformly sample within + /// The accuracy of this sampler. + /// Higher values are more accurate, but are more costly to calculate for every new curve shape + public UniformCurveSampler( Polynomial2D curve, FloatRange interval, int resolution = 12 ) { + this.resolution = resolution; + cumulativeDistances = new float[resolution]; + Recalculate( curve, interval ); + } + + /// + public UniformCurveSampler( Polynomial2D curve, int resolution = 12 ) : this( curve, FloatRange.unit, resolution ) { + } + + /// + public UniformCurveSampler( Polynomial3D curve, FloatRange interval, int resolution = 12 ) { + this.resolution = resolution; + cumulativeDistances = new float[resolution]; + Recalculate( curve, interval ); + } + + /// + public UniformCurveSampler( Polynomial3D curve, int resolution = 12 ) : this( curve, FloatRange.unit, resolution ) { + } + + /// Recalculates the internal lookup table so that the curve can be sampled by distance or by uniform t-values. + /// Only call this before sampling a different curve, or if the curve has changed shape since last time it was calculated + /// The curve to use when sampling + /// The interval you want to uniformly sample within + public void Recalculate( Polynomial2D curve, FloatRange interval ) { + this.paramInterval = interval; + float cumulativeLength = 0; + Vector2 prevPt = curve.Eval( paramInterval.a ); + cumulativeDistances[0] = 0; + for( int i = 1; i < resolution; i++ ) { + Vector2 pt = curve.Eval( paramInterval.Lerp( i / ( resolution - 1f ) ) ); + cumulativeLength += Vector2.Distance( prevPt, pt ); + cumulativeDistances[i] = cumulativeLength; + prevPt = pt; + } + } + + /// + public void Recalculate( Polynomial2D curve ) => Recalculate( curve, FloatRange.unit ); + + /// + public void Recalculate( Polynomial3D curve, FloatRange interval ) { + this.paramInterval = interval; + float cumulativeLength = 0; + Vector3 prevPt = curve.Eval( paramInterval.a ); + cumulativeDistances[0] = 0; + for( int i = 1; i < resolution; i++ ) { + Vector3 pt = curve.Eval( paramInterval.Lerp( i / ( resolution - 1f ) ) ); + cumulativeLength += Vector3.Distance( prevPt, pt ); + cumulativeDistances[i] = cumulativeLength; + prevPt = pt; + } + } + + /// + public void Recalculate( Polynomial3D curve ) => Recalculate( curve, FloatRange.unit ); + + #endregion + + float GetParamAtSegmentTValue( float t ) => paramInterval.InverseLerp( t ); + + /// Converts a t-value along the segment to a raw parameter value. Useful to uniformly sample a curve + /// A value from 0 to 1 representing uniform position along the curve interval + public float UniformTToParam( float t ) => DistanceToParam( t * CurveIntervalLength ); + + /// Converts a uniform parameter value to a raw parameter value. Useful to uniformly sample a curve + /// The input parameter for uniform position along the curve + public float UniformParamToParam( float u ) => DistanceToParam( paramInterval.InverseLerp( u ) * CurveIntervalLength ); + + /// Converts a distance value (relative to the start of the interval) to a parameter value. Useful to sample a curve by distance + /// The distance along the curve segment parameter interval at which you'd like to get the parameter value for + public float DistanceToParam( float distance ) { + // check if the value is within the length of the curve + if( distance.Between( 0, CurveIntervalLength ) ) { + // find which two distance values our input distance lies between + for( int i = 0; i < resolution - 1; i++ ) { + float distPrev = cumulativeDistances[i]; + float distNext = cumulativeDistances[i + 1]; + if( distance.Within( distPrev, distNext ) ) { // check if our input distance lies between the two distances + // get t-values at the samples + float tPrev = i / ( resolution - 1f ); + float tNext = ( i + 1 ) / ( resolution - 1f ); + // remap the distance range to the t-value range + float tLocal = distance.Remap( distPrev, distNext, tPrev, tNext ); + return paramInterval.Lerp( tLocal ); + } + } + } + + // distance is outside the length of the curve - extrapolate values outside + return paramInterval.Lerp( distance / CurveIntervalLength ); + } + + + } + +} \ No newline at end of file diff --git a/Runtime/Splines/UniformCurveSampler.cs.meta b/Runtime/Splines/UniformCurveSampler.cs.meta new file mode 100644 index 0000000..ce04353 --- /dev/null +++ b/Runtime/Splines/UniformCurveSampler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 776ce704c8cb64df1b019a96a3121d6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UtilityTypes.cs b/Runtime/UtilityTypes.cs similarity index 52% rename from UtilityTypes.cs rename to Runtime/UtilityTypes.cs index 763ecc8..be582bc 100644 --- a/UtilityTypes.cs +++ b/Runtime/UtilityTypes.cs @@ -5,9 +5,133 @@ using System; using System.Collections; using System.Collections.Generic; +using UnityEngine; namespace Freya { + /// Contains either 0 to 4 valid return values + public readonly struct ResultsMax4 : IEnumerable where T : struct { + + /// The number of valid values + public readonly int count; + + /// The first value. This may or may not be set/defined - use .count to see how many are valid + public readonly T a; + + /// The second value. This may or may not be set/defined - use .count to see how many are valid + public readonly T b; + + /// The third value. This may or may not be set/defined - use .count to see how many are valid + public readonly T c; + + /// The third value. This may or may not be set/defined - use .count to see how many are valid + public readonly T d; + + public ResultsMax4( T a, T b, T c, T d ) => ( this.a, this.b, this.c, this.d, this.count ) = ( a, b, c, d, 4 ); + public ResultsMax4( T a, T b, T c ) => ( this.a, this.b, this.c, this.d, this.count ) = ( a, b, c, default, 3 ); + public ResultsMax4( T a, T b ) => ( this.a, this.b, this.c, this.d, this.count ) = ( a, b, default, default, 2 ); + public ResultsMax4( T a ) => ( this.a, this.b, this.c, this.d, this.count ) = ( a, default, default, default, 1 ); + + /// Returns the valid values at index i. Will throw an index out of range exception for invalid values. Use toghether with .count to ensure you don't get invalid values + /// The index of the result to get + public T this[ int i ] => i switch { 0 => a, 1 => b, 2 => c, 3 => d, _ => throw new IndexOutOfRangeException() }; + + /// Returns a version of these results with one more element added to it. Note: this does not mutate the original struct + /// The value to add + public ResultsMax4 Add( T value ) => + count switch { + 0 => new ResultsMax4( value ), + 1 => new ResultsMax4( a, value ), + 2 => new ResultsMax4( a, b, value ), + 3 => new ResultsMax4( a, b, c, value ), + _ => throw new IndexOutOfRangeException( "Can't add more than four values to ResultsMax4" ) + }; + + + /// Implicitly casts from a tuple with nullables to a results structure + /// The tuple to cast + public static implicit operator ResultsMax4( (T?, T?, T?, T?) tuple ) { + ResultsMax4 results = new ResultsMax4(); + ( T? a, T? b, T? c, T? d ) = tuple; + if( a.HasValue ) results = results.Add( a.Value ); + if( b.HasValue ) results = results.Add( b.Value ); + if( c.HasValue ) results = results.Add( c.Value ); + if( d.HasValue ) results = results.Add( d.Value ); + return results; + } + + /// Implicitly casts a value to a results structure + /// The value to cast + public static implicit operator ResultsMax4( T v ) => new ResultsMax4( v ); + + /// Implicitly casts ResultsMax2 to ResultsMax4 + /// The results to cast + public static implicit operator ResultsMax4( ResultsMax2 m2 ) => + m2.count switch { + 0 => default, + 1 => new ResultsMax4( m2.a ), + 2 => new ResultsMax4( m2.a, m2.b ), + _ => throw new InvalidCastException( "Failed to cast ResultsMax2 to ResultsMax4" ) + }; + + /// Implicitly casts ResultsMax2 to ResultsMax4 + /// The results to cast + public static implicit operator ResultsMax4( ResultsMax3 m3 ) => + m3.count switch { + 0 => default, + 1 => new ResultsMax4( m3.a ), + 2 => new ResultsMax4( m3.a, m3.b ), + 3 => new ResultsMax4( m3.a, m3.b, m3.b ), + _ => throw new InvalidCastException( "Failed to cast ResultsMax2 to ResultsMax4" ) + }; + + /// Explicitly casts ResultsMax4 to ResultsMax3 + /// The results to cast + public static explicit operator ResultsMax3( ResultsMax4 m4 ) => + m4.count switch { + 0 => default, + 1 => new ResultsMax3( m4.a ), + 2 => new ResultsMax3( m4.a, m4.b ), + 3 => new ResultsMax3( m4.a, m4.b, m4.c ), + _ => throw new IndexOutOfRangeException( $"Attempt to cast ResultsMax4 to ResultsMax2 when it had {m4.count} results" ), + }; + + /// Explicitly casts ResultsMax4 to ResultsMax3 + /// The results to cast + public static explicit operator ResultsMax2( ResultsMax4 m4 ) => + m4.count switch { + 0 => default, + 1 => new ResultsMax2( m4.a ), + 2 => new ResultsMax2( m4.a, m4.b ), + _ => throw new IndexOutOfRangeException( $"Attempt to cast ResultsMax4 to ResultsMax2 when it had {m4.count} results" ), + }; + + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct ResultsMax4Enumerator : IEnumerator { + + int currentIndex; + readonly ResultsMax4 value; + + public ResultsMax4Enumerator( ResultsMax4 value ) { + this.value = value; + currentIndex = -1; + } + + public bool MoveNext() => ++currentIndex < value.count; + public void Reset() => currentIndex = -1; + public T Current => value[currentIndex]; + object IEnumerator.Current => Current; + public void Dispose() => _ = 0; + } + + public ResultsMax4Enumerator GetEnumerator() => new ResultsMax4Enumerator( this ); + + } + /// Contains either 0, 1, 2 or 3 valid return values public readonly struct ResultsMax3 : IEnumerable where T : struct { @@ -110,6 +234,19 @@ public static implicit operator ResultsMax3( ResultsMax2 m2 ) { throw new InvalidCastException( "Failed to cast ResultsMax2 to ResultsMax3" ); } + /// Explicitly casts ResultsMax3 to ResultsMax2 + /// The results to cast + public static explicit operator ResultsMax2( ResultsMax3 m3 ) { + switch( m3.count ) { + case 0: return default; + case 1: return new ResultsMax2( m3.a ); + case 2: return new ResultsMax2( m3.a, m3.b ); + case 3: throw new IndexOutOfRangeException( "Attempt to cast ResultsMax3 to ResultsMax2 when it had 3 results" ); + } + + throw new InvalidCastException( "Failed to cast ResultsMax2 to ResultsMax3" ); + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -183,6 +320,8 @@ public ResultsMax2 Add( T value ) { } } + public static implicit operator ResultsMax2( T v ) => new ResultsMax2( v ); + public static implicit operator ResultsMax2( T? v ) => v.HasValue ? new ResultsMax2( v.Value ) : default; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/Runtime/UtilityTypes.cs.meta b/Runtime/UtilityTypes.cs.meta new file mode 100644 index 0000000..7557008 --- /dev/null +++ b/Runtime/UtilityTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 737098dc7b1184e1f91a1d3b18362028 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..5e5a1dc --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "com.acegikmo.mathfs", + "version": "1.0.0", + "displayName": "Mathfs", + "description": "Advanced math functionality for Unity", + "unity": "6000.0", + "documentationUrl": "https://github.com/FreyaHolmer/Mathfs", + "licensesUrl": "https://github.com/FreyaHolmer/Mathfs/LICENSE.txt", + "author": { + "name": "Freya Holmér", + "email": "acegikmo@gmail.com", + "url": "https://acegikmo.com/" + } +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..9ba3ca1 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2b5953667ba52bc47bb20558274637f2 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: