Skip to content

Instantly share code, notes, and snippets.

@mminer
Last active July 5, 2021 23:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mminer/aecc5c758f0ad9f8566da67bf1b0660f to your computer and use it in GitHub Desktop.
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.
/// <summary>
/// Modes that specify how the control points affect a Bezier curve.
/// </summary>
public enum BezierControlPointMode
{
Aligned,
Free,
Mirrored,
}
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;
}
}
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;
}
}
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;
}
}
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