Last active
July 5, 2021 23:17
-
-
Save mminer/aecc5c758f0ad9f8566da67bf1b0660f to your computer and use it in GitHub Desktop.
Unity spline / Bezier curves adapted from Catlike Coding's "Curves and Splines" tutorial.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Modes that specify how the control points affect a Bezier curve. | |
/// </summary> | |
public enum BezierControlPointMode | |
{ | |
Aligned, | |
Free, | |
Mirrored, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
/// <summary> | |
/// Math functions for calculating Bezier curves. | |
/// </summary> | |
public static class BezierMath | |
{ | |
public static Vector3 FirstDerivative(Vector3 point0, Vector3 point1, Vector3 point2, Vector3 point3, float t) | |
{ | |
t = Mathf.Clamp01(t); | |
var oneMinusT = 1f - t; | |
return | |
3f * oneMinusT * oneMinusT * (point1 - point0) + | |
6f * oneMinusT * t * (point2 - point1) + | |
3f * t * t * (point3 - point2); | |
} | |
public static Vector3 Point(Vector3 point0, Vector3 point1, Vector3 point2, Vector3 point3, float t) | |
{ | |
t = Mathf.Clamp01(t); | |
var oneMinusT = 1f - t; | |
return | |
oneMinusT * oneMinusT * oneMinusT * point0 + | |
3f * oneMinusT * oneMinusT * t * point1 + | |
3f * oneMinusT * t * t * point2 + | |
t * t * t * point3; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UnityEngine; | |
/// <summary> | |
/// A path constructed from a series of Bezier curves. | |
/// Adapted from Catlike Coding's "Curves and Splines" tutorial. | |
/// https://catlikecoding.com/unity/tutorials/curves-and-splines/ | |
/// </summary> | |
public class Spline : MonoBehaviour | |
{ | |
[SerializeField] Vector3[] points; | |
[SerializeField] BezierControlPointMode[] modes; | |
[SerializeField] bool loop; | |
public int ControlPointCount => points.Length; | |
public int CurveCount => (points.Length - 1) / 3; | |
public bool Loop | |
{ | |
get => loop; | |
set | |
{ | |
loop = value; | |
if (value) | |
{ | |
modes[modes.Length - 1] = modes[0]; | |
SetControlPoint(0, points[0]); | |
} | |
} | |
} | |
public Vector3 GetControlPoint(int index) | |
{ | |
return points[index]; | |
} | |
public void SetControlPoint(int index, Vector3 point) | |
{ | |
if (index % 3 == 0) | |
{ | |
var delta = point - points[index]; | |
if (loop) | |
{ | |
if (index == 0) | |
{ | |
points[1] += delta; | |
points[points.Length - 2] += delta; | |
points[points.Length - 1] = point; | |
} | |
else if (index == points.Length - 1) | |
{ | |
points[0] = point; | |
points[1] += delta; | |
points[index - 1] += delta; | |
} | |
else | |
{ | |
points[index - 1] += delta; | |
points[index + 1] += delta; | |
} | |
} | |
else | |
{ | |
if (index > 0) | |
{ | |
points[index - 1] += delta; | |
} | |
if (index + 1 < points.Length) | |
{ | |
points[index + 1] += delta; | |
} | |
} | |
} | |
points[index] = point; | |
EnforceMode(index); | |
} | |
public BezierControlPointMode GetControlPointMode(int index) | |
{ | |
return modes[(index + 1) / 3]; | |
} | |
public void SetControlPointMode(int index, BezierControlPointMode mode) | |
{ | |
var modeIndex = (index + 1) / 3; | |
modes[modeIndex] = mode; | |
if (loop) | |
{ | |
if (modeIndex == 0) | |
{ | |
modes[modes.Length - 1] = mode; | |
} | |
else if (modeIndex == modes.Length - 1) | |
{ | |
modes[0] = mode; | |
} | |
} | |
EnforceMode(index); | |
} | |
public Vector3 GetDirection(float t) | |
{ | |
return GetVelocity(t).normalized; | |
} | |
public Vector3 GetPoint(float t) | |
{ | |
int i; | |
if (t >= 1f) | |
{ | |
t = 1f; | |
i = points.Length - 4; | |
} | |
else | |
{ | |
t = Mathf.Clamp01(t) * CurveCount; | |
i = (int)t; | |
t -= i; | |
i *= 3; | |
} | |
var bezierPoint = BezierMath.Point(points[i], points[i + 1], points[i + 2], points[i + 3], t); | |
return transform.TransformPoint(bezierPoint); | |
} | |
public void AddCurve() | |
{ | |
Array.Resize(ref modes, modes.Length + 1); | |
Array.Resize(ref points, points.Length + 3); | |
var point = points[points.Length - 1]; | |
point.x += 1f; | |
points[points.Length - 3] = point; | |
point.x += 1f; | |
points[points.Length - 2] = point; | |
point.x += 1f; | |
points[points.Length - 1] = point; | |
modes[modes.Length - 1] = modes[modes.Length - 2]; | |
EnforceMode(points.Length - 4); | |
if (loop) | |
{ | |
points[points.Length - 1] = points[0]; | |
modes[modes.Length - 1] = modes[0]; | |
EnforceMode(0); | |
} | |
} | |
public void Reset() | |
{ | |
points = new[] | |
{ | |
new Vector3(1, 0, 0), | |
new Vector3(2, 0, 0), | |
new Vector3(3, 0, 0), | |
new Vector3(4, 0, 0), | |
}; | |
modes = new[] | |
{ | |
BezierControlPointMode.Free, | |
BezierControlPointMode.Free, | |
}; | |
} | |
void EnforceMode(int index) | |
{ | |
var modeIndex = (index + 1) / 3; | |
var mode = modes[modeIndex]; | |
if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) | |
{ | |
return; | |
} | |
var middleIndex = modeIndex * 3; | |
int fixedIndex; | |
int enforcedIndex; | |
if (index <= middleIndex) | |
{ | |
fixedIndex = middleIndex - 1; | |
if (fixedIndex < 0) | |
{ | |
fixedIndex = points.Length - 2; | |
} | |
enforcedIndex = middleIndex + 1; | |
if (enforcedIndex >= points.Length) | |
{ | |
enforcedIndex = 1; | |
} | |
} | |
else | |
{ | |
fixedIndex = middleIndex + 1; | |
if (fixedIndex >= points.Length) | |
{ | |
fixedIndex = 1; | |
} | |
enforcedIndex = middleIndex - 1; | |
if (enforcedIndex < 0) | |
{ | |
enforcedIndex = points.Length - 2; | |
} | |
} | |
var middle = points[middleIndex]; | |
var enforcedTangent = middle - points[fixedIndex]; | |
if (mode == BezierControlPointMode.Aligned) | |
{ | |
enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]); | |
} | |
points[enforcedIndex] = middle + enforcedTangent; | |
} | |
Vector3 GetVelocity(float t) | |
{ | |
int i; | |
if (t >= 1f) | |
{ | |
t = 1f; | |
i = points.Length - 4; | |
} | |
else | |
{ | |
t = Mathf.Clamp01(t) * CurveCount; | |
i = (int)t; | |
t -= i; | |
i *= 3; | |
} | |
var bezierFirstDerivative = BezierMath.FirstDerivative(points[i], points[i + 1], points[i + 2], points[i + 3], t); | |
return transform.TransformPoint(bezierFirstDerivative) - transform.position; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEditor; | |
using UnityEngine; | |
/// <summary> | |
/// Custom inspector to control spline points interactively in the scene view. | |
/// </summary> | |
[CustomEditor(typeof(Spline))] | |
public class SplineEditor : UnityEditor.Editor | |
{ | |
const float handleSize = 0.04f; | |
const float pickSize = 0.06f; | |
static readonly Dictionary<BezierControlPointMode, Color> modeColors = new Dictionary<BezierControlPointMode, Color> | |
{ | |
{ BezierControlPointMode.Aligned, Color.yellow }, | |
{ BezierControlPointMode.Free, Color.white }, | |
{ BezierControlPointMode.Mirrored, Color.cyan }, | |
}; | |
Spline spline; | |
Transform handleTransform; | |
Quaternion handleRotation; | |
int selectedIndex = -1; | |
public override void OnInspectorGUI() | |
{ | |
spline = target as Spline; | |
if (spline == null) | |
{ | |
return; | |
} | |
using (var check = new EditorGUI.ChangeCheckScope()) | |
{ | |
var loop = EditorGUILayout.Toggle("Loop", spline.Loop); | |
if (check.changed) | |
{ | |
Undo.RecordObject(spline, "Toggle Loop"); | |
EditorUtility.SetDirty(spline); | |
spline.Loop = loop; | |
} | |
} | |
var isPointSelected = selectedIndex >= 0 && selectedIndex < spline.ControlPointCount; | |
if (isPointSelected) | |
{ | |
DrawSelectedPointInspector(); | |
} | |
if (GUILayout.Button("Add Curve")) | |
{ | |
Undo.RecordObject(spline, "Add Curve"); | |
EditorUtility.SetDirty(spline); | |
spline.AddCurve(); | |
} | |
} | |
void OnSceneGUI() | |
{ | |
spline = target as Spline; | |
if (spline == null) | |
{ | |
return; | |
} | |
handleTransform = spline.transform; | |
handleRotation = Tools.pivotRotation == PivotRotation.Local | |
? handleTransform.rotation | |
: Quaternion.identity; | |
var point0 = ShowPoint(0); | |
for (var i = 1; i < spline.ControlPointCount; i += 3) | |
{ | |
var point1 = ShowPoint(i); | |
var point2 = ShowPoint(i + 1); | |
var point3 = ShowPoint(i + 2); | |
Handles.color = Color.gray; | |
Handles.DrawLine(point0, point1); | |
Handles.DrawLine(point2, point3); | |
Handles.DrawBezier(point0, point3, point1, point2, Color.white, null, 2f); | |
point0 = point3; | |
} | |
} | |
void DrawSelectedPointInspector() | |
{ | |
using (var check = new EditorGUI.ChangeCheckScope()) | |
{ | |
var point = EditorGUILayout.Vector3Field("Selected Point", spline.GetControlPoint(selectedIndex)); | |
if (check.changed) | |
{ | |
Undo.RecordObject(spline, "Move Point"); | |
EditorUtility.SetDirty(spline); | |
spline.SetControlPoint(selectedIndex, point); | |
} | |
} | |
using (var check = new EditorGUI.ChangeCheckScope()) | |
using (new EditorGUILayout.HorizontalScope()) | |
{ | |
EditorGUILayout.PrefixLabel(" "); | |
var mode = (BezierControlPointMode)EditorGUILayout.EnumPopup(spline.GetControlPointMode(selectedIndex)); | |
if (check.changed) | |
{ | |
Undo.RecordObject(spline, "Change Point Mode"); | |
EditorUtility.SetDirty(spline); | |
spline.SetControlPointMode(selectedIndex, mode); | |
} | |
} | |
} | |
Vector3 ShowPoint(int index) | |
{ | |
var point = handleTransform.TransformPoint(spline.GetControlPoint(index)); | |
var size = HandleUtility.GetHandleSize(point); | |
if (index == 0) | |
{ | |
size *= 2; | |
} | |
var controlPointMode = spline.GetControlPointMode(index); | |
Handles.color = modeColors[controlPointMode]; | |
if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotHandleCap)) | |
{ | |
selectedIndex = index; | |
Repaint(); | |
} | |
if (selectedIndex == index) | |
{ | |
using var check = new EditorGUI.ChangeCheckScope(); | |
point = Handles.DoPositionHandle(point, handleRotation); | |
if (check.changed) | |
{ | |
Undo.RecordObject(spline, "Move Point"); | |
EditorUtility.SetDirty(spline); | |
spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point)); | |
} | |
} | |
return point; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
/// <summary> | |
/// Follows a spline path. | |
/// </summary> | |
public class SplineWalker : MonoBehaviour | |
{ | |
enum WrapMode | |
{ | |
Loop, | |
Once, | |
PingPong, | |
} | |
[SerializeField] Spline spline; | |
[SerializeField] float duration; | |
[SerializeField] bool lookForward; | |
[SerializeField] WrapMode mode; | |
bool goingForward = true; | |
float progress; | |
void Update() | |
{ | |
UpdateProgress(); | |
transform.localPosition = spline.GetPoint(progress); | |
if (lookForward) | |
{ | |
var direction = spline.GetDirection(progress); | |
transform.LookAt(transform.localPosition + direction); | |
} | |
} | |
void UpdateProgress() | |
{ | |
var delta = Time.deltaTime / duration; | |
if (goingForward) | |
{ | |
progress += delta; | |
} | |
else | |
{ | |
progress -= delta; | |
} | |
if (progress > 1) | |
{ | |
switch (mode) | |
{ | |
case WrapMode.Loop: | |
progress -= 1; | |
break; | |
case WrapMode.Once: | |
progress = 1; | |
break; | |
case WrapMode.PingPong: | |
progress = 2 - progress; | |
goingForward = false; | |
break; | |
} | |
} | |
if (progress < 0) | |
{ | |
progress = -progress; | |
goingForward = true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment