#region UsingStatements
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
#endregion
///
/// - Class for describing and drawing Bezier Curves
/// - Efficiently handles approximate length calculation through 'dirty' system
/// - Has static functions for getting points on curves constructed by Vector3 parameters (GetPoint, GetCubicPoint, GetQuadraticPoint, and GetLinearPoint)
///
[ExecuteInEditMode]
[Serializable]
public class BezierCurve : MonoBehaviour {
#region PublicVariables
///
/// - the number of mid-points calculated for each pair of bezier points
/// - used for drawing the curve in the editor
/// - used for calculating the "length" variable
///
public int resolution = 30;
///
/// Gets or sets a value indicating whether this is dirty.
///
///
/// true if dirty; otherwise, false.
///
public bool dirty { get; private set; }
///
/// - color this curve will be drawn with in the editor
/// - set in the editor
///
public Color drawColor = Color.white;
#endregion
#region PublicProperties
///
/// - set in the editor
/// - used to determine if the curve should be drawn as "closed" in the editor
/// - used to determine if the curve's length should include the curve between the first and the last points in "points" array
/// - setting this value will cause the curve to become dirty
///
[SerializeField] private bool _close;
public bool close
{
get { return _close; }
set
{
if(_close == value) return;
_close = value;
dirty = true;
}
}
///
/// - set internally
/// - gets point corresponding to "index" in "points" array
/// - does not allow direct set
///
///
/// - the index
///
public BezierPoint this[int index]
{
get { return points[index]; }
}
///
/// - number of points stored in 'points' variable
/// - set internally
/// - does not include "handles"
///
///
/// - The point count
///
public int pointCount
{
get { return points.Length; }
}
///
/// - The approximate length of the curve
/// - recalculates if the curve is "dirty"
///
private float _length;
public float length
{
get
{
if(dirty)
{
_length = 0;
for(int i = 0; i < points.Length - 1; i++){
_length += ApproximateLength(points[i], points[i + 1], resolution);
}
if(close) _length += ApproximateLength(points[points.Length - 1], points[0], resolution);
dirty = false;
}
return _length;
}
}
#endregion
#region PrivateVariables
///
/// - Array of point objects that make up this curve
/// - Populated through editor
///
[SerializeField] private BezierPoint[] points = new BezierPoint[0];
#endregion
#region UnityFunctions
void OnDrawGizmos () {
Gizmos.color = drawColor;
if(points.Length > 1){
for(int i = 0; i < points.Length - 1; i++){
DrawCurve(points[i], points[i+1], resolution);
}
if (close) DrawCurve(points[points.Length - 1], points[0], resolution);
}
}
void Awake(){
dirty = true;
}
#endregion
#region PublicFunctions
///
/// - Adds the given point to the end of the curve ("points" array)
///
///
/// - The point to add.
///
public void AddPoint(BezierPoint point)
{
List tempArray = new List(points);
tempArray.Add(point);
points = tempArray.ToArray();
dirty = true;
}
///
/// - Adds a point at position
///
///
/// - The point object
///
///
/// - Where to add the point
///
public BezierPoint AddPointAt(Vector3 position)
{
GameObject pointObject = new GameObject("Point "+pointCount);
pointObject.transform.parent = transform;
pointObject.transform.position = position;
BezierPoint newPoint = pointObject.AddComponent();
newPoint.curve = this;
return newPoint;
}
///
/// - Removes the given point from the curve ("points" array)
///
///
/// - The point to remove
///
public void RemovePoint(BezierPoint point)
{
List tempArray = new List(points);
tempArray.Remove(point);
points = tempArray.ToArray();
dirty = false;
}
///
/// - Gets a copy of the bezier point array used to define this curve
///
///
/// - The cloned array of points
///
public BezierPoint[] GetAnchorPoints()
{
return (BezierPoint[])points.Clone();
}
///
/// - Gets the point at 't' percent along this curve
///
///
/// - Returns the point at 't' percent
///
///
/// - Value between 0 and 1 representing the percent along the curve (0 = 0%, 1 = 100%)
///
public Vector3 GetPointAt(float t)
{
if(t <= 0) return points[0].position;
else if (t >= 1) return points[points.Length - 1].position;
float totalPercent = 0;
float curvePercent = 0;
BezierPoint p1 = null;
BezierPoint p2 = null;
for(int i = 0; i < points.Length - 1; i++)
{
curvePercent = ApproximateLength(points[i], points[i + 1], 10) / length;
if(totalPercent + curvePercent > t)
{
p1 = points[i];
p2 = points[i + 1];
break;
}
else totalPercent += curvePercent;
}
if(close && p1 == null)
{
p1 = points[points.Length - 1];
p2 = points[0];
}
t -= totalPercent;
return GetPoint(p1, p2, t / curvePercent);
}
///
/// - Get the index of the given point in this curve
///
///
/// - The index, or -1 if the point is not found
///
///
/// - Point to search for
///
public int GetPointIndex(BezierPoint point)
{
int result = -1;
for(int i = 0; i < points.Length; i++)
{
if(points[i] == point)
{
result = i;
break;
}
}
return result;
}
///
/// - Sets this curve to 'dirty'
/// - Forces the curve to recalculate its length
///
public void SetDirty()
{
dirty = true;
}
#endregion
#region PublicStaticFunctions
///
/// - Draws the curve in the Editor
///
///
/// - The bezier point at the beginning of the curve
///
///
/// - The bezier point at the end of the curve
///
///
/// - The number of segments along the curve to draw
///
public static void DrawCurve(BezierPoint p1, BezierPoint p2, int resolution)
{
int limit = resolution+1;
float _res = resolution;
Vector3 lastPoint = p1.position;
Vector3 currentPoint = Vector3.zero;
for(int i = 1; i < limit; i++){
currentPoint = GetPoint(p1, p2, i/_res);
Gizmos.DrawLine(lastPoint, currentPoint);
lastPoint = currentPoint;
}
}
///
/// - Gets the point 't' percent along a curve
/// - Automatically calculates for the number of relevant points
///
///
/// - The point 't' percent along the curve
///
///
/// - The bezier point at the beginning of the curve
///
///
/// - The bezier point at the end of the curve
///
///
/// - Value between 0 and 1 representing the percent along the curve (0 = 0%, 1 = 100%)
///
public static Vector3 GetPoint(BezierPoint p1, BezierPoint p2, float t)
{
if(p1.handle2 != Vector3.zero)
{
if(p2.handle1 != Vector3.zero) return GetCubicCurvePoint(p1.position, p1.globalHandle2, p2.globalHandle1, p2.position, t);
else return GetQuadraticCurvePoint(p1.position, p1.globalHandle2, p2.position, t);
}
else
{
if(p2.handle1 != Vector3.zero) return GetQuadraticCurvePoint(p1.position, p2.globalHandle1, p2.position, t);
else return GetLinearPoint(p1.position, p2.position, t);
}
}
///
/// - Gets the point 't' percent along a third-order curve
///
///
/// - The point 't' percent along the curve
///
///
/// - The point at the beginning of the curve
///
///
/// - The second point along the curve
///
///
/// - The third point along the curve
///
///
/// - The point at the end of the curve
///
///
/// - Value between 0 and 1 representing the percent along the curve (0 = 0%, 1 = 100%)
///
public static Vector3 GetCubicCurvePoint(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float t)
{
t = Mathf.Clamp01(t);
Vector3 part1 = Mathf.Pow(1 - t, 3) * p1;
Vector3 part2 = 3 * Mathf.Pow(1 - t, 2) * t * p2;
Vector3 part3 = 3 * (1 - t) * Mathf.Pow(t, 2) * p3;
Vector3 part4 = Mathf.Pow(t, 3) * p4;
return part1 + part2 + part3 + part4;
}
///
/// - Gets the point 't' percent along a second-order curve
///
///
/// - The point 't' percent along the curve
///
///
/// - The point at the beginning of the curve
///
///
/// - The second point along the curve
///
///
/// - The point at the end of the curve
///
///
/// - Value between 0 and 1 representing the percent along the curve (0 = 0%, 1 = 100%)
///
public static Vector3 GetQuadraticCurvePoint(Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
t = Mathf.Clamp01(t);
Vector3 part1 = Mathf.Pow(1 - t, 2) * p1;
Vector3 part2 = 2 * (1 - t) * t * p2;
Vector3 part3 = Mathf.Pow(t, 2) * p3;
return part1 + part2 + part3;
}
///
/// - Gets point 't' percent along a linear "curve" (line)
/// - This is exactly equivalent to Vector3.Lerp
///
///
/// - The point 't' percent along the curve
///
///
/// - The point at the beginning of the line
///
///
/// - The point at the end of the line
///
///
/// - Value between 0 and 1 representing the percent along the line (0 = 0%, 1 = 100%)
///
public static Vector3 GetLinearPoint(Vector3 p1, Vector3 p2, float t)
{
return p1 + ((p2 - p1) * t);
}
///
/// - Gets point 't' percent along n-order curve
///
///
/// - The point 't' percent along the curve
///
///
/// - Value between 0 and 1 representing the percent along the curve (0 = 0%, 1 = 100%)
///
///
/// - The points used to define the curve
///
public static Vector3 GetPoint(float t, params Vector3[] points){
t = Mathf.Clamp01(t);
int order = points.Length-1;
Vector3 point = Vector3.zero;
Vector3 vectorToAdd;
for(int i = 0; i < points.Length; i++){
vectorToAdd = points[points.Length-i-1] * (BinomialCoefficient(i, order) * Mathf.Pow(t, order-i) * Mathf.Pow((1-t), i));
point += vectorToAdd;
}
return point;
}
///
/// - Approximates the length
///
///
/// - The approximate length
///
///
/// - The bezier point at the start of the curve
///
///
/// - The bezier point at the end of the curve
///
///
/// - The number of points along the curve used to create measurable segments
///
public static float ApproximateLength(BezierPoint p1, BezierPoint p2, int resolution = 10)
{
float _res = resolution;
float total = 0;
Vector3 lastPosition = p1.position;
Vector3 currentPosition;
for(int i = 0; i < resolution + 1; i++)
{
currentPosition = GetPoint(p1, p2, i / _res);
total += (currentPosition - lastPosition).magnitude;
lastPosition = currentPosition;
}
return total;
}
#endregion
#region UtilityFunctions
private static int BinomialCoefficient(int i, int n){
return Factoral(n)/(Factoral(i)*Factoral(n-i));
}
private static int Factoral(int i){
if(i == 0) return 1;
int total = 1;
while(i-1 >= 0){
total *= i;
i--;
}
return total;
}
#endregion
/* needs testing
public Vector3 GetPointAtDistance(float distance)
{
if(close)
{
if(distance < 0) while(distance < 0) { distance += length; }
else if(distance > length) while(distance > length) { distance -= length; }
}
else
{
if(distance <= 0) return points[0].position;
else if(distance >= length) return points[points.Length - 1].position;
}
float totalLength = 0;
float curveLength = 0;
BezierPoint firstPoint = null;
BezierPoint secondPoint = null;
for(int i = 0; i < points.Length - 1; i++)
{
curveLength = ApproximateLength(points[i], points[i + 1], resolution);
if(totalLength + curveLength >= distance)
{
firstPoint = points[i];
secondPoint = points[i+1];
break;
}
else totalLength += curveLength;
}
if(firstPoint == null)
{
firstPoint = points[points.Length - 1];
secondPoint = points[0];
curveLength = ApproximateLength(firstPoint, secondPoint, resolution);
}
distance -= totalLength;
return GetPoint(distance / curveLength, firstPoint, secondPoint);
}
*/
}