/// /// SURGE FRAMEWORK /// Author: Bob Berkebile /// Email: bobb@pixelplacement.com /// /// Creates and manages splines. /// /// using UnityEngine; using System.Collections.Generic; using System; namespace Pixelplacement { public enum SplineDirection { Forward, Backwards } [ExecuteInEditMode] public class Spline : MonoBehaviour { //Public Events: public event Action OnSplineChanged; //Private Classes private class SplineReparam { //Public Variables: public float length; public float percentage; //Constructors: public SplineReparam(float length, float percentage) { this.length = length; this.percentage = percentage; } } //Public Variables: public Color color = Color.yellow; [Range(0, 1)] public float toolScale = .1f; public TangentMode defaultTangentMode; public SplineDirection direction; public bool loop; public SplineFollower[] followers; //Private Variables: private SplineAnchor[] _anchors; private int _curveCount; private int _previousAnchorCount; private int _previousChildCount; private bool _wasLooping; private bool _previousLoopChoice; private bool _anchorsChanged; private SplineDirection _previousDirection; private float _curvePercentage = 0; private int _operatingCurve = 0; private float _currentCurve = 0; private int _previousLength; private int _slicesPerCurve = 10; private List _splineReparams = new List(); private bool _lengthDirty = true; //Public Properties: public float Length { get; private set; } public SplineAnchor[] Anchors { get { //if loop is toggled make sure we reset anchors: if (loop != _wasLooping) { _previousAnchorCount = -1; _wasLooping = loop; } if (!loop) { if (transform.childCount != _previousAnchorCount || transform.childCount == 0) { _anchors = GetComponentsInChildren(); _previousAnchorCount = transform.childCount; } return _anchors; } else { if (transform.childCount != _previousAnchorCount || transform.childCount == 0) { //for a loop we need an array whose last element is the first element: _anchors = GetComponentsInChildren(); Array.Resize(ref _anchors, _anchors.Length + 1); _anchors[_anchors.Length - 1] = _anchors[0]; _previousAnchorCount = transform.childCount; } return _anchors; } } } public Color SecondaryColor { get { Color secondaryColor = Color.Lerp(color, Color.black, .2f); return secondaryColor; } } //Init: void Reset() { //if we don't have at least 2 anchors, fix it: if (Anchors.Length < 2) { AddAnchors(2 - Anchors.Length); } } //Loop: void Update() { //place followers (if supplied and something relavent changed): if (followers != null && followers.Length > 0 && Anchors.Length >= 2) { bool needToUpdate = false; //was anything else changed? if (_anchorsChanged || _previousChildCount != transform.childCount || direction != _previousDirection || loop != _previousLoopChoice) { _previousChildCount = transform.childCount; _previousLoopChoice = loop; _previousDirection = direction; _anchorsChanged = false; needToUpdate = true; } //were any followers moved? for (int i = 0; i < followers.Length; i++) { if (followers[i].WasMoved || needToUpdate) { followers[i].UpdateOrientation(this); } } } //manage anchors: bool anchorChanged = false; if (Anchors.Length > 1) { for (int i = 0; i < Anchors.Length; i++) { //if this spline has changed notify and wipe cached percentage: if (Anchors[i].Changed) { anchorChanged = true; Anchors[i].Changed = false; _anchorsChanged = true; } //if this isn't a loop then the first and last tangents are unnecessary: if (!loop) { //turn first tangent off: if (i == 0) { Anchors[i].SetTangentStatus(false, true); continue; } //turn last tangent off: if (i == Anchors.Length - 1) { Anchors[i].SetTangentStatus(true, false); continue; } //turn both tangents on: Anchors[i].SetTangentStatus(true, true); } else { //all tangents are needed in a loop: Anchors[i].SetTangentStatus(true, true); } } } //length changed: if (_previousLength != Anchors.Length || anchorChanged) { HangleLengthChange(); _previousLength = Anchors.Length; } } //Event Handlers: private void HangleLengthChange() { _lengthDirty = true; //fire event: OnSplineChanged?.Invoke(); } //Private Methods: private float Reparam(float percent) { if (_lengthDirty) CalculateLength(); //TODO: consider optimization of reversing this if the percent is > .5f to go in either direction: for (int i = 0; i < _splineReparams.Count; i++) { float currentPercentage = _splineReparams[i].length / Length; if (currentPercentage == percent) { return _splineReparams[i].percentage; } if (currentPercentage > percent) { float fromP = _splineReparams[i - 1].length / Length; float toP = currentPercentage; //slide scale to 0: float maxAdjusted = toP - fromP; float percentAdjusted = percent - fromP; //find out percentage: float inBetweenPercentage = percentAdjusted / maxAdjusted; float location = Mathf.Lerp(_splineReparams[i - 1].percentage, _splineReparams[i].percentage, inBetweenPercentage); return location; } } return 0; } //Public Methods: /// /// Calculates the length of this spline and puts the result into the Length property. /// public void CalculateLength() { //prep: int totalSlices = (Anchors.Length - 1) * _slicesPerCurve; Length = 0; _splineReparams.Clear(); //initial entries: _splineReparams.Add(new SplineReparam(0, 0)); //find spline length: for (int i = 1; i < totalSlices + 1; i++) { //percent ends: float percent = i / (float)totalSlices; float previousPercent = (i - 1) / (float)totalSlices; //position ends: Vector3 start = GetPosition(previousPercent, false); Vector3 end = GetPosition(percent, false); //length: float distance = Vector3.Distance(start, end); Length += distance; //reparameterization cache: _splineReparams.Add(new SplineReparam(Length, percent)); } _lengthDirty = false; return; } /// /// Get the up vector at a percentage along the spline. /// public Vector3 Up(float percentage, bool normalized = true) { Quaternion lookRotation = Quaternion.LookRotation(GetDirection(percentage, normalized)); return lookRotation * Vector3.up; } /// /// Get the right vector at a percentage along the spline. /// public Vector3 Right(float percentage, bool normalized = true) { Quaternion lookRotation = Quaternion.LookRotation(GetDirection(percentage, normalized)); return lookRotation * Vector3.right; } /// /// Get the forward vector at a percentage along the spline - this is simply a wrapper for the direction since they are the same thing. /// public Vector3 Forward(float percentage, bool normalized = true) { return GetDirection(percentage, normalized); } /// /// Returns a facing vector at the given percentage along the spline to allow content to properly orient along the spline. /// public Vector3 GetDirection(float percentage, bool normalized = true) { if (normalized) percentage = Reparam(percentage); //get direction: CurveDetail curveDetail = GetCurve(percentage); //avoid an error in editor usage where this index can be -1: if (curveDetail.currentCurve < 0) return Vector3.zero; SplineAnchor startAnchor = Anchors[curveDetail.currentCurve]; SplineAnchor endAnchor = Anchors[curveDetail.currentCurve + 1]; return BezierCurves.GetFirstDerivative(startAnchor.Anchor.position, endAnchor.Anchor.position, startAnchor.OutTangent.position, endAnchor.InTangent.position, curveDetail.currentCurvePercentage).normalized; } /// /// Returns a position on the spline at the given percentage. /// public Vector3 GetPosition(float percentage, bool normalized = true) { if (normalized) percentage = Reparam(percentage); //evaluate curve: CurveDetail curveDetail = GetCurve(percentage); //avoid an error in editor usage where this index can be -1: if (curveDetail.currentCurve < 0) return Vector3.zero; SplineAnchor startAnchor = Anchors[curveDetail.currentCurve]; SplineAnchor endAnchor = Anchors[curveDetail.currentCurve + 1]; return BezierCurves.GetPoint(startAnchor.Anchor.position, endAnchor.Anchor.position, startAnchor.OutTangent.position, endAnchor.InTangent.position, curveDetail.currentCurvePercentage, true, 100); } /// /// Returns a position on the spline at the given percentage with a relative offset. /// public Vector3 GetPosition(float percentage, Vector3 relativeOffset, bool normalized = true) { if (normalized) percentage = Reparam(percentage); //get position and look rotation: Vector3 position = GetPosition(percentage); Quaternion lookRotation = Quaternion.LookRotation(GetDirection(percentage)); //get each axis at the current position: Vector3 up = lookRotation * Vector3.up; Vector3 right = lookRotation * Vector3.right; Vector3 forward = lookRotation * Vector3.forward; //translate position: Vector3 offset = position + right * relativeOffset.x; offset += up * relativeOffset.y; offset += forward * relativeOffset.z; return offset; } /// /// Given a world point and a number of divisions (think resolution) this returns the closest point on the spline. /// public float ClosestPoint(Vector3 point, int divisions = 100) { //make sure we have at least one division: if (divisions <= 0) divisions = 1; //variables: float shortestDistance = float.MaxValue; Vector3 position = Vector3.zero; Vector3 offset = Vector3.zero; float closestPercentage = 0; float percentage = 0; float distance = 0; //iterate spline and find the closest point on the spline to the provided point: for (float i = 0; i < divisions + 1; i++) { percentage = i / divisions; position = GetPosition(percentage); offset = position - point; distance = offset.sqrMagnitude; //if this point is closer than any others so far: if (distance < shortestDistance) { shortestDistance = distance; closestPercentage = percentage; } } return closestPercentage; } /// /// Makes a spline longer. /// public GameObject[] AddAnchors(int count) { //refs: GameObject anchorTemplate = Resources.Load("Anchor") as GameObject; //create return array: GameObject[] returnObjects = new GameObject[count]; for (int i = 0; i < count; i++) { //previous anchor refs: Transform previousPreviousAnchor = null; Transform previousAnchor = null; if (Anchors.Length == 1) { previousPreviousAnchor = transform; previousAnchor = Anchors[0].transform; } else if (Anchors.Length > 1) { previousPreviousAnchor = Anchors[Anchors.Length - 2].transform; previousAnchor = Anchors[Anchors.Length - 1].transform; } //create a new anchor: GameObject newAnchor = Instantiate(anchorTemplate); newAnchor.name = newAnchor.name.Replace("(Clone)", ""); SplineAnchor anchor = newAnchor.GetComponent(); anchor.tangentMode = defaultTangentMode; newAnchor.transform.parent = transform; newAnchor.transform.rotation = Quaternion.LookRotation(transform.forward); //tilt tangents for variety as we add new anchors: //anchor.Tilt (new Vector3 (0, 0, 0)); anchor.InTangent.Translate(Vector3.up * .5f); anchor.OutTangent.Translate(Vector3.up * -.5f); //position new anchor: if (previousPreviousAnchor != null && previousAnchor != null) { //determine direction for next placement: Vector3 direction = (previousAnchor.position - previousPreviousAnchor.position).normalized; if (direction == Vector3.zero) direction = transform.forward; //place from the previous anchor in the correct direction: newAnchor.transform.position = previousAnchor.transform.position + (direction * 1.5f); } else { newAnchor.transform.localPosition = Vector3.zero; } //catalog this new anchor for return: returnObjects[i] = newAnchor; } return returnObjects; } /// /// Gets the current curve at the percentage. /// public CurveDetail GetCurve(float percentage) { //clamp or loop percentage: if (loop) { percentage = Mathf.Repeat(percentage, 1); } else { percentage = Mathf.Clamp01(percentage); } //curve identification and evaluation: if (Anchors.Length == 2) { //direction reversed? if (direction == SplineDirection.Backwards) { percentage = 1 - percentage; } //simply evaluate the curve since there is only one: return new CurveDetail(0, percentage); } else { //figure out which curve we are operating on from the spline and a percentage along it: _curveCount = Anchors.Length - 1; _currentCurve = _curveCount * percentage; if ((int)_currentCurve == _curveCount) { _currentCurve = _curveCount - 1; _curvePercentage = 1; } else { _curvePercentage = _currentCurve - (int)_currentCurve; } _currentCurve = (int)_currentCurve; _operatingCurve = (int)_currentCurve; //direction reversed? if (direction == SplineDirection.Backwards) { _curvePercentage = 1 - _curvePercentage; _operatingCurve = (_curveCount - 1) - _operatingCurve; } return new CurveDetail(_operatingCurve, _curvePercentage); } } } }