BezierSpline.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace BezierSolution
  5. {
  6. [ExecuteInEditMode]
  7. public class BezierSpline : MonoBehaviour
  8. {
  9. private static Material gizmoMaterial;
  10. private Color gizmoColor = Color.white;
  11. private float gizmoStep = 0.05f;
  12. private List<BezierPoint> endPoints = new List<BezierPoint>();
  13. public bool loop = false;
  14. public bool drawGizmos = false;
  15. public int Count { get { return endPoints.Count; } }
  16. public float Length { get { return GetLengthApproximately( 0f, 1f ); } }
  17. public BezierPoint this[int index]
  18. {
  19. get
  20. {
  21. if( index < Count )
  22. return endPoints[index];
  23. Debug.LogError( "Bezier index " + index + " is out of range: " + Count );
  24. return null;
  25. }
  26. }
  27. private void Awake()
  28. {
  29. Refresh();
  30. }
  31. #if UNITY_EDITOR
  32. private void OnTransformChildrenChanged()
  33. {
  34. Refresh();
  35. }
  36. #endif
  37. public void Initialize( int endPointsCount )
  38. {
  39. if( endPointsCount < 2 )
  40. {
  41. Debug.LogError( "Can't initialize spline with " + endPointsCount + " point(s). At least 2 points are needed" );
  42. return;
  43. }
  44. Refresh();
  45. for( int i = endPoints.Count - 1; i >= 0; i-- )
  46. DestroyImmediate( endPoints[i].gameObject );
  47. endPoints.Clear();
  48. for( int i = 0; i < endPointsCount; i++ )
  49. InsertNewPointAt( i );
  50. Refresh();
  51. }
  52. public void Refresh()
  53. {
  54. endPoints.Clear();
  55. GetComponentsInChildren( endPoints );
  56. }
  57. public BezierPoint InsertNewPointAt( int index )
  58. {
  59. if( index < 0 || index > endPoints.Count )
  60. {
  61. Debug.LogError( "Index " + index + " is out of range: [0," + endPoints.Count + "]" );
  62. return null;
  63. }
  64. int prevCount = endPoints.Count;
  65. BezierPoint point = new GameObject( "Point" ).AddComponent<BezierPoint>();
  66. point.transform.SetParent( endPoints.Count == 0 ? transform : ( index == 0 ? endPoints[0].transform.parent : endPoints[index - 1].transform.parent ), false );
  67. point.transform.SetSiblingIndex( index == 0 ? 0 : endPoints[index - 1].transform.GetSiblingIndex() + 1 );
  68. if( endPoints.Count == prevCount ) // If spline is not automatically Refresh()'ed
  69. endPoints.Insert( index, point );
  70. return point;
  71. }
  72. //public Vector3 GetVelocity(float t)
  73. //{
  74. // return transform.TransformPoint(
  75. // GetFirstDerivative(points[0], points[1], t)) - transform.position;
  76. //}
  77. public Vector3 GetFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
  78. {
  79. t = Mathf.Clamp01(t);
  80. float oneMinusT = 1f - t;
  81. return
  82. 3f * oneMinusT * oneMinusT * (p1 - p0) +
  83. 6f * oneMinusT * t * (p2 - p1) +
  84. 3f * t * t * (p3 - p2);
  85. }
  86. //public Vector3 GetDirection(float t)
  87. //{
  88. // return GetVelocity(t).normalized;
  89. //}
  90. public BezierPoint DuplicatePointAt( int index )
  91. {
  92. if( index < 0 || index >= endPoints.Count )
  93. {
  94. Debug.LogError( "Index " + index + " is out of range: [0," + ( endPoints.Count - 1 ) + "]" );
  95. return null;
  96. }
  97. BezierPoint newPoint = InsertNewPointAt( index + 1 );
  98. endPoints[index].CopyTo( newPoint );
  99. return newPoint;
  100. }
  101. public void RemovePointAt( int index )
  102. {
  103. if( endPoints.Count <= 2 )
  104. {
  105. Debug.LogError( "Can't remove point: spline must consist of at least two points!" );
  106. return;
  107. }
  108. if( index < 0 || index >= endPoints.Count )
  109. {
  110. Debug.LogError( "Index " + index + " is out of range: [0," + endPoints.Count + ")" );
  111. return;
  112. }
  113. BezierPoint point = endPoints[index];
  114. endPoints.RemoveAt( index );
  115. DestroyImmediate( point.gameObject );
  116. }
  117. public void SwapPointsAt( int index1, int index2 )
  118. {
  119. if( index1 == index2 )
  120. {
  121. Debug.LogError( "Indices can't be equal to each other" );
  122. return;
  123. }
  124. if( index1 < 0 || index1 >= endPoints.Count || index2 < 0 || index2 >= endPoints.Count )
  125. {
  126. Debug.LogError( "Indices must be in range [0," + ( endPoints.Count - 1 ) + "]" );
  127. return;
  128. }
  129. BezierPoint point1 = endPoints[index1];
  130. int point1SiblingIndex = point1.transform.GetSiblingIndex();
  131. endPoints[index1] = endPoints[index2];
  132. endPoints[index2] = point1;
  133. point1.transform.SetSiblingIndex( endPoints[index1].transform.GetSiblingIndex() );
  134. endPoints[index1].transform.SetSiblingIndex( point1SiblingIndex );
  135. }
  136. public int IndexOf( BezierPoint point )
  137. {
  138. return endPoints.IndexOf( point );
  139. }
  140. public void DrawGizmos( Color color, int smoothness = 4 )
  141. {
  142. drawGizmos = true;
  143. gizmoColor = color;
  144. gizmoStep = 1f / ( endPoints.Count * Mathf.Clamp( smoothness, 1, 30 ) );
  145. }
  146. public void HideGizmos()
  147. {
  148. drawGizmos = false;
  149. }
  150. public Vector3 GetPoint( float normalizedT )
  151. {
  152. if( !loop )
  153. {
  154. if( normalizedT <= 0f )
  155. return endPoints[0].position;
  156. else if( normalizedT >= 1f )
  157. return endPoints[endPoints.Count - 1].position;
  158. }
  159. else
  160. {
  161. if( normalizedT < 0f )
  162. normalizedT += 1f;
  163. else if( normalizedT >= 1f )
  164. normalizedT -= 1f;
  165. }
  166. float t = normalizedT * ( loop ? endPoints.Count : ( endPoints.Count - 1 ) );
  167. BezierPoint startPoint, endPoint;
  168. int startIndex = (int) t;
  169. int endIndex = startIndex + 1;
  170. if( endIndex == endPoints.Count )
  171. endIndex = 0;
  172. startPoint = endPoints[startIndex];
  173. endPoint = endPoints[endIndex];
  174. float localT = t - startIndex;
  175. float oneMinusLocalT = 1f - localT;
  176. return oneMinusLocalT * oneMinusLocalT * oneMinusLocalT * startPoint.position +
  177. 3f * oneMinusLocalT * oneMinusLocalT * localT * startPoint.followingControlPointPosition +
  178. 3f * oneMinusLocalT * localT * localT * endPoint.precedingControlPointPosition +
  179. localT * localT * localT * endPoint.position;
  180. }
  181. public Quaternion GetRotation(float normalizedT)
  182. {
  183. if (!loop)
  184. {
  185. if (normalizedT <= 0f)
  186. return endPoints[0].rotation;
  187. else if (normalizedT >= 1f)
  188. return endPoints[endPoints.Count - 1].rotation;
  189. }
  190. else
  191. {
  192. if (normalizedT < 0f)
  193. normalizedT += 1f;
  194. else if (normalizedT >= 1f)
  195. normalizedT -= 1f;
  196. }
  197. float t = normalizedT * (loop ? endPoints.Count : (endPoints.Count - 1));
  198. BezierPoint startPoint, endPoint;
  199. int startIndex = (int)t;
  200. int endIndex = startIndex + 1;
  201. if (endIndex == endPoints.Count)
  202. endIndex = 0;
  203. startPoint = endPoints[startIndex];
  204. endPoint = endPoints[endIndex];
  205. return Quaternion.LerpUnclamped(startPoint.rotation, endPoint.rotation, t - startIndex);
  206. }
  207. public Vector3 GetTangent( float normalizedT )
  208. {
  209. if( !loop )
  210. {
  211. if( normalizedT <= 0f )
  212. return 3f * ( endPoints[0].followingControlPointPosition - endPoints[0].position );
  213. else if( normalizedT >= 1f )
  214. {
  215. int index = endPoints.Count - 1;
  216. return 3f * ( endPoints[index].position - endPoints[index].precedingControlPointPosition );
  217. }
  218. }
  219. else
  220. {
  221. if( normalizedT < 0f )
  222. normalizedT += 1f;
  223. else if( normalizedT >= 1f )
  224. normalizedT -= 1f;
  225. }
  226. float t = normalizedT * ( loop ? endPoints.Count : ( endPoints.Count - 1 ) );
  227. BezierPoint startPoint, endPoint;
  228. int startIndex = (int) t;
  229. int endIndex = startIndex + 1;
  230. if( endIndex == endPoints.Count )
  231. endIndex = 0;
  232. startPoint = endPoints[startIndex];
  233. endPoint = endPoints[endIndex];
  234. float localT = t - startIndex;
  235. float oneMinusLocalT = 1f - localT;
  236. return 3f * oneMinusLocalT * oneMinusLocalT * ( startPoint.followingControlPointPosition - startPoint.position ) +
  237. 6f * oneMinusLocalT * localT * ( endPoint.precedingControlPointPosition - startPoint.followingControlPointPosition ) +
  238. 3f * localT * localT * ( endPoint.position - endPoint.precedingControlPointPosition );
  239. }
  240. public float GetLengthApproximately( float startNormalizedT, float endNormalizedT, float accuracy = 50f )
  241. {
  242. if( endNormalizedT < startNormalizedT )
  243. {
  244. float temp = startNormalizedT;
  245. startNormalizedT = endNormalizedT;
  246. endNormalizedT = temp;
  247. }
  248. if( startNormalizedT < 0f )
  249. startNormalizedT = 0f;
  250. if( endNormalizedT > 1f )
  251. endNormalizedT = 1f;
  252. float step = AccuracyToStepSize( accuracy ) * ( endNormalizedT - startNormalizedT );
  253. float length = 0f;
  254. Vector3 lastPoint = GetPoint( startNormalizedT );
  255. for( float i = startNormalizedT + step; i < endNormalizedT; i += step )
  256. {
  257. Vector3 thisPoint = GetPoint( i );
  258. length += Vector3.Distance( thisPoint, lastPoint );
  259. lastPoint = thisPoint;
  260. }
  261. length += Vector3.Distance( lastPoint, GetPoint( endNormalizedT ) );
  262. return length;
  263. }
  264. public Vector3 FindNearestPointTo( Vector3 worldPos, float accuracy = 100f )
  265. {
  266. float normalizedT;
  267. return FindNearestPointTo( worldPos, out normalizedT, accuracy );
  268. }
  269. public Vector3 FindNearestPointTo( Vector3 worldPos, out float normalizedT, float accuracy = 100f )
  270. {
  271. Vector3 result = Vector3.zero;
  272. normalizedT = -1f;
  273. float step = AccuracyToStepSize( accuracy );
  274. float minDistance = Mathf.Infinity;
  275. for( float i = 0f; i < 1f; i += step )
  276. {
  277. Vector3 thisPoint = GetPoint( i );
  278. float thisDistance = ( worldPos - thisPoint ).sqrMagnitude;
  279. if( thisDistance < minDistance )
  280. {
  281. minDistance = thisDistance;
  282. result = thisPoint;
  283. normalizedT = i;
  284. }
  285. }
  286. return result;
  287. }
  288. public Vector3 MoveAlongSpline( ref float normalizedT, float deltaMovement, int accuracy = 3 )
  289. {
  290. // Credit: https://gamedev.stackexchange.com/a/27138
  291. float constant = deltaMovement / ( ( loop ? endPoints.Count : endPoints.Count - 1 ) * accuracy );
  292. for( int i = 0; i < accuracy; i++ )
  293. normalizedT += constant / GetTangent( normalizedT ).magnitude;
  294. return GetPoint( normalizedT );
  295. }
  296. public void ConstructLinearPath()
  297. {
  298. for( int i = 0; i < endPoints.Count; i++ )
  299. {
  300. endPoints[i].handleMode = BezierPoint.HandleMode.Free;
  301. if( i < endPoints.Count - 1 )
  302. {
  303. Vector3 midPoint = ( endPoints[i].position + endPoints[i + 1].position ) * 0.5f;
  304. endPoints[i].followingControlPointPosition = midPoint;
  305. endPoints[i + 1].precedingControlPointPosition = midPoint;
  306. }
  307. else
  308. {
  309. Vector3 midPoint = ( endPoints[i].position + endPoints[0].position ) * 0.5f;
  310. endPoints[i].followingControlPointPosition = midPoint;
  311. endPoints[0].precedingControlPointPosition = midPoint;
  312. }
  313. }
  314. }
  315. public void AutoConstructSpline()
  316. {
  317. // Credit: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit
  318. for( int i = 0; i < endPoints.Count; i++ )
  319. endPoints[i].handleMode = BezierPoint.HandleMode.Mirrored;
  320. int n = endPoints.Count - 1;
  321. if( n == 1 )
  322. {
  323. endPoints[0].followingControlPointPosition = ( 2 * endPoints[0].position + endPoints[1].position ) / 3f;
  324. endPoints[1].precedingControlPointPosition = 2 * endPoints[0].followingControlPointPosition - endPoints[0].position;
  325. return;
  326. }
  327. Vector3[] rhs;
  328. if( loop )
  329. rhs = new Vector3[n + 1];
  330. else
  331. rhs = new Vector3[n];
  332. for( int i = 1; i < n - 1; i++ )
  333. {
  334. rhs[i] = 4 * endPoints[i].position + 2 * endPoints[i + 1].position;
  335. }
  336. rhs[0] = endPoints[0].position + 2 * endPoints[1].position;
  337. if( !loop )
  338. rhs[n - 1] = ( 8 * endPoints[n - 1].position + endPoints[n].position ) * 0.5f;
  339. else
  340. {
  341. rhs[n - 1] = 4 * endPoints[n - 1].position + 2 * endPoints[n].position;
  342. rhs[n] = ( 8 * endPoints[n].position + endPoints[0].position ) * 0.5f;
  343. }
  344. // Get first control points
  345. Vector3[] controlPoints = GetFirstControlPoints( rhs );
  346. for( int i = 0; i < n; i++ )
  347. {
  348. // First control point
  349. endPoints[i].followingControlPointPosition = controlPoints[i];
  350. if( loop )
  351. {
  352. endPoints[i + 1].precedingControlPointPosition = 2 * endPoints[i + 1].position - controlPoints[i + 1];
  353. }
  354. else
  355. {
  356. // Second control point
  357. if( i < n - 1 )
  358. endPoints[i + 1].precedingControlPointPosition = 2 * endPoints[i + 1].position - controlPoints[i + 1];
  359. else
  360. endPoints[i + 1].precedingControlPointPosition = ( endPoints[n].position + controlPoints[n - 1] ) * 0.5f;
  361. }
  362. }
  363. if( loop )
  364. {
  365. float controlPointDistance = Vector3.Distance( endPoints[0].followingControlPointPosition, endPoints[0].position );
  366. Vector3 direction = Vector3.Normalize( endPoints[n].position - endPoints[1].position );
  367. endPoints[0].precedingControlPointPosition = endPoints[0].position + direction * controlPointDistance;
  368. endPoints[0].followingControlPointLocalPosition = -endPoints[0].precedingControlPointLocalPosition;
  369. }
  370. }
  371. private static Vector3[] GetFirstControlPoints( Vector3[] rhs )
  372. {
  373. // Credit: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit
  374. int n = rhs.Length;
  375. Vector3[] x = new Vector3[n]; // Solution vector.
  376. float[] tmp = new float[n]; // Temp workspace.
  377. float b = 2f;
  378. x[0] = rhs[0] / b;
  379. for( int i = 1; i < n; i++ ) // Decomposition and forward substitution.
  380. {
  381. float val = 1f / b;
  382. tmp[i] = val;
  383. b = ( i < n - 1 ? 4f : 3.5f ) - val;
  384. x[i] = ( rhs[i] - x[i - 1] ) / b;
  385. }
  386. for( int i = 1; i < n; i++ )
  387. {
  388. x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.
  389. }
  390. return x;
  391. }
  392. public void AutoConstructSpline2()
  393. {
  394. // Credit: http://stackoverflow.com/questions/3526940/how-to-create-a-cubic-bezier-curve-when-given-n-points-in-3d
  395. for( int i = 0; i < endPoints.Count; i++ )
  396. {
  397. Vector3 pMinus1, p1, p2;
  398. if( i == 0 )
  399. {
  400. if( loop )
  401. pMinus1 = endPoints[endPoints.Count - 1].position;
  402. else
  403. pMinus1 = endPoints[0].position;
  404. }
  405. else
  406. {
  407. pMinus1 = endPoints[i - 1].position;
  408. }
  409. if( loop )
  410. {
  411. p1 = endPoints[( i + 1 ) % endPoints.Count].position;
  412. p2 = endPoints[( i + 2 ) % endPoints.Count].position;
  413. }
  414. else
  415. {
  416. if( i < endPoints.Count - 2 )
  417. {
  418. p1 = endPoints[i + 1].position;
  419. p2 = endPoints[i + 2].position;
  420. }
  421. else if( i == endPoints.Count - 2 )
  422. {
  423. p1 = endPoints[i + 1].position;
  424. p2 = endPoints[i + 1].position;
  425. }
  426. else
  427. {
  428. p1 = endPoints[i].position;
  429. p2 = endPoints[i].position;
  430. }
  431. }
  432. endPoints[i].followingControlPointPosition = endPoints[i].position + ( p1 - pMinus1 ) / 6f;
  433. endPoints[i].handleMode = BezierPoint.HandleMode.Mirrored;
  434. if( i < endPoints.Count - 1 )
  435. endPoints[i + 1].precedingControlPointPosition = p1 - ( p2 - endPoints[i].position ) / 6f;
  436. else if( loop )
  437. endPoints[0].precedingControlPointPosition = p1 - ( p2 - endPoints[i].position ) / 6f;
  438. }
  439. }
  440. /*public void AutoConstructSpline3()
  441. {
  442. // Todo? http://www.math.ucla.edu/~baker/149.1.02w/handouts/dd_splines.pdf
  443. }*/
  444. private float AccuracyToStepSize( float accuracy )
  445. {
  446. if( accuracy <= 0f )
  447. return 0.2f;
  448. return Mathf.Clamp( 1f / accuracy, 0.001f, 0.2f );
  449. }
  450. // Renders the spline gizmo during gameplay
  451. // Credit: https://docs.unity3d.com/ScriptReference/GL.html
  452. private void OnRenderObject()
  453. {
  454. if( !drawGizmos || endPoints.Count < 2 )
  455. return;
  456. if( !gizmoMaterial )
  457. {
  458. Shader shader = Shader.Find( "Hidden/Internal-Colored" );
  459. gizmoMaterial = new Material( shader ) { hideFlags = HideFlags.HideAndDontSave };
  460. gizmoMaterial.SetInt( "_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha );
  461. gizmoMaterial.SetInt( "_DstBlend", (int) UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha );
  462. gizmoMaterial.SetInt( "_Cull", (int) UnityEngine.Rendering.CullMode.Off );
  463. gizmoMaterial.SetInt( "_ZWrite", 0 );
  464. }
  465. gizmoMaterial.SetPass( 0 );
  466. GL.Begin( GL.LINES );
  467. GL.Color( gizmoColor );
  468. Vector3 lastPos = endPoints[0].position;
  469. for( float i = gizmoStep; i < 1f; i += gizmoStep )
  470. {
  471. GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
  472. lastPos = GetPoint( i );
  473. GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
  474. }
  475. GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
  476. lastPos = GetPoint( 1f );
  477. GL.Vertex3( lastPos.x, lastPos.y, lastPos.z );
  478. GL.End();
  479. }
  480. #if UNITY_EDITOR
  481. public void Reset()
  482. {
  483. for( int i = endPoints.Count - 1; i >= 0; i-- )
  484. UnityEditor.Undo.DestroyObjectImmediate( endPoints[i].gameObject );
  485. Initialize( 2 );
  486. endPoints[0].localPosition = Vector3.back;
  487. endPoints[1].localPosition = Vector3.forward;
  488. UnityEditor.Undo.RegisterCreatedObjectUndo( endPoints[0].gameObject, "Initialize Spline" );
  489. UnityEditor.Undo.RegisterCreatedObjectUndo( endPoints[1].gameObject, "Initialize Spline" );
  490. UnityEditor.Selection.activeTransform = endPoints[0].transform;
  491. }
  492. #endif
  493. }
  494. }