using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
using UnityEngine.Serialization;
[AddComponentMenu("Miscellaneous/Bezier Spline")]
public class Bezier3DSpline : MonoBehaviour{
public int KnotCount { get { return curves.Length+(closed?0:1); } }
public int CurveCount { get { return curves.Length; } }
/// <summary> Interpolation steps per curve </summary>
public int cacheDensity { get { return _cacheDensity; } }
[SerializeField] protected int _cacheDensity = 60;
/// <summary> Whether the end of the spline connects to the start of the spline </summary>
public bool closed { get { return _closed; } }
[SerializeField] protected bool _closed = false;
/// <summary> Sum of all curve lengths </summary>
public float totalLength { get { return _totalLength; } }
[SerializeField] protected float _totalLength = 2.370671f;
/// <summary> Curves of the spline </summary>
[SerializeField] protected Bezier3DCurve[] curves = new Bezier3DCurve[] { new Bezier3DCurve( new Vector3(-1,0,0), new Vector3(1,0,1), new Vector3(-1,0,-1), new Vector3(1,0,0), 60)};
/// <summary> Automatic knots don't have handles. Instead they have a percentage and adjust their handles accordingly. A percentage of 0 indicates that this is not automatic </summary>
[SerializeField] protected List<float> autoKnot = new List<float>() { 0, 0 };
[SerializeField] protected List<NullableQuaternion> orientations = new List<NullableQuaternion>() { new NullableQuaternion(null), new NullableQuaternion(null) };
[SerializeField] protected Vector3[] tangentCache = new Vector3[0];
#region Public methods
#region Public: get
public float DistanceToTime(float dist) {
float t = 0f;
for (int i = 0; i < CurveCount; i++) {
if (curves[i].length < dist) {
dist -= curves[i].length;
t += 1f / CurveCount;
}
else {
t += curves[i].Dist2Time(dist) / CurveCount;
return t;
}
}
return 1f;
}
/// <summary> Get <see cref="Bezier3DCurve"/> by index </summary>
public Bezier3DCurve GetCurve(int i) {
if (i >= CurveCount || i < 0) throw new System.IndexOutOfRangeException("Cuve index " + i + " out of range");
return curves[i];
}
/// <summary> Return <see cref="Knot"/> info in local coordinates </summary>
public Knot GetKnot(int i) {
if (i == 0) {
if (closed) return new Knot(curves[0].a, curves[CurveCount - 1].c, curves[0].b, autoKnot[i], orientations[i].NullableValue);
else return new Knot(curves[0].a, Vector3.zero, curves[0].b, autoKnot[i], orientations[i].NullableValue);
}
else if (i == CurveCount) {
return new Knot(curves[i - 1].d, curves[i - 1].c, Vector3.zero, autoKnot[i], orientations[i].NullableValue);
}
else {
return new Knot(curves[i].a, curves[i - 1].c, curves[i].b, autoKnot[i], orientations[i].NullableValue);
}
}
#region Public get: Forward
/// <summary> Return forward vector at set distance along the <see cref="Bezier3DSpline"/>. </summary>
public Vector3 GetForward(float dist) {
return transform.TransformDirection(GetForwardLocal(dist));
}
/// <summary> Return forward vector at set distance along the <see cref="Bezier3DSpline"/> in local coordinates. </summary>
public Vector3 GetForwardLocal(float dist) {
Bezier3DCurve curve = GetCurveDistance(dist, out dist);
return curve.GetForward(curve.Dist2Time(dist));
}
/// <summary> Return forward vector at set distance along the <see cref="Bezier3DSpline"/>. Uses approximation. </summary>
public Vector3 GetForwardFast(float dist) {
return transform.TransformDirection(GetForwardLocalFast(dist));
}
/// <summary> Return forward vector at set distance along the <see cref="Bezier3DSpline"/> in local coordinates. Uses approximation. </summary>
public Vector3 GetForwardLocalFast(float dist) {
Bezier3DCurve curve = GetCurveDistance(dist, out dist);
return curve.GetForwardFast(curve.Dist2Time(dist));
}
#endregion
#region Public get: Up
/// <summary> Return up vector at set distance along the <see cref="Bezier3DSpline"/>. </summary>
public Vector3 GetUp(float dist) {
return GetUp(dist, GetForward(dist), false);
}
/// <summary> Return up vector at set distance along the <see cref="Bezier3DSpline"/> in local coordinates. </summary>
public Vector3 GetUpLocal(float dist) {
return GetUp(dist, GetForward(dist), true);
}
#endregion
#region Public get: Point
/// <summary> Return up vector at set distance along the <see cref="Bezier3DSpline"/>. </summary>
public Vector3 GetPoint(float dist) {
Bezier3DCurve curve = GetCurveDistance(dist, out dist);
return transform.TransformPoint(curve.GetPoint(curve.Dist2Time(dist)));
}
/// <summary> Return point at lerped position where 0 = start, 1 = end </summary>
public Vector3 GetPointLocal(float dist) {
Bezier3DCurve curve = GetCurveDistance(dist, out dist);
return curve.GetPoint(curve.Dist2Time(dist));
}
#endregion
#region Public get: Orientation
public Quaternion GetOrientation(float dist) {
Vector3 forward = GetForward(dist);
Vector3 up = GetUp(dist, forward, false);
if (forward.sqrMagnitude != 0) return Quaternion.LookRotation(forward, up);
else return Quaternion.identity;
}
public Quaternion GetOrientationFast(float dist) {
Vector3 forward = GetForwardFast(dist);
Vector3 up = GetUp(dist, forward, false);
if (forward.sqrMagnitude != 0) return Quaternion.LookRotation(forward, up);
else return Quaternion.identity;
}
public Quaternion GetOrientationLocal(float dist) {
Vector3 forward = GetForwardLocal(dist);
Vector3 up = GetUp(dist, forward, true);
if (forward.sqrMagnitude != 0) return Quaternion.LookRotation(forward, up);
else return Quaternion.identity;
}
public Quaternion GetOrientationLocalFast(float dist) {
Vector3 forward = GetForwardLocalFast(dist);
Vector3 up = GetUp(dist, forward, true);
if (forward.sqrMagnitude != 0) return Quaternion.LookRotation(forward, up);
else return Quaternion.identity;
}
#endregion
#endregion
#region Public: Set
/// <summary> Setting spline to closed will generate an extra curve, connecting end point to start point </summary>
public void SetClosed(bool closed) {
if (closed != _closed) {
_closed = closed;
if (closed) {
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.Add(new Bezier3DCurve(curves[CurveCount - 1].d, -curves[CurveCount - 1].c, -curves[0].b, curves[0].a, cacheDensity));
curves = curveList.ToArray();
}
else {
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.RemoveAt(CurveCount - 1);
curves = curveList.ToArray();
}
_totalLength = GetTotalLength();
}
}
/// <summary> Recache all individual curves with new step amount </summary>
/// <param name="density"> Number of steps per curve </param>
public void SetCacheDensity(int steps) {
_cacheDensity = steps;
for (int i = 0; i < CurveCount; i++) {
curves[i] = new Bezier3DCurve(curves[i].a, curves[i].b, curves[i].c, curves[i].d, _cacheDensity);
}
_totalLength = GetTotalLength();
}
public void RemoveKnot(int i) {
if (i == 0) {
Knot knot = GetKnot(1);
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.RemoveAt(0);
curves = curveList.ToArray();
autoKnot.RemoveAt(0);
orientations.RemoveAt(0);
SetKnot(0, knot);
}
else if (i == CurveCount) {
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.RemoveAt(i - 1);
curves = curveList.ToArray();
autoKnot.RemoveAt(i);
orientations.RemoveAt(i);
if (autoKnot[KnotCount - 1] != 0) SetKnot(KnotCount - 1, GetKnot(KnotCount - 1));
}
else {
int preCurveIndex, postCurveIndex;
GetCurveIndicesForKnot(i, out preCurveIndex, out postCurveIndex);
Bezier3DCurve curve = new Bezier3DCurve(curves[preCurveIndex].a, curves[preCurveIndex].b, curves[postCurveIndex].c, curves[postCurveIndex].d, cacheDensity);
curves[preCurveIndex] = curve;
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.RemoveAt(postCurveIndex);
curves = curveList.ToArray();
autoKnot.RemoveAt(i);
orientations.RemoveAt(i);
int preKnotIndex, postKnotIndex;
GetKnotIndicesForKnot(i, out preKnotIndex, out postKnotIndex);
SetKnot(preKnotIndex, GetKnot(preKnotIndex));
}
}
public void AddKnot(Knot knot) {
Bezier3DCurve curve = new Bezier3DCurve(curves[CurveCount - 1].d, -curves[CurveCount - 1].c, knot.handleIn, knot.position, cacheDensity);
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.Add(curve);
curves = curveList.ToArray();
autoKnot.Add(knot.auto);
orientations.Add(knot.orientation);
SetKnot(KnotCount - 1, knot);
}
public void InsertKnot(int i, Knot knot) {
Bezier3DCurve curve;
if (i == 0) curve = new Bezier3DCurve(knot.position, knot.handleOut, -curves[0].b, curves[0].a, cacheDensity);
else if (i == CurveCount) curve = GetCurve(i - 1);
else curve = GetCurve(i);
List<Bezier3DCurve> curveList = new List<Bezier3DCurve>(curves);
curveList.Insert(i, curve);
curves = curveList.ToArray();
autoKnot.Insert(i, knot.auto);
orientations.Insert(i, knot.orientation);
SetKnot(i, knot);
}
/// <summary> Set Knot info in local coordinates </summary>
public void SetKnot(int i, Knot knot) {
//If knot is set to auto, adjust handles accordingly
orientations[i] = knot.orientation;
autoKnot[i] = knot.auto;
if (knot.auto != 0) AutomateHandles(i, ref knot);
//Automate knots around this knot
int preKnotIndex, postKnotIndex;
GetKnotIndicesForKnot(i, out preKnotIndex, out postKnotIndex);
Knot preKnot = new Knot();
if (preKnotIndex != -1) {
preKnot = GetKnot(preKnotIndex);
if (preKnot.auto != 0) {
int preKnotPreCurveIndex, preKnotPostCurveIndex;
GetCurveIndicesForKnot(preKnotIndex, out preKnotPreCurveIndex, out preKnotPostCurveIndex);
if (preKnotPreCurveIndex != -1) {
AutomateHandles(preKnotIndex, ref preKnot, curves[preKnotPreCurveIndex].a, knot.position);
curves[preKnotPreCurveIndex] = new Bezier3DCurve(curves[preKnotPreCurveIndex].a, curves[preKnotPreCurveIndex].b, preKnot.handleIn, preKnot.position, cacheDensity);
}
else {
AutomateHandles(preKnotIndex, ref preKnot, Vector3.zero, knot.position);
}
}
}
Knot postKnot = new Knot();
if (postKnotIndex != -1) {
postKnot = GetKnot(postKnotIndex);
if (postKnot.auto != 0) {
int postKnotPreCurveIndex, postKnotPostCurveIndex;
GetCurveIndicesForKnot(postKnotIndex, out postKnotPreCurveIndex, out postKnotPostCurveIndex);
if (postKnotPostCurveIndex != -1) {
AutomateHandles(postKnotIndex, ref postKnot, knot.position, curves[postKnotPostCurveIndex].d);
curves[postKnotPostCurveIndex] = new Bezier3DCurve(postKnot.position, postKnot.handleOut, curves[postKnotPostCurveIndex].c, curves[postKnotPostCurveIndex].d, cacheDensity);
}
else {
AutomateHandles(postKnotIndex, ref postKnot, knot.position, Vector3.zero);
}
}
}
//Get the curve indices in direct contact with knot
int preCurveIndex, postCurveIndex;
GetCurveIndicesForKnot(i, out preCurveIndex, out postCurveIndex);
//Adjust curves in direct contact with the knot
if (preCurveIndex != -1) curves[preCurveIndex] = new Bezier3DCurve(preKnot.position, preKnot.handleOut, knot.handleIn, knot.position, cacheDensity);
if (postCurveIndex != -1) curves[postCurveIndex] = new Bezier3DCurve(knot.position, knot.handleOut, postKnot.handleIn, postKnot.position, cacheDensity);
_totalLength = GetTotalLength();
}
/// <summary> Flip the spline </summary>
public void Flip() {
Bezier3DCurve[] curves = new Bezier3DCurve[CurveCount];
for (int i = 0; i < CurveCount; i++) {
curves[CurveCount - 1 - i] = new Bezier3DCurve(this.curves[i].d, this.curves[i].c, this.curves[i].b, this.curves[i].a, cacheDensity);
}
this.curves = curves;
autoKnot.Reverse();
orientations.Reverse();
}
#endregion
#endregion
public struct Knot {
public Vector3 position;
public Vector3 handleIn;
public Vector3 handleOut;
public float auto;
public Quaternion? orientation;
/// <summary> Constructor </summary>
/// <param name="position">Position of the knot local to spline transform</param>
/// <param name="handleIn">Left handle position local to knot position</param>
/// <param name="handleOut">Right handle position local to knot position</param>
/// <param name="automatic">Any value above 0 will result in an automatically configured knot (ignoring handle inputs)</param>
public Knot(Vector3 position, Vector3 handleIn, Vector3 handleOut, float automatic = 0f, Quaternion? orientation = null) {
this.position = position;
this.handleIn = handleIn;
this.handleOut = handleOut;
this.auto = automatic;
this.orientation = orientation;
}
}
#region Private methods
private Vector3 GetUp(float dist, Vector3 tangent, bool local) {
float t = DistanceToTime(dist);
t *= CurveCount;
Quaternion rot_a = Quaternion.identity, rot_b = Quaternion.identity;
int t_a = 0, t_b = 0;
//Find preceding rotation
for (int i = Mathf.Min((int)t, CurveCount); i >= 0; i--) {
i = (int)Mathf.Repeat(i, KnotCount - 1);
if (orientations[i].HasValue) {
rot_a = orientations[i].Value;
rot_b = orientations[i].Value;
t_a = i;
t_b = i;
break;
}
}
//Find proceding rotation
for (int i = Mathf.Max((int)t + 1, 0); i < orientations.Count; i++) {
if (orientations[i].HasValue) {
rot_b = orientations[i].Value;
t_b = i;
break;
}
}
t = Mathf.InverseLerp(t_a, t_b, t);
Quaternion rot = Quaternion.Lerp(rot_a, rot_b, t);
if (!local) rot = transform.rotation * rot;
//Debug.Log(t_a + " / " + t_b + " / " + t);
return Vector3.ProjectOnPlane(rot * Vector3.up, tangent).normalized;
}
/// <summary> Get the curve indices in direct contact with knot </summary>
private void GetCurveIndicesForKnot(int knotIndex, out int preCurveIndex, out int postCurveIndex) {
//Get the curve index in direct contact with, before the knot
preCurveIndex = -1;
if (knotIndex != 0) preCurveIndex = knotIndex - 1;
else if (closed) preCurveIndex = CurveCount - 1;
//Get the curve index in direct contact with, after the knot
postCurveIndex = -1;
if (knotIndex != CurveCount) postCurveIndex = knotIndex;
else if (closed) postCurveIndex = 0;
}
/// <summary> Get the knot indices in direct contact with knot </summary>
private void GetKnotIndicesForKnot(int knotIndex, out int preKnotIndex, out int postKnotIndex) {
//Get the curve index in direct contact with, before the knot
preKnotIndex = -1;
if (knotIndex != 0) preKnotIndex = knotIndex - 1;
else if (closed) preKnotIndex = KnotCount - 1;
//Get the curve index in direct contact with, after the knot
postKnotIndex = -1;
if (knotIndex != KnotCount - 1) postKnotIndex = knotIndex + 1;
else if (closed) postKnotIndex = 0;
}
private Bezier3DCurve GetCurve(float splineT, out float curveT) {
splineT *= CurveCount;
for (int i = 0; i < CurveCount; i++) {
if (splineT > 1f) splineT -= 1f;
else {
curveT = splineT;
return curves[i];
}
}
curveT = 1f;
return curves[CurveCount - 1];
}
private Bezier3DCurve GetCurveDistance(float splineDist, out float curveDist) {
for (int i = 0; i < CurveCount; i++) {
if (curves[i].length < splineDist) splineDist -= curves[i].length;
else {
curveDist = splineDist;
return curves[i];
}
}
curveDist = curves[CurveCount -1].length;
return curves[CurveCount - 1];
}
/// <summary> Automate handles based on previous and next point positions </summary>
private void AutomateHandles(int i, ref Knot knot) {
//Terminology: Points are referred to as A B and C
//A = prev point, B = current point, C = next point
Vector3 prevPos;
if (i != 0) prevPos = curves[i - 1].a;
else if (closed) prevPos = curves[CurveCount - 1].a;
else prevPos = Vector3.zero;
Vector3 nextPos;
if (i != KnotCount - 1) nextPos = curves[i].d;
else if (closed) nextPos = curves[0].a;
else nextPos = Vector3.zero;
AutomateHandles(i, ref knot, prevPos, nextPos);
}
/// <summary> Automate handles based on previous and next point positions </summary>
private void AutomateHandles(int i, ref Knot knot, Vector3 prevPos, Vector3 nextPos) {
//Terminology: Points are referred to as A B and C
//A = prev point, B = current point, C = next point
float amount = knot.auto;
//Calculate directional vectors
Vector3 AB = knot.position - prevPos;
Vector3 CB = knot.position - nextPos;
//Calculate the across vector
Vector3 AB_CB = (CB.normalized - AB.normalized).normalized;
if (!closed) {
if (i == 0) {
knot.handleOut = CB * -amount;
}
else if (i == CurveCount) {
knot.handleIn = AB * -amount;
}
else {
knot.handleOut = -AB_CB * CB.magnitude * amount;
knot.handleIn = AB_CB * AB.magnitude * amount;
}
}
else {
if (KnotCount == 2) {
Vector3 left = new Vector3(AB.z, 0,-AB.x) * amount;
if (i == 0) {
knot.handleIn = left;
knot.handleOut = -left;
}
if (i == 1) {
knot.handleIn = left;
knot.handleOut = -left;
}
}
else {
knot.handleIn = AB_CB * AB.magnitude * amount;
knot.handleOut = -AB_CB * CB.magnitude * amount;
}
}
}
private float GetTotalLength() {
float length = 0f;
for (int i = 0; i < CurveCount; i++) {
length += curves[i].length;
}
return length;
}
#endregion
/// <summary> Unity doesn't support serialization of nullable types, so here's a custom struct that does exactly the same thing </summary>
[Serializable]
protected struct NullableQuaternion {
public Quaternion Value { get { return rotation; } }
public Quaternion? NullableValue { get { if (hasValue) return rotation; else return null; } }
public bool HasValue { get { return hasValue; } }
[SerializeField] private Quaternion rotation;
[SerializeField] private bool hasValue;
public NullableQuaternion(Quaternion? rot) {
rotation = rot.HasValue?rot.Value:Quaternion.identity;
hasValue = rot.HasValue;
}
// User-defined conversion from nullable type to NullableQuaternion
public static implicit operator NullableQuaternion(Quaternion? r) {
return new NullableQuaternion(r);
}
}
#if UNITY_EDITOR
void OnDrawGizmos() {
//Set color depending on selection
if (Array.IndexOf(UnityEditor.Selection.gameObjects, gameObject) >= 0) {
Gizmos.color = Color.yellow;
} else Gizmos.color = new Color(1, 0.6f, 0f);
//Loop through each curve in spline
for (int i = 0; i < CurveCount; i++) {
Bezier3DCurve curve = GetCurve(i);
//Get curve in world space
Vector3 a, b, c, d;
a = transform.TransformPoint(curve.a);
b = transform.TransformPoint(curve.b + curve.a);
c = transform.TransformPoint(curve.c + curve.d);
d = transform.TransformPoint(curve.d);
int segments = 50;
float spacing = 1f / segments;
Vector3 prev = Bezier3DCurve.GetPoint(a, b, c, d, 0f);
for (int k = 0; k <= segments; k++) {
Vector3 cur = Bezier3DCurve.GetPoint(a, b, c, d, k * spacing);
Gizmos.DrawLine(prev, cur);
prev = cur;
}
}
}
#endif
}