Utilities: Spline

5

To make use of the Bezier class from the last post, here is a Spline class. It is not the most grabage friendly class, so I might revisit it at some time, but it does what I wanted it to do when I wrote it. As I said it uses the Bezier class for smooth curves, but there is much automatisation, so no user-input is needed/possible to define the typical Bezier handles. This has the drawback that there is a risk of overshooting, if the distance between spline-vertices varies too much. Splines can be smooth or segmented, closed or open, and are visualized via a LineRenderer.

As usual, the most current code is found on my Bitbucket repository.

<pre>using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Utilities {
	public class Spline {

		public Vector3[] vertices { get; private set; }

		private bool _closed;
		private bool _smooth;
		private float _thickness;

		private bool _isInitialized;

		private GameObject _visualizationObject;
		private LineRenderer _visualizationRenderer;

		public struct Segment {
			public Vector3 start;
			public Vector3 end;

			public Vector3 A, B;

			public float length;
			public float startLength;
		};

		private Segment[] _segments;

		public float length { get; private set; }

		public bool visible {
			get { return _visualizationRenderer.enabled; }
			set { _visualizationRenderer.enabled = value; }
		}

		private Spline() {}

		public static Spline Create(Vector3[] points, Material material, float thickness = 0.1f, bool smooth = true, bool closed = true) {

			if (points.Length < 2) return null;

			Spline spline = new Spline {
				vertices = points,
				_smooth = smooth,
				_closed = closed,
				_thickness = thickness,
				_visualizationObject = new GameObject("Spline Vertex") {layer = GeneralUtilities.GIZMOS_LAYER}
			};

			spline._visualizationRenderer = spline._visualizationObject.AddComponent<LineRenderer>();
			spline._visualizationRenderer.material = material;

			spline.Setup();

			return spline;
		}

		public void Update(Vector3[] points) {
			vertices = points;
			Setup();
		}

		void Setup() {
			if (vertices.Length < 2) return;

			_segments = new Segment[_closed ? vertices.Length : vertices.Length - 1];
			length = 0;

			if (!_smooth || vertices.Length == 2) {
				for (int i = 0; i < vertices.Length - 1; ++i) {

					float l = (vertices[i + 1] - vertices[i]).magnitude;

					_segments[i] = new Segment {
						start = vertices[i],
						end = vertices[i + 1],
						length = l,
						startLength = length
					};

					length += l;
				}

				if (_closed) {
					float l = (vertices[0] - vertices[vertices.Length - 1]).magnitude;

					_segments[_segments.Length - 1] = new Segment {
						start = vertices[vertices.Length - 1],
						end = vertices[0],
						length = l,
						startLength = length
					};

					length += l;
				}

			} else if (_closed) {
				for (int i = 0; i < vertices.Length; ++i) {
					Vector3 start = vertices[i];
					Vector3 end = vertices[(i + 1) % vertices.Length];
					Vector3 controlPointA = start + (end - vertices[(i - 1 + vertices.Length) % vertices.Length]) / 4;
					Vector3 controlPointB = end + (start - vertices[(i + 2) % vertices.Length]) / 4;

					float l = Bezier.Length(start, end, controlPointA, controlPointB, 10);

					_segments[i] = new Segment {
						start = start,
						end = end,
						length = l,
						startLength = length,
						A = controlPointA,
						B = controlPointB
					};

					length += l;
				}

			} else {
				Vector3 start = vertices[0];
				Vector3 end = vertices[1];
				Vector3 controlPointB = end + (start - vertices[2]) / 4;
				Vector3 controlPointA = (controlPointB + 2 * start) / 3;

				float l = Bezier.Length(start, end, controlPointA, controlPointB, 10);

				_segments[0] = new Segment {
					start = start,
					end = end,
					length = l,
					startLength = 0,
					A = controlPointA,
					B = controlPointB
				};

				length += l;

				for (int i = 1; i < vertices.Length - 2; ++i) {
					start = vertices[i];
					end = vertices[i + 1];
					controlPointA = start + (end - vertices[i - 1]) / 4;
					controlPointB = end + (start - vertices[i + 2]) / 4;

					l = Bezier.Length(start, end, controlPointA, controlPointB, 10);

					_segments[i] = new Segment {
						start = start,
						end = end,
						length = l,
						startLength = length,
						A = controlPointA,
						B = controlPointB
					};

					length += l;
				}

				start = vertices[vertices.Length - 2];
				end = vertices[vertices.Length - 1];
				controlPointA = start + (end - vertices[vertices.Length - 3]) / 4;
				controlPointB = (controlPointA + 2 * end) / 3;

				l = Bezier.Length(start, end, controlPointA, controlPointB, 10);

				_segments[vertices.Length - 2] = new Segment {
					start = start,
					end = end,
					length = l,
					startLength = length,
					A = controlPointA,
					B = controlPointB
				};
				length += l;
			}

			_isInitialized = true;
		}

		public void SetMaterial(Material material) {
			_visualizationRenderer.material = material;
		}

		public void SetColor(Color color) {
			_visualizationRenderer.material.color = color;
		}

		public void Draw() {
			if (vertices.Length < 2) {
				_visualizationRenderer.SetVertexCount(0);
				return;
			}

			if (!_isInitialized) Setup();

			List<Vector3> segmentPoints;

			if (!_smooth || vertices.Length == 2) {
				segmentPoints = _segments.Select(s => s.start).ToList();
			} else {
				segmentPoints = _segments.SelectMany(s => GetCurvePoints(s)).ToList();
			}

			segmentPoints.Add(_segments[_segments.Length - 1].end);

			_visualizationRenderer.SetVertexCount(segmentPoints.Count);
			_visualizationRenderer.SetPositions(segmentPoints.ToArray());

			_visualizationRenderer.SetWidth(_thickness, _thickness);
			visible = true;
		}

		public void Hide() {
			visible = false;
		}

		public void Clear() {
			vertices = null;
			Hide();
		}

		private static Vector3[] GetCurvePoints(Segment segment, int segments = 10) {
			Vector3[] positions = new Vector3[segments];

			for (int i = 0; i < segments; ++i) {
				positions[i] = Bezier.Interpolate(segment.start, segment.end, segment.A, segment.B, (float) i / segments);
			}

			return positions;
		}

		public Vector3 GetPoint(float alongPath) {
			if (!_isInitialized) {
				Setup();
			}

			if (alongPath >= 1) {
				return vertices[vertices.Length - 1];
			}

			if (alongPath <= 0) {
				return vertices[0];
			}

			if (vertices.Length == 2) {
				return Vector3.Lerp(vertices[0], vertices[1], alongPath);
			}

			float alongLength = alongPath * length;

			int segIndex = 1;

			while (segIndex < _segments.Length && _segments[segIndex].startLength < alongLength) {
				segIndex++;
			}

			Segment seg = _segments[segIndex - 1];
			float localPercentage = (alongLength - seg.startLength) / (seg.length);

			return !_smooth ? Vector3.Lerp(seg.start, seg.end, localPercentage) : Bezier.Interpolate(seg.start, seg.end, seg.A, seg.B, localPercentage);
		}
	}
}

Comments

Your email address will not be published. Required fields are marked *