using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Mathematics; namespace UnityEngine.Splines { /// /// To calculate a value at some distance along a spline, interpolation is required. The IInterpolator interface /// allows you to define how data is interpreted given a start value, end value, and normalized interpolation value /// (commonly referred to as 't'). /// /// /// The data type to interpolate. /// public interface IInterpolator { /// /// Calculate a value between from and to at time interval. /// /// The starting value. At t = 0 this method should return an unmodified 'from' value. /// The ending value. At t = 1 this method should return an unmodified 'to' value. /// A percentage between 'from' and 'to'. Must be between 0 and 1. /// A value between 'from' and 'to'. T Interpolate(T from, T to, float t); } /// /// Describes the unit of measurement used by . /// public enum PathIndexUnit { /// /// The 't' value used when interpolating is measured in game units. Values range from 0 (start of Spline) to /// (end of Spline). /// Distance, /// /// The 't' value used when interpolating is normalized. Values range from 0 (start of Spline) to 1 (end of Spline). /// Normalized, /// /// The 't' value used when interpolating is defined by knot indices and a fractional value representing the /// normalized interpolation between the specific knot index and the next knot. /// Knot } // Used internally to try preserving index positioning for SplineData embedded in Spline class. It is not very // useful outside of this specific context, which is why it is not public. Additionally, in the future I'd like to // explore passing the previous and new spline length to enable better preservation of distance and normalized // indices. interface ISplineModificationHandler { void OnSplineModified(SplineModificationData info); } /// /// The SplineData{T} class is used to store information relative to a without coupling data /// directly to the Spline class. SplineData can store any type of data, and provides options for how to index /// DataPoints. /// /// The type of data to store. [Serializable] public class SplineData : IEnumerable>, ISplineModificationHandler { static readonly DataPointComparer> k_DataPointComparer = new DataPointComparer>(); [SerializeField] PathIndexUnit m_IndexUnit = PathIndexUnit.Knot; [SerializeField] T m_DefaultValue; [SerializeField] List> m_DataPoints = new List>(); // When working with IMGUI it's necessary to keep keys array consistent while a hotcontrol is active. Most // accessors will keep the SplineData sorted, but sometimes it's not possible. [NonSerialized] bool m_NeedsSort; /// /// Access a by index. DataPoints are sorted in ascending order by the /// value. /// /// /// The index of the DataPoint to access. /// public DataPoint this[int index] { get => m_DataPoints[index]; set => SetDataPoint(index, value); } /// /// PathIndexUnit defines how SplineData will interpret 't' values when interpolating data. /// /// public PathIndexUnit PathIndexUnit { get => m_IndexUnit; set => m_IndexUnit = value; } /// /// Default value to use when a new DataPoint is automatically added. /// /// public T DefaultValue { get => m_DefaultValue; set => m_DefaultValue = value; } /// /// How many data points the SplineData collection contains. /// public int Count => m_DataPoints.Count; /// /// The DataPoint Indexes of the current SplineData. /// public IEnumerable Indexes => m_DataPoints.Select(dp => dp.Index); /// /// Invoked any time a SplineData is modified. /// /// /// In the editor this can be invoked many times per-frame. /// Prefer to use when working with /// splines in the editor. /// [Obsolete("Use Changed instead.", false)] public event Action changed; /// /// Invoked any time a SplineData is modified. /// /// /// In the editor this can be invoked many times per-frame. /// Prefer to use when working with /// splines in the editor. /// public event Action Changed; #if UNITY_EDITOR bool m_Dirty = false; internal static Action> afterSplineDataWasModified; #endif /// /// Create a new SplineData instance. /// public SplineData() {} /// /// Create a new SplineData instance with a single value in it. /// /// /// A single value to add to the spline data at t = 0.` /// public SplineData(T init) { Add(0f, init); SetDirty(); } /// /// Create a new SplineData instance and initialize it with a collection of data points. DataPoints will be sorted and stored /// in ascending order by . /// /// /// A collection of DataPoints to initialize SplineData.` /// public SplineData(IEnumerable> dataPoints) { foreach(var dataPoint in dataPoints) Add(dataPoint); SetDirty(); } void SetDirty() { changed?.Invoke(); Changed?.Invoke(); #if UNITY_EDITOR if(m_Dirty) return; m_Dirty = true; UnityEditor.EditorApplication.delayCall += () => { afterSplineDataWasModified?.Invoke(this); m_Dirty = false; }; #endif } /// /// Append a to this collection. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// The data to store in the created data point. /// public void Add(float t, T data) => Add(new DataPoint(t, data)); /// /// Append a to this collection. /// /// /// The data point to append to the SplineData collection. /// /// /// The index of the inserted dataPoint. /// public int Add(DataPoint dataPoint) { int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer); index = index < 0 ? ~index : index; m_DataPoints.Insert(index, dataPoint); SetDirty(); return index; } /// /// Append a with default value to this collection. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// If true will use to set the value, otherwise will interpolate the value regarding the closest DataPoints. /// /// /// The index of the inserted dataPoint. /// public int AddDataPointWithDefaultValue(float t, bool useDefaultValue = false) { var dataPoint = new DataPoint(t, m_DefaultValue); if(Count == 0 || useDefaultValue) return Add(dataPoint); if(Count == 1) { dataPoint.Value = m_DataPoints[0].Value; return Add(dataPoint); } int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer); index = index < 0 ? ~index : index; dataPoint.Value = index == 0 ? m_DataPoints[0].Value : m_DataPoints[index-1].Value; m_DataPoints.Insert(index, dataPoint); SetDirty(); return index; } /// /// Remove a at index. /// /// The index to remove. public void RemoveAt(int index) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); m_DataPoints.RemoveAt(index); SetDirty(); } /// /// Remove a from this collection, if one exists. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// True is deleted, false otherwise. /// public bool RemoveDataPoint(float t) { var removed = m_DataPoints.Remove(m_DataPoints.FirstOrDefault(point => Mathf.Approximately(point.Index, t))); if(removed) SetDirty(); return removed; } /// /// Move a (if it exists) from this collection, from one index to the another. /// /// The index of the to move. This is the index into the collection, not the PathIndexUnit.Knot. /// The new index () for this . /// The index of the modified . /// Thrown when the specified is out of range. public int MoveDataPoint(int index, float newIndex) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); var dataPoint = m_DataPoints[index]; if(Mathf.Approximately(newIndex, dataPoint.Index)) return index; RemoveAt(index); dataPoint.Index = newIndex; int newRealIndex = Add(dataPoint); return newRealIndex; } /// /// Remove all data points. /// public void Clear() { m_DataPoints.Clear(); SetDirty(); } static int Wrap(int value, int lowerBound, int upperBound) { int range_size = upperBound - lowerBound + 1; if(value < lowerBound) value += range_size * ( ( lowerBound - value ) / range_size + 1 ); return lowerBound + ( value - lowerBound ) % range_size; } int ResolveBinaryIndex(int index, bool wrap) { index = ( index < 0 ? ~index : index ) - 1; if(wrap) index = Wrap(index, 0, Count - 1); return math.clamp(index, 0, Count - 1); } (int, int, float) GetIndex(float t, float splineLength, int knotCount, bool closed) { if(Count < 1) return default; SortIfNecessary(); float splineLengthInIndexUnits = splineLength; if(m_IndexUnit == PathIndexUnit.Normalized) splineLengthInIndexUnits = 1f; else if(m_IndexUnit == PathIndexUnit.Knot) splineLengthInIndexUnits = closed ? knotCount : knotCount - 1; float maxDataPointTime = m_DataPoints[m_DataPoints.Count - 1].Index; float maxRevolutionLength = math.ceil(maxDataPointTime / splineLengthInIndexUnits) * splineLengthInIndexUnits; float maxTime = closed ? math.max(maxRevolutionLength, splineLengthInIndexUnits) : splineLengthInIndexUnits; if(closed) { if(t < 0f) t = maxTime + t % maxTime; else t = t % maxTime; } else t = math.clamp(t, 0f, maxTime); int index = m_DataPoints.BinarySearch(0, Count, new DataPoint(t, default), k_DataPointComparer); int fromIndex = ResolveBinaryIndex(index, closed); int toIndex = closed ? ( fromIndex + 1 ) % Count : math.clamp(fromIndex + 1, 0, Count - 1); float fromTime = m_DataPoints[fromIndex].Index; float toTime = m_DataPoints[toIndex].Index; if(fromIndex > toIndex) toTime += maxTime; if(t < fromTime && closed) t += maxTime; if(fromTime == toTime) return ( fromIndex, toIndex, fromTime ); return ( fromIndex, toIndex, math.abs(math.max(0f, t - fromTime) / ( toTime - fromTime )) ); } /// /// Calculate an interpolated value at a given 't' along a spline. /// /// The Spline to interpolate. /// The interpolator value. How this is interpreted is defined by . /// The that is represented as. /// The to use. A collection of commonly used /// interpolators are available in the namespace. /// The IInterpolator type. /// The Spline type. /// An interpolated value. public T Evaluate(TSpline spline, float t, PathIndexUnit indexUnit, TInterpolator interpolator) where TSpline : ISpline where TInterpolator : IInterpolator { if(indexUnit == m_IndexUnit) return Evaluate(spline, t, interpolator); return Evaluate(spline, SplineUtility.ConvertIndexUnit(spline, t, indexUnit, m_IndexUnit), interpolator); } /// /// Calculate an interpolated value at a given 't' along a spline. /// /// The Spline to interpolate. /// The interpolator value. How this is interpreted is defined by . /// The to use. A collection of commonly used /// interpolators are available in the namespace. /// The IInterpolator type. /// The Spline type. /// An interpolated value. public T Evaluate(TSpline spline, float t, TInterpolator interpolator) where TSpline : ISpline where TInterpolator : IInterpolator { var knotCount = spline.Count; if(knotCount < 1 || m_DataPoints.Count == 0) return default; var indices = GetIndex(t, spline.GetLength(), knotCount, spline.Closed); DataPoint a = m_DataPoints[indices.Item1]; DataPoint b = m_DataPoints[indices.Item2]; return interpolator.Interpolate(a.Value, b.Value, indices.Item3); } /// /// Set the data for a at an index. /// /// The DataPoint index. /// The value to set. /// /// Using this method will search the DataPoint list and invoke the /// callback every time. This may be inconvenient when setting multiple DataPoints during the same frame. /// In this case, consider calling for each DataPoint, followed by /// a single call to . Note that the call to is /// optional and can be omitted if DataPoint sorting is not required and the callback /// should not be invoked. /// public void SetDataPoint(int index, DataPoint value) { if(index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index"); RemoveAt(index); Add(value); SetDirty(); } /// /// Set the data for a at an index. /// /// The DataPoint index. /// The value to set. /// /// Use this method as an altenative to when manual control /// over DataPoint sorting and the callback is required. /// See also . /// public void SetDataPointNoSort(int index, DataPoint value) { if(index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index"); // could optimize this by storing affected range m_NeedsSort = true; m_DataPoints[index] = value; } /// /// Triggers sorting of the list if the data is dirty. /// /// /// Call this after a single or series of calls to . /// This will trigger DataPoint sort and invoke the callback. /// This method has two main use cases: to prevent frequent callback /// calls within the same frame and to reduce multiple DataPoints list searches /// to a single sort in performance critical paths. /// public void SortIfNecessary() { if(!m_NeedsSort) return; m_NeedsSort = false; m_DataPoints.Sort(); SetDirty(); } internal void ForceSort() { m_NeedsSort = true; SortIfNecessary(); } /// /// Given a spline and a target PathIndex Unit, convert the SplineData to a new PathIndexUnit without changing the final positions on the Spline. /// /// The Spline type. /// The Spline to use for the conversion, this is necessary to compute most of PathIndexUnits. /// The unit to convert SplineData to. public void ConvertPathUnit(TSplineType spline, PathIndexUnit toUnit) where TSplineType : ISpline { if(toUnit == m_IndexUnit) return; for(int i = 0; i < m_DataPoints.Count; i++) { var dataPoint = m_DataPoints[i]; var newTime = spline.ConvertIndexUnit(dataPoint.Index, m_IndexUnit, toUnit); m_DataPoints[i] = new DataPoint(newTime, dataPoint.Value); } m_IndexUnit = toUnit; SetDirty(); } /// /// Given a time value using a certain PathIndexUnit type, calculate the normalized time value regarding a specific spline. /// /// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits. /// The time to normalize in the original PathIndexUnit. /// The Spline type. /// The normalized time. public float GetNormalizedInterpolation(TSplineType spline, float t) where TSplineType : ISpline { return SplineUtility.GetNormalizedInterpolation(spline, t, m_IndexUnit); } /// /// Returns an enumerator that iterates through the DataPoints collection. /// /// /// An IEnumerator{DataPoint{T}} for this collection. public IEnumerator> GetEnumerator() { for (int i = 0, c = Count; i < c; ++i) yield return m_DataPoints[i]; } /// /// Returns an enumerator that iterates through the DataPoints collection. /// /// /// An IEnumerator{DataPoint{T}} for this collection. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); static float WrapInt(float index, int lowerBound, int upperBound) { return Wrap((int)math.floor(index), lowerBound, upperBound) + math.frac(index); } static float ClampInt(float index, int lowerBound, int upperBound) { return math.clamp((int)math.floor(index), lowerBound, upperBound) + math.frac(index); } // IMPORTANT - NOT PUBLIC API. See ISplineModificationHandler for more information. /// /// Attempts to preserve knot indices relative to their current position after a Spline has been modified. This /// is only valid for SplineData that is indexed using . /// /// /// This function is only valid for PathIndexUnit.Knot because other representations are (1) implicitly better /// suited to handle knot insertion/deletion while preserving locality, and (2) converting to the Knot index /// format for the purposes of order preservation would need to be done _before_ knot insertion/deletion in /// order to be correct. /// void ISplineModificationHandler.OnSplineModified(SplineModificationData data) { if (m_IndexUnit != PathIndexUnit.Knot) return; if (data.Modification == SplineModification.KnotModified || data.Modification == SplineModification.KnotReordered || data.Modification == SplineModification.Default) return; var editedKnotOldIdx = data.KnotIndex; var prevLength = data.PrevCurveLength; var nextLength = data.NextCurveLength; var dataPointsToRemove = new List(); for (int dataIdx = 0, c = Count; dataIdx < c; ++dataIdx) { var point = m_DataPoints[dataIdx]; var dataKnotOldIdx = (int)math.floor(point.Index); var fracIdx = point.Index - dataKnotOldIdx; if (data.Modification == SplineModification.KnotInserted) { var currentLength = data.Spline.GetCurveLength(data.Spline.PreviousIndex(editedKnotOldIdx)); if (dataKnotOldIdx == editedKnotOldIdx - 1) { if (fracIdx < currentLength / prevLength) point.Index = dataKnotOldIdx + fracIdx * (prevLength / currentLength); else point.Index = (dataKnotOldIdx + 1) + (fracIdx * prevLength - currentLength) / (prevLength - currentLength); } else if (data.Spline.Closed && dataKnotOldIdx == data.Spline.Count - 2 && editedKnotOldIdx == 0) { if (fracIdx < currentLength / prevLength) point.Index = (dataKnotOldIdx + 1) + fracIdx * (prevLength / currentLength); else point.Index = (fracIdx * prevLength - currentLength) / (prevLength - currentLength); } else point.Index += 1; } else if (data.Modification == SplineModification.KnotRemoved) { // The spline is cleared. if (editedKnotOldIdx == -1) { dataPointsToRemove.Add(dataIdx); continue; } var removingHardLink = (fracIdx == 0f && dataKnotOldIdx == editedKnotOldIdx); var removingEndKnots = !data.Spline.Closed && ((dataKnotOldIdx <= 0 && editedKnotOldIdx == 0) || // Removed first curve with data points on it. (editedKnotOldIdx == data.Spline.Count && // Removed last curve math.ceil(point.Index) >= editedKnotOldIdx)); // and data point is either on last curve or beyond it/clamped. if (removingHardLink || removingEndKnots || data.Spline.Count == 1) dataPointsToRemove.Add(dataIdx); else if (dataKnotOldIdx == editedKnotOldIdx - 1) point.Index = dataKnotOldIdx + fracIdx * prevLength / (prevLength + nextLength); else if (dataKnotOldIdx == editedKnotOldIdx) point.Index = (dataKnotOldIdx - 1) + (prevLength + fracIdx * nextLength) / (prevLength + nextLength); else if ((data.Spline.Closed && editedKnotOldIdx == 0) /*acting on the previous first knot of a closed spline*/ && dataKnotOldIdx == data.Spline.Count /*and considering data that is on the last curve closing the spline*/) point.Index = (dataKnotOldIdx - 1) + (fracIdx * prevLength) / (prevLength + nextLength); else if (dataKnotOldIdx >= editedKnotOldIdx) point.Index -= 1; } else // Closed Modified { if (!data.Spline.Closed && // Spline has been opened and (math.ceil(point.Index) >= data.Spline.Count)) // data point is on connecting curve or the very end of it (treat same as open spline last curve deletion). dataPointsToRemove.Add(dataIdx); } m_DataPoints[dataIdx] = point; } for (int i = dataPointsToRemove.Count - 1; i > -1; --i) { m_DataPoints.RemoveAt(dataPointsToRemove[i]); } } } }