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
}
}