using System; using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Splines { /// /// A collection of methods for extracting information about types. /// public static class SplineUtility { const int k_ResolutionSegmentCountMin = 6; const int k_ResolutionSegmentCountMax = 1024; /// /// The minimum resolution allowable when unrolling a curve to hit test while picking (selecting a spline with a cursor). /// /// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the /// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility /// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result /// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use /// higher resolution where a fine degree of accuracy is necessary and performance is less important. /// public const int PickResolutionMin = 2; /// /// The default resolution used when unrolling a curve to hit test while picking (selecting a spline with a cursor). /// /// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the /// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility /// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result /// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use /// higher resolution where a fine degree of accuracy is necessary and performance is less important. /// public const int PickResolutionDefault = 4; /// /// The maximum resolution allowed when unrolling a curve to hit test while picking (selecting a spline with a cursor). /// /// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the /// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility /// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result /// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use /// higher resolution where a fine degree of accuracy is necessary and performance is less important. /// public const int PickResolutionMax = 64; /// /// The default resolution used when unrolling a curve to draw a preview in the Scene View. /// /// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the /// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility /// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result /// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use /// higher resolution where a fine degree of accuracy is necessary and performance is less important. /// public const int DrawResolutionDefault = 10; /// /// Compute interpolated position, direction and upDirection at ratio t. Calling this method to get the /// 3 vectors is faster than calling independently EvaluatePosition, EvaluateDirection and EvaluateUpVector /// for the same time t as it reduces some redundant computation. /// /// The spline to interpolate. /// A value between 0 and 1 representing the ratio along the curve. /// Output variable for the float3 position at t. /// Output variable for the float3 tangent at t. /// Output variable for the float3 up direction at t. /// A type implementing ISpline. /// Boolean value, true if a valid set of output variables as been computed. public static bool Evaluate(this T spline, float t, out float3 position, out float3 tangent, out float3 upVector ) where T : ISpline { if(spline.Count < 1) { position = float3.zero; tangent = new float3(0, 0, 1); upVector = new float3(0, 1, 0); return false; } var curveIndex = SplineToCurveT(spline, t, out var curveT); var curve = spline.GetCurve(curveIndex); position = CurveUtility.EvaluatePosition(curve, curveT); tangent = CurveUtility.EvaluateTangent(curve, curveT); upVector = spline.EvaluateUpVector(curveIndex, curveT); return true; } /// /// Return an interpolated position at ratio t. /// /// The spline to interpolate. /// A value between 0 and 1 representing the ratio along the curve. /// A type implementing ISpline. /// A position on the spline. public static float3 EvaluatePosition(this T spline, float t) where T : ISpline { if (spline.Count < 1) return float.PositiveInfinity; var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT)); return CurveUtility.EvaluatePosition(curve, curveT); } /// /// Return an interpolated direction at ratio t. /// /// The spline to interpolate. /// A value between 0 and 1 representing the ratio along the curve. /// A type implementing ISpline. /// A direction on the spline. public static float3 EvaluateTangent(this T spline, float t) where T : ISpline { if (spline.Count < 1) return float.PositiveInfinity; var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT)); return CurveUtility.EvaluateTangent(curve, curveT); } /// /// Evaluate an up vector of a spline at a specific t /// /// The to evaluate. /// A value between 0 and 1 representing a percentage of the curve. /// A type implementing ISpline. /// An up vector public static float3 EvaluateUpVector(this T spline, float t) where T : ISpline { if (spline.Count < 1) return float3.zero; var curveIndex = SplineToCurveT(spline, t, out var curveT); return spline.EvaluateUpVector(curveIndex, curveT); } static float3 EvaluateUpVector(this T spline, int curveIndex, float curveT) where T : ISpline { if (spline.Count < 1) return float3.zero; var curve = spline.GetCurve(curveIndex); var curveStartRotation = spline[curveIndex].Rotation; var curveStartUp = math.rotate(curveStartRotation, math.up()); if (curveT == 0f) return curveStartUp; var endKnotIndex = spline.NextIndex(curveIndex); var curveEndRotation = spline[endKnotIndex].Rotation; var curveEndUp = math.rotate(curveEndRotation, math.up()); if (curveT == 1f) return curveEndUp; return CurveUtility.EvaluateUpVector(curve, curveT, curveStartUp, curveEndUp); } /// /// Return an interpolated acceleration at ratio t. /// /// The spline to interpolate. /// A type implementing ISpline. /// A value between 0 and 1 representing the ratio along the curve. /// An acceleration on the spline. public static float3 EvaluateAcceleration(this T spline, float t) where T : ISpline { if (spline.Count < 1) return float3.zero; var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT)); return CurveUtility.EvaluateAcceleration(curve, curveT); } /// /// Return an interpolated curvature at ratio t. /// /// The spline to interpolate. /// A type implementing ISpline. /// A value between 0 and 1 representing the ratio along the curve. /// A curvature on the spline. public static float EvaluateCurvature(this T spline, float t) where T : ISpline { if (spline.Count < 1) return 0f; var curveIndex = SplineToCurveT(spline, t, out var curveT); var curve = spline.GetCurve(curveIndex); return CurveUtility.EvaluateCurvature(curve, curveT); } /// /// Return the curvature center at ratio t. The curvature center represents the center of the circle /// that is tangent to the curve at t. This circle is in the plane defined by the curve velocity (tangent) /// and the curve acceleration at that point. /// /// The spline to interpolate. /// A type implementing ISpline. /// A value between 0 and 1 representing the ratio along the curve. /// A point representing the curvature center associated to the position at t on the spline. public static float3 EvaluateCurvatureCenter(this T spline, float t) where T : ISpline { if (spline.Count < 1) return 0f; var curveIndex = SplineToCurveT(spline, t, out var curveT); var curve = spline.GetCurve(curveIndex); var curvature = CurveUtility.EvaluateCurvature(curve, curveT); if(curvature != 0) { var radius = 1f / curvature; var position = CurveUtility.EvaluatePosition(curve, curveT); var velocity = CurveUtility.EvaluateTangent(curve, curveT); var acceleration = CurveUtility.EvaluateAcceleration(curve, curveT); var curvatureUp = math.normalize(math.cross(acceleration, velocity)); var curvatureRight = math.normalize(math.cross(velocity, curvatureUp)); return position + radius * curvatureRight; } return float3.zero; } internal static float3 GetContinuousTangent(float3 otherTangent, float3 tangentToAlign) { // Mirror tangent but keep the same length float3 dir = math.length(otherTangent) > 0 ? -math.normalize(otherTangent) : new float3(0,0,0); float tangentToAlignLength = math.length(tangentToAlign); return dir * tangentToAlignLength; } /// /// Given a normalized interpolation (t) for a spline, calculate the curve index and curve-relative /// normalized interpolation. /// /// The target spline. /// A normalized spline interpolation value to be converted into curve space. /// A normalized curve interpolation value. /// A type implementing ISpline. /// The curve index. public static int SplineToCurveT(this T spline, float splineT, out float curveT) where T : ISpline { return SplineToCurveT(spline, splineT, out curveT, true); } static int SplineToCurveT(this T spline, float splineT, out float curveT, bool useLUT) where T : ISpline { var knotCount = spline.Count; if(knotCount <= 1) { curveT = 0f; return 0; } splineT = math.clamp(splineT, 0, 1); var tLength = splineT * spline.GetLength(); var start = 0f; var closed = spline.Closed; for (int i = 0, c = closed ? knotCount : knotCount - 1; i < c; i++) { var index = i % knotCount; var curveLength = spline.GetCurveLength(index); if (tLength <= (start + curveLength)) { curveT = useLUT ? spline.GetCurveInterpolation(index, tLength - start) : (tLength - start)/ curveLength; return index; } start += curveLength; } curveT = 1f; return closed ? knotCount - 1 : knotCount - 2; } /// /// Given an interpolation value for a curve, calculate the relative normalized spline interpolation. /// /// The target spline. /// A curve index and normalized interpolation. The curve index is represented by the /// integer part of the float, and interpolation is the fractional part. This is the format used by /// . /// /// A type implementing ISpline. /// An interpolation value relative to normalized Spline length (0 to 1). /// public static float CurveToSplineT(this T spline, float curve) where T : ISpline { if(spline.Count <= 1 || curve < 0f) return 0f; if(curve >= ( spline.Closed ? spline.Count : spline.Count - 1 )) return 1f; var curveIndex = (int) math.floor(curve); float t = 0f; for (int i = 0; i < curveIndex; i++) t += spline.GetCurveLength(i); t += spline.GetCurveLength(curveIndex) * math.frac(curve); return t / spline.GetLength(); } /// /// Calculate the length of a spline when transformed by a matrix. /// /// /// A type implementing ISpline. /// /// public static float CalculateLength(this T spline, float4x4 transform) where T : ISpline { using var nativeSpline = new NativeSpline(spline, transform); return nativeSpline.GetLength(); } /// /// Calculate the bounding box of a Spline. /// /// The spline for which to calculate bounds. /// A type implementing ISpline. /// The bounds of a spline. public static Bounds GetBounds(this T spline) where T : ISpline { if (spline.Count < 1) return default; var knot = spline[0]; Bounds bounds = new Bounds(knot.Position, Vector3.zero); for (int i = 1, c = spline.Count; i < c; ++i) { knot = spline[i]; bounds.Encapsulate(knot.Position); } return bounds; } // Get the point on a line segment at the smallest distance to intersection static float3 RayLineSegmentNearestPoint(float3 rayOrigin, float3 rayDir, float3 s0, float3 s1, out float t) { float3 am = s1 - s0; float al = math.length(am); float3 ad = (1f / al) * am; float dot = math.dot(ad, rayDir); if (1f - math.abs(dot) < Mathf.Epsilon) { t = 0f; return s0; } float3 c = rayOrigin - s0; float rm = math.dot(rayDir, rayDir); float n = -dot * math.dot(rayDir, c) + math.dot(ad, c) * rm; float d = math.dot(ad, ad) * rm - dot * dot; float mag = math.min(al, math.max(0f, n / d)); t = mag / al; return s0 + ad * mag; } static float3 PointLineSegmentNearestPoint(float3 p, float3 a, float3 b, out float t) { float l2 = math.lengthsq(b - a); if (l2 == 0.0) { t = 0f; return a; } t = math.dot(p - a, b - a) / l2; if (t < 0.0) return a; if (t > 1.0) return b; return a + t * (b - a); } // Same as ProjectPointLine but without clamping. static float3 ProjectPointRay(float3 point, float3 ro, float3 rd) { float3 relativePoint = point - ro; float dot = math.dot(rd, relativePoint); return ro + rd * dot; } /// /// Use this function to calculate the number of segments for a given spline length and resolution. /// /// A distance value in . /// A value used to calculate the number of segments for a length. This is calculated /// as max(MIN_SEGMENTS, min(MAX_SEGMENTS, sqrt(length) * resolution)). /// /// /// The number of segments as calculated for given length and resolution. /// public static int GetSegmentCount(float length, int resolution) { return (int) math.max(k_ResolutionSegmentCountMin, math.min(k_ResolutionSegmentCountMax, math.sqrt(length) * resolution)); } struct Segment { public float start, length; public Segment(float start, float length) { this.start = start; this.length = length; } } static Segment GetNearestPoint(T spline, float3 ro, float3 rd, Segment range, out float distance, out float3 nearest, out float time, int segments) where T : ISpline { distance = float.PositiveInfinity; nearest = float.PositiveInfinity; time = float.PositiveInfinity; Segment segment = new Segment(-1f, 0f); float t0 = range.start; float3 a = EvaluatePosition(spline, t0); for (int i = 1; i < segments; i++) { float t1 = range.start + (range.length * (i / (segments - 1f))); float3 b = EvaluatePosition(spline, t1); float3 p = RayLineSegmentNearestPoint(ro, rd, a, b, out float st); float d = math.length(ProjectPointRay(p, ro, rd) - p); if (d < distance) { segment.start = t0; segment.length = t1 - t0; time = segment.start + segment.length * st; distance = d; nearest = p; } t0 = t1; a = b; } return segment; } static Segment GetNearestPoint(T spline, float3 point, Segment range, out float distance, out float3 nearest, out float time, int segments) where T : ISpline { distance = float.PositiveInfinity; nearest = float.PositiveInfinity; time = float.PositiveInfinity; Segment segment = new Segment(-1f, 0f); float t0 = range.start; float3 a = EvaluatePosition(spline, t0); float dsqr = distance; for (int i = 1; i < segments; i++) { float t1 = range.start + (range.length * (i / (segments - 1f))); float3 b = EvaluatePosition(spline, t1); float3 p = PointLineSegmentNearestPoint(point, a, b, out float st); float d = math.distancesq(p, point); if (d < dsqr) { segment.start = t0; segment.length = t1 - t0; time = segment.start + segment.length * st; dsqr = d; distance = math.sqrt(d); nearest = p; } t0 = t1; a = b; } return segment; } /// /// Calculate the point on a spline nearest to a ray. /// /// The input spline to search for nearest point. /// The input ray to search against. /// The point on a spline nearest to the input ray. The accuracy of this value is /// affected by the . /// A type implementing ISpline. /// The normalized time value to the nearest point. /// Affects how many segments to split a spline into when calculating the nearest point. /// Higher values mean smaller and more segments, which increases accuracy at the cost of processing time. /// The minimum resolution is defined by , and the maximum is defined by /// . /// In most cases, the default resolution is appropriate. Use with to fine tune /// point accuracy. /// /// /// The nearest point is calculated by finding the nearest point on the entire length /// of the spline using to divide into equally spaced line segments. Successive /// iterations will then subdivide further the nearest segment, producing more accurate results. In most cases, /// the default value is sufficient, but if extreme accuracy is required this value can be increased to a /// maximum of . /// /// The distance from ray to nearest point. public static float GetNearestPoint(T spline, Ray ray, out float3 nearest, out float t, int resolution = PickResolutionDefault, int iterations = 2) where T : ISpline { float distance = float.PositiveInfinity; nearest = float.PositiveInfinity; float3 ro = ray.origin, rd = ray.direction; Segment segment = new Segment(0f, 1f); t = 0f; int res = math.min(math.max(PickResolutionMin, resolution), PickResolutionMax); for (int i = 0, c = math.min(10, iterations); i < c; i++) { int segments = GetSegmentCount(spline.GetLength() * segment.length, res); segment = GetNearestPoint(spline, ro, rd, segment, out distance, out nearest, out t, segments); } return distance; } /// /// Calculate the point on a spline nearest to a point. /// /// The input spline to search for nearest point. /// The input point to compare. /// The point on a spline nearest to the input point. The accuracy of this value is /// affected by the . /// The normalized interpolation ratio corresponding to the nearest point. /// Affects how many segments to split a spline into when calculating the nearest point. /// Higher values mean smaller and more segments, which increases accuracy at the cost of processing time. /// The minimum resolution is defined by , and the maximum is defined by /// . /// In most cases, the default resolution is appropriate. Use with to fine tune /// point accuracy. /// /// /// The nearest point is calculated by finding the nearest point on the entire length /// of the spline using to divide into equally spaced line segments. Successive /// iterations will then subdivide further the nearest segment, producing more accurate results. In most cases, /// the default value is sufficient, but if extreme accuracy is required this value can be increased to a /// maximum of . /// /// A type implementing ISpline. /// The distance from input point to nearest point on spline. public static float GetNearestPoint(T spline, float3 point, out float3 nearest, out float t, int resolution = PickResolutionDefault, int iterations = 2) where T : ISpline { float distance = float.PositiveInfinity; nearest = float.PositiveInfinity; Segment segment = new Segment(0f, 1f); t = 0f; int res = math.min(math.max(PickResolutionMin, resolution), PickResolutionMax); for (int i = 0, c = math.min(10, iterations); i < c; i++) { int segments = GetSegmentCount(spline.GetLength() * segment.length, res); segment = GetNearestPoint(spline, point, segment, out distance, out nearest, out t, segments); } return distance; } /// /// Given a Spline and interpolation ratio, calculate the 3d point at a linear distance from point at spline.EvaluatePosition(t). /// Returns the corresponding time associated to this 3d position on the Spline. /// /// The Spline on which to compute the point. /// A type implementing ISpline. /// The Spline interpolation ratio 't' (normalized) from which the next position need to be computed. /// /// The relative distance at which the new point should be placed. A negative value will compute a point at a /// 'resultPointTime' previous to 'fromT' (backward search). /// /// The normalized interpolation ratio of the resulting point. /// The 3d point from the spline located at a linear distance from the point at t. public static float3 GetPointAtLinearDistance(this T spline, float fromT, float relativeDistance, out float resultPointT) where T : ISpline { const float epsilon = 0.001f; if(fromT <0) { resultPointT = 0f; return spline.EvaluatePosition(0f); } var length = spline.GetLength(); var lengthAtT = fromT * length; float currentLength = lengthAtT; if(currentLength + relativeDistance >= length) //relativeDistance >= 0 -> Forward search { resultPointT = 1f; return spline.EvaluatePosition(1f); } else if(currentLength + relativeDistance <= 0) //relativeDistance < 0 -> Forward search { resultPointT = 0f; return spline.EvaluatePosition(0f); } var currentPos = spline.EvaluatePosition(fromT); resultPointT = fromT; var forwardSearch = relativeDistance >= 0; var residual = math.abs(relativeDistance); float linearDistance = 0; float3 point = spline.EvaluatePosition(fromT); while(residual > epsilon && (forwardSearch ? resultPointT < 1f : resultPointT > 0)) { currentLength += forwardSearch ? residual : -residual; resultPointT = currentLength / length; if(resultPointT > 1f) //forward search { resultPointT = 1f; point = spline.EvaluatePosition(1f); }else if(resultPointT < 0f) //backward search { resultPointT = 0f; point = spline.EvaluatePosition(0f); } point = spline.EvaluatePosition(resultPointT); linearDistance = math.distance(currentPos, point); residual = math.abs(relativeDistance) - linearDistance; } return point; } /// /// Given a normalized interpolation ratio, calculate the associated interpolation value in another targetPathUnit regarding a specific spline. /// /// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits. /// Normalized interpolation ratio (0 to 1). /// The PathIndexUnit to which 't' should be converted. /// A type implementing ISpline. /// The interpolation value converted to targetPathUnit. public static float ConvertIndexUnit(this T spline, float t, PathIndexUnit targetPathUnit) where T : ISpline { if (targetPathUnit == PathIndexUnit.Normalized) return WrapInterpolation(t); return ConvertNormalizedIndexUnit(spline, t, targetPathUnit); } /// /// Given an interpolation value using a certain PathIndexUnit type, calculate the associated interpolation value in another targetPathUnit regarding a specific spline. /// /// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits. /// Interpolation in the original PathIndexUnit. /// The PathIndexUnit for the original interpolation value. /// The PathIndexUnit to which 't' should be converted. /// A type implementing ISpline. /// The interpolation value converted to targetPathUnit. public static float ConvertIndexUnit(this T spline, float t, PathIndexUnit fromPathUnit, PathIndexUnit targetPathUnit) where T : ISpline { if (fromPathUnit == targetPathUnit) { if (targetPathUnit == PathIndexUnit.Normalized) t = WrapInterpolation(t); return t; } return ConvertNormalizedIndexUnit(spline, GetNormalizedInterpolation(spline, t, fromPathUnit), targetPathUnit); } static float ConvertNormalizedIndexUnit(T spline, float t, PathIndexUnit targetPathUnit) where T : ISpline { switch(targetPathUnit) { case PathIndexUnit.Knot: //LUT SHOULD NOT be used here as PathIndexUnit.KnotIndex is linear regarding the distance //(and thus not be interpreted using the LUT and the interpolated T) int splineIndex = spline.SplineToCurveT(t, out float curveTime, false); return splineIndex + curveTime; case PathIndexUnit.Distance: return t * spline.GetLength(); default: return t; } } static float WrapInterpolation(float t) { return t % 1f == 0f ? math.clamp(t, 0f, 1f) : t - math.floor(t); } /// /// Given an interpolation value in any PathIndexUnit type, calculate the normalized interpolation ratio value /// relative to a . /// /// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits. /// The 't' value to normalize in the original PathIndexUnit. /// The PathIndexUnit from the original 't'. /// A type implementing ISpline. /// The normalized interpolation ratio (0 to 1). public static float GetNormalizedInterpolation(T spline, float t, PathIndexUnit originalPathUnit) where T : ISpline { switch(originalPathUnit) { case PathIndexUnit.Knot: return WrapInterpolation(CurveToSplineT(spline, t)); case PathIndexUnit.Distance: var length = spline.GetLength(); return WrapInterpolation(length > 0 ? t / length : 0f); default: return WrapInterpolation(t); } } internal static int PreviousIndex(this T spline, int index) where T : ISpline => PreviousIndex(index, spline.Count, spline.Closed); internal static int NextIndex(this T spline, int index) where T : ISpline => NextIndex(index, spline.Count, spline.Closed); internal static int PreviousIndex(int index, int count, bool wrap) { return wrap ? (index + (count-1)) % count : math.max(index - 1, 0); } internal static int NextIndex(int index, int count, bool wrap) { return wrap ? (index + 1) % count : math.min(index + 1, count - 1); } internal static float3 GetLinearTangent(float3 point, float3 to) { return (to - point) / 3.0f; } /// /// Reset a transform position to a position while keeping knot positions in the same place. This modifies both /// knot positions and transform position. /// /// The target spline. /// The public static void SetPivot(SplineContainer container, Vector3 position) { var transform = container.transform; var delta = position - transform.position; transform.position = position; var spline = container.Spline; for (int i = 0, c = spline.Count; i < c; i++) spline[i] = spline[i] - delta; } } }