using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Splines { /// /// A component that holds a list of objects. /// #if UNITY_2021_2_OR_NEWER [Icon(k_IconPath)] #endif [AddComponentMenu("Splines/Spline Container")] [ExecuteAlways] public sealed class SplineContainer : MonoBehaviour, ISplineContainer, ISerializationCallbackReceiver { const string k_IconPath = "Packages/com.unity.splines/Editor/Resources/Icons/SplineComponent.png"; // Keeping a main spline to be backwards compatible with older versions of the spline package [SerializeField, Obsolete, HideInInspector] Spline m_Spline; [SerializeField] Spline[] m_Splines = { new Spline() }; [SerializeField] KnotLinkCollection m_Knots = new KnotLinkCollection(); List<(int previousIndex, int newIndex)> m_ReorderedSplinesIndices = new List<(int, int)>(); List m_RemovedSplinesIndices = new List(); List m_AddedSplinesIndices = new List(); /// /// Invoked any time a spline is added to the container. /// /// /// The parameter corresponds to the spline index. /// public static event Action SplineAdded; /// /// Invoked any time a spline is removed from the container. /// /// /// The parameter corresponds to the spline index. /// public static event Action SplineRemoved; /// /// Invoked any time a spline is reordered in the container. /// /// /// The first parameter corresponds to the previous spline index, /// the second parameter corresponds to the new spline index. /// public static event Action SplineReordered; ReadOnlyCollection m_ReadOnlySplines; Dictionary m_NativeSplinesCache = new(); float4x4 m_NativeSplinesCacheTransform = float4x4.identity; /// /// The list of all splines attached to that container. /// public IReadOnlyList Splines { get => m_ReadOnlySplines ??= new ReadOnlyCollection(m_Splines); set { if (value == null) { m_Splines = Array.Empty(); return; } ClearCaches(); DisposeNativeSplinesCache(); for (var i = 0; i < m_Splines.Length; i++) { var index = IndexOf(value, m_Splines[i]); if (index == -1) m_RemovedSplinesIndices.Add(i); else if (index != i) m_ReorderedSplinesIndices.Add((i, index)); } for (var i = 0; i < value.Count; i++) { var index = Array.FindIndex(m_Splines, spline => spline == value[i]); if (index == -1) m_AddedSplinesIndices.Add(i); } m_Splines = new Spline[value.Count]; for (int i = 0; i < m_Splines.Length; ++i) { m_Splines[i] = value[i]; if (IsNonUniformlyScaled) GetOrBakeNativeSpline(m_Splines[i]); } m_ReadOnlySplines = new ReadOnlyCollection(m_Splines); foreach (var removedIndex in m_RemovedSplinesIndices) SplineRemoved?.Invoke(this, removedIndex); foreach (var addedIndex in m_AddedSplinesIndices) SplineAdded?.Invoke(this, addedIndex); foreach (var reorderedSpline in m_ReorderedSplinesIndices) SplineReordered?.Invoke(this, reorderedSpline.previousIndex, reorderedSpline.newIndex); } } static int IndexOf(IReadOnlyList self, Spline elementToFind) { for (var i = 0; i < self.Count; i++) { var element = self[i]; if (element == elementToFind) return i; } return -1; } /// /// A collection of all linked knots. Linked knots can be on different splines. However, knots can /// only link to other knots within the same container. This collection is used to maintain /// the validity of the links when operations such as knot insertions or removals are performed on the splines. /// public KnotLinkCollection KnotLinkCollection => m_Knots; /// /// Gets or sets the at . /// /// The zero-based index of the element to get or set. public Spline this[int index] => m_Splines[index]; void OnEnable() { Spline.Changed += OnSplineChanged; } void OnDisable() { Spline.Changed -= OnSplineChanged; } void OnDestroy() { DisposeNativeSplinesCache(); } /// /// Ensure that all caches contain valid data. Call this to avoid unexpected performance costs when evaluating /// splines data. Caches remain valid until any part of the splines state is modified. /// public void Warmup() { for (int i = 0; i < Splines.Count; ++i) { var spline = Splines[i]; spline.Warmup(); GetOrBakeNativeSpline(spline); } } internal void ClearCaches() { m_ReorderedSplinesIndices.Clear(); m_RemovedSplinesIndices.Clear(); m_AddedSplinesIndices.Clear(); m_ReadOnlySplines = null; } void DisposeNativeSplinesCache() { // Dispose cached native splines foreach (var splineToNativePair in m_NativeSplinesCache) splineToNativePair.Value.Dispose(); m_NativeSplinesCache.Clear(); } void OnSplineChanged(Spline spline, int index, SplineModification modificationType) { var splineIndex = Array.IndexOf(m_Splines, spline); if (splineIndex < 0) return; switch (modificationType) { case SplineModification.KnotModified: this.SetLinkedKnotPosition(new SplineKnotIndex(splineIndex, index)); break; case SplineModification.KnotReordered: case SplineModification.KnotInserted: m_Knots.KnotInserted(splineIndex, index); break; case SplineModification.KnotRemoved: m_Knots.KnotRemoved(splineIndex, index); break; } m_NativeSplinesCache.Remove(spline); } void OnKnotModified(Spline spline, int index) { var splineIndex = Array.IndexOf(m_Splines, spline); if (splineIndex >= 0) this.SetLinkedKnotPosition(new SplineKnotIndex(splineIndex, index)); } bool IsNonUniformlyScaled { get { float3 lossyScale = transform.lossyScale; return !math.all(lossyScale == lossyScale.x); } } /// /// The main attached to this component. /// public Spline Spline { get => m_Splines.Length > 0 ? m_Splines[0] : null; set { if (m_Splines.Length > 0) m_Splines[0] = value; } } /// /// Computes interpolated position, direction and upDirection at ratio t. Calling this method to get the /// 3 vectors is faster than calling independently EvaluateSplinePosition, EvaluateSplineTangent and EvaluateSplineUpVector /// for the same time t as it reduces some redundant computation. /// /// A value between 0 and 1 representing the ratio along the curve. /// The output variable for the float3 position at t. /// The output variable for the float3 tangent at t. /// The output variable for the float3 up direction at t. /// Boolean value, true if a valid set of output variables as been computed. public bool Evaluate(float t, out float3 position, out float3 tangent, out float3 upVector) => Evaluate(0, t, out position, out tangent, out upVector); /// /// Computes the interpolated position, direction and upDirection at ratio t for the spline at index `splineIndex`. Calling this method to get the /// 3 vectors is faster than calling independently EvaluateSplinePosition, EvaluateSplineTangent and EvaluateSplineUpVector /// for the same time t as it reduces some redundant computation. /// /// The index of the spline to evaluate. /// A value between 0 and 1 that represents the ratio along the curve. /// The output variable for the float3 position at t. /// The output variable for the float3 tangent at t. /// The output variable for the float3 up direction at t. /// True if a valid set of output variables is computed and false otherwise. public bool Evaluate(int splineIndex, float t, out float3 position, out float3 tangent, out float3 upVector) => Evaluate(m_Splines[splineIndex], t, out position, out tangent, out upVector); /// /// Gets the interpolated position, direction, and upDirection at ratio t for a spline. This method gets the three /// vectors faster than EvaluateSplinePosition, EvaluateSplineTangent and EvaluateSplineUpVector for the same /// time t, because it reduces some redundant computation. /// /// The spline type. /// The spline to evaluate. /// A value between 0 and 1 that represents the ratio along the curve. /// The output variable for the float3 position at t. /// The output variable for the float3 tangent at t. /// The output variable for the float3 up direction at t. /// True if a valid set of output variables is computed and false otherwise. public bool Evaluate(T spline, float t, out float3 position, out float3 tangent, out float3 upVector) where T : ISpline { if (spline == null) { position = float3.zero; tangent = new float3(0, 0, 1); upVector = new float3(0, 1, 0); return false; } if (IsNonUniformlyScaled) return SplineUtility.Evaluate(GetOrBakeNativeSpline(spline), t, out position, out tangent, out upVector); var evaluationStatus = SplineUtility.Evaluate(spline, t, out position, out tangent, out upVector); if (evaluationStatus) { position = transform.TransformPoint(position); tangent = transform.TransformVector(tangent); upVector = transform.TransformDirection(upVector); } return evaluationStatus; } /// /// Evaluates the position of a point, t, on a spline in world space. /// /// A value between 0 and 1 representing a percentage of the curve. /// A tangent vector. public float3 EvaluatePosition(float t) => EvaluatePosition(0, t); /// /// Evaluates the position of a point, t, on a spline at an index, `splineIndex`, in world space. /// /// The index of the spline to evaluate. /// A value between 0 and 1 representing a percentage of the curve. /// A world position along the spline. public float3 EvaluatePosition(int splineIndex, float t) => EvaluatePosition(m_Splines[splineIndex], t); /// /// Evaluates the position of a point, t, on a given spline, in world space. /// /// The spline type. /// The spline to evaluate. /// A value between 0 and 1 representing a percentage of the curve. /// A world position along the spline. public float3 EvaluatePosition(T spline, float t) where T : ISpline { if (spline== null) return float.PositiveInfinity; if (IsNonUniformlyScaled) return SplineUtility.EvaluatePosition(GetOrBakeNativeSpline(spline), t); return transform.TransformPoint(SplineUtility.EvaluatePosition(spline, t)); } /// /// Evaluates the tangent vector of a point, t, on a spline in world space. /// /// A value between 0 and 1 representing a percentage of entire spline. /// The computed tangent vector. public float3 EvaluateTangent(float t) => EvaluateTangent(0, t); /// /// Evaluates the tangent vector of a point, t, on a spline at an index, `splineIndex`, in world space. /// /// The index of the spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed tangent vector. public float3 EvaluateTangent(int splineIndex, float t) => EvaluateTangent(m_Splines[splineIndex], t); /// /// Evaluates the tangent vector of a point, t, on a given spline, in world space. /// /// The spline type. /// The spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed tangent vector. public float3 EvaluateTangent(T spline, float t) where T : ISpline { if (spline == null) return float.PositiveInfinity; if (IsNonUniformlyScaled) return SplineUtility.EvaluateTangent(GetOrBakeNativeSpline(spline), t); return transform.TransformVector(SplineUtility.EvaluateTangent(spline, t)); } /// /// Evaluates the up vector of a point, t, on a spline in world space. /// /// A value between 0 and 1 representing a percentage of entire spline. /// The computed up direction. public float3 EvaluateUpVector(float t) => EvaluateUpVector(0, t); /// /// Evaluates the up vector of a point, t, on a spline at an index, `splineIndex`, in world space. /// /// The index of the Spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed up direction. public float3 EvaluateUpVector(int splineIndex, float t) => EvaluateUpVector(m_Splines[splineIndex], t); /// /// Evaluates the up vector of a point, t, on a given spline, in world space. /// /// The spline type. /// The Spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed up direction. public float3 EvaluateUpVector(T spline, float t) where T : ISpline { if (spline == null) return float3.zero; if (IsNonUniformlyScaled) return SplineUtility.EvaluateUpVector(GetOrBakeNativeSpline(spline), t); //Using TransformDirection as up direction is not sensible to scale. return transform.TransformDirection(SplineUtility.EvaluateUpVector(spline, t)); } /// /// Evaluates the acceleration vector of a point, t, on a spline in world space. /// /// A value between 0 and 1 representing a percentage of entire spline. /// The computed acceleration vector. public float3 EvaluateAcceleration(float t) => EvaluateAcceleration(0, t); /// /// Evaluates the acceleration vector of a point, t, on a spline at an index, `splineIndex, in world space. /// /// The index of the spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed acceleration vector. public float3 EvaluateAcceleration(int splineIndex, float t) => EvaluateAcceleration(m_Splines[splineIndex], t); /// /// Evaluates the acceleration vector of a point, t, on a given Spline, in world space. /// /// The spline type. /// The Spline to evaluate. /// A value between 0 and 1 representing a percentage of entire spline. /// The computed acceleration vector. public float3 EvaluateAcceleration(T spline, float t) where T : ISpline { if (spline == null) return float3.zero; if (IsNonUniformlyScaled) return SplineUtility.EvaluateAcceleration(GetOrBakeNativeSpline(spline), t); return transform.TransformVector(SplineUtility.EvaluateAcceleration(spline, t)); } /// /// Calculate the length of in world space. /// /// The length of in world space public float CalculateLength() => CalculateLength(0); /// /// Calculates the length of `Splines[splineIndex]` in world space. /// /// The index of the spline to evaluate. /// The length of `Splines[splineIndex]` in world space public float CalculateLength(int splineIndex) { return SplineUtility.CalculateLength(m_Splines[splineIndex], transform.localToWorldMatrix); } /// /// See ISerializationCallbackReceiver. /// public void OnBeforeSerialize() { } /// /// See ISerializationCallbackReceiver. /// public void OnAfterDeserialize() { #pragma warning disable 612, 618 if (m_Spline != null && m_Spline.Count > 0) { if (m_Splines == null || m_Splines.Length == 0 || m_Splines.Length == 1 && m_Splines[0].Count == 0) { m_Splines = new[] { m_Spline }; m_ReadOnlySplines = new ReadOnlyCollection(m_Splines); } m_Spline = new Spline(); //Clear spline } #pragma warning restore 612, 618 } struct SplineToNative { public ISpline spline; public NativeSpline nativeSpline; } static List m_AllocPreventionHelperBuffer = new (capacity:32); NativeSpline GetOrBakeNativeSpline(T spline) where T : ISpline { // Build native spline if we don't have one cached for this spline if (!m_NativeSplinesCache.TryGetValue(spline, out var cachedNativeSpline)) { m_NativeSplinesCacheTransform = transform.localToWorldMatrix; cachedNativeSpline = new NativeSpline(spline, m_NativeSplinesCacheTransform, true, Allocator.Persistent); m_NativeSplinesCache.Add(spline, cachedNativeSpline); } // or if the cached spline was baked using different transform else if (!MathUtility.All(m_NativeSplinesCacheTransform, transform.localToWorldMatrix)) { // Since we have one m_NativeSplinesCacheTransform for all cached splines we need to rebake all m_NativeSplinesCacheTransform = transform.localToWorldMatrix; m_AllocPreventionHelperBuffer.Clear(); foreach (ISpline iSpline in m_NativeSplinesCache.Keys) { var oldCache = m_NativeSplinesCache[iSpline]; var newCache = new NativeSpline(spline, m_NativeSplinesCacheTransform, true, Allocator.Persistent); if (iSpline == (ISpline)spline) cachedNativeSpline = newCache; oldCache.Dispose(); // Doing this dance as dictionaries can't be modified mid-iteration m_AllocPreventionHelperBuffer.Add(new SplineToNative() { spline = iSpline, nativeSpline = newCache }); } for (int i = 0; i < m_AllocPreventionHelperBuffer.Count; ++i) { var dataToSet = m_AllocPreventionHelperBuffer[i]; m_NativeSplinesCache[dataToSet.spline] = dataToSet.nativeSpline; } } return cachedNativeSpline; } } }