using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; namespace SplineMesh { /// /// Mathematical object for cubic Bézier curve definition. /// It is made of two spline nodes which hold the four needed control points : two positions and two directions /// It provides methods to get positions and tangent along the curve, specifying a distance or a ratio, plus the curve length. /// /// Note that a time of 0.5 and half the total distance won't necessarily define the same curve point as the curve curvature is not linear. /// [Serializable] public class CubicBezierCurve { private const int STEP_COUNT = 30; private const float T_STEP = 1.0f / STEP_COUNT; private readonly List samples = new List(STEP_COUNT); public SplineNode n1, n2; /// /// Length of the curve in world unit. /// public float Length { get; private set; } /// /// This event is raised when of of the control points has moved. /// public UnityEvent Changed = new UnityEvent(); /// /// Build a new cubic Bézier curve between two given spline node. /// /// /// public CubicBezierCurve(SplineNode n1, SplineNode n2) { this.n1 = n1; this.n2 = n2; n1.Changed += ComputeSamples; n2.Changed += ComputeSamples; ComputeSamples(null, null); } /// /// Change the start node of the curve. /// /// public void ConnectStart(SplineNode n1) { this.n1.Changed -= ComputeSamples; this.n1 = n1; n1.Changed += ComputeSamples; ComputeSamples(null, null); } /// /// Change the end node of the curve. /// /// public void ConnectEnd(SplineNode n2) { this.n2.Changed -= ComputeSamples; this.n2 = n2; n2.Changed += ComputeSamples; ComputeSamples(null, null); } /// /// Convinent method to get the third control point of the curve, as the direction of the end spline node indicates the starting tangent of the next curve. /// /// public Vector3 GetInverseDirection() { return (2 * n2.Position) - n2.Direction; } /// /// Returns point on curve at given time. Time must be between 0 and 1. /// /// /// private Vector3 GetLocation(float t) { float omt = 1f - t; float omt2 = omt * omt; float t2 = t * t; return n1.Position * (omt2 * omt) + n1.Direction * (3f * omt2 * t) + GetInverseDirection() * (3f * omt * t2) + n2.Position * (t2 * t); } /// /// Returns tangent of curve at given time. Time must be between 0 and 1. /// /// /// private Vector3 GetTangent(float t) { float omt = 1f - t; float omt2 = omt * omt; float t2 = t * t; Vector3 tangent = n1.Position * (-omt2) + n1.Direction * (3 * omt2 - 2 * omt) + GetInverseDirection() * (-3 * t2 + 2 * t) + n2.Position * (t2); return tangent.normalized; } private Vector3 GetUp(float t) { return Vector3.Lerp(n1.Up, n2.Up, t); } private Vector2 GetScale(float t) { return Vector2.Lerp(n1.Scale, n2.Scale, t); } private float GetRoll(float t) { return Mathf.Lerp(n1.Roll, n2.Roll, t); } private void ComputeSamples(object sender, EventArgs e) { samples.Clear(); Length = 0; Vector3 previousPosition = GetLocation(0); for (float t = 0; t < 1; t += T_STEP) { Vector3 position = GetLocation(t); Length += Vector3.Distance(previousPosition, position); previousPosition = position; samples.Add(CreateSample(Length, t)); } Length += Vector3.Distance(previousPosition, GetLocation(1)); samples.Add(CreateSample(Length, 1)); if (Changed != null) Changed.Invoke(); } private CurveSample CreateSample(float distance, float time) { return new CurveSample( GetLocation(time), GetTangent(time), GetUp(time), GetScale(time), GetRoll(time), distance, time, this); } /// /// Returns an interpolated sample of the curve, containing all curve data at this time. /// /// /// public CurveSample GetSample(float time) { AssertTimeInBounds(time); CurveSample previous = samples[0]; CurveSample next = default(CurveSample); bool found = false; foreach (CurveSample cp in samples) { if (cp.timeInCurve >= time) { next = cp; found = true; break; } previous = cp; } if (!found) throw new Exception("Can't find curve samples."); float t = next == previous ? 0 : (time - previous.timeInCurve) / (next.timeInCurve - previous.timeInCurve); return CurveSample.Lerp(previous, next, t); } /// /// Returns an interpolated sample of the curve, containing all curve data at this distance. /// /// /// public CurveSample GetSampleAtDistance(float d) { if (d < 0 || d > Length) throw new ArgumentException("Distance must be positive and less than curve length. Length = " + Length + ", given distance was " + d); CurveSample previous = samples[0]; CurveSample next = default(CurveSample); bool found = false; foreach (CurveSample cp in samples) { if (cp.distanceInCurve >= d) { next = cp; found = true; break; } previous = cp; } if (!found) throw new Exception("Can't find curve samples."); float t = next == previous ? 0 : (d - previous.distanceInCurve) / (next.distanceInCurve - previous.distanceInCurve); return CurveSample.Lerp(previous, next, t); } private static void AssertTimeInBounds(float time) { if (time < 0 || time > 1) throw new ArgumentException("Time must be between 0 and 1 (was " + time + ")."); } public CurveSample GetProjectionSample(Vector3 pointToProject) { float minSqrDistance = float.PositiveInfinity; int closestIndex = -1; int i = 0; foreach (var sample in samples) { float sqrDistance = (sample.location - pointToProject).sqrMagnitude; if (sqrDistance < minSqrDistance) { minSqrDistance = sqrDistance; closestIndex = i; } i++; } CurveSample previous, next; if(closestIndex == 0) { previous = samples[closestIndex]; next = samples[closestIndex + 1]; } else if(closestIndex == samples.Count - 1) { previous = samples[closestIndex - 1]; next = samples[closestIndex]; } else { var toPreviousSample = (pointToProject - samples[closestIndex - 1].location).sqrMagnitude; var toNextSample = (pointToProject - samples[closestIndex + 1].location).sqrMagnitude; if (toPreviousSample < toNextSample) { previous = samples[closestIndex - 1]; next = samples[closestIndex]; } else { previous = samples[closestIndex]; next = samples[closestIndex + 1]; } } var onCurve = Vector3.Project(pointToProject - previous.location, next.location - previous.location) + previous.location; var rate = (onCurve - previous.location).sqrMagnitude / (next.location - previous.location).sqrMagnitude; rate = Mathf.Clamp(rate, 0, 1); var result = CurveSample.Lerp(previous, next, rate); return result; } } }