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