2161 lines
103 KiB
C#
2161 lines
103 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Unity.Collections;
|
|
using Unity.Mathematics;
|
|
|
|
namespace UnityEngine.Splines
|
|
{
|
|
/// <summary>
|
|
/// A collection of methods for extracting information about <see cref="Spline"/> types.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// `SplineUtility` methods do not consider Transform values except where explicitly requested. To perform operations in world space, you can use the <see cref="SplineContainer"/> evaluate methods or build a <see cref="NativeSpline"/> with a constructor that accepts a matrix and evaluate that spline.
|
|
/// </remarks>
|
|
public static class SplineUtility
|
|
{
|
|
const int k_SubdivisionCountMin = 6;
|
|
const int k_SubdivisionCountMax = 1024;
|
|
|
|
/// <summary>
|
|
/// The default tension value used for <see cref="TangentMode.AutoSmooth"/> knots.
|
|
/// Use with <see cref="Spline.SetTangentMode(UnityEngine.Splines.TangentMode)"/> and
|
|
/// <see cref="Spline.SetAutoSmoothTension(int,float)"/> to control the curvature of the spline at control
|
|
/// points.
|
|
/// </summary>
|
|
public const float DefaultTension = 1 / 3f;
|
|
|
|
/// <summary>
|
|
/// The tension value for a Catmull-Rom type spline.
|
|
/// Use with <see cref="Spline.SetTangentMode(UnityEngine.Splines.TangentMode)"/> and
|
|
/// <see cref="Spline.SetAutoSmoothTension(int,float)"/> to control the curvature of the spline at control
|
|
/// points.
|
|
/// </summary>
|
|
//todo Deprecate in 3.0.
|
|
public const float CatmullRomTension = 1 / 2f;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public const int PickResolutionMin = 2;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public const int PickResolutionDefault = 4;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public const int PickResolutionMax = 64;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public const int DrawResolutionDefault = 10;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <param name="position">Output variable for the float3 position at t.</param>
|
|
/// <param name="tangent">Output variable for the float3 tangent at t.</param>
|
|
/// <param name="upVector">Output variable for the float3 up direction at t.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>True if successful.</returns>
|
|
public static bool Evaluate<T>(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.GetCurveUpVector(curveIndex, curveT);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the interpolated position for NURBS defined by order, controlPoints, and knotVector at ratio t.
|
|
/// </summary>
|
|
/// <param name="t">The value between knotVector[0] and knotVector[-1] that represents the ratio along the curve.</param>
|
|
/// <param name="controlPoints">The control points for the NURBS.</param>
|
|
/// <param name="knotVector">The knot vector for the NURBS. There must be at least order + controlPoints.Length - 1 knots.</param>
|
|
/// <param name="order">The order of the curve. For example, 4 for a cubic curve or 3 for quadratic.</param>
|
|
/// <param name="position">The output variable for the float3 position at t.</param>
|
|
/// <returns>True if successful.</returns>
|
|
public static bool EvaluateNurbs(
|
|
float t,
|
|
List<float3> controlPoints,
|
|
List<double> knotVector,
|
|
int order,
|
|
out float3 position
|
|
)
|
|
{
|
|
position = float3.zero;
|
|
if (knotVector.Count < controlPoints.Count + order - 1 || controlPoints.Count < order || t < 0 || 1 < t)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
knotVector = new List<double>(knotVector);
|
|
|
|
var originalFirstKnot = knotVector[0];
|
|
var fullKnotSpan = knotVector[knotVector.Count - 1] - knotVector[0];
|
|
|
|
//normalize knots
|
|
if (knotVector[0] != 0 || knotVector[knotVector.Count - 1] != 1)
|
|
{
|
|
for (int i = 0; i < knotVector.Count; ++i)
|
|
{
|
|
knotVector[i] = (knotVector[i] - originalFirstKnot) / fullKnotSpan;
|
|
}
|
|
}
|
|
|
|
var span = order;
|
|
while (span < controlPoints.Count && knotVector[span] <= t)
|
|
{
|
|
span++;
|
|
}
|
|
span--;
|
|
|
|
var basis = SplineUtility.GetNurbsBasisFunctions(order, t, knotVector, span);
|
|
|
|
for (int i = 0; i < order; ++i)
|
|
{
|
|
position += basis[i] * controlPoints[span - order + 1 + i];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static float[] GetNurbsBasisFunctions(int degree, float t, List<double> knotVector, int span)
|
|
{
|
|
//Constructs the Basis functions at t for the nurbs curve.
|
|
//The nurbs basis function form can be found at this link under the section
|
|
//"Construction of the basis functions": https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline
|
|
//This is an iterative way of computing the same thing.
|
|
var left = new float[degree];
|
|
var right = new float[degree];
|
|
var N = new float[degree];
|
|
|
|
for (int j = 0; j < degree; ++j)
|
|
{
|
|
left[j] = 0f;
|
|
right[j] = 0f;
|
|
N[j] = 1f;
|
|
}
|
|
|
|
for (int j = 1; j < degree; ++j)
|
|
{
|
|
|
|
left[j] = (float)(t - knotVector[span + 1 - j]);
|
|
right[j] = (float)(knotVector[span + j] - t);
|
|
var saved = 0f;
|
|
for (int k = 0; k < j; k++)
|
|
{
|
|
float temp = N[k] / (right[k + 1] + left[j - k]);
|
|
N[k] = saved + right[k + 1] * temp;
|
|
saved = left[j - k] * temp;
|
|
}
|
|
|
|
N[j] = saved;
|
|
|
|
}
|
|
|
|
return N;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return an interpolated position at ratio t.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>A position on the spline.</returns>
|
|
public static float3 EvaluatePosition<T>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return an interpolated direction at ratio t.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>A direction on the spline.</returns>
|
|
public static float3 EvaluateTangent<T>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluate the normal (up) vector of a spline.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to evaluate.</param>
|
|
/// <param name="t">A value between 0 and 1 representing a ratio of the curve.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>An up vector</returns>
|
|
public static float3 EvaluateUpVector<T>(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.GetCurveUpVector(curveIndex, curveT);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the normal (up) vector of a spline. This is a more accurate but more expensive operation
|
|
/// than <see cref="EvaluateUpVector{T}"/>.
|
|
/// </summary>
|
|
/// <param name="spline">The <see cref="NativeSpline"/> to evaluate.</param>
|
|
/// <param name="t">A value between 0 and 1 representing a ratio of the curve.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>An up vector</returns>
|
|
public static float3 CalculateUpVector<T>(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.CalculateUpVector(curveIndex, curveT);
|
|
}
|
|
|
|
internal static float3 CalculateUpVector<T>(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;
|
|
|
|
var up = CurveUtility.EvaluateUpVector(curve, curveT, curveStartUp, curveEndUp);
|
|
|
|
return up;
|
|
}
|
|
|
|
internal static void EvaluateUpVectorsForCurve<T>(this T spline, int curveIndex, NativeArray<float3> upVectors)
|
|
where T : ISpline
|
|
{
|
|
if (spline.Count < 1 || !upVectors.IsCreated)
|
|
return;
|
|
|
|
var curveStartRotation = spline[curveIndex].Rotation;
|
|
var curveStartUp = math.rotate(curveStartRotation, math.up());
|
|
|
|
var endKnotIndex = spline.NextIndex(curveIndex);
|
|
var curveEndRotation = spline[endKnotIndex].Rotation;
|
|
var curveEndUp = math.rotate(curveEndRotation, math.up());
|
|
|
|
CurveUtility.EvaluateUpVectors(spline.GetCurve(curveIndex), curveStartUp, curveEndUp, upVectors);
|
|
}
|
|
|
|
internal static void EvaluateUpVectorsForCurve<T>(this T spline, int curveIndex, float3[] upVectors) where T : ISpline
|
|
{
|
|
if (upVectors == null)
|
|
return;
|
|
|
|
var upVectorsNative = new NativeArray<float3>(upVectors, Allocator.Temp);
|
|
EvaluateUpVectorsForCurve(spline, curveIndex, upVectorsNative);
|
|
upVectorsNative.CopyTo(upVectors);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return an interpolated acceleration at ratio t.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <returns>An acceleration on the spline.</returns>
|
|
public static float3 EvaluateAcceleration<T>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return an interpolated curvature at ratio t.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <returns>A curvature on the spline.</returns>
|
|
public static float EvaluateCurvature<T>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to interpolate.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="t">A value between 0 and 1 representing the ratio along the curve.</param>
|
|
/// <returns>A point representing the curvature center associated to the position at t on the spline.</returns>
|
|
public static float3 EvaluateCurvatureCenter<T>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a normalized interpolation (t) for a spline, calculate the curve index and curve-relative
|
|
/// normalized interpolation.
|
|
/// </summary>
|
|
/// <param name="spline">The target spline.</param>
|
|
/// <param name="splineT">A normalized spline interpolation value to be converted into curve space.</param>
|
|
/// <param name="curveT">A normalized curve interpolation value.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>The curve index.</returns>
|
|
public static int SplineToCurveT<T>(this T spline, float splineT, out float curveT) where T : ISpline
|
|
{
|
|
return SplineToCurveT(spline, splineT, out curveT, true);
|
|
}
|
|
|
|
static int SplineToCurveT<T>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given an interpolation value for a curve, calculate the relative normalized spline interpolation.
|
|
/// </summary>
|
|
/// <param name="spline">The target spline.</param>
|
|
/// <param name="curve">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
|
|
/// <see cref="PathIndexUnit.Knot"/>.
|
|
/// </param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>An interpolation value relative to normalized Spline length (0 to 1).</returns>
|
|
/// <seealso cref="SplineToCurveT{T}"/>
|
|
public static float CurveToSplineT<T>(this T spline, float curve) where T : ISpline
|
|
{
|
|
// Clamp negative curve index to 0
|
|
if (spline.Count <= 1 || curve < 0f)
|
|
return 0f;
|
|
|
|
// Clamp postive curve index beyond last knot to 1
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the length of a spline when transformed by a matrix.
|
|
/// </summary>
|
|
/// <param name="spline">The spline whose length is to be calculated.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="transform">The transformation matrix to apply to the spline.</param>
|
|
/// <returns>The length of the transformed spline.</returns>
|
|
public static float CalculateLength<T>(this T spline, float4x4 transform) where T : ISpline
|
|
{
|
|
using var nativeSpline = new NativeSpline(spline, transform);
|
|
return nativeSpline.GetLength();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the number of curves in a spline.
|
|
/// </summary>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="spline">The spline for which to calculate the number of curves.</param>
|
|
/// <returns>The number of curves in a spline.</returns>
|
|
public static int GetCurveCount<T>(this T spline) where T : ISpline
|
|
{
|
|
return math.max(0, spline.Count - (spline.Closed ? 0 : 1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the bounding box of a Spline.
|
|
/// </summary>
|
|
/// <param name="spline">The spline for which to calculate bounds.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>The bounds of a spline.</returns>
|
|
public static Bounds GetBounds<T>(this T spline) where T : ISpline
|
|
{
|
|
return GetBounds(spline, float4x4.identity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a bounding box for a spline.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to calculate bounds for.</param>
|
|
/// <param name="transform">The matrix to transform the spline's elements with.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>The bounds of a spline.</returns>
|
|
public static Bounds GetBounds<T>(this T spline, float4x4 transform) where T : ISpline
|
|
{
|
|
if (spline.Count < 1)
|
|
return default;
|
|
|
|
var knot = spline[0];
|
|
Bounds bounds = new Bounds(math.transform(transform, knot.Position), Vector3.zero);
|
|
|
|
// Only encapsulate first tangentIn if the spline is closed - otherwise it's not contributing to the spline's shape.
|
|
if (spline.Closed)
|
|
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentIn)));
|
|
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentOut)));
|
|
|
|
for (int i = 1, c = spline.Count; i < c; ++i)
|
|
{
|
|
knot = spline[i];
|
|
bounds.Encapsulate(math.transform(transform, knot.Position));
|
|
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentIn)));
|
|
|
|
// Encapsulate last tangentOut if the spline is closed - otherwise it's not contributing to the spline's shape.
|
|
if (spline.Closed || (!spline.Closed && i < c - 1))
|
|
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentOut)));
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of segments for a specified spline length and resolution.
|
|
/// </summary>
|
|
/// <param name="length">The length of the spline to consider.</param>
|
|
/// <param name="resolution">The value used to calculate the number of segments for a length. This is calculated
|
|
/// as max(MIN_SEGMENTS, min(MAX_SEGMENTS, sqrt(length) * resolution)).
|
|
/// </param>
|
|
/// <returns>
|
|
/// The number of segments for a length and resolution.
|
|
/// </returns>
|
|
[Obsolete("Use " + nameof(GetSubdivisionCount) + " instead.", false)]
|
|
public static int GetSegmentCount(float length, int resolution) => GetSubdivisionCount(length, resolution);
|
|
|
|
/// <summary>
|
|
/// Gets the number of subdivisions for a spline length and resolution.
|
|
/// </summary>
|
|
/// <param name="length">The length of the spline to consider.</param>
|
|
/// <param name="resolution">The resolution to consider. Higher resolutions result in more
|
|
/// precise representations. However, higher resolutions have higher performance requirements.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The number of subdivisions as calculated for given length and resolution.
|
|
/// </returns>
|
|
public static int GetSubdivisionCount(float length, int resolution)
|
|
{
|
|
return (int)math.max(k_SubdivisionCountMin, math.min(k_SubdivisionCountMax, 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>(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);
|
|
var (rayPoint, linePoint) = SplineMath.RayLineNearestPoint(ro, rd, a, b, out _, out var lineParam);
|
|
float dsqr = math.lengthsq(linePoint - rayPoint);
|
|
|
|
if (dsqr < distance)
|
|
{
|
|
segment.start = t0;
|
|
segment.length = t1 - t0;
|
|
time = segment.start + segment.length * lineParam;
|
|
distance = dsqr;
|
|
nearest = linePoint;
|
|
}
|
|
|
|
t0 = t1;
|
|
a = b;
|
|
}
|
|
|
|
distance = math.sqrt(distance);
|
|
return segment;
|
|
}
|
|
|
|
static Segment GetNearestPoint<T>(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);
|
|
|
|
|
|
for (int i = 1; i < segments; i++)
|
|
{
|
|
float t1 = range.start + (range.length * (i / (segments - 1f)));
|
|
float3 b = EvaluatePosition(spline, t1);
|
|
var p = SplineMath.PointLineNearestPoint(point, a, b, out var lineParam);
|
|
float dsqr = math.distancesq(p, point);
|
|
|
|
if (dsqr < distance)
|
|
{
|
|
segment.start = t0;
|
|
segment.length = t1 - t0;
|
|
time = segment.start + segment.length * lineParam;
|
|
distance = dsqr;
|
|
|
|
nearest = p;
|
|
}
|
|
|
|
t0 = t1;
|
|
a = b;
|
|
}
|
|
|
|
distance = math.sqrt(distance);
|
|
return segment;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the point on a spline nearest to a ray.
|
|
/// </summary>
|
|
/// <param name="spline">The input spline to search for nearest point.</param>
|
|
/// <param name="ray">The input ray to search against.</param>
|
|
/// <param name="nearest">The point on a spline nearest to the input ray. The accuracy of this value is
|
|
/// affected by the <paramref name="resolution"/>.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="t">The normalized time value to the nearest point.</param>
|
|
/// <param name="resolution">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 <see cref="PickResolutionMin"/>, and the maximum is defined by
|
|
/// <see cref="PickResolutionMax"/>.
|
|
/// In most cases, the default resolution is appropriate. Use with <paramref name="iterations"/> to fine tune
|
|
/// point accuracy.
|
|
/// </param>
|
|
/// <param name="iterations">
|
|
/// The nearest point is calculated by finding the nearest point on the entire length
|
|
/// of the spline using <paramref name="resolution"/> 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 <see cref="PickResolutionMax"/>.
|
|
/// </param>
|
|
/// <returns>The distance from ray to nearest point.</returns>
|
|
public static float GetNearestPoint<T>(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 = GetSubdivisionCount(spline.GetLength() * segment.length, res);
|
|
segment = GetNearestPoint(spline, ro, rd, segment, out distance, out nearest, out t, segments);
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the point on a spline nearest to a point.
|
|
/// </summary>
|
|
/// <param name="spline">The input spline to search for nearest point.</param>
|
|
/// <param name="point">The input point to compare.</param>
|
|
/// <param name="nearest">The point on a spline nearest to the input point. The accuracy of this value is
|
|
/// affected by the <paramref name="resolution"/>.</param>
|
|
/// <param name="t">The normalized interpolation ratio corresponding to the nearest point.</param>
|
|
/// <param name="resolution">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 <see cref="PickResolutionMin"/>, and the maximum is defined by
|
|
/// <see cref="PickResolutionMax"/>.
|
|
/// In most cases, the default resolution is appropriate. Use with <paramref name="iterations"/> to fine tune
|
|
/// point accuracy.
|
|
/// </param>
|
|
/// <param name="iterations">
|
|
/// The nearest point is calculated by finding the nearest point on the entire length
|
|
/// of the spline using <paramref name="resolution"/> 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 <see cref="PickResolutionMax"/>.
|
|
/// </param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>The distance from input point to nearest point on spline.</returns>
|
|
public static float GetNearestPoint<T>(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 = GetSubdivisionCount(spline.GetLength() * segment.length, res);
|
|
segment = GetNearestPoint(spline, point, segment, out distance, out nearest, out t, segments);
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="spline">The Spline on which to compute the point.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <param name="fromT">The Spline interpolation ratio `t` (normalized) from which the next position need to be computed.</param>
|
|
/// <param name="relativeDistance">
|
|
/// 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).
|
|
/// </param>
|
|
/// <param name="resultPointT">The normalized interpolation ratio of the resulting point.</param>
|
|
/// <returns>The 3d point from the spline located at a linear distance from the point at t.</returns>
|
|
public static float3 GetPointAtLinearDistance<T>(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 = currentPos;
|
|
|
|
while (residual > epsilon && (forwardSearch ? resultPointT < 1f : resultPointT > 0))
|
|
{
|
|
currentLength += forwardSearch ? residual : -residual;
|
|
resultPointT = currentLength / length;
|
|
|
|
if (resultPointT > 1f) //forward search
|
|
resultPointT = 1f;
|
|
else if (resultPointT < 0f) //backward search
|
|
resultPointT = 0f;
|
|
|
|
point = spline.EvaluatePosition(resultPointT);
|
|
linearDistance = math.distance(currentPos, point);
|
|
residual = math.abs(relativeDistance) - linearDistance;
|
|
}
|
|
|
|
return point;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a normalized interpolation ratio, calculate the associated interpolation value in another <see cref="PathIndexUnit"/> regarding a specific spline.
|
|
/// </summary>
|
|
/// <param name="spline">The Spline to use for the conversion.</param>
|
|
/// <param name="t">Normalized interpolation ratio (0 to 1).</param>
|
|
/// <param name="targetPathUnit">The <see cref="PathIndexUnit"/> to which `t` should be converted.</param>
|
|
/// <typeparam name="T">A type implementing <see cref="ISpline"/>.</typeparam>
|
|
/// <returns>The interpolation value converted to targetPathUnit.</returns>
|
|
public static float ConvertIndexUnit<T>(this T spline, float t, PathIndexUnit targetPathUnit)
|
|
where T : ISpline
|
|
{
|
|
if (targetPathUnit == PathIndexUnit.Normalized)
|
|
return WrapInterpolation(t, spline.Closed);
|
|
|
|
return ConvertNormalizedIndexUnit(spline, t, targetPathUnit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given an interpolation value using one of the various <see cref="PathIndexUnit"/> types, calculate the associated interpolation value in another <see cref="PathIndexUnit"/> regarding a specific spline.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to use for the conversion.</param>
|
|
/// <param name="value">Interpolation value in the original <see cref="PathIndexUnit"/> `fromPathUnit`.</param>
|
|
/// <param name="fromPathUnit">The <see cref="PathIndexUnit"/> for the original interpolation value type.</param>
|
|
/// <param name="targetPathUnit">The <see cref="PathIndexUnit"/> to which `value` should be converted.</param>
|
|
/// <typeparam name="T">A type implementing <see cref="ISpline"/>.</typeparam>
|
|
/// <returns>The interpolation value converted to targetPathUnit.</returns>
|
|
public static float ConvertIndexUnit<T>(this T spline, float value, PathIndexUnit fromPathUnit, PathIndexUnit targetPathUnit)
|
|
where T : ISpline
|
|
{
|
|
if (fromPathUnit == targetPathUnit)
|
|
{
|
|
if (targetPathUnit == PathIndexUnit.Normalized)
|
|
value = WrapInterpolation(value, spline.Closed);
|
|
|
|
return value;
|
|
}
|
|
|
|
return ConvertNormalizedIndexUnit(spline, GetNormalizedInterpolation(spline, value, fromPathUnit), targetPathUnit);
|
|
}
|
|
|
|
static float ConvertNormalizedIndexUnit<T>(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, bool closed)
|
|
{
|
|
if (!closed)
|
|
return math.clamp(t, 0f, 1f);
|
|
|
|
return t % 1f == 0f ? math.clamp(t, 0f, 1f) : t - math.floor(t);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given an interpolation value in any PathIndexUnit type, calculate the normalized interpolation ratio value
|
|
/// relative to a <see cref="Spline"/>.
|
|
/// </summary>
|
|
/// <param name="spline">The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits.</param>
|
|
/// <param name="t">The `t` value to normalize in the original PathIndexUnit.</param>
|
|
/// <param name="originalPathUnit">The PathIndexUnit from the original `t`.</param>
|
|
/// <typeparam name="T">A type implementing ISpline.</typeparam>
|
|
/// <returns>The normalized interpolation ratio (0 to 1).</returns>
|
|
public static float GetNormalizedInterpolation<T>(T spline, float t, PathIndexUnit originalPathUnit) where T : ISpline
|
|
{
|
|
switch (originalPathUnit)
|
|
{
|
|
case PathIndexUnit.Knot:
|
|
return WrapInterpolation(CurveToSplineT(spline, t), spline.Closed);
|
|
case PathIndexUnit.Distance:
|
|
var length = spline.GetLength();
|
|
return WrapInterpolation(length > 0 ? t / length : 0f, spline.Closed);
|
|
default:
|
|
return WrapInterpolation(t, spline.Closed);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the index of a knot that precedes a spline index. This method uses the <see cref="Spline.Count"/>
|
|
/// and <see cref="Spline.Closed"/> properties to ensure that it returns the correct index of the knot.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to consider.</param>
|
|
/// <param name="index">The current index to consider.</param>
|
|
/// <typeparam name="T">A type that implements ISpline.</typeparam>
|
|
/// <returns>Returns a knot index that precedes the `index` on the considered spline.</returns>
|
|
public static int PreviousIndex<T>(this T spline, int index) where T : ISpline
|
|
=> PreviousIndex(index, spline.Count, spline.Closed);
|
|
|
|
/// <summary>
|
|
/// Gets the index of a knot that follows a spline index. This method uses the <see cref="Spline.Count"/> and
|
|
/// <see cref="Spline.Closed"/> properties to ensure that it returns the correct index of the knot.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to consider.</param>
|
|
/// <param name="index">The current index to consider.</param>
|
|
/// <typeparam name="T">A type that implements ISpline.</typeparam>
|
|
/// <returns>The knot index after `index` on the considered spline.</returns>
|
|
public static int NextIndex<T>(this T spline, int index) where T : ISpline
|
|
=> NextIndex(index, spline.Count, spline.Closed);
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="BezierKnot"/> before spline[index]. This method uses the <see cref="Spline.Count"/>
|
|
/// and <see cref="Spline.Closed"/> properties to ensure that it returns the correct knot.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to consider.</param>
|
|
/// <param name="index">The current index to consider.</param>
|
|
/// <typeparam name="T">A type that implements ISpline.</typeparam>
|
|
/// <returns>The knot before the knot at spline[index].</returns>
|
|
public static BezierKnot Previous<T>(this T spline, int index) where T : ISpline
|
|
=> spline[PreviousIndex(spline, index)];
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="BezierKnot"/> after spline[index]. This method uses the <see cref="Spline.Count"/>
|
|
/// and <see cref="Spline.Closed"/> properties to ensure that it returns the correct knot.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to consider.</param>
|
|
/// <param name="index">The current index to consider.</param>
|
|
/// <typeparam name="T">A type that implements ISpline.</typeparam>
|
|
/// <returns>The knot after the knot at spline[index].</returns>
|
|
public static BezierKnot Next<T>(this T spline, int index) where T : ISpline
|
|
=> spline[NextIndex(spline, index)];
|
|
|
|
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 GetExplicitLinearTangent(float3 point, float3 to)
|
|
{
|
|
return (to - point) / 3.0f;
|
|
}
|
|
|
|
// Typically a linear tangent is stored as a zero length vector. When a tangent is user controlled, we need to
|
|
// extrude out the tangent to something a user can grab rather than leaving the tangent on top of the knot.
|
|
internal static float3 GetExplicitLinearTangent(BezierKnot from, BezierKnot to)
|
|
{
|
|
var tin = to.Position - from.Position;
|
|
return math.mul(math.inverse(from.Rotation), tin * .33f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates a tangent from the previous and next knot positions.
|
|
/// </summary>
|
|
/// <param name="previous">The position of the previous <see cref="BezierKnot"/>.</param>
|
|
/// <param name="next">The position of the next <see cref="BezierKnot"/>.</param>
|
|
/// <returns>Returns a tangent calculated from the previous and next knot positions.</returns>
|
|
// todo Deprecate in 3.0 - this is not a correct uniform catmull rom parameterization.
|
|
public static float3 GetCatmullRomTangent(float3 previous, float3 next) =>
|
|
GetAutoSmoothTangent(previous, next, CatmullRomTension);
|
|
|
|
/// <summary>
|
|
/// Calculates a tangent from the previous and next knot positions.
|
|
/// </summary>
|
|
/// <param name="previous">The position of the previous <see cref="BezierKnot"/>.</param>
|
|
/// <param name="next">The position of the next <see cref="BezierKnot"/>.</param>
|
|
/// <param name="tension">Set the length of the tangent vectors.</param>
|
|
/// <returns>Returns a tangent calculated from the previous and next knot positions.</returns>
|
|
public static float3 GetAutoSmoothTangent(float3 previous, float3 next, float tension = DefaultTension)
|
|
{
|
|
if (next.Equals(previous))
|
|
return 0f;
|
|
|
|
return (next - previous) / math.sqrt(math.length(next - previous)) * tension;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a tangent from the previous, current, and next knot positions.
|
|
/// </summary>
|
|
/// <param name="previous">The position of the previous <see cref="BezierKnot"/>.</param>
|
|
/// <param name="current">The position of the current <see cref="BezierKnot"/>.</param>
|
|
/// <param name="next">The position of the next <see cref="BezierKnot"/>.</param>
|
|
/// <param name="tension">The length of the tangent vectors.</param>
|
|
/// <returns>Returns a tangent calculated from the previous, current, and next knot positions.</returns>
|
|
public static float3 GetAutoSmoothTangent(float3 previous, float3 current, float3 next, float tension = DefaultTension)
|
|
{
|
|
var d1 = math.length(current - previous);
|
|
var d2 = math.length(next - current);
|
|
|
|
if (d1 == 0f) // If we're only working with 2 valid points, then calculate roughly Uniform parametrization tangent and scale it down so we can atleast build rotation.
|
|
return (next - current) * 0.1f;
|
|
else if (d2 == 0f)
|
|
return (current - previous) * 0.1f;
|
|
|
|
// Calculations below are based on (pp. 5-6): http://www.cemyuksel.com/research/catmullrom_param/catmullrom_cad.pdf
|
|
// Catmull-Rom parameterization: a = 0 - Uniform, a = 0.5 - Centripetal, a = 1.0 - Chordal.
|
|
var a = tension;
|
|
var twoA = 2f * tension;
|
|
|
|
var d1PowA = math.pow(d1, a);
|
|
var d1Pow2A = math.pow(d1, twoA);
|
|
var d2PowA = math.pow(d2, a);
|
|
var d2Pow2A = math.pow(d2, twoA);
|
|
|
|
return (d1Pow2A * next - d2Pow2A * previous + (d2Pow2A - d1Pow2A) * current) / (3f * d1PowA * (d1PowA + d2PowA));
|
|
}
|
|
|
|
static float3 GetUniformAutoSmoothTangent(float3 previous, float3 next, float tension)
|
|
{
|
|
return (next - previous) * tension;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="BezierKnot"/> with its tangents and rotation calculated using the previous and next knot positions.
|
|
/// </summary>
|
|
/// <param name="position">The position of the knot.</param>
|
|
/// <param name="previous">The knot that immediately precedes the requested knot.</param>
|
|
/// <param name="next">The knot that immediately follows the requested knot.</param>
|
|
/// <returns>A <see cref="BezierKnot"/> with tangent and rotation values calculated from the previous and next knot positions.</returns>
|
|
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next) =>
|
|
GetAutoSmoothKnot(position, previous, next, math.up());
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="BezierKnot"/> with its tangents and rotation calculated using the previous and next knot positions.
|
|
/// </summary>
|
|
/// <param name="position">The position of the knot.</param>
|
|
/// <param name="previous">The knot that immediately precedes the requested knot.</param>
|
|
/// <param name="next">The knot that immediately follows the requested knot.</param>
|
|
/// <param name="normal">The normal vector of the knot.</param>
|
|
/// <returns>A <see cref="BezierKnot"/> with tangent and rotation values calculated from the previous and next knot positions.</returns>
|
|
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next, float3 normal) =>
|
|
GetAutoSmoothKnot(position, previous, next, normal, CatmullRomTension);
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="BezierKnot"/> with its tangents and rotation calculated using the previous and next knot positions.
|
|
/// </summary>
|
|
/// <param name="position">The position of the knot.</param>
|
|
/// <param name="previous">The knot that immediately precedes the requested knot.</param>
|
|
/// <param name="next">The knot that immediately follows the requested knot.</param>
|
|
/// <param name="normal">The normal vector of the knot.</param>
|
|
/// <param name="tension">Set the length of the tangent vectors.</param>
|
|
/// <returns>A <see cref="BezierKnot"/> with tangent and rotation values calculated from the previous and next knot positions.</returns>
|
|
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next, float3 normal, float tension = DefaultTension)
|
|
{
|
|
var tanIn = GetAutoSmoothTangent(next, position, previous, tension);
|
|
var tanOut = GetAutoSmoothTangent(previous, position, next, tension);
|
|
var dirIn = new float3(0, 0, math.length(tanIn));
|
|
var dirOut = new float3(0, 0, math.length(tanOut));
|
|
var dirRot = tanOut;
|
|
|
|
if (dirIn.z == 0f)
|
|
dirIn.z = dirOut.z;
|
|
if (dirOut.z == 0f)
|
|
{
|
|
dirOut.z = dirIn.z;
|
|
dirRot = -tanIn;
|
|
}
|
|
|
|
return new BezierKnot(position, -dirIn, dirOut, GetKnotRotation(dirRot, normal));
|
|
}
|
|
|
|
internal static quaternion GetKnotRotation(float3 tangent, float3 normal)
|
|
{
|
|
if (math.lengthsq(tangent) == 0f)
|
|
tangent = math.rotate(Quaternion.FromToRotation(math.up(), normal), math.forward());
|
|
|
|
float3 up = Mathf.Approximately(math.abs(math.dot(math.normalizesafe(tangent), math.normalizesafe(normal))), 1f)
|
|
? math.cross(math.normalizesafe(tangent), math.right())
|
|
: Vector3.ProjectOnPlane(normal, tangent).normalized;
|
|
|
|
return quaternion.LookRotationSafe(math.normalizesafe(tangent), up);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset a transform position to a position while keeping knot positions in the same place. This modifies both
|
|
/// knot positions and transform position.
|
|
/// </summary>
|
|
/// <param name="container">The target spline.</param>
|
|
/// <param name="position">The point in world space to move the pivot to.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes a <see cref="Spline"/> to approximate the curve formed by the list of provided points
|
|
/// within the given error threshold.
|
|
/// </summary>
|
|
/// <param name="points">The list of <see cref="float3"/> points that define the curve to approximate.</param>
|
|
/// <param name="errorThreshold">The error threshold to use. Represents the largest distance between any of the
|
|
/// provided points and the curve.</param>
|
|
/// <param name="closed">Whether to close the <see cref="Spline"/>.</param>
|
|
/// <param name="spline">The output <see cref="Spline"/> fitted to the points, or an empty spline if could not be fitted.</param>
|
|
/// <returns>Whether a curve could be fitted according to the provided error threshold.</returns>
|
|
public static bool FitSplineToPoints(List<float3> points, float errorThreshold, bool closed,
|
|
out Spline spline)
|
|
{
|
|
/*
|
|
Implementation based on:
|
|
"Algorithm for Automatically Fitting Digitized Curves"
|
|
by Philip J. Schneider
|
|
"Graphics Gems", Academic Press, 1990
|
|
*/
|
|
const float epsilon = 0.001f;
|
|
if (points == null || points.Count < 2)
|
|
{
|
|
Debug.LogError("FitSplineToPoints failed: The provided points list is either null, empty or contains only one point.");
|
|
spline = new Spline();
|
|
return false;
|
|
}
|
|
|
|
var pointsCopy = new List<float3>(points);
|
|
if (closed)
|
|
{
|
|
// As the base algorithm does not have a notion of closed splines, we add 2-3 extra/extension points to the input list
|
|
// to help the algorithm smooth out tangents at the connecting/closing point. They are removed from the resulting
|
|
// spline at the end of the processing.
|
|
|
|
// 1) If the first and last points do not match, we duplicate the first and add it to the end of the curve
|
|
if (math.length(pointsCopy[0] - pointsCopy[^1]) > epsilon)
|
|
pointsCopy.Add(pointsCopy[0]);
|
|
|
|
// 2) We then do a forward loop/revolution of one point by duplicating the second knot and attaching it to the end
|
|
pointsCopy.Add(pointsCopy[1]);
|
|
|
|
// 3) What was initially the last point, we duplicate and insert at front - backward loop/revolution of one point
|
|
pointsCopy.Insert(0, pointsCopy[^3]);
|
|
}
|
|
|
|
var leftTangent = math.normalize(pointsCopy[1] - pointsCopy[0]);
|
|
var rightTangent = math.normalize(pointsCopy[^2] - pointsCopy[^1]);
|
|
|
|
// Do recursive fitting
|
|
if (FitSplineToPointsStepInternal(pointsCopy, 0, pointsCopy.Count - 1, leftTangent, rightTangent, errorThreshold, closed, closed, out spline))
|
|
{
|
|
// For closed spline, clean up/merge knots that were added due to the appended extension points
|
|
if (closed && spline.Count > 2)
|
|
{
|
|
// Remove 2 of the 3 extension knots of closed splines.
|
|
spline.RemoveAt(0);
|
|
spline.RemoveAt(spline.Count - 1);
|
|
|
|
// Merge the overlaping front/end knots.
|
|
var firstKnot = spline[0];
|
|
firstKnot.TangentIn = spline[^1].TangentOut;
|
|
spline[0] = firstKnot;
|
|
spline.RemoveAt(spline.Count - 1);
|
|
|
|
spline.Closed = true;
|
|
}
|
|
|
|
// At this point we have the final shape of the spline - tangents can now be baked into rotation.
|
|
var up = float3.zero;
|
|
for (int i = 0; i < spline.Count; ++i)
|
|
{
|
|
var knot = spline[i];
|
|
|
|
var knotDir = knot.TangentOut;
|
|
if (math.lengthsq(knotDir) == 0f)
|
|
knotDir = spline.EvaluateTangent(GetNormalizedInterpolation(spline, i, PathIndexUnit.Knot));
|
|
var knotDirNorm = math.normalizesafe(knotDir);
|
|
|
|
if (i == 0)
|
|
up = CalculatePreferredNormalForDirection(knotDirNorm);
|
|
else
|
|
up = CurveUtility.EvaluateUpVector(spline.GetCurve(i - 1), 1f, up, float3.zero, false);
|
|
|
|
var tangentMode = TangentMode.Broken;
|
|
if (math.dot(math.normalizesafe(knot.TangentIn), math.normalizesafe(knot.TangentOut)) <= -1f + epsilon)
|
|
{
|
|
if (math.abs(math.length(knot.TangentIn) - math.length(knot.TangentOut)) <= epsilon)
|
|
tangentMode = TangentMode.Mirrored;
|
|
else
|
|
tangentMode = TangentMode.Continuous;
|
|
}
|
|
|
|
knot.TangentIn = new float3(0f, 0f, -math.length(knot.TangentIn));
|
|
knot.TangentOut = new float3(0f, 0f, math.length(knot.TangentOut));
|
|
knot.Rotation = quaternion.LookRotationSafe(knotDirNorm, up);
|
|
spline[i] = knot;
|
|
|
|
if (tangentMode != TangentMode.Broken)
|
|
spline.SetTangentModeNoNotify(i, tangentMode);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool FitSplineToPointsStepInternal(IReadOnlyList<float3> allPoints, int rangeStart, int rangeEnd, float3 leftTangent, float3 rightTangent, float errorThreshold, bool closed, bool splineClosed,
|
|
out Spline spline)
|
|
{
|
|
const int maxIterations = 4;
|
|
spline = new Spline();
|
|
|
|
var curRangePointCount = rangeEnd - rangeStart + 1;
|
|
if (curRangePointCount < 2)
|
|
return false;
|
|
|
|
if (curRangePointCount == 2)
|
|
{
|
|
var difference = allPoints[rangeStart] - allPoints[rangeEnd];
|
|
//we use 1/3 of the distance between each of the points as described in the graphics gems paper.
|
|
var diffLength = math.length(difference) / 3f;
|
|
var leftTanScaled = leftTangent * diffLength;
|
|
var rightTanScaled = rightTangent * diffLength;
|
|
|
|
var firstKnot = new BezierKnot(allPoints[rangeStart], -leftTanScaled, leftTanScaled, Quaternion.identity);
|
|
var secondKnot = new BezierKnot(allPoints[rangeStart + 1], rightTanScaled, -rightTanScaled, Quaternion.identity);
|
|
|
|
spline = new Spline();
|
|
spline.Add(firstKnot, TangentMode.Broken);
|
|
spline.Add(secondKnot, TangentMode.Broken);
|
|
spline.Closed = closed;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Find corresponding t values for each point so we can compute errors (chord-length parameterization)
|
|
var correspondingTValues = new float[curRangePointCount];
|
|
var firstPassTValues = new float[curRangePointCount];
|
|
|
|
firstPassTValues[0] = 0f;
|
|
var cumulativeChordLengths = new float[curRangePointCount - 1];
|
|
|
|
var chordLengthSum = 0f;
|
|
for (int i = 1; i < curRangePointCount; i++)
|
|
{
|
|
var chord = allPoints[rangeStart + i] - allPoints[rangeStart + i - 1];
|
|
chordLengthSum += math.length(chord);
|
|
cumulativeChordLengths[i - 1] = chordLengthSum;
|
|
}
|
|
|
|
for (int i = 0; i < cumulativeChordLengths.Length; i++)
|
|
{
|
|
firstPassTValues[i + 1] = cumulativeChordLengths[i] / chordLengthSum;
|
|
}
|
|
|
|
correspondingTValues = firstPassTValues;
|
|
|
|
spline = GenerateSplineFromTValues(allPoints, rangeStart, rangeEnd, closed, correspondingTValues, leftTangent, rightTangent);
|
|
|
|
var tPositions = new float3[curRangePointCount];
|
|
for (int i = 0; i < correspondingTValues.Length; i++)
|
|
{
|
|
float t = correspondingTValues[i];
|
|
float3 position = spline.EvaluatePosition(t);
|
|
tPositions[i] = position;
|
|
}
|
|
|
|
var errorResult = ComputeMaxError(allPoints, rangeStart, rangeEnd, tPositions, errorThreshold, splineClosed);
|
|
|
|
var errorBeforeReparameterization = errorResult.maxError;
|
|
var splitPoint = errorResult.maxErrorIndex;
|
|
|
|
if (errorBeforeReparameterization < errorThreshold)
|
|
return true;
|
|
// If error is small enough, try reparameterizing and fitting again
|
|
else if (errorBeforeReparameterization < 4 * errorThreshold)
|
|
{
|
|
var numIterations = 0;
|
|
while (numIterations < maxIterations)
|
|
{
|
|
var curveKnots = new BezierKnot[spline.Count];
|
|
int i = 0;
|
|
foreach (BezierKnot bezierKnot in spline.Knots)
|
|
{
|
|
curveKnots[i] = bezierKnot;
|
|
++i;
|
|
}
|
|
|
|
var P0 = curveKnots[0].Position;
|
|
var P1 = curveKnots[0].Position + curveKnots[0].TangentOut;
|
|
var P2 = curveKnots[1].Position + curveKnots[1].TangentIn;
|
|
var P3 = curveKnots[1].Position;
|
|
|
|
var P = new float3[] { P0, P1, P2, P3 };
|
|
|
|
var q1 = new float3[3];
|
|
for (int j = 0; j <= 2; ++j)
|
|
{
|
|
q1[j] = (P[j + 1] - P[j]) * 3f;
|
|
}
|
|
|
|
var q2 = new float3[2];
|
|
for (int j = 0; j <= 1; ++j)
|
|
{
|
|
q2[j] = (q1[j + 1] - q1[j]) * 2f;
|
|
}
|
|
|
|
for (int k = rangeStart; k < curRangePointCount-1; k++)
|
|
{
|
|
var t = correspondingTValues[k];
|
|
var q = Bernstein(t, P, 3);
|
|
|
|
var q1_t = Bernstein(t, q1, 2);
|
|
var q2_t = Bernstein(t, q2, 1);
|
|
|
|
var numerator = math.dot(q - tPositions[k], q1_t);
|
|
var denominator = math.dot(q1_t, q1_t) + math.dot(q - tPositions[k], q2_t);
|
|
if (denominator != 0)
|
|
correspondingTValues[k] -= (numerator / denominator);
|
|
}
|
|
|
|
spline = GenerateSplineFromTValues(allPoints, rangeStart, rangeEnd, closed, correspondingTValues, leftTangent, rightTangent);
|
|
|
|
for (int k = 0; k < tPositions.Length; k++)
|
|
{
|
|
var t = correspondingTValues[k];
|
|
var position = spline.EvaluatePosition(t);
|
|
tPositions[k] = position;
|
|
}
|
|
|
|
errorResult = ComputeMaxError(allPoints, rangeStart, rangeEnd, tPositions, errorThreshold, splineClosed);
|
|
var errorAfterReparameterization = errorResult.maxError;
|
|
splitPoint = errorResult.maxErrorIndex;
|
|
if (errorAfterReparameterization < errorThreshold)
|
|
{
|
|
return true;
|
|
}
|
|
numIterations++;
|
|
}
|
|
}
|
|
|
|
// Still not good enough. Try splitting at point of max error.
|
|
if (curRangePointCount == 3)
|
|
splitPoint = rangeStart + 1;
|
|
else
|
|
{
|
|
if (splitPoint == 0)
|
|
splitPoint++;
|
|
else if (splitPoint == curRangePointCount - 1)
|
|
splitPoint--;
|
|
}
|
|
|
|
var splitTangent = CalculateCenterTangent(allPoints[splitPoint - 1], allPoints[splitPoint], allPoints[splitPoint + 1]);
|
|
if (!FitSplineToPointsStepInternal(allPoints, rangeStart, splitPoint, leftTangent, splitTangent, errorThreshold, false, splineClosed, out var firstSpline) ||
|
|
!FitSplineToPointsStepInternal(allPoints, splitPoint, rangeEnd, -splitTangent, rightTangent, errorThreshold, false, splineClosed, out var secondSpline))
|
|
{
|
|
spline = new Spline();
|
|
return false;
|
|
}
|
|
|
|
var splitPointIdx = firstSpline.Count - 1;
|
|
var splitPointTangentIn = firstSpline[^1].TangentIn;
|
|
|
|
firstSpline.RemoveAt(splitPointIdx);
|
|
firstSpline.Add(secondSpline);
|
|
spline = firstSpline;
|
|
|
|
var splitKnot = spline[splitPointIdx];
|
|
splitKnot.TangentIn = splitPointTangentIn;
|
|
spline[splitPointIdx] = splitKnot;
|
|
|
|
return true;
|
|
}
|
|
|
|
static float3 CalculatePreferredNormalForDirection(float3 splineDirection)
|
|
{
|
|
var dirNorm = math.normalizesafe(splineDirection);
|
|
|
|
float3 absDotRUF;
|
|
absDotRUF.x = math.abs(math.dot(dirNorm, math.right()));
|
|
absDotRUF.y = math.abs(math.dot(dirNorm, math.up()));
|
|
absDotRUF.z = math.abs(math.dot(dirNorm, math.forward()));
|
|
|
|
float3 right;
|
|
// Pick the right vector based on a world axis that the spline tangent is most orthogonal to
|
|
if (absDotRUF.x < absDotRUF.y && absDotRUF.x < absDotRUF.z)
|
|
right = math.normalizesafe(math.cross(math.right(), dirNorm));
|
|
else if (absDotRUF.y < absDotRUF.x && absDotRUF.y < absDotRUF.z)
|
|
right = math.normalizesafe(math.cross(math.up(), dirNorm));
|
|
else
|
|
right = math.normalizesafe(math.cross(math.forward(), dirNorm));
|
|
|
|
return math.normalizesafe(math.cross(dirNorm, right));
|
|
}
|
|
|
|
static float3 CalculateCenterTangent(float3 prevPoint, float3 centerPoint, float3 nextPoint)
|
|
{
|
|
var v1 = prevPoint - centerPoint;
|
|
var v2 = centerPoint - nextPoint;
|
|
|
|
var centerTangent = math.normalize((v1 + v2) / 2.0f);
|
|
return centerTangent;
|
|
}
|
|
|
|
//A helper method for FitSplineToPoints that computes bernstein basis functions
|
|
static float3 Bernstein(float t, float3[] bezier, int degree)
|
|
{
|
|
|
|
var copy = new float3[bezier.Length];
|
|
bezier.CopyTo(copy, 0);
|
|
|
|
for (int i = 1; i <= degree; ++i)
|
|
{
|
|
for (int j = 0; j <= degree - i; ++j)
|
|
{
|
|
copy[j] = (1 - t) * copy[j] + t * copy[j + 1];
|
|
}
|
|
}
|
|
|
|
return copy[0];
|
|
}
|
|
|
|
//A helper method for FitSplineToPoints that generates a spline from a provided array of
|
|
// interpolation values.
|
|
static Spline GenerateSplineFromTValues(IReadOnlyList<float3> allPoints, int rangeStart, int rangeEnd, bool closed, float[] tValues, float3 leftTangent, float3 rightTangent)
|
|
{
|
|
var curRangePointCount = rangeEnd - rangeStart + 1;
|
|
var aLeft = new float3[curRangePointCount];
|
|
var aRight = new float3[curRangePointCount];
|
|
for (int i = 0; i < curRangePointCount; ++i)
|
|
{
|
|
var t = tValues[i];
|
|
aLeft[i] = leftTangent * 3f * t * (1f - t) * (1f - t);
|
|
aRight[i] = rightTangent * 3f * t * t * (1f - t);
|
|
}
|
|
|
|
var c = new float[2, 2];
|
|
|
|
float x1 = 0f, x2 = 0f;
|
|
|
|
for (int i = 0; i < curRangePointCount; ++i)
|
|
{
|
|
var t = tValues[i];
|
|
c[0, 0] += math.dot(aLeft[i], aLeft[i]);
|
|
c[0, 1] += math.dot(aLeft[i], aRight[i]);
|
|
c[1, 0] = c[0, 1];
|
|
c[1, 1] += math.dot(aRight[i], aRight[i]);
|
|
|
|
var tmp = allPoints[rangeStart + i] - (
|
|
allPoints[rangeStart] * (1f - t) * (1f - t) * (1f - t)
|
|
+ allPoints[rangeStart] * 3f * t * (1f - t) * (1f - t)
|
|
+ allPoints[rangeEnd] * 3f * t * t * (1f - t)
|
|
+ allPoints[rangeEnd] * t * t * t
|
|
);
|
|
|
|
x1 += math.dot(aLeft[i], tmp);
|
|
x2 += math.dot(aRight[i], tmp);
|
|
}
|
|
|
|
var c0_c1_determinant = c[0, 0] * c[1, 1] - c[1, 0] * c[0, 1];
|
|
var c0_x_determinant = c[0, 0] * x2 - c[1, 0] * x1;
|
|
var x_c1_determinant = x1 * c[1, 1] - x2 * c[0, 1];
|
|
|
|
//now we compute the coefficients for our tangents
|
|
var alpha1 = c0_c1_determinant == 0 ? 0 : x_c1_determinant / c0_c1_determinant;
|
|
var alpha2 = c0_c1_determinant == 0 ? 0 : c0_x_determinant / c0_c1_determinant;
|
|
|
|
// alpha1 and alpha2 can be zero and in that case the original algorithm just falls back to the value
|
|
// of 1/3 distance between the range's first and last points. Omitting this can introduce
|
|
// zero tangent curves that break the resulting spline's continuity around certain knots.
|
|
var segLength = math.length(allPoints[rangeEnd] - allPoints[rangeStart]);
|
|
var epsilon = 1.0e-6f * segLength;
|
|
if (alpha1 < epsilon || alpha2 < epsilon)
|
|
alpha1 = alpha2 = segLength / 3f;
|
|
|
|
var first = new BezierKnot(allPoints[rangeStart], leftTangent * -alpha1, leftTangent * alpha1, quaternion.identity);
|
|
var second = new BezierKnot(allPoints[rangeEnd], rightTangent * alpha2, rightTangent * -alpha2, quaternion.identity);
|
|
|
|
var spline = new Spline();
|
|
// Use broken tangent modes for now as we'll pick the correct mode once the spline curvature is finalized.
|
|
spline.Add(first, TangentMode.Broken);
|
|
spline.Add(second, TangentMode.Broken);
|
|
|
|
spline.Closed = closed;
|
|
|
|
return spline;
|
|
}
|
|
|
|
// Helper function for FitSplineToPoints that computes the maximum least squares distance error
|
|
// and the index of the point for which the error is maximum.
|
|
static (float maxError, int maxErrorIndex) ComputeMaxError(IReadOnlyList<float3> allPoints, int rangeStart, int rangeEnd, float3[] positions, float errorThreshold, bool splineClosed)
|
|
{
|
|
var rangeCount = rangeEnd - rangeStart + 1;
|
|
|
|
// For closed spline, we force-split the fit curve at the extra/extension points that we add for closed spline points at the beginning.
|
|
// This gives the first and last knots additional adjacent knots that help the algorithm to smoothly unify the spline
|
|
// at the closing point (essentially treating closed splines as open splines that "loop" one extra knot forward and two extra knots backward).
|
|
// This also ensures that for all of the extension points that were added, there will be matching temporary spline knots
|
|
// that we can safely remove/merge at the end of the process.
|
|
if (splineClosed && rangeCount > 2)
|
|
{
|
|
var secondKnotIdx = 1;
|
|
if (rangeStart < secondKnotIdx && rangeEnd > secondKnotIdx)
|
|
return (errorThreshold, secondKnotIdx);
|
|
|
|
var knotBeforeLastIdx = allPoints.Count - 2;
|
|
if (rangeStart < knotBeforeLastIdx && rangeEnd > knotBeforeLastIdx)
|
|
return (errorThreshold, knotBeforeLastIdx);
|
|
}
|
|
|
|
// Use least squares difference to evaluate error.
|
|
var maxError = 0f;
|
|
var splitPoint = 0;
|
|
for (int i = 0; i < rangeCount; ++i)
|
|
{
|
|
var globalIndex = rangeStart + i;
|
|
var difference = allPoints[globalIndex] - positions[i];
|
|
var error = math.length(difference);
|
|
|
|
if (error > maxError)
|
|
{
|
|
maxError = error;
|
|
splitPoint = globalIndex;
|
|
}
|
|
}
|
|
|
|
return (maxError, splitPoint);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new spline and adds it to the <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target container.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <returns>Returns the spline that was created and added to the container.</returns>
|
|
public static Spline AddSpline<T>(this T container) where T : ISplineContainer
|
|
{
|
|
var spline = new Spline();
|
|
AddSpline(container, spline);
|
|
return spline;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a new <see cref="Spline"/> to the <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target container.</param>
|
|
/// <param name="spline">The spline to append to this container.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
public static void AddSpline<T>(this T container, Spline spline) where T : ISplineContainer
|
|
{
|
|
var splines = new List<Spline>(container.Splines);
|
|
splines.Add(spline);
|
|
container.Splines = splines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a spline from a <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target container.</param>
|
|
/// <param name="splineIndex">The index of the spline to remove from the SplineContainer.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <returns>Returns true if the spline was removed from the container.</returns>
|
|
public static bool RemoveSplineAt<T>(this T container, int splineIndex) where T : ISplineContainer
|
|
{
|
|
if (splineIndex < 0 || splineIndex >= container.Splines.Count)
|
|
return false;
|
|
|
|
var splines = new List<Spline>(container.Splines);
|
|
splines.RemoveAt(splineIndex);
|
|
container.KnotLinkCollection.SplineRemoved(splineIndex);
|
|
container.Splines = splines;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a spline from a <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="spline">The spline to remove from the SplineContainer.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <returns>Returns true if the spline was removed from the container.</returns>
|
|
public static bool RemoveSpline<T>(this T container, Spline spline) where T : ISplineContainer
|
|
{
|
|
var splines = new List<Spline>(container.Splines);
|
|
var index = splines.IndexOf(spline);
|
|
if (index < 0)
|
|
return false;
|
|
|
|
splines.RemoveAt(index);
|
|
container.KnotLinkCollection.SplineRemoved(index);
|
|
container.Splines = splines;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reorders a spline in a <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="previousSplineIndex">The previous index of the spline to reorder in the SplineContainer.</param>
|
|
/// <param name="newSplineIndex">The new index of the spline to reorder in the SplineContainer.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <returns>Returns true if the spline was reordered in the container.</returns>
|
|
public static bool ReorderSpline<T>(this T container, int previousSplineIndex, int newSplineIndex) where T : ISplineContainer
|
|
{
|
|
if (previousSplineIndex < 0 || previousSplineIndex >= container.Splines.Count ||
|
|
newSplineIndex < 0 || newSplineIndex >= container.Splines.Count)
|
|
return false;
|
|
|
|
var splines = new List<Spline>(container.Splines);
|
|
var spline = splines[previousSplineIndex];
|
|
splines.RemoveAt(previousSplineIndex);
|
|
splines.Insert(newSplineIndex, spline);
|
|
|
|
container.KnotLinkCollection.SplineIndexChanged(previousSplineIndex, newSplineIndex);
|
|
container.Splines = splines;
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static bool IsIndexValid<T>(T container, SplineKnotIndex index) where T : ISplineContainer
|
|
{
|
|
return index.Knot >= 0 && index.Knot < container.Splines[index.Spline].Count &&
|
|
index.Spline < container.Splines.Count && index.Knot < container.Splines[index.Spline].Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the position of all knots linked to the knot at `index` in an <see cref="ISplineContainer"/> to the same position.
|
|
/// </summary>
|
|
/// <param name="container">The target container.</param>
|
|
/// <param name="index">The `SplineKnotIndex` of the knot to use to synchronize the positions.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
public static void SetLinkedKnotPosition<T>(this T container, SplineKnotIndex index) where T : ISplineContainer
|
|
{
|
|
if (!container.KnotLinkCollection.TryGetKnotLinks(index, out var knots))
|
|
return;
|
|
|
|
var splines = container.Splines;
|
|
var position = splines[index.Spline][index.Knot].Position;
|
|
|
|
foreach (var i in knots)
|
|
{
|
|
if (!IsIndexValid(container, i))
|
|
return;
|
|
|
|
var knot = splines[i.Spline][i.Knot];
|
|
var originalPosition = knot.Position;
|
|
knot.Position = position;
|
|
splines[i.Spline].SetKnotNoNotify(i.Knot, knot);
|
|
|
|
// If the knot has been moved, notifies a knotModified event
|
|
if (!Mathf.Approximately(Vector3.Distance(position, originalPosition), 0f))
|
|
splines[i.Spline].SetDirty(SplineModification.KnotModified, i.Knot);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Links two knots in an <see cref="ISplineContainer"/>. The two knots can be on different splines, but both must be in the referenced SplineContainer.
|
|
/// If these knots are linked to other knots, all existing links are kept and updated.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="knotA">The first knot to link.</param>
|
|
/// <typeparam name="T">A type that implements <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <param name="knotB">The second knot to link.</param>
|
|
public static void LinkKnots<T>(this T container, SplineKnotIndex knotA, SplineKnotIndex knotB) where T : ISplineContainer
|
|
{
|
|
bool similarPositions = Mathf.Approximately(math.length(container.Splines[knotA.Spline][knotA.Knot].Position - container.Splines[knotB.Spline][knotB.Knot].Position), 0f);
|
|
var knotsToNotify = similarPositions ? null : container.KnotLinkCollection.GetKnotLinks(knotB);
|
|
|
|
container.KnotLinkCollection.Link(knotA, knotB);
|
|
|
|
if (knotsToNotify != null)
|
|
{
|
|
foreach (var ski in knotsToNotify)
|
|
container.Splines[ski.Spline].SetDirty(SplineModification.KnotModified, ski.Knot);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlinks several knots from an <see cref="ISplineContainer"/>. A knot in `knots` disconnects from other knots it was linked to.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="knots">The knot to unlink.</param>
|
|
/// <typeparam name="T">A type implementing <see cref="ISplineContainer"/>.</typeparam>
|
|
public static void UnlinkKnots<T>(this T container, IReadOnlyList<SplineKnotIndex> knots) where T : ISplineContainer
|
|
{
|
|
foreach (var knot in knots)
|
|
container.KnotLinkCollection.Unlink(knot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if two knots from an <see cref="ISplineContainer"/> are linked together.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="knotA">The first knot to check.</param>
|
|
/// <param name="knotB">The second knot to check against.</param>
|
|
/// <returns>Returns <c>true</c> if <paramref name="knotA"/> and <paramref name="knotB"/> are linked; otherwise, <c>false</c>.</returns>
|
|
public static bool AreKnotLinked(this ISplineContainer container, SplineKnotIndex knotA, SplineKnotIndex knotB)
|
|
{
|
|
if (!container.KnotLinkCollection.TryGetKnotLinks(knotA, out var linkedKnots))
|
|
return false;
|
|
|
|
for (int i = 0; i < linkedKnots.Count; ++i)
|
|
if (linkedKnots[i] == knotB)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies knot links between two splines of the same <see cref="ISplineContainer"/>.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="srcSplineIndex">The index of the source spline to copy from.</param>
|
|
/// <param name="destSplineIndex">The index of the destination spline to copy to.</param>
|
|
/// <typeparam name="T">A type implementing <see cref="ISplineContainer"/>.</typeparam>
|
|
/// <remarks>
|
|
/// The knot links will only be copied if both of the spline indices are valid and both splines have the same amount of knots.
|
|
/// </remarks>
|
|
public static void CopyKnotLinks<T>(this T container, int srcSplineIndex, int destSplineIndex) where T : ISplineContainer
|
|
{
|
|
if ((srcSplineIndex < 0 || srcSplineIndex >= container.Splines.Count) ||
|
|
(destSplineIndex < 0 || destSplineIndex >= container.Splines.Count))
|
|
return;
|
|
|
|
var srcSpline = container.Splines[srcSplineIndex];
|
|
var dstSpline = container.Splines[destSplineIndex];
|
|
|
|
if (srcSpline.Count == 0 || srcSpline.Count != dstSpline.Count)
|
|
return;
|
|
|
|
for (int i = 0, c = srcSpline.Count; i < c; ++i)
|
|
{
|
|
if (container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(srcSplineIndex, i), out _))
|
|
container.KnotLinkCollection.Link(new SplineKnotIndex(srcSplineIndex, i), new SplineKnotIndex(destSplineIndex, i));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes redundant points in a poly line to form a similar shape with fewer points.
|
|
/// </summary>
|
|
/// <param name="line">The poly line to act on.</param>
|
|
/// <param name="epsilon">The maximum distance from the reduced poly line shape for a point to be discarded.</param>
|
|
/// <typeparam name="T">The collection type. Usually this will be list or array of float3.</typeparam>
|
|
/// <returns>Returns a new list with a poly line matching the shape of the original line with fewer points.</returns>
|
|
public static List<float3> ReducePoints<T>(T line, float epsilon = .15f) where T : IList<float3>
|
|
{
|
|
var ret = new List<float3>();
|
|
var rdp = new RamerDouglasPeucker<T>(line);
|
|
rdp.Reduce(ret, epsilon);
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes redundant points in a poly line to form a similar shape with fewer points.
|
|
/// </summary>
|
|
/// <param name="line">The poly line to act on.</param>
|
|
/// <param name="results">A pre-allocated list to be filled with the new reduced line points.</param>
|
|
/// <param name="epsilon">The maximum distance from the reduced poly line shape for a point to be discarded.</param>
|
|
/// <typeparam name="T">The collection type. Usually this will be list or array of float3.</typeparam>
|
|
public static void ReducePoints<T>(T line, List<float3> results, float epsilon = .15f) where T : IList<float3>
|
|
{
|
|
var rdp = new RamerDouglasPeucker<T>(line);
|
|
rdp.Reduce(results, epsilon);
|
|
}
|
|
|
|
internal static bool AreTangentsModifiable(TangentMode mode)
|
|
{
|
|
return mode == TangentMode.Broken || mode == TangentMode.Continuous || mode == TangentMode.Mirrored;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverses the flow direction of a spline.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="splineIndex">The index of the spline to reverse.</param>
|
|
public static void ReverseFlow(this ISplineContainer container, int splineIndex)
|
|
{
|
|
ReverseFlow(new SplineInfo(container, splineIndex));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverses the flow direction of a spline.
|
|
/// </summary>
|
|
/// <param name="splineInfo">The spline to reverse.</param>
|
|
public static void ReverseFlow(SplineInfo splineInfo)
|
|
{
|
|
var spline = splineInfo.Spline;
|
|
|
|
var knots = splineInfo.Spline.ToArray();
|
|
var tangentModes = new TangentMode[spline.Count];
|
|
|
|
for (int i = 0; i < tangentModes.Length; ++i)
|
|
tangentModes[i] = spline.GetTangentMode(i);
|
|
|
|
var splineLinks = new List<List<SplineKnotIndex>>();
|
|
|
|
// GetAll LinkedKnots on the spline
|
|
for(int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
|
|
{
|
|
var knot = new SplineKnotIndex(splineInfo.Index, previousKnotIndex);
|
|
var collection = splineInfo.Container.KnotLinkCollection.GetKnotLinks(knot).ToList();
|
|
splineLinks.Add(collection);
|
|
}
|
|
|
|
//Unlink all knots in the spline
|
|
foreach(var linkedKnots in splineLinks)
|
|
splineInfo.Container.UnlinkKnots(linkedKnots);
|
|
|
|
// Reverse order and tangents
|
|
for (int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
|
|
{
|
|
var knot = knots[previousKnotIndex];
|
|
var worldKnot = knot.Transform(splineInfo.LocalToWorld);
|
|
var tangentIn = worldKnot.TangentIn;
|
|
var tangentOut = worldKnot.TangentOut;
|
|
|
|
var reverseRotation = quaternion.AxisAngle(math.mul(knot.Rotation, math.up()), math.radians(180));
|
|
reverseRotation = math.normalizesafe(reverseRotation);
|
|
|
|
// Reverse the tangents to keep the same shape while reversing the order
|
|
knot.Rotation = math.mul(reverseRotation, knot.Rotation);
|
|
if(tangentModes[previousKnotIndex] is TangentMode.Broken)
|
|
{
|
|
var localRot = quaternion.AxisAngle(math.up(), math.radians(180));
|
|
knot.TangentIn = math.rotate(localRot, tangentOut);
|
|
knot.TangentOut = math.rotate(localRot, tangentIn);
|
|
}
|
|
else if(tangentModes[previousKnotIndex] is TangentMode.Continuous)
|
|
{
|
|
knot.TangentIn = -tangentOut;
|
|
knot.TangentOut = -tangentIn;
|
|
}
|
|
|
|
var newKnotIndex = spline.Count - 1 - previousKnotIndex;
|
|
spline.SetTangentMode(newKnotIndex, tangentModes[previousKnotIndex]);
|
|
spline[newKnotIndex] = knot;
|
|
}
|
|
|
|
//Redo all links
|
|
foreach (var linkedKnots in splineLinks)
|
|
{
|
|
if(linkedKnots.Count == 1)
|
|
continue;
|
|
|
|
var originalKnot = linkedKnots[0];
|
|
originalKnot = new SplineKnotIndex(originalKnot.Spline,
|
|
originalKnot.Spline.Equals(splineInfo.Index) ? spline.Count - 1 - originalKnot.Knot : originalKnot.Knot);
|
|
for(int i = 1; i < linkedKnots.Count; ++i)
|
|
{
|
|
var knotInfo = linkedKnots[i];
|
|
if (knotInfo.Spline.Equals(splineInfo.Index))
|
|
linkedKnots[i] = new SplineKnotIndex(splineInfo.Index, spline.Count - 1 - knotInfo.Knot);
|
|
|
|
splineInfo.Container.LinkKnots(originalKnot, linkedKnots[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverses the flow direction of a spline. Should only be used with Splines that aren't inside any container.
|
|
/// </summary>
|
|
/// <param name="spline">The spline to reverse.</param>
|
|
public static void ReverseFlow(Spline spline)
|
|
{
|
|
var knots = spline.ToArray();
|
|
var tangentModes = new TangentMode[spline.Count];
|
|
|
|
for (int i = 0; i < tangentModes.Length; ++i)
|
|
tangentModes[i] = spline.GetTangentMode(i);
|
|
|
|
// Reverse order and tangents
|
|
for (int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
|
|
{
|
|
var knot = knots[previousKnotIndex];
|
|
var tangentIn = knot.TangentIn;
|
|
var tangentOut = knot.TangentOut;
|
|
|
|
var reverseRotation = quaternion.AxisAngle(math.mul(knot.Rotation, math.up()), math.radians(180));
|
|
reverseRotation = math.normalizesafe(reverseRotation);
|
|
|
|
// Reverse the tangents to keep the same shape while reversing the order
|
|
knot.Rotation = math.mul(reverseRotation, knot.Rotation);
|
|
if (tangentModes[previousKnotIndex] is TangentMode.Broken)
|
|
{
|
|
var localRot = quaternion.AxisAngle(math.up(), math.radians(180));
|
|
knot.TangentIn = math.rotate(localRot, tangentOut);
|
|
knot.TangentOut = math.rotate(localRot, tangentIn);
|
|
}
|
|
else if (tangentModes[previousKnotIndex] is TangentMode.Continuous)
|
|
{
|
|
knot.TangentIn = -tangentOut;
|
|
knot.TangentOut = -tangentIn;
|
|
}
|
|
|
|
var newKnotIndex = spline.Count - 1 - previousKnotIndex;
|
|
spline.SetTangentMode(newKnotIndex, tangentModes[previousKnotIndex]);
|
|
spline[newKnotIndex] = knot;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Joins two splines together at the specified knots. The two splines must belong to the same container and
|
|
/// the knots must be at an extremity of their respective splines.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="mainKnot">The first spline extremity to join.</param>
|
|
/// <param name="otherKnot">The second spline extremity to join.</param>
|
|
/// <returns> The `SplineKnotIndex` of the junction knot.</returns>
|
|
/// <remarks>
|
|
/// In case a spline needs to be reversed to join the two extremities, the mainKnot defines which spline will be kept.
|
|
/// Hence, the second one will be the reversed one.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentException">
|
|
/// An exception is thrown on impossible join request (out of bounds
|
|
/// parameters, knots on the same spline, non-extremity knots)
|
|
/// </exception>
|
|
public static SplineKnotIndex JoinSplinesOnKnots(this ISplineContainer container, SplineKnotIndex mainKnot, SplineKnotIndex otherKnot)
|
|
{
|
|
if (mainKnot.Spline == otherKnot.Spline)
|
|
{
|
|
Debug.LogError("Trying to join Knots already belonging to the same spline.", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
|
|
if (mainKnot.Spline < 0 || mainKnot.Spline > container.Splines.Count)
|
|
{
|
|
Debug.LogError($"Spline index {mainKnot.Spline} does not exist for the current container.", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
if(otherKnot.Spline < 0 || otherKnot.Spline > container.Splines.Count)
|
|
{
|
|
Debug.LogError($"Spline index {otherKnot.Spline} does not exist for the current container.", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
|
|
if(mainKnot.Knot < 0 || mainKnot.Knot > container.Splines[mainKnot.Spline].Count)
|
|
{
|
|
Debug.LogError($"Knot index {mainKnot.Knot} does not exist for the current container for Spline[{mainKnot.Spline}].", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
if(otherKnot.Knot < 0 || otherKnot.Knot > container.Splines[otherKnot.Spline].Count)
|
|
{
|
|
Debug.LogError($"Knot index {otherKnot.Knot} does not exist for the current container for Spline[{otherKnot.Spline}].", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
|
|
if(mainKnot.Knot != 0 && mainKnot.Knot != container.Splines[mainKnot.Spline].Count - 1)
|
|
{
|
|
Debug.LogError($"Knot index {mainKnot.Knot} is not an extremity knot for the current container for Spline[{mainKnot.Spline}]." +
|
|
"Only extremity knots can be joined.", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
if(otherKnot.Knot != 0 && otherKnot.Knot != container.Splines[otherKnot.Spline].Count - 1)
|
|
{
|
|
Debug.LogError($"Knot index {otherKnot.Knot} is not an extremity knot for the current container for Spline[{otherKnot.Spline}]." +
|
|
"Only extremity knots can be joined.", container as SplineContainer);
|
|
return new SplineKnotIndex();
|
|
}
|
|
|
|
var isActiveKnotAtStart = mainKnot.Knot == 0;
|
|
var isOtherKnotAtStart = otherKnot.Knot == 0;
|
|
|
|
//Reverse spline if needed, this is needed when the 2 knots are both starts or ends of their respective spline
|
|
if(isActiveKnotAtStart == isOtherKnotAtStart)
|
|
//We give more importance to the main knot, so we reverse the spline associated to otherKnot
|
|
container.ReverseFlow(otherKnot.Spline);
|
|
|
|
//Save Links
|
|
var links = new List<List<SplineKnotIndex>>();
|
|
|
|
//Save relevant data before joining the splines
|
|
var activeSplineIndex = mainKnot.Spline;
|
|
var activeSpline = container.Splines[activeSplineIndex];
|
|
var activeSplineCount = activeSpline.Count;
|
|
var otherSplineIndex = otherKnot.Spline;
|
|
var otherSpline = container.Splines[otherSplineIndex];
|
|
var otherSplineCount = otherSpline.Count;
|
|
|
|
// Get all LinkedKnots on the splines
|
|
for(int i = 0; i < activeSplineCount; ++i)
|
|
{
|
|
var knot = new SplineKnotIndex(mainKnot.Spline, i);
|
|
links.Add(container.KnotLinkCollection.GetKnotLinks(knot).ToList());
|
|
}
|
|
for(int i = 0; i < otherSplineCount; ++i)
|
|
{
|
|
var knot = new SplineKnotIndex(otherKnot.Spline, i);
|
|
links.Add(container.KnotLinkCollection.GetKnotLinks(knot).ToList());
|
|
}
|
|
|
|
//Unlink all knots in the spline
|
|
foreach(var linkedKnots in links)
|
|
container.UnlinkKnots(linkedKnots);
|
|
|
|
if(otherSplineCount > 1)
|
|
{
|
|
//Join Splines
|
|
if(isActiveKnotAtStart)
|
|
{
|
|
//All position from the other spline must be added before the knot A
|
|
//Don't copy the last knot of the other spline as this is the one to join
|
|
for(int i = otherSplineCount - 2; i >= 0 ; i--)
|
|
activeSpline.Insert(0,otherSpline[i], otherSpline.GetTangentMode(i));
|
|
}
|
|
else
|
|
{
|
|
//All position from the other spline must be added after the knot A
|
|
//Don't copy the first knot of the other spline as this is the one to join
|
|
for(int i = 1; i < otherSplineCount; i++)
|
|
activeSpline.Add(otherSpline[i], otherSpline.GetTangentMode(i));
|
|
}
|
|
}
|
|
|
|
container.RemoveSplineAt(otherSplineIndex);
|
|
var newActiveSplineIndex = otherSplineIndex > activeSplineIndex ? activeSplineIndex : mainKnot.Spline - 1;
|
|
|
|
//Restore links
|
|
foreach (var linkedKnots in links)
|
|
{
|
|
if(linkedKnots.Count == 1)
|
|
continue;
|
|
|
|
for(int i = 0; i < linkedKnots.Count; ++i)
|
|
{
|
|
var knotInfo = linkedKnots[i];
|
|
if(knotInfo.Spline == activeSplineIndex || knotInfo.Spline == otherSplineIndex)
|
|
{
|
|
var newIndex = knotInfo.Knot;
|
|
|
|
if(knotInfo.Spline == activeSplineIndex && isActiveKnotAtStart)
|
|
newIndex += otherSplineCount - 1;
|
|
|
|
if(knotInfo.Spline == otherSplineIndex && !isActiveKnotAtStart)
|
|
newIndex += activeSplineCount - 1;
|
|
|
|
linkedKnots[i] = new SplineKnotIndex(activeSplineIndex, newIndex);
|
|
}
|
|
else
|
|
{
|
|
if(knotInfo.Spline > otherSplineIndex)
|
|
linkedKnots[i] = new SplineKnotIndex(knotInfo.Spline - 1,knotInfo.Knot);
|
|
}
|
|
}
|
|
|
|
var originalKnot = linkedKnots[0];
|
|
for(int i = 1; i < linkedKnots.Count; ++i)
|
|
container.LinkKnots(originalKnot, linkedKnots[i]);
|
|
}
|
|
|
|
return new SplineKnotIndex(newActiveSplineIndex, isActiveKnotAtStart ? otherSplineCount - 1 : mainKnot.Knot);
|
|
}
|
|
|
|
internal static SplineKnotIndex DuplicateKnot(this ISplineContainer container, SplineKnotIndex originalKnot, int targetIndex)
|
|
{
|
|
var spline = container.Splines[originalKnot.Spline];
|
|
var knot = spline[originalKnot.Knot];
|
|
spline.Insert(targetIndex, knot);
|
|
spline.SetTangentMode(targetIndex, spline.GetTangentMode(originalKnot.Knot));
|
|
return new SplineKnotIndex(originalKnot.Spline, targetIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Duplicate a spline between 2 knots of a source spline.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="fromKnot">The start knot to use to duplicate the spline.</param>
|
|
/// <param name="toKnot">The end knot to use to duplicate the spline.</param>
|
|
/// <param name="newSplineIndex">The index of the new created spline in the container.</param>
|
|
/// <exception cref="ArgumentException">Thrown when the provided knots aren't valid or aren't on the same spline.</exception>
|
|
public static void DuplicateSpline(
|
|
this ISplineContainer container,
|
|
SplineKnotIndex fromKnot,
|
|
SplineKnotIndex toKnot,
|
|
out int newSplineIndex)
|
|
{
|
|
newSplineIndex = -1;
|
|
|
|
if (!(fromKnot.IsValid() && toKnot.IsValid()))
|
|
throw new ArgumentException("Duplicate failed: The 2 provided knots must be valid knots.");
|
|
|
|
if(fromKnot.Spline != toKnot.Spline)
|
|
throw new ArgumentException("Duplicate failed: The 2 provided knots must be on the same Spline.");
|
|
|
|
var duplicate = container.AddSpline();
|
|
|
|
//Copy knots to the new spline
|
|
int startIndex = Math.Min(fromKnot.Knot, toKnot.Knot);
|
|
int toIndex = Math.Max(fromKnot.Knot, toKnot.Knot);
|
|
|
|
var originalSplineIndex = fromKnot.Spline;
|
|
var originalSpline = container.Splines[originalSplineIndex];
|
|
newSplineIndex = container.Splines.Count - 1;
|
|
for (int i = startIndex; i <= toIndex; ++i)
|
|
{
|
|
duplicate.Add(originalSpline[i], originalSpline.GetTangentMode(i));
|
|
|
|
// If the old knot had any links we link both old and new knot.
|
|
// This will result in the new knot linking to what the old knot was linked to.
|
|
// The old knot being removed right after that takes care of cleaning the old knot from the link.
|
|
if (container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(originalSplineIndex, i), out _))
|
|
container.KnotLinkCollection.Link(new SplineKnotIndex(originalSplineIndex, i), new SplineKnotIndex(newSplineIndex, i - startIndex));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a spline in a SplineContainer into two splines at a specified knot.
|
|
/// </summary>
|
|
/// <param name="container">The target SplineContainer.</param>
|
|
/// <param name="knotInfo">The SplineKnotIndex of the spline to split. </param>
|
|
/// <returns>The `SplineKnotIndex` of the first knot of the spline that was created from the split.</returns>
|
|
/// <exception cref="IndexOutOfRangeException">
|
|
/// An exception is thrown when the knot belongs to a spline not contained by
|
|
/// the provided container or when the knot index is out of range.
|
|
/// </exception>
|
|
public static SplineKnotIndex SplitSplineOnKnot(this ISplineContainer container, SplineKnotIndex knotInfo)
|
|
{
|
|
if (knotInfo.Spline < 0 || knotInfo.Spline > container.Splines.Count)
|
|
{
|
|
throw new IndexOutOfRangeException($"Spline index {knotInfo.Spline} does not exist for the current container.");
|
|
}
|
|
|
|
if (knotInfo.Knot < 0 || knotInfo.Knot > container.Splines[knotInfo.Spline].Count)
|
|
{
|
|
throw new IndexOutOfRangeException($"Knot index {knotInfo.Knot} does not exist for the current container for Spline[{knotInfo.Spline}].");
|
|
}
|
|
|
|
var originalSpline = container.Splines[knotInfo.Spline];
|
|
if (originalSpline.Closed)
|
|
{
|
|
// Unclose and add a knot to the end with the same data
|
|
originalSpline.Closed = false;
|
|
var firstKnot = new SplineKnotIndex(knotInfo.Spline, 0);
|
|
var lastKnot = container.DuplicateKnot(firstKnot, originalSpline.Count);
|
|
|
|
// If the knot was the first one of the spline nothing else needs to be done to split the knot
|
|
if (knotInfo.Knot == 0)
|
|
return firstKnot;
|
|
|
|
// If the knot wasn't the first one we also need need to link both ends of the spline to keep the same spline we had before
|
|
// Link knots is recording the changes to prefab instances so we don't need to add a call to RecordPrefabInstancePropertyModifications
|
|
container.LinkKnots(firstKnot, lastKnot);
|
|
}
|
|
// If not closed, split does nothing on the extremity of the spline
|
|
else if (knotInfo.Knot == 0 || knotInfo.Knot == originalSpline.Count - 1)
|
|
return knotInfo;
|
|
|
|
container.DuplicateSpline(knotInfo, new SplineKnotIndex(knotInfo.Spline, originalSpline.Count - 1),
|
|
out int newSplineIndex);
|
|
originalSpline.Resize(knotInfo.Knot + 1);
|
|
return new SplineKnotIndex(newSplineIndex, 0);
|
|
}
|
|
}
|
|
}
|