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;
}
}
}