I suggest looking at the code I shared then. It’s not editor only… actually it’s NO editor, (though I have an editor extension for it, not shared).
It’s all pretty much there, you’ll just need to remove the IConfigurableIndexedWaypointPath declaration, as well as need the IWapoint interface:
public interface IWaypoint
{
Vector3 Position { get; set; }
Vector3 Heading { get; set; }
}
Or just use the IWaypointPath interface and its children:
public interface IWaypointPath
{
bool IsClosed { get; set; }
float GetArcLength();
Vector3 GetPositionAt(float t);
Waypoint GetWaypointAt(float t);
Vector3[] GetDetailedPositions(float segmentLength);
}
public interface IIndexedWaypointPath : IWaypointPath, IEnumerable<IWaypoint>
{
int Count { get; }
IWaypoint ControlPoint(int index);
int IndexOf(IWaypoint waypoint);
Vector3 GetPositionAfter(int index, float t);
Waypoint GetWaypointAfter(int index, float t);
/// <summary>
/// Returns data pertaining to the relative position between the 2 control points on either side of 't'.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
RelativePositionData GetRelativePositionData(float t);
void Clean();
}
public struct RelativePositionData
{
public int Index;
public float TPrime;
public RelativePositionData(int index, float t)
{
this.Index = index;
this.TPrime = t;
}
}
public interface IConfigurableIndexedWaypointPath : IIndexedWaypointPath
{
void AddControlPoint(IWaypoint waypoint);
void InsertControlPoint(int index, IWaypoint waypoint);
void ReplaceControlPoint(int index, IWaypoint waypoint);
void RemoveControlPointAt(int index);
void Clear();
}
With that you can have a bezier curve working.
I also have a catmull-rom, bezier chain (rather than spline), and a generic linear chain.
Actually thinking about it, you probably expect a bezier chain, not a bezier spline. The chain is a sequence of quadratic beziers, where as the spline is more like an N-dimensional bezier. Basically with a chain, the curve goes through the end waypoints of each sub curve, where as the spline they head toward the waypoints but never actually touch.
chain is a much simpler implementation, but the heading of the IWaypoint matters a lot more:
/// <summary>
/// Acts like a composite of multiple bezier curves. Where one curve ends, the next starts.
/// The path will travel through control points.
/// </summary>
public class BezierChainPath : IConfigurableIndexedWaypointPath
{
#region Fields
private bool _isClosed;
private List<IWaypoint> _waypoints = new List<IWaypoint>();
private Vector3[] _points; //null if dirty
private float[] _lengths;
private float _totalArcLength = float.NaN;
#endregion
#region CONSTRUCTOR
public BezierChainPath()
{
}
public BezierChainPath(IEnumerable<IWaypoint> waypoints)
{
_waypoints.AddRange(waypoints);
this.Clean_Imp();
}
#endregion
#region Properties
#endregion
#region Methods
private void Clean_Imp()
{
if (_waypoints.Count == 0)
{
_points = new Vector3[] { };
_totalArcLength = float.NaN;
}
else if (_waypoints.Count == 1)
{
_points = new Vector3[] { _waypoints[0].Position };
_totalArcLength = 0f;
}
else
{
_totalArcLength = 0f;
//get points
int cnt = _waypoints.Count;
if (_isClosed) cnt++;
_points = new Vector3[(cnt - 1) * 3 + 1];
_points[0] = _waypoints[0].Position;
for (int i = 1; i < cnt; i++)
{
var w1 = _waypoints[i % _waypoints.Count]; //if we're closed, this will result in the first entry
var w2 = _waypoints[i - 1];
var v = (w1.Position - w2.Position);
var s = v.magnitude / 2.0f; //distance of the control point
float s1 = (w1 is IWeightedWaypoint) ? (w1 as IWeightedWaypoint).Strength : 0f;
float s2 = (w2 is IWeightedWaypoint) ? (w2 as IWeightedWaypoint).Strength : 0f;
int j = (i - 1) * 3 + 1;
_points[j] = w2.Position + (w2.Heading * s1 * s);
_points[j + 1] = w1.Position - (w1.Heading * s2 * s);
_points[j + 2] = w1.Position;
}
//calculate lengths
_lengths = new float[cnt];
for (int i = 0; i < _lengths.Length - 1; i++)
{
int j = i * 3;
var p0 = _points[j];
var p1 = _points[j + 1];
var p2 = _points[j + 2];
var p3 = _points[j + 3];
//approximation is sum of all 3 legs, and the coord from p0 to p3, all divided by 2.
float t = (p1 - p0).magnitude + (p2 - p1).magnitude + (p3 - p2).magnitude * (p3 - p0).magnitude;
_lengths[i] = t / 2.0f;
}
_totalArcLength = 0f;
foreach (var l in _lengths) _totalArcLength += l;
}
}
#endregion
#region IIndexedWaypointPath Interface
public int Count
{
get { return _waypoints.Count; }
}
public bool IsClosed
{
get { return _isClosed; }
set
{
if (_isClosed == value) return;
_isClosed = value;
_points = null;
}
}
public IWaypoint ControlPoint(int index)
{
return _waypoints[index];
}
public int IndexOf(IWaypoint waypoint)
{
return _waypoints.IndexOf(waypoint);
}
public float GetArcLength()
{
if (_points == null) this.Clean_Imp();
return _totalArcLength;
}
public Vector3 GetPositionAt(float t)
{
if (_points == null) this.Clean_Imp();
if (_waypoints.Count == 0) return VectorUtil.NaNVector3;
if (_waypoints.Count == 1) return _waypoints[0].Position;
if (_waypoints.Count == 2) return this.GetPositionAfter(0, t);
float len = _lengths[0];
float tot = len;
int i = 0;
while (tot / _totalArcLength < t && i < _lengths.Length)
{
i++;
len = _lengths[i];
tot += len;
}
float lt = (tot - len) / _totalArcLength;
float ht = tot / _totalArcLength;
float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
return this.GetPositionAfter(i, dt);
}
public Waypoint GetWaypointAt(float t)
{
if (_points == null) this.Clean_Imp();
if (_waypoints.Count == 0) return Waypoint.Invalid;
if (_waypoints.Count == 1) return new Waypoint(_waypoints[0]);
if (_waypoints.Count == 2) return this.GetWaypointAfter(0, t);
float len = _lengths[0];
float tot = len;
int i = 0;
while (tot / _totalArcLength < t && i < _lengths.Length)
{
i++;
len = _lengths[i];
tot += len;
}
float lt = (tot - len) / _totalArcLength;
float ht = tot / _totalArcLength;
float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
return this.GetWaypointAfter(i, dt);
}
public Vector3 GetPositionAfter(int index, float t)
{
if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
if (_points == null) this.Clean_Imp();
if (_waypoints.Count == 0) return VectorUtil.NaNVector3;
if (_waypoints.Count == 1) return _waypoints[0].Position;
if (!_isClosed && index == _waypoints.Count - 1) return _waypoints[index].Position;
t = Mathf.Clamp01(t);
var ft = 1 - t;
var i = index * 3;
var p0 = _points[i];
var p1 = _points[i + 1];
var p2 = _points[i + 2];
var p3 = _points[i + 3];
/*
C(t) = P0*(1-t)^3 + P1*3*t(1-t)^2 + 3*P2*t^2*(1-t) + P3*t^3
dC(t)/dt = T(t) =
-3*P0*(1 - t)^2 +
P1*(3*(1 - t)^2 - 6*(1 - t)*t) +
P2*(6*(1 - t)*t - 3*t^2) +
3*P3*t^2
*/
var p = (ft * ft * ft) * p0 +
3 * (ft * ft) * t * p1 +
3 * (1 - t) * (t * t) * p2 +
(t * t * t) * p3;
var tan = -3 * p0 * (ft * ft) +
p1 * (3 * (ft * ft) - 6 * ft * t) +
p2 * (6 * ft * t - 3 * (t * t)) +
3 * p3 * (t * t);
return p;
}
public Waypoint GetWaypointAfter(int index, float t)
{
if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
if (_points == null) this.Clean_Imp();
if (_waypoints.Count == 0) return Waypoint.Invalid;
if (_waypoints.Count == 1) return new Waypoint(_waypoints[0]);
if (!_isClosed && index == _waypoints.Count - 1) return new Waypoint(_waypoints[index]);
t = Mathf.Clamp01(t);
var ft = 1 - t;
var i = index * 3;
var p0 = _points[i];
var p1 = _points[i + 1];
var p2 = _points[i + 2];
var p3 = _points[i + 3];
/*
C(t) = P0*(1-t)^3 + P1*3*t(1-t)^2 + 3*P2*t^2*(1-t) + P3*t^3
dC(t)/dt = T(t) =
-3*P0*(1 - t)^2 +
P1*(3*(1 - t)^2 - 6*(1 - t)*t) +
P2*(6*(1 - t)*t - 3*t^2) +
3*P3*t^2
*/
var p = (ft * ft * ft) * p0 +
3 * (ft * ft) * t * p1 +
3 * (1 - t) * (t * t) * p2 +
(t * t * t) * p3;
var tan = -3 * p0 * (ft * ft) +
p1 * (3 * (ft * ft) - 6 * ft * t) +
p2 * (6 * ft * t - 3 * (t * t)) +
3 * p3 * (t * t);
return new Waypoint(p, tan);
}
public Vector3[] GetDetailedPositions(float segmentLength)
{
int detail = Mathf.FloorToInt(this.GetArcLength() / segmentLength) + 1;
Vector3[] arr = new Vector3[detail + 1];
for (int i = 0; i <= detail; i++)
{
arr[i] = this.GetPositionAt((float)i / (float)detail);
}
return arr;
}
public RelativePositionData GetRelativePositionData(float t)
{
var cnt = _waypoints.Count;
switch (cnt)
{
case 0:
return new RelativePositionData(-1, 0f);
case 1:
return new RelativePositionData(0, 0f);
case 2:
return new RelativePositionData(0, t);
default:
{
if (_points == null) this.Clean_Imp();
float len = _lengths[0];
float tot = len;
int i = 0;
while (tot / _totalArcLength < t && i < _lengths.Length)
{
i++;
len = _lengths[i];
tot += len;
}
float lt = (tot - len) / _totalArcLength;
float ht = tot / _totalArcLength;
float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
return new RelativePositionData(i, dt);
}
}
}
public void Clean()
{
_points = null;
}
#endregion
#region IConfigurableIndexedWaypointPath Interface
public void AddControlPoint(IWaypoint waypoint)
{
_waypoints.Add(waypoint);
_points = null;
}
public void InsertControlPoint(int index, IWaypoint waypoint)
{
_waypoints.Insert(index, waypoint);
_points = null;
}
public void ReplaceControlPoint(int index, IWaypoint waypoint)
{
_waypoints[index] = waypoint;
_points = null;
}
public void RemoveControlPointAt(int index)
{
_waypoints.RemoveAt(index);
_points = null;
}
public void Clear()
{
_waypoints.Clear();
_points = null;
}
#endregion
#region IEnumerable Interface
public IEnumerator<IWaypoint> GetEnumerator()
{
return _waypoints.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _waypoints.GetEnumerator();
}
#endregion
}
It may seem like more code, but it’s technically less work over all as the formula is more straightforward, and doesn’t require the speedTable from the spline.
And for shits and giggles, here’s catmull-rom. Personally I think catmull-rom’s look the best.
/// <summary>
/// Represents a catmull-rom cardinal spline.
/// </summary>
public class CardinalSplinePath : IConfigurableIndexedWaypointPath
{
#region Fields
private const int SUBDIVISIONS_MULTIPLIER = 16;
private bool _isClosed;
private List<IWaypoint> _waypoints = new List<IWaypoint>();
private bool _useConstantSpeed = true;
private Vector3[] _points;
private CurveConstantSpeedTable _speedTable = new CurveConstantSpeedTable();
#endregion
#region CONSTRUCTOR
public CardinalSplinePath()
{
}
public CardinalSplinePath(IEnumerable<IWaypoint> waypoints)
{
_waypoints.AddRange(waypoints);
this.Clean_Imp();
}
#endregion
#region Properties
public bool UseConstantSpeed
{
get { return _useConstantSpeed; }
set { _useConstantSpeed = value; }
}
#endregion
#region Methods
private void Clean_Imp()
{
if (_waypoints.Count == 0)
{
_points = new Vector3[] { };
_speedTable.SetZero();
return;
}
else if (_waypoints.Count == 1)
{
_points = new Vector3[] { _waypoints[0].Position };
_speedTable.SetZero();
return;
}
else
{
//get points
_points = (_isClosed) ? new Vector3[_waypoints.Count + 3] : new Vector3[_waypoints.Count + 2];
for (int i = 0; i < _waypoints.Count; i++) _points[i + 1] = _waypoints[i].Position;
if (_isClosed)
{
_points[0] = _waypoints[_waypoints.Count - 1].Position;
_points[_points.Length - 2] = _points[1];
_points[_points.Length - 1] = _points[2];
}
else
{
_points[0] = _points[1];
var lastPnt = _waypoints[_waypoints.Count - 1].Position;
var diffV = lastPnt - _waypoints[_waypoints.Count - 2].Position;
_points[_points.Length - 1] = lastPnt + diffV;
}
_speedTable.Clean(SUBDIVISIONS_MULTIPLIER * _points.Length, this.GetRealPositionAt);
}
}
/// <summary>
/// Returns the position with out speed correction on the path of points.
/// This method does NOT validate itself, make sure the curve is clean before calling.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
private Vector3 GetRealPositionAt(float t)
{
int numSections = _points.Length - 3;
int tSec = Mathf.FloorToInt(t * numSections);
int currPt = numSections - 1;
if (currPt > tSec) currPt = tSec;
float u = t * numSections - currPt;
Vector3 a = _points[currPt];
Vector3 b = _points[currPt + 1];
Vector3 c = _points[currPt + 2];
Vector3 d = _points[currPt + 3];
return 0.5f * (
(-a + 3f * b - 3f * c + d) * (u * u * u)
+ (2f * a - 5f * b + 4f * c - d) * (u * u)
+ (-a + c) * u
+ 2f * b
);
}
#endregion
#region IWaypointPath
public bool IsClosed
{
get { return _isClosed; }
set
{
if (value == _isClosed) return;
_isClosed = value;
_points = null;
}
}
public Vector3 GetPositionAt(float t)
{
if (_speedTable.IsDirty) this.Clean_Imp();
if (_points.Length < 2) return (_points.Length == 0) ? VectorUtil.NaNVector3 : _points[0];
if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);
return GetRealPositionAt(t);
}
public Waypoint GetWaypointAt(float t)
{
if (_speedTable.IsDirty) this.Clean_Imp();
if (_points.Length < 2) return (_points.Length == 0) ? Waypoint.Invalid : new Waypoint(_points[0], Vector3.zero);
if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);
var p1 = this.GetRealPositionAt(t);
var p2 = this.GetRealPositionAt(t + 0.01f); //TODO - figure out a more efficient way of calculating the tangent
return new Waypoint(p1, (p2 - p1).normalized);
}
public float GetArcLength()
{
if (_speedTable.IsDirty) this.Clean_Imp();
return _speedTable.TotalArcLength;
}
public Vector3[] GetDetailedPositions(float segmentLength)
{
int detail = Mathf.FloorToInt(this.GetArcLength() / segmentLength) + 1;
Vector3[] arr = new Vector3[detail + 1];
for (int i = 0; i <= detail; i++)
{
arr[i] = this.GetPositionAt((float)i / (float)detail);
}
return arr;
}
#endregion
#region IIndexedWaypointPath Interface
public int Count
{
get { return _waypoints.Count; }
}
public IWaypoint ControlPoint(int index)
{
return _waypoints[index];
}
public int IndexOf(IWaypoint waypoint)
{
return _waypoints.IndexOf(waypoint);
}
public Vector3 GetPositionAfter(int index, float t)
{
if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
if (_speedTable.IsDirty) this.Clean_Imp();
if (_points.Length < 2) return (_points.Length == 0) ? VectorUtil.NaNVector3 : _points[0];
index++; //index at 0 is an ignored control point, index should be 1-base
int i = index * SUBDIVISIONS_MULTIPLIER;
int j = (index + 1) * SUBDIVISIONS_MULTIPLIER;
//float nt = _timesTable[i] + (_timesTable[j] - _timesTable[i]) * t;
float nt = _speedTable.GetTimeAtSubdivision(i) + (_speedTable.GetTimeAtSubdivision(j) - _speedTable.GetTimeAtSubdivision(i)) * t;
return this.GetRealPositionAt(nt);
}
public Waypoint GetWaypointAfter(int index, float t)
{
if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
if (_speedTable.IsDirty) this.Clean_Imp();
if (_points.Length < 2) return (_points.Length == 0) ? Waypoint.Invalid : new Waypoint(_points[0], Vector3.zero);
index++; //index at 0 is an ignored control point, index should be 1-base
int i = index * SUBDIVISIONS_MULTIPLIER;
int j = (index + 1) * SUBDIVISIONS_MULTIPLIER;
//float nt = _timesTable[i] + (_timesTable[j] - _timesTable[i]) * t;
float nt = _speedTable.GetTimeAtSubdivision(i) + (_speedTable.GetTimeAtSubdivision(j) - _speedTable.GetTimeAtSubdivision(i)) * t;
var p1 = this.GetRealPositionAt(nt);
var p2 = this.GetRealPositionAt(nt + 0.01f); //TODO - figure out a more efficient way of calculating the tangent
return new Waypoint(p1, (p2 - p1).normalized);
}
public RelativePositionData GetRelativePositionData(float t)
{
int cnt = _waypoints.Count;
switch (cnt)
{
case 0:
return new RelativePositionData(-1, 0f);
case 1:
return new RelativePositionData(0, 0f);
case 2:
return new RelativePositionData(0, t);
default:
{
if (_speedTable.IsDirty) this.Clean_Imp();
if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);
t = Mathf.Clamp01(t);
if (MathUtil.FuzzyEqual(t, 1f)) return new RelativePositionData(_waypoints.Count - 1, 0f);
int index;
float segmentTime;
if(_isClosed)
{
index = Mathf.FloorToInt(cnt * t);
segmentTime = 1f / cnt;
}
else
{
index = Mathf.FloorToInt((cnt - 1) * t);
segmentTime = 1f / (cnt - 1);
}
float lt = index * segmentTime;
float ht = (index + 1) * segmentTime;
float dt = MathUtil.PercentageMinMax(t, ht, lt);
return new RelativePositionData(index, dt);
}
}
}
public void Clean()
{
_points = null;
}
#endregion
#region IConfigurableIndexedWaypointPath Interface
public void AddControlPoint(IWaypoint waypoint)
{
_waypoints.Add(waypoint);
_points = null;
}
public void InsertControlPoint(int index, IWaypoint waypoint)
{
_waypoints.Insert(index, waypoint);
_points = null;
}
public void ReplaceControlPoint(int index, IWaypoint waypoint)
{
_waypoints[index] = waypoint;
_points = null;
}
public void RemoveControlPointAt(int index)
{
_waypoints.RemoveAt(index);
_points = null;
}
public void Clear()
{
_waypoints.Clear();
_points = null;
}
public void DrawGizmos(float segmentLength)
{
if (_waypoints.Count <= 1) return;
var length = this.GetArcLength();
int divisions = Mathf.FloorToInt(length / segmentLength) + 1;
for (int i = 0; i < divisions; i++)
{
float t1 = (float)i / (float)divisions;
float t2 = (float)(i + 1) / (float)divisions;
Gizmos.DrawLine(this.GetPositionAt(t1), this.GetPositionAt(t2));
}
}
#endregion
#region IEnumerable Interface
public IEnumerator<IWaypoint> GetEnumerator()
{
return _waypoints.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _waypoints.GetEnumerator();
}
#endregion
}