using System.Collections.Generic;
using Unity.Mathematics;
namespace UnityEngine.Splines
{
///
/// A collection of methods for extracting information about types.
///
public static class CurveUtility
{
struct FrenetFrame
{
public float3 origin;
public float3 tangent;
public float3 normal;
public float3 binormal;
}
const int k_NormalsPerCurve = 16;
static readonly float3[] s_NormalBuffer = new float3[k_NormalsPerCurve];
///
/// Given a bezier curve, return an interpolated position at ratio t.
///
/// A cubic bezier curve.
/// A value between 0 and 1 representing the ratio along the curve.
/// A position on the curve.
public static float3 EvaluatePosition(BezierCurve curve, float t)
{
t = math.clamp(t, 0, 1);
var t2 = t * t;
var t3 = t2 * t;
var position =
curve.P0 * ( -1f * t3 + 3f * t2 - 3f * t + 1f ) +
curve.P1 * ( 3f * t3 - 6f * t2 + 3f * t) +
curve.P2 * ( -3f * t3 + 3f * t2) +
curve.P3 * ( t3 );
return position;
}
///
/// Given a bezier curve, return an interpolated tangent at ratio t.
///
/// A cubic bezier curve.
/// A value between 0 and 1 representing the ratio along the curve.
/// A tangent on the curve.
public static float3 EvaluateTangent(BezierCurve curve, float t)
{
t = math.clamp(t, 0, 1);
float t2 = t * t;
var tangent =
curve.P0 * ( -3f * t2 + 6f * t - 3f ) +
curve.P1 * ( 9f * t2 - 12f * t + 3f) +
curve.P2 * ( -9f * t2 + 6f * t ) +
curve.P3 * ( 3f * t2 );
return tangent;
}
///
/// Given a bezier curve, return an interpolated acceleration at ratio t.
///
/// A cubic bezier curve.
/// A value between 0 and 1 representing the ratio along the curve.
/// An acceleration vector on the curve.
public static float3 EvaluateAcceleration(BezierCurve curve, float t)
{
t = math.clamp(t, 0, 1);
var acceleration =
curve.P0 * ( -6f * t + 6f ) +
curve.P1 * ( 18f * t - 12f) +
curve.P2 * (-18f * t + 6f ) +
curve.P3 * ( 6f * t );
return acceleration;
}
///
/// Given a bezier curve, return an interpolated curvature at ratio t.
///
/// A cubic bezier curve.
/// A value between 0 and 1 representing the ratio along the curve.
/// A curvature value on the curve.
public static float EvaluateCurvature(BezierCurve curve, float t)
{
t = math.clamp(t, 0, 1);
var firstDerivative = EvaluateTangent(curve, t);
var secondDerivative = EvaluateAcceleration(curve, t);
var firstDerivativeNormSq = math.lengthsq(firstDerivative);
var secondDerivativeNormSq = math.lengthsq(secondDerivative);
var derivativesDot = math.dot(firstDerivative, secondDerivative);
var kappa = math.sqrt(
( firstDerivativeNormSq * secondDerivativeNormSq ) - ( derivativesDot * derivativesDot ))
/ ( firstDerivativeNormSq * math.length(firstDerivative));
return kappa;
}
///
/// Given a bezier curve, return an interpolated position at ratio t.
///
/// A cubic bezier curve.
/// A value between 0 and 1 representing the ratio along the curve.
/// A position on the curve.
static float3 DeCasteljau(BezierCurve curve, float t)
{
float3 p0 = curve.P0, p1 = curve.P1;
float3 p2 = curve.P2, p3 = curve.P3;
float3 a0 = math.lerp(p0, p1, t);
float3 a1 = math.lerp(p1, p2, t);
float3 a2 = math.lerp(p2, p3, t);
float3 b0 = math.lerp(a0, a1, t);
float3 b1 = math.lerp(a1, a2, t);
return math.lerp(b0, b1, t);
}
///
/// Decompose a curve into two smaller curves matching the source curve.
///
/// The source curve.
/// A mid-point on the source curve defining where the two smaller curves control points meet.
/// A curve from the source curve first control point to the mid-point, matching the curvature of the source curve.
/// A curve from the mid-point to the source curve fourth control point, matching the curvature of the source curve.
public static void Split(BezierCurve curve, float t, out BezierCurve left, out BezierCurve right)
{
t = math.clamp(t, 0f, 1f);
// subdivide control points, first iteration
float3 split0 = math.lerp(curve.P0, curve.P1, t);
float3 split1 = math.lerp(curve.P1, curve.P2, t);
float3 split2 = math.lerp(curve.P2, curve.P3, t);
// subdivide control points, second iteration
float3 split3 = math.lerp(split0, split1, t);
float3 split4 = math.lerp(split1, split2, t);
// subdivide control points, third iteration
float3 split5 = math.lerp(split3, split4, t);
left = new BezierCurve(curve.P0, split0, split3, split5);
right = new BezierCurve(split5, split4, split2, curve.P3);
}
///
/// Calculate the length of a by unrolling the curve into linear segments and summing
/// the lengths of the lines. This is equivalent to accessing .
///
/// The to calculate length.
/// The number of linear segments used to calculate the curve length.
/// The sum length of a collection of linear segments fitting this curve.
///
public static float CalculateLength(BezierCurve curve, int resolution = 30)
{
float magnitude = 0f;
float3 prev = EvaluatePosition(curve, 0f);
for (int i = 1; i < resolution; i++)
{
var point = EvaluatePosition(curve, i / (resolution - 1f));
var dir = point - prev;
magnitude += math.length(dir);
prev = point;
}
return magnitude;
}
///
/// Populate a pre-allocated lookupTable array with distance to 't' values. The number of table entries is
/// dependent on the size of the passed lookupTable.
///
/// The to create a distance to 't' lookup table for.
/// A pre-allocated array to populate with distance to interpolation ratio data.
public static void CalculateCurveLengths(BezierCurve curve, DistanceToInterpolation[] lookupTable)
{
var resolution = lookupTable.Length;
float magnitude = 0f;
float3 prev = EvaluatePosition(curve, 0f);
lookupTable[0] = new DistanceToInterpolation() { Distance = 0f , T = 0f };
for (int i = 1; i < resolution; i++)
{
var t = i / ( resolution - 1f );
var point = EvaluatePosition(curve, t);
var dir = point - prev;
magnitude += math.length(dir);
lookupTable[i] = new DistanceToInterpolation() { Distance = magnitude , T = t};
prev = point;
}
}
///
/// Calculate the approximate length of a . This is less accurate than
/// , but can be significantly faster. Use this when accuracy is
/// not paramount and the curve control points are changing frequently.
///
/// The to calculate length.
/// An estimate of the length of a curve.
public static float ApproximateLength(BezierCurve curve)
{
float chord = math.length(curve.P3 - curve.P0);
float net = math.length(curve.P0 - curve.P1) + math.length(curve.P2 - curve.P1) + math.length(curve.P3 - curve.P2);
return (net + chord) / 2;
}
internal static float3 EvaluateUpVector(BezierCurve curve, float t, float3 startUp, float3 endUp)
{
// Ensure we have workable tangents by linearizing ones that are of zero length
var linearTangentLen = math.length(SplineUtility.GetLinearTangent(curve.P0, curve.P3));
var linearTangentOut = math.normalize(curve.P3 - curve.P0) * linearTangentLen;
if (Mathf.Approximately(math.length(curve.P0 - curve.P1), 0f))
curve.P1 = curve.P0 + linearTangentOut;
if (Mathf.Approximately(math.length(curve.P2 - curve.P3), 0f))
curve.P2 = curve.P3 - linearTangentOut;
// Construct initial frenet frame
FrenetFrame frame;
frame.origin = curve.P0;
frame.tangent = curve.P1 - curve.P0;
frame.normal = startUp;
frame.binormal = math.normalize(math.cross(frame.tangent, frame.normal));
s_NormalBuffer[0] = frame.normal;
// Continue building remaining rotation minimizing frames
var stepSize = 1f / (k_NormalsPerCurve - 1);
var currentT = stepSize;
var prevT = 0f;
var upVector = float3.zero;
FrenetFrame prevFrame;
for (int i = 1; i < k_NormalsPerCurve; ++i)
{
prevFrame = frame;
frame = GetNextRotationMinimizingFrame(curve, prevFrame, currentT);
s_NormalBuffer[i] = frame.normal;
if (prevT <= t && currentT >= t)
{
var lerpT = (t - prevT) / stepSize;
upVector = Vector3.Slerp(prevFrame.normal, frame.normal, lerpT);
}
prevT = currentT;
currentT += stepSize;
}
if (prevT <= t && currentT >= t)
upVector = endUp;
var lastFrameNormal = s_NormalBuffer[k_NormalsPerCurve - 1];
var angleBetweenNormals = math.acos(math.clamp(math.dot(lastFrameNormal, endUp), -1f, 1f));
if (angleBetweenNormals == 0f)
return upVector;
// Since there's an angle difference between the end knot's normal and the last evaluated frenet frame's normal,
// the remaining code gradually applies the angle delta across the evaluated frames' normals.
var lastNormalTangent = math.normalize(frame.tangent);
var positiveRotation = quaternion.AxisAngle(lastNormalTangent, angleBetweenNormals);
var negativeRotation = quaternion.AxisAngle(lastNormalTangent, -angleBetweenNormals);
var positiveRotationResult = math.acos(math.clamp(math.dot(math.rotate(positiveRotation, endUp), lastFrameNormal), -1f, 1f));
var negativeRotationResult = math.acos(math.clamp(math.dot(math.rotate(negativeRotation, endUp), lastFrameNormal), -1f, 1f));
if (positiveRotationResult > negativeRotationResult)
angleBetweenNormals *= -1f;
currentT = stepSize;
prevT = 0f;
for (int i = 1; i < s_NormalBuffer.Length; i++)
{
var normal = s_NormalBuffer[i];
var adjustmentAngle = math.lerp(0f, angleBetweenNormals, currentT);
var tangent = math.normalize(CurveUtility.EvaluateTangent(curve, currentT));
var adjustedNormal = math.rotate(quaternion.AxisAngle(tangent, -adjustmentAngle), normal);
s_NormalBuffer[i] = adjustedNormal;
// Early exit if we've already adjusted the normals at offsets that curveT is in between
if (prevT <= t && currentT >= t)
{
var lerpT = (t - prevT) / stepSize;
upVector = Vector3.Slerp(s_NormalBuffer[i - 1], s_NormalBuffer[i], lerpT);
return upVector;
}
prevT = currentT;
currentT += stepSize;
}
return endUp;
}
static FrenetFrame GetNextRotationMinimizingFrame(BezierCurve curve, FrenetFrame previousRMFrame, float nextRMFrameT)
{
FrenetFrame nextRMFrame;
// Evaluate position and tangent for next RM frame
nextRMFrame.origin = CurveUtility.EvaluatePosition(curve, nextRMFrameT);
nextRMFrame.tangent = CurveUtility.EvaluateTangent(curve, nextRMFrameT);
// Mirror the rotational axis and tangent
float3 toCurrentFrame = nextRMFrame.origin - previousRMFrame.origin;
float c1 = math.dot(toCurrentFrame, toCurrentFrame);
float3 riL = previousRMFrame.binormal - toCurrentFrame * 2f / c1 * math.dot(toCurrentFrame, previousRMFrame.binormal);
float3 tiL = previousRMFrame.tangent - toCurrentFrame * 2f / c1 * math.dot(toCurrentFrame, previousRMFrame.tangent);
// Compute a more stable binormal
float3 v2 = nextRMFrame.tangent - tiL;
float c2 = math.dot(v2, v2);
// Fix binormal's axis
nextRMFrame.binormal = math.normalize(riL - v2 * 2f / c2 * math.dot(v2, riL));
nextRMFrame.normal = math.normalize(math.cross(nextRMFrame.binormal, nextRMFrame.tangent));
return nextRMFrame;
}
///
/// Return the normalized interpolation (t) corresponding to a distance on a . This
/// method accepts a look-up table (referred to in code with acronym "LUT") that may be constructed using
/// . The built-in Spline class implementations ( and
/// ) cache these look-up tables internally.
///
/// The collection type.
/// A look-up table of distance to 't' values. See for creating
/// this collection.
/// The curve-relative distance to convert to an interpolation ratio (also referred to as 't').
/// The normalized interpolation ratio associated to distance on the designated curve.
public static float GetDistanceToInterpolation(T lut, float distance) where T : IReadOnlyList
{
if(lut == null || lut.Count < 1 || distance <= 0)
return 0f;
var resolution = lut.Count;
var curveLength = lut[resolution-1].Distance;
if(distance >= curveLength)
return 1f;
var prev = lut[0];
for(int i = 1; i < resolution; i++)
{
var current = lut[i];
if(distance < current.Distance)
return math.lerp(prev.T, current.T, (distance - prev.Distance) / (current.Distance - prev.Distance));
prev = current;
}
return 1f;
}
}
}