Files
AppleHillsProduction/Assets/External/Pixelplacement/Surge/Spline/Objects/Spline.cs

527 lines
18 KiB
C#

/// <summary>
/// SURGE FRAMEWORK
/// Author: Bob Berkebile
/// Email: bobb@pixelplacement.com
///
/// Creates and manages splines.
///
/// </summary>
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<SplineReparam> _splineReparams = new List<SplineReparam>();
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<SplineAnchor>();
_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<SplineAnchor>();
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:
/// <summary>
/// Calculates the length of this spline and puts the result into the Length property.
/// </summary>
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;
}
/// <summary>
/// Get the up vector at a percentage along the spline.
/// </summary>
public Vector3 Up(float percentage, bool normalized = true)
{
Quaternion lookRotation = Quaternion.LookRotation(GetDirection(percentage, normalized));
return lookRotation * Vector3.up;
}
/// <summary>
/// Get the right vector at a percentage along the spline.
/// </summary>
public Vector3 Right(float percentage, bool normalized = true)
{
Quaternion lookRotation = Quaternion.LookRotation(GetDirection(percentage, normalized));
return lookRotation * Vector3.right;
}
/// <summary>
/// Get the forward vector at a percentage along the spline - this is simply a wrapper for the direction since they are the same thing.
/// </summary>
public Vector3 Forward(float percentage, bool normalized = true)
{
return GetDirection(percentage, normalized);
}
/// <summary>
/// Returns a facing vector at the given percentage along the spline to allow content to properly orient along the spline.
/// </summary>
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;
}
/// <summary>
/// Returns a position on the spline at the given percentage.
/// </summary>
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);
}
/// <summary>
/// Returns a position on the spline at the given percentage with a relative offset.
/// </summary>
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;
}
/// <summary>
/// Given a world point and a number of divisions (think resolution) this returns the closest point on the spline.
/// </summary>
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;
}
/// <summary>
/// Makes a spline longer.
/// </summary>
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<GameObject>(anchorTemplate);
newAnchor.name = newAnchor.name.Replace("(Clone)", "");
SplineAnchor anchor = newAnchor.GetComponent<SplineAnchor>();
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;
}
/// <summary>
/// Gets the current curve at the percentage.
/// </summary>
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);
}
}
}
}