using System.Collections.Generic;
using Unity.Collections;
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;
///
/// 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 nativeLUT = new NativeArray(lookupTable, Allocator.Temp);
CalculateCurveLengths(curve, nativeLUT);
nativeLUT.CopyTo(lookupTable);
}
///
/// 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 native array to populate with distance to interpolation ratio data.
public static void CalculateCurveLengths(BezierCurve curve, NativeArray 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;
}
}
const float k_Epsilon = 0.0001f;
///
/// Mathf.Approximately is not working when using BurstCompile, causing NaN values in the EvaluateUpVector
/// method when tangents have a 0 length. Using this method instead fixes that.
///
static bool Approximately(float a, float b)
{
// Reusing Mathf.Approximately code
return math.abs(b - a) < math.max(0.000001f * math.max(math.abs(a), math.abs(b)), k_Epsilon * 8);
}
///
/// 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 void EvaluateUpVectors(BezierCurve curve, float3 startUp, float3 endUp, NativeArray upVectors)
{
upVectors[0] = startUp;
upVectors[upVectors.Length - 1] = endUp;
for(int i = 1; i < upVectors.Length - 1; i++)
{
var curveT = i / (float)(upVectors.Length - 1);
upVectors[i] = EvaluateUpVector(curve, curveT, upVectors[0], endUp);
}
}
internal static float3 EvaluateUpVector(BezierCurve curve, float t, float3 startUp, float3 endUp,
bool fixEndUpMismatch = true)
{
// Ensure we have workable tangents by linearizing ones that are of zero length
var linearTangentLen = math.length(SplineUtility.GetExplicitLinearTangent(curve.P0, curve.P3));
var linearTangentOut = math.normalize(curve.P3 - curve.P0) * linearTangentLen;
if (Approximately(math.length(curve.P1 - curve.P0), 0f))
curve.P1 = curve.P0 + linearTangentOut;
if (Approximately(math.length(curve.P2 - curve.P3), 0f))
curve.P2 = curve.P3 - linearTangentOut;
var normalBuffer = new NativeArray(k_NormalsPerCurve, Allocator.Temp);
// 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));
// SPLB-185 : If the tangent and normal are parallel, we can't construct a valid frame
// rather than returning a value based on startUp and endUp, we return a zero vector
// to indicate that this is not a valid up vector.
if(float.IsNaN(frame.binormal.x))
return float3.zero;
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);
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 (!fixEndUpMismatch)
return upVector;
if (prevT <= t && currentT >= t)
upVector = endUp;
var lastFrameNormal = 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 < normalBuffer.Length; i++)
{
var normal = normalBuffer[i];
var adjustmentAngle = math.lerp(0f, angleBetweenNormals, currentT);
var tangent = math.normalize(EvaluateTangent(curve, currentT));
var adjustedNormal = math.rotate(quaternion.AxisAngle(tangent, -adjustmentAngle), normal);
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(normalBuffer[i - 1], 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 = EvaluatePosition(curve, nextRMFrameT);
nextRMFrame.tangent = 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;
}
static readonly DistanceToInterpolation[] k_DistanceLUT = new DistanceToInterpolation[24];
///
/// Gets the normalized interpolation, (t), that corresponds to a distance on a .
///
///
/// It is inefficient to call this method frequently. For better performance create a
/// cache with and use the
/// overload of this method which accepts a lookup table.
///
/// The to calculate the distance to interpolation ratio for.
/// The curve-relative distance to convert to an interpolation ratio (also referred to as 't').
/// Returns the normalized interpolation ratio associated to distance on the designated curve.
public static float GetDistanceToInterpolation(BezierCurve curve, float distance)
{
CalculateCurveLengths(curve, k_DistanceLUT);
return GetDistanceToInterpolation(k_DistanceLUT, distance);
}
///
/// 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;
}
///
/// Gets the point on a nearest to a ray.
///
/// The to compare.
/// The input ray.
/// The number of line segments on this curve that are rasterized when testing
/// for the nearest point. A higher value is more accurate, but slower to calculate.
/// Returns the nearest position on the curve to a ray.
public static float3 GetNearestPoint(BezierCurve curve, Ray ray, int resolution = 16)
{
GetNearestPoint(curve, ray, out var position, out _, resolution);
return position;
}
///
/// Gets the point on a nearest to a ray.
///
/// The to compare.
/// The input ray.
/// The nearest position on the curve to a ray.
/// The ratio from range 0 to 1 along the curve at which the nearest point is located.
/// The number of line segments that this curve will be rasterized to when testing
/// for nearest point. A higher value will be more accurate, but slower to calculate.
/// The distance from ray to nearest point on a .
public static float GetNearestPoint(BezierCurve curve, Ray ray, out float3 position, out float interpolation, int resolution = 16)
{
float bestDistSqr = float.PositiveInfinity;
float bestLineParam = 0f;
interpolation = 0f;
position = float3.zero;
float3 a = EvaluatePosition(curve, 0f);
float3 ro = ray.origin, rd = ray.direction;
for (int i = 1; i < resolution; ++i)
{
float t = i / (resolution - 1f);
float3 b = EvaluatePosition(curve, t);
var (rayPoint, linePoint) = SplineMath.RayLineNearestPoint(ro, rd, a, b, out _, out var lineParam);
var distSqr = math.lengthsq(linePoint - rayPoint);
if (distSqr < bestDistSqr)
{
position = linePoint;
bestDistSqr = distSqr;
bestLineParam = lineParam;
interpolation = (i - 1) / (resolution - 1f);
}
a = b;
}
interpolation += bestLineParam * (1f / (resolution - 1f));
return math.sqrt(bestDistSqr);
}
}
}