using UnityEngine; using System; using System.Collections.Generic; /// /// Collection of methods for working with splines. /// This is based on the great learning tutorial Curves and Splines by Jasper Flick. /// /// References: /// http://catlikecoding.com/unity/tutorials/curves-and-splines/ /// http://answers.unity3d.com/questions/374333/positioning-an-object-on-a-spline-relative-to-play.html /// namespace PlaygroundSplines { /// /// Holds information about a spline and contains functions for working with the nodes and bezier handles. /// [ExecuteInEditMode()] public class PlaygroundSpline : MonoBehaviour { /// /// The list of nodes and bezier handles making the spline. /// [SerializeField] private List points = new List(); /// /// The modes of the bezier handles. /// [SerializeField] private List modes = new List(); /// /// Determines if the spline is looping. /// [SerializeField] private bool loop; /// /// The list of transform nodes to set positions live of an existing node. /// [HideInInspector] public List transformNodes = new List(); /// /// Determines if the spline time should be reversed. If you'd like to physically reverse the arrays making the spline then call ReverseAllNodes(). /// [HideInInspector] public bool reverse; /// /// The time offset of the spline. /// [HideInInspector] public float timeOffset; /// /// The position offset of the spline in relation to its transform. /// [HideInInspector] public Vector3 positionOffset; [HideInInspector] public Transform splineTransform; [HideInInspector] public Matrix4x4 splineTransformMx; [HideInInspector] public List usedBy = new List(); [HideInInspector] public float fixedVelocityOnNewNode = .5f; [HideInInspector] public bool moveTransformsAsBeziers = false; [HideInInspector] public bool exportWithNodeStructure = false; // Gizmos public static bool drawSplinePreviews = true; [HideInInspector] public bool drawGizmo = true; [HideInInspector] public float bezierWidth = 2f; #if UNITY_EDITOR void OnDrawGizmos () { if (drawSplinePreviews && drawGizmo) { Color innerBezier = new Color(1f,1f,1f,1f); Color outerBezier = new Color(.5f,.5f,0,.2f); Vector3 p0 = ShowPoint(0); for (int i = 1; i < ControlPointCount; i += 3) { Vector3 p1 = ShowPoint(i); Vector3 p2 = ShowPoint(i + 1); Vector3 p3 = ShowPoint(i + 2); UnityEditor.Handles.DrawBezier(p0, p3, p1, p2, innerBezier, null, bezierWidth); UnityEditor.Handles.DrawBezier(p0, p3, p1, p2, outerBezier, null, bezierWidth*10f); p0 = p3; } } } Vector3 ShowPoint (int index) { return transformNodes[index].IsAvailable()? GetPoint(index)+positionOffset : splineTransform.TransformPoint(GetInversePoint(index)+positionOffset); } #endif Vector3 previousPosition; Quaternion previousRotation; Vector3 previousScale; bool isReady; public bool IsReady () { return isReady; } /// /// Adds a user to the spline. This helps keeping track of which objects are using the spline. /// /// true, if user was added, false otherwise. /// . public bool AddUser (Transform thisTransform) { if (!usedBy.Contains(thisTransform)) { usedBy.Add (thisTransform); return true; } return false; } /// /// Removes a user from the spline. This helps keeping track of which objects are using the spline. /// /// true, if user was removed, false otherwise. /// . public bool RemoveUser (Transform thisTransform) { if (usedBy.Contains(thisTransform)) { usedBy.Remove (thisTransform); return true; } return false; } /// /// Determines whether this spline has the user of passed in transform. /// /// true if this spline has the user of the passed in transform; otherwise, false. /// This transform. public bool HasUser (Transform thisTransform) { return usedBy.Contains (thisTransform); } /// /// Gets or sets a value indicating whether this is set to loop. /// /// true if set to loop; otherwise, false. public bool Loop { get { return loop; } set { loop = value; if (value == true && NodeCount>1) { modes[modes.Count - 1] = modes[0]; SetControlPoint(0, points[0]); } } } /// /// Gets the control point count. /// /// The control point count. public int ControlPointCount { get { return points.Count; } } /// /// Gets the control point. /// /// The control point. /// Index. public Vector3 GetControlPoint (int index) { return GetPoint(index); } /// /// Sets the control point and withdraws the offset. /// /// Index. /// Point. /// Offset. public void SetControlPoint (int index, Vector3 point, Vector3 offset) { SetControlPoint(index, point-offset); } /// /// Sets the control point. /// /// Index. /// Position. public void SetControlPoint (int index, Vector3 point) { if (index<0) index = 0; if (index % 3 == 0) { Vector3 delta = (point - GetPoint(index)); Vector3 v; if (loop) { if (index == 0) { //if (!PointHasTransform(1)) {v = GetPoint(1); SetPoint(1, v+delta);} //if (!PointHasTransform(points.Count-2)) {v = GetPoint(points.Count-2); SetPoint(points.Count-2, v+delta);} if (moveTransformsAsBeziers || !PointHasTransform(points.Count-1)) {SetPoint(points.Count-1, point);} } else if (index == points.Count - 1) { //if (!PointHasTransform(0)) {SetPoint(0, point);} //if (!PointHasTransform(1)) {v = GetPoint(1); SetPoint(1, v+delta);} //if (!PointHasTransform(index-1)) {v = GetPoint(index-1); SetPoint(index-1, v+delta);} } else { //if (!PointHasTransform(index-1)) {v = GetPoint(index-1); SetPoint(index-1, v+delta);} //if (!PointHasTransform(index+1)) {v = GetPoint(index+1); SetPoint(index+1, v+delta);} } } else { if (index > 0) { if (moveTransformsAsBeziers || !PointHasTransform(index-1)) {v = GetPoint(index-1); SetPoint(index-1, v+delta);} } if (index + 1 < points.Count) { if (moveTransformsAsBeziers || !PointHasTransform(index+1)) {v = GetPoint(index+1); SetPoint(index+1, v+delta);} } } } SetPoint(index, point); EnforceMode(index); } /// /// Sets all points from an array. Please ensure the same length of your passed in vectors as PlaygroundSpline.ControlPointCount. /// /// Vectors. public void SetPoints (Vector3[] vectors) { if (vectors.Length!=points.Count) { Debug.Log ("Please ensure the same length of your passed in vectors ("+vectors.Length+") as the current points ("+points.Count+"). Use PlaygroundSpline.ControlPointCount to get the current count."); return; } for (int i = 0; i /// Moves the entire spline separate from its transform component. Use this if you'd like to offset the spline from its transform separately from the positionOffset. /// /// The amount to move the spline in Units. public void TranslateSpline (Vector3 translation) { for (int i = 0; i= points.Count) { enforcedIndex = 1; } } else { fixedIndex = middleIndex + 1; if (fixedIndex >= points.Count) { fixedIndex = 1; } enforcedIndex = middleIndex - 1; if (enforcedIndex < 0) { enforcedIndex = points.Count - 2; } } Vector3 middle = GetPoint(middleIndex); Vector3 enforcedTangent = middle - GetPoint(fixedIndex); if (mode == BezierControlPointMode.Aligned) { enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, GetPoint(enforcedIndex)); } if (moveTransformsAsBeziers || !PointHasTransform(enforcedIndex)) SetPoint(enforcedIndex, middle + enforcedTangent); } public int NodeCount { get { return (points.Count - 1) / 3; } } /// /// Get position from time. /// /// The point in world space. /// Time. public Vector3 GetPoint (float t) { int i; if (reverse) { t = 1f-t; t = (t-timeOffset)%1f; if (t<0) t = 1f+t; } else t = (t+timeOffset)%1f; if (t >= 1f) { //t = 1f; i = points.Count - 4; } else { t = Mathf.Clamp01(t) * NodeCount; i = (int)t; t -= i; i *= 3; } return splineTransformMx.MultiplyPoint3x4(Bezier.GetPoint(GetInversePoint(i), GetInversePoint(i + 1), GetInversePoint(i + 2), GetInversePoint(i + 3), t)+positionOffset); } public Vector3 GetVelocity (float t) { int i; if (reverse) t = 1f-t; t = (t+timeOffset)%1f; if (t >= 1f) { t = 1f; i = points.Count - 4; } else { t = Mathf.Clamp01(t) * NodeCount; i = (int)t; t -= i; i *= 3; } return splineTransformMx.MultiplyPoint3x4(Bezier.GetFirstDerivative(GetInversePoint(i), GetInversePoint(i + 1), GetInversePoint(i + 2), GetInversePoint(i + 3), t)+positionOffset) - previousPosition; } /// /// Get position from node index in the spline. If the node consists of an available transform its position will be returned, otherwise the user-specified Vector3 position. /// /// The point in world space. /// Index. public Vector3 GetPoint (int index) { if (transformNodes[index].IsAvailable()) return transformNodes[index].GetPosition(); else return points[index]; } public Vector3 GetInversePoint (int index) { if (transformNodes[index].IsAvailable()) return transformNodes[index].GetInvsersePosition(); else return points[index]; } public Vector3 GetPointWorldSpace (int index) { if (transformNodes[index].IsAvailable()) return transformNodes[index].GetPosition(); else return splineTransformMx.MultiplyPoint3x4(points[index]+positionOffset); } /// /// Sets a point to specified position. /// /// Index. /// Position. void SetPoint (int index, Vector3 position) { if (transformNodes[index].IsAvailable()) transformNodes[index].SetPosition(position); else points[index] = position; } /// /// Translates a point. /// /// Index. /// Translation. void TranslatePoint (int index, Vector3 translation) { if (transformNodes[index].IsAvailable()) transformNodes[index].Translate(translation); else points[index] += translation; } // Calculates the best fitting time in the given interval private float CPOB(Vector3 aP, float aStart, float aEnd, int aSteps) { aStart = Mathf.Clamp01(aStart); aEnd = Mathf.Clamp01(aEnd); float step = (aEnd-aStart) / (float)aSteps; float Res = 0; float Ref = float.MaxValue; for (int i = 0; i < aSteps; i++) { float t = aStart + step*i; float L = (GetPoint(t)-aP).sqrMagnitude; if (L < Ref) { Ref = L; Res = t; } } return Res; } public float ClosestTimeFromPoint (Vector3 aP) { float t = CPOB(aP, 0, 1, 10); float delta = 1.0f / 10.0f; for (int i = 0; i < 4; i++) { t = CPOB(aP, t - delta, t + delta, 10); delta /= 9; } return t; } public Vector3 ClosestPointFromPosition (Vector3 aP) { return GetPoint(ClosestTimeFromPoint(aP)); } public Vector3 GetDirection (float t) { return (GetPoint(t+.001f)-GetPoint(t)).normalized; } /// /// Adds a node at the last position of the node index. /// public void AddNode () { AddNode ((points.Count-1)/3); } /// /// Adds a node at specified node index. /// /// Index. public void AddNode (int index) { int nodeIndex = index*3; Vector3 point = GetPoint(nodeIndex); Vector3 direction; if (index>0) { direction = GetPoint(nodeIndex)-GetPoint(nodeIndex-1); } else direction = GetPoint(nodeIndex+1)-GetPoint(nodeIndex); direction*=fixedVelocityOnNewNode; points.InsertRange(nodeIndex+1, new Vector3[3]); point += direction; points[nodeIndex+2] = point; point += direction; points[nodeIndex+1] = point; point += direction; points[nodeIndex+3] = point; transformNodes.InsertRange(nodeIndex+1, new TransformNode[]{new TransformNode(), new TransformNode(), new TransformNode()}); BezierControlPointMode currentIndexMode = modes[index]; modes.Insert (index, new BezierControlPointMode()); modes[index] = currentIndexMode; EnforceMode(index); SetControlPoint((index+1)*3, GetPoint((index+1)*3)); if (loop) { points[points.Count - 1] = points[0]; modes[modes.Count - 1] = modes[0]; EnforceMode(0); } } /// /// Removes the first node in the node index. /// public void RemoveFirst () { RemoveNode(0); } /// /// Removes the last node in the node index. /// public void RemoveLast () { RemoveNode((points.Count-1)/3); } /// /// Removes a node at specified node index. /// /// Index. public void RemoveNode (int index) { index = Mathf.Clamp (index, 0, points.Count-1); int pointIndex = index*3; if (points.Count<=4) return; if (pointIndex0) SetControlPoint((index-1)*3, GetPoint((index-1)*3)); else SetControlPoint(0, GetPoint(0)); } /// /// Reverses all nodes in the node index. /// public void ReverseAllNodes () { points.Reverse(); transformNodes.Reverse(); modes.Reverse(); } public void SwapNodes (int from, int to) { Vector3[] fromPoints = points.GetRange (from, 3).ToArray(); Vector3[] toPoints = points.GetRange (to, 3).ToArray(); TransformNode[] fromTnode = transformNodes.GetRange (from, 3).ToArray(); TransformNode[] toTnode = transformNodes.GetRange (to, 3).ToArray(); BezierControlPointMode fromMode = modes[from]; BezierControlPointMode toMode = modes[to]; for (int i = from; i<3; i++) { points[i] = toPoints[i]; transformNodes[i] = toTnode[i]; } for (int i = to; i<3; i++) { points[i] = fromPoints[i]; transformNodes[i] = fromTnode[i]; } modes[from] = toMode; modes[to] = fromMode; } /// /// Exports all nodes to Transform[]. Enable exportWithNodeStructure to parent each bezier handle to their node. /// /// A built-in array of Transforms. public Transform[] ExportToTransforms () { Transform[] transforms = new Transform[points.Count]; for (int i = 0; i /// Exports all nodes to Vector3[]. /// /// A built-in array of Vector3 public Vector3[] ExportToVector3 () { Vector3[] vectors = new Vector3[points.Count]; for (int i = 0; i /// Reset this Playground Spline. Two nodes and two bezier handles will be created. /// public void Reset () { points = new List { new Vector3(1f, 0f, 0f), new Vector3(2f, 0f, 0f), new Vector3(3f, 0f, 0f), new Vector3(4f, 0f, 0f) }; modes = new List { BezierControlPointMode.Aligned, BezierControlPointMode.Aligned }; transformNodes = new List { new TransformNode(), new TransformNode(), new TransformNode(), new TransformNode() }; } /************************************************************************************************************************************************* MonoBehaviours *************************************************************************************************************************************************/ void OnEnable () { isReady = false; splineTransform = transform; SetMatrix(); } void Update () { SetMatrix(); for (int i = 0; i /// Class for common bezier operations on a spline. /// public static class Bezier { public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) { t = Mathf.Clamp01(t); float oneMinusT = 1f - t; return oneMinusT * oneMinusT * p0 + 2f * oneMinusT * t * p1 + t * t * p2; } public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, float t) { return 2f * (1f - t) * (p1 - p0) + 2f * t * (p2 - p1); } public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { t = Mathf.Clamp01(t); float OneMinusT = 1f - t; return OneMinusT * OneMinusT * OneMinusT * p0 + 3f * OneMinusT * OneMinusT * t * p1 + 3f * OneMinusT * t * t * p2 + t * t * t * p3; } public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { t = Mathf.Clamp01(t); float oneMinusT = 1f - t; return 3f * oneMinusT * oneMinusT * (p1 - p0) + 6f * oneMinusT * t * (p2 - p1) + 3f * t * t * (p3 - p2); } } [Serializable] public class TransformNode { public bool enabled; public Transform transform; bool isAvailable; Vector3 position; Vector3 inversePosition; Vector3 previousPosition; public bool Update (Transform splineTransform) { if (enabled && transform!=null) { previousPosition = position; position = transform.position; inversePosition = splineTransform.InverseTransformPoint(transform.position); isAvailable = true; return true; } isAvailable = false; return false; } public bool IsAvailable () { return enabled&&isAvailable; } public Vector3 GetPosition () { return position; } public Vector3 GetInvsersePosition () { return inversePosition; } public void SetPosition (Vector3 newPosition) { if (transform==null) return; transform.position = newPosition; } public void Translate (Vector3 translation) { if (transform==null) return; transform.position += translation; } public Vector3 GetPositionDelta () { return previousPosition-position; } } public enum SplineMode { Vector3, Transform } /// /// The bezier mode for a spline node. This controls how one bezier handle acts in relation to the other. /// public enum BezierControlPointMode { /// /// Align the angle between the two bezier handles but keep individual lengths. Has a differential smooth in and out angle. /// Aligned, /// /// Align the angle and length between the two bezier handles. Has an equally smooth in and out angle. /// Mirrored, /// /// Bezier handles are freely aligned without consideration to the other. Ables you to have sharp angles. /// Free } }