123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608 |
- using System;
- using System.Collections.Generic;
- using UnityEngine;
- namespace BezierSolution
- {
- [ExecuteInEditMode]
- public class BezierSpline : MonoBehaviour
- {
- private static Material gizmoMaterial;
- private Color gizmoColor = Color.white;
- private float gizmoStep = 0.05f;
- private List<BezierPoint> endPoints = new List<BezierPoint>();
- public bool loop = false;
- public bool drawGizmos = false;
- public int Count { get { return endPoints.Count; } }
- public float Length { get { return GetLengthApproximately( 0f, 1f ); } }
- public BezierPoint this[int index]
- {
- get
- {
- if( index < Count )
- return endPoints[index];
- Debug.LogError( "Bezier index " + index + " is out of range: " + Count );
- return null;
- }
- }
- private void Awake()
- {
- Refresh();
- }
- #if UNITY_EDITOR
- private void OnTransformChildrenChanged()
- {
- Refresh();
- }
- #endif
- public void Initialize( int endPointsCount )
- {
- if( endPointsCount < 2 )
- {
- Debug.LogError( "Can't initialize spline with " + endPointsCount + " point(s). At least 2 points are needed" );
- return;
- }
- Refresh();
- for( int i = endPoints.Count - 1; i >= 0; i-- )
- DestroyImmediate( endPoints[i].gameObject );
- endPoints.Clear();
- for( int i = 0; i < endPointsCount; i++ )
- InsertNewPointAt( i );
- Refresh();
- }
- public void Refresh()
- {
- endPoints.Clear();
- GetComponentsInChildren( endPoints );
- }
- public BezierPoint InsertNewPointAt( int index )
- {
- if( index < 0 || index > endPoints.Count )
- {
- Debug.LogError( "Index " + index + " is out of range: [0," + endPoints.Count + "]" );
- return null;
- }
- int prevCount = endPoints.Count;
- BezierPoint point = new GameObject( "Point" ).AddComponent<BezierPoint>();
- point.transform.SetParent( endPoints.Count == 0 ? transform : ( index == 0 ? endPoints[0].transform.parent : endPoints[index - 1].transform.parent ), false );
- point.transform.SetSiblingIndex( index == 0 ? 0 : endPoints[index - 1].transform.GetSiblingIndex() + 1 );
- if( endPoints.Count == prevCount ) // If spline is not automatically Refresh()'ed
- endPoints.Insert( index, point );
- return point;
- }
- //public Vector3 GetVelocity(float t)
- //{
- // return transform.TransformPoint(
- // GetFirstDerivative(points[0], points[1], t)) - transform.position;
- //}
- public 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);
- }
- //public Vector3 GetDirection(float t)
- //{
- // return GetVelocity(t).normalized;
- //}
- public BezierPoint DuplicatePointAt( int index )
- {
- if( index < 0 || index >= endPoints.Count )
- {
- Debug.LogError( "Index " + index + " is out of range: [0," + ( endPoints.Count - 1 ) + "]" );
- return null;
- }
- BezierPoint newPoint = InsertNewPointAt( index + 1 );
- endPoints[index].CopyTo( newPoint );
- return newPoint;
- }
- public void RemovePointAt( int index )
- {
- if( endPoints.Count <= 2 )
- {
- Debug.LogError( "Can't remove point: spline must consist of at least two points!" );
- return;
- }
- if( index < 0 || index >= endPoints.Count )
- {
- Debug.LogError( "Index " + index + " is out of range: [0," + endPoints.Count + ")" );
- return;
- }
- BezierPoint point = endPoints[index];
- endPoints.RemoveAt( index );
- DestroyImmediate( point.gameObject );
- }
- public void SwapPointsAt( int index1, int index2 )
- {
- if( index1 == index2 )
- {
- Debug.LogError( "Indices can't be equal to each other" );
- return;
- }
- if( index1 < 0 || index1 >= endPoints.Count || index2 < 0 || index2 >= endPoints.Count )
- {
- Debug.LogError( "Indices must be in range [0," + ( endPoints.Count - 1 ) + "]" );
- return;
- }
- BezierPoint point1 = endPoints[index1];
- int point1SiblingIndex = point1.transform.GetSiblingIndex();
- endPoints[index1] = endPoints[index2];
- endPoints[index2] = point1;
- point1.transform.SetSiblingIndex( endPoints[index1].transform.GetSiblingIndex() );
- endPoints[index1].transform.SetSiblingIndex( point1SiblingIndex );
- }
- public int IndexOf( BezierPoint point )
- {
- return endPoints.IndexOf( point );
- }
- public void DrawGizmos( Color color, int smoothness = 4 )
- {
- drawGizmos = true;
- gizmoColor = color;
- gizmoStep = 1f / ( endPoints.Count * Mathf.Clamp( smoothness, 1, 30 ) );
- }
- public void HideGizmos()
- {
- drawGizmos = false;
- }
- public Vector3 GetPoint( float normalizedT )
- {
- if( !loop )
- {
- if( normalizedT <= 0f )
- return endPoints[0].position;
- else if( normalizedT >= 1f )
- return endPoints[endPoints.Count - 1].position;
- }
- else
- {
- if( normalizedT < 0f )
- normalizedT += 1f;
- else if( normalizedT >= 1f )
- normalizedT -= 1f;
- }
- float t = normalizedT * ( loop ? endPoints.Count : ( endPoints.Count - 1 ) );
- BezierPoint startPoint, endPoint;
- int startIndex = (int) t;
- int endIndex = startIndex + 1;
- if( endIndex == endPoints.Count )
- endIndex = 0;
- startPoint = endPoints[startIndex];
- endPoint = endPoints[endIndex];
- float localT = t - startIndex;
- float oneMinusLocalT = 1f - localT;
- return oneMinusLocalT * oneMinusLocalT * oneMinusLocalT * startPoint.position +
- 3f * oneMinusLocalT * oneMinusLocalT * localT * startPoint.followingControlPointPosition +
- 3f * oneMinusLocalT * localT * localT * endPoint.precedingControlPointPosition +
- localT * localT * localT * endPoint.position;
- }
- public Quaternion GetRotation(float normalizedT)
- {
- if (!loop)
- {
- if (normalizedT <= 0f)
- return endPoints[0].rotation;
- else if (normalizedT >= 1f)
- return endPoints[endPoints.Count - 1].rotation;
- }
- else
- {
- if (normalizedT < 0f)
- normalizedT += 1f;
- else if (normalizedT >= 1f)
- normalizedT -= 1f;
- }
- float t = normalizedT * (loop ? endPoints.Count : (endPoints.Count - 1));
- BezierPoint startPoint, endPoint;
- int startIndex = (int)t;
- int endIndex = startIndex + 1;
- if (endIndex == endPoints.Count)
- endIndex = 0;
- startPoint = endPoints[startIndex];
- endPoint = endPoints[endIndex];
- return Quaternion.LerpUnclamped(startPoint.rotation, endPoint.rotation, t - startIndex);
- }
- public Vector3 GetTangent( float normalizedT )
- {
- if( !loop )
- {
- if( normalizedT <= 0f )
- return 3f * ( endPoints[0].followingControlPointPosition - endPoints[0].position );
- else if( normalizedT >= 1f )
- {
- int index = endPoints.Count - 1;
- return 3f * ( endPoints[index].position - endPoints[index].precedingControlPointPosition );
- }
- }
- else
- {
- if( normalizedT < 0f )
- normalizedT += 1f;
- else if( normalizedT >= 1f )
- normalizedT -= 1f;
- }
- float t = normalizedT * ( loop ? endPoints.Count : ( endPoints.Count - 1 ) );
- BezierPoint startPoint, endPoint;
- int startIndex = (int) t;
- int endIndex = startIndex + 1;
- if( endIndex == endPoints.Count )
- endIndex = 0;
- startPoint = endPoints[startIndex];
- endPoint = endPoints[endIndex];
- float localT = t - startIndex;
- float oneMinusLocalT = 1f - localT;
- return 3f * oneMinusLocalT * oneMinusLocalT * ( startPoint.followingControlPointPosition - startPoint.position ) +
- 6f * oneMinusLocalT * localT * ( endPoint.precedingControlPointPosition - startPoint.followingControlPointPosition ) +
- 3f * localT * localT * ( endPoint.position - endPoint.precedingControlPointPosition );
- }
- public float GetLengthApproximately( float startNormalizedT, float endNormalizedT, float accuracy = 50f )
- {
- if( endNormalizedT < startNormalizedT )
- {
- float temp = startNormalizedT;
- startNormalizedT = endNormalizedT;
- endNormalizedT = temp;
- }
- if( startNormalizedT < 0f )
- startNormalizedT = 0f;
- if( endNormalizedT > 1f )
- endNormalizedT = 1f;
- float step = AccuracyToStepSize( accuracy ) * ( endNormalizedT - startNormalizedT );
- float length = 0f;
- Vector3 lastPoint = GetPoint( startNormalizedT );
- for( float i = startNormalizedT + step; i < endNormalizedT; i += step )
- {
- Vector3 thisPoint = GetPoint( i );
- length += Vector3.Distance( thisPoint, lastPoint );
- lastPoint = thisPoint;
- }
- length += Vector3.Distance( lastPoint, GetPoint( endNormalizedT ) );
- return length;
- }
- public Vector3 FindNearestPointTo( Vector3 worldPos, float accuracy = 100f )
- {
- float normalizedT;
- return FindNearestPointTo( worldPos, out normalizedT, accuracy );
- }
- public Vector3 FindNearestPointTo( Vector3 worldPos, out float normalizedT, float accuracy = 100f )
- {
- Vector3 result = Vector3.zero;
- normalizedT = -1f;
- float step = AccuracyToStepSize( accuracy );
- float minDistance = Mathf.Infinity;
- for( float i = 0f; i < 1f; i += step )
- {
- Vector3 thisPoint = GetPoint( i );
- float thisDistance = ( worldPos - thisPoint ).sqrMagnitude;
- if( thisDistance < minDistance )
- {
- minDistance = thisDistance;
- result = thisPoint;
- normalizedT = i;
- }
- }
- return result;
- }
- public Vector3 MoveAlongSpline( ref float normalizedT, float deltaMovement, int accuracy = 3 )
- {
- // Credit: https://gamedev.stackexchange.com/a/27138
- float constant = deltaMovement / ( ( loop ? endPoints.Count : endPoints.Count - 1 ) * accuracy );
- for( int i = 0; i < accuracy; i++ )
- normalizedT += constant / GetTangent( normalizedT ).magnitude;
- return GetPoint( normalizedT );
- }
- public void ConstructLinearPath()
- {
- for( int i = 0; i < endPoints.Count; i++ )
- {
- endPoints[i].handleMode = BezierPoint.HandleMode.Free;
- if( i < endPoints.Count - 1 )
- {
- Vector3 midPoint = ( endPoints[i].position + endPoints[i + 1].position ) * 0.5f;
- endPoints[i].followingControlPointPosition = midPoint;
- endPoints[i + 1].precedingControlPointPosition = midPoint;
- }
- else
- {
- Vector3 midPoint = ( endPoints[i].position + endPoints[0].position ) * 0.5f;
- endPoints[i].followingControlPointPosition = midPoint;
- endPoints[0].precedingControlPointPosition = midPoint;
- }
- }
- }
- public void AutoConstructSpline()
- {
- // Credit: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit
- for( int i = 0; i < endPoints.Count; i++ )
- endPoints[i].handleMode = BezierPoint.HandleMode.Mirrored;
- int n = endPoints.Count - 1;
- if( n == 1 )
- {
- endPoints[0].followingControlPointPosition = ( 2 * endPoints[0].position + endPoints[1].position ) / 3f;
- endPoints[1].precedingControlPointPosition = 2 * endPoints[0].followingControlPointPosition - endPoints[0].position;
- return;
- }
- Vector3[] rhs;
- if( loop )
- rhs = new Vector3[n + 1];
- else
- rhs = new Vector3[n];
- for( int i = 1; i < n - 1; i++ )
- {
- rhs[i] = 4 * endPoints[i].position + 2 * endPoints[i + 1].position;
- }
- rhs[0] = endPoints[0].position + 2 * endPoints[1].position;
- if( !loop )
- rhs[n - 1] = ( 8 * endPoints[n - 1].position + endPoints[n].position ) * 0.5f;
- else
- {
- rhs[n - 1] = 4 * endPoints[n - 1].position + 2 * endPoints[n].position;
- rhs[n] = ( 8 * endPoints[n].position + endPoints[0].position ) * 0.5f;
- }
- // Get first control points
- Vector3[] controlPoints = GetFirstControlPoints( rhs );
- for( int i = 0; i < n; i++ )
- {
- // First control point
- endPoints[i].followingControlPointPosition = controlPoints[i];
- if( loop )
- {
- endPoints[i + 1].precedingControlPointPosition = 2 * endPoints[i + 1].position - controlPoints[i + 1];
- }
- else
- {
- // Second control point
- if( i < n - 1 )
- endPoints[i + 1].precedingControlPointPosition = 2 * endPoints[i + 1].position - controlPoints[i + 1];
- else
- endPoints[i + 1].precedingControlPointPosition = ( endPoints[n].position + controlPoints[n - 1] ) * 0.5f;
- }
- }
- if( loop )
- {
- float controlPointDistance = Vector3.Distance( endPoints[0].followingControlPointPosition, endPoints[0].position );
- Vector3 direction = Vector3.Normalize( endPoints[n].position - endPoints[1].position );
- endPoints[0].precedingControlPointPosition = endPoints[0].position + direction * controlPointDistance;
- endPoints[0].followingControlPointLocalPosition = -endPoints[0].precedingControlPointLocalPosition;
- }
- }
- private static Vector3[] GetFirstControlPoints( Vector3[] rhs )
- {
- // Credit: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit
- int n = rhs.Length;
- Vector3[] x = new Vector3[n]; // Solution vector.
- float[] tmp = new float[n]; // Temp workspace.
- float b = 2f;
- x[0] = rhs[0] / b;
- for( int i = 1; i < n; i++ ) // Decomposition and forward substitution.
- {
- float val = 1f / b;
- tmp[i] = val;
- b = ( i < n - 1 ? 4f : 3.5f ) - val;
- x[i] = ( rhs[i] - x[i - 1] ) / b;
- }
- for( int i = 1; i < n; i++ )
- {
- x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.
- }
- return x;
- }
- public void AutoConstructSpline2()
- {
- // Credit: http://stackoverflow.com/questions/3526940/how-to-create-a-cubic-bezier-curve-when-given-n-points-in-3d
- for( int i = 0; i < endPoints.Count; i++ )
- {
- Vector3 pMinus1, p1, p2;
- if( i == 0 )
- {
- if( loop )
- pMinus1 = endPoints[endPoints.Count - 1].position;
- else
- pMinus1 = endPoints[0].position;
- }
- else
- {
- pMinus1 = endPoints[i - 1].position;
- }
- if( loop )
- {
- p1 = endPoints[( i + 1 ) % endPoints.Count].position;
- p2 = endPoints[( i + 2 ) % endPoints.Count].position;
- }
- else
- {
- if( i < endPoints.Count - 2 )
- {
- p1 = endPoints[i + 1].position;
- p2 = endPoints[i + 2].position;
- }
- else if( i == endPoints.Count - 2 )
- {
- p1 = endPoints[i + 1].position;
- p2 = endPoints[i + 1].position;
- }
- else
- {
- p1 = endPoints[i].position;
- p2 = endPoints[i].position;
- }
- }
- endPoints[i].followingControlPointPosition = endPoints[i].position + ( p1 - pMinus1 ) / 6f;
- endPoints[i].handleMode = BezierPoint.HandleMode.Mirrored;
- if( i < endPoints.Count - 1 )
- endPoints[i + 1].precedingControlPointPosition = p1 - ( p2 - endPoints[i].position ) / 6f;
- else if( loop )
- endPoints[0].precedingControlPointPosition = p1 - ( p2 - endPoints[i].position ) / 6f;
- }
- }
- /*public void AutoConstructSpline3()
- {
- // Todo? http://www.math.ucla.edu/~baker/149.1.02w/handouts/dd_splines.pdf
- }*/
- private float AccuracyToStepSize( float accuracy )
- {
- if( accuracy <= 0f )
- return 0.2f;
- return Mathf.Clamp( 1f / accuracy, 0.001f, 0.2f );
- }
- // Renders the spline gizmo during gameplay
- // Credit: https://docs.unity3d.com/ScriptReference/GL.html
- private void OnRenderObject()
- {
- if( !drawGizmos || endPoints.Count < 2 )
- return;
- if( !gizmoMaterial )
- {
- Shader shader = Shader.Find( "Hidden/Internal-Colored" );
- gizmoMaterial = new Material( shader ) { hideFlags = HideFlags.HideAndDontSave };
- gizmoMaterial.SetInt( "_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha );
- gizmoMaterial.SetInt( "_DstBlend", (int) UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha );
- gizmoMaterial.SetInt( "_Cull", (int) UnityEngine.Rendering.CullMode.Off );
- gizmoMaterial.SetInt( "_ZWrite", 0 );
- }
- gizmoMaterial.SetPass( 0 );
- GL.Begin( GL.LINES );
- GL.Color( gizmoColor );
- Vector3 lastPos = endPoints[0].position;
- for( float i = gizmoStep; i < 1f; i += gizmoStep )
- {
- GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
- lastPos = GetPoint( i );
- GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
- }
- GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
- lastPos = GetPoint( 1f );
- GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
- GL.End();
- }
- #if UNITY_EDITOR
- public void Reset()
- {
- for( int i = endPoints.Count - 1; i >= 0; i-- )
- UnityEditor.Undo.DestroyObjectImmediate( endPoints[i].gameObject );
- Initialize( 2 );
- endPoints[0].localPosition = Vector3.back;
- endPoints[1].localPosition = Vector3.forward;
- UnityEditor.Undo.RegisterCreatedObjectUndo( endPoints[0].gameObject, "Initialize Spline" );
- UnityEditor.Undo.RegisterCreatedObjectUndo( endPoints[1].gameObject, "Initialize Spline" );
- UnityEditor.Selection.activeTransform = endPoints[0].transform;
- }
- #endif
- }
- }
|