using UnityEngine; using System; using System.Collections.Generic; /// /// The Spline class represents three-dimensional curves. /// /// /// It provides the most important functions that are necessary to create, calculate and render Splines. /// The class derives from MonoBehaviour so it can be attached to gameObjects and used like any other self-written script. /// [AddComponentMenu("SuperSplines/Spline")] public partial class Spline : MonoBehaviour { public List splineNodesArray = new List( ); ///< A collection of SplineNodes that are used as control nodes. private List splineNodesInternal = new List( ); public InterpolationMode interpolationMode = InterpolationMode.Hermite; ///< Specifies what kind of curve interpolation will be used. public RotationMode rotationMode = RotationMode.Tangent; ///< Specifies how to calculate rotations on the spline. public TangentMode tangentMode = TangentMode.UseTangents; ///< Specifies how tangents are calculated in hermite mode. public NormalMode normalMode = NormalMode.UseGlobalSplineNormal; ///< Specifies how the spline's normal is defined. (mostly needed for RotationMode.Tangent) public UpdateMode updateMode = UpdateMode.DontUpdate; ///< Specifies when the spline will be updated. public int deltaFrames = 1; ///< The number of frames that need to pass before the spline will be updated again. (for UpdateMode.EveryXFrames) public float deltaTime = 0.1f; ///< The amount of time that needs to pass before the spline will be updated again. (for UpdateMode.EveryXSeconds) private int updateFrame = 0; private float updateTime = 0f; public bool perNodeTension = false; ///< If true, the curve's tension can be defined per node public float tension = 0.5f; ///< Curve Tension (only has an effect on Hermite splines). public Vector3 normal = Vector3.up; ///< Spline's Normal / Up-Vector used to calculate rotations (only needed for RotationMode.Tangent) public bool autoClose = false; ///< If set to true the spline start and end points of the spline will be connected. (Note that Bézier-Curves can't be auto-closed!) public int interpolationAccuracy = 5; ///< Defines how accurately numeric calculations will be done. private LengthData lengthData = new LengthData( ); public float Length { get{ return (float) lengthData.length; } } ///< Returns the length of the spline in game units. public bool AutoClose { get{ return autoClose && interpolationMode!=InterpolationMode.Bezier; } } ///< Returns true if spline is auto-closed. If the spline is a Bézier-Curve, false will always be returned. public int NodesPerSegment { get{ return IsBezier ? 3 : 1; } } ///< Returns the number of spline nodes that are needed to describe a spline segment. public int SegmentCount { get{ return Mathf.Max((ControlNodeCount-1)/NodesPerSegment,0); } } ///< Returns the number of spline segments. (Note that a spline segment of a Bézier-Curve is defined by 4 control nodes!) public bool HasBeenUpdated { get{ return updateFrame >= Time.frameCount-1; } } ///< Returns true if the spline has been updated in the current or previous frame. public int UpdateFrame { get{ return updateFrame; } } ///< Returns the frame in which the spline has lastly been updated. private int ControlNodeCount{ get{ return AutoClose ? splineNodesInternal.Count + 1 : splineNodesInternal.Count; } } private double InvertedAccuracy{ get{ return 1.0 / interpolationAccuracy; } } private bool IsBezier{ get{ return interpolationMode == InterpolationMode.Bezier; } } private bool HasNodes{ get{ return splineNodesInternal.Count > 0; } } /// /// Returns an array containing all relevant control nodes that are used internally. /// /// /// Because references to not existing spline nodes and spline nodes that can't be used (more or less than x*3+1 nodes in bézier mode) are removed from /// the internal node array this array might differ from the values in the splineNodesArray. /// public SplineNode[] SplineNodes { get{ if( splineNodesInternal == null ) splineNodesInternal = new List( ); return splineNodesInternal.ToArray( ); } } ///< /// /// Returns an array containing the start and end nodes of the spline's segments. /// /// /// If the used interpolation method isn't bézier-interpolation, it is identical to the returned array of SplineNodes. /// public SplineNode[] SegmentNodes { get{ if( !IsBezier ) return SplineNodes; List nodes = new List( ); for( int i = 0; i < splineNodesInternal.Count; i+=NodesPerSegment ) nodes.Add( splineNodesInternal[i] ); return nodes.ToArray( ); } } /// /// Returns an array containing the spline's segments. /// public SplineSegment[] SplineSegments { get { SplineSegment[] sSegments = new SplineSegment[SegmentCount]; for( int i = 0; i < sSegments.Length; i++ ) sSegments[i] = new SplineSegment( this, GetNode( i*NodesPerSegment, 0 ), GetNode( i*NodesPerSegment, NodesPerSegment ) ); return sSegments; } } void OnEnable( ) { UpdateSpline( ); } void LateUpdate( ) { switch( updateMode ) { case UpdateMode.DontUpdate: break; case UpdateMode.EveryXFrames: if( Time.frameCount % deltaFrames == 0 ) goto case UpdateMode.EveryFrame; break; case UpdateMode.EveryXSeconds: if( deltaTime < Time.realtimeSinceStartup - updateTime ) { updateTime = Time.realtimeSinceStartup; goto case UpdateMode.EveryFrame; } break; // case UpdateMode.WhenNodeMoved: // bool transformChanged = false; // // foreach( SplineNode node in splineNodesInternal ) // { // if( node != null ) // { // if( node.transform.hasChanged ) // { // node.transform.hasChanged = false; // transformChanged = true; // } // } // } // // if( transformChanged ) // goto case UpdateMode.EveryFrame; // // break; case UpdateMode.EveryFrame: UpdateSpline( ); break; } } /// /// This function updates the spline. It is called automatically once in a while, if updateMode isn't set to DontUpdate. /// public void UpdateSpline( ) { switch( interpolationMode ) { case InterpolationMode.Linear: if( !(splineInterpolator is LinearInterpolator) ) splineInterpolator = new LinearInterpolator( ); break; case InterpolationMode.Bezier: if( !(splineInterpolator is BezierInterpolator) ) splineInterpolator = new BezierInterpolator( ); break; case InterpolationMode.Hermite: if( !(splineInterpolator is HermiteInterpolator) ) splineInterpolator = new HermiteInterpolator( ); break; case InterpolationMode.BSpline: if( !(splineInterpolator is BSplineInterpolator) ) splineInterpolator = new BSplineInterpolator( ); break; } //Count valid spline nodes int validNodes = 0; foreach( SplineNode sNode in splineNodesArray ) if( sNode != null ) ++validNodes; //Get relevant count int relevantNodeCount = GetRelevantNodeCount( validNodes ); //Initialize the internal node array if( splineNodesInternal == null ) splineNodesInternal = new List( ); splineNodesInternal.Clear( ); if( !EnoughNodes( relevantNodeCount ) ) return; splineNodesInternal.AddRange( splineNodesArray.GetRange( 0, relevantNodeCount ) ); splineNodesInternal.Remove( null ); ReparameterizeCurve( ); updateFrame = Time.frameCount; } /// /// This function returns a point on the spline for a parameter between 0 and 1 /// /// /// A point on the spline. /// /// /// A normalized spline parameter ([0..1]). /// public Vector3 GetPositionOnSpline( float param ) { if( !HasNodes ) return Vector3.zero; return GetPositionInternal( RecalculateParameter( param ) ); } /// /// This function returns a tangent to the spline for a parameter between 0 and 1 /// /// /// A tangent to the spline. /// /// /// A normalized spline parameter ([0..1]). /// public Vector3 GetTangentToSpline( float param ) { if( !HasNodes ) return Vector3.zero; return GetTangentInternal( RecalculateParameter( param ) ); } /// /// This function returns a normal to the spline for a parameter between 0 and 1. /// /// /// If per-node normals are enabled, it will interpolate the spline's normals. Otherwise it will use the spline's default normal. /// /// /// A normal to the spline. /// /// /// A normalized spline parameter ([0..1]). /// public Vector3 GetNormalToSpline( float param ) { if( !HasNodes ) return Vector3.zero; if( normalMode != NormalMode.UseGlobalSplineNormal ) return GetNormalInternal( RecalculateParameter( param ) ); else return normal.normalized; } /// /// This function returns the curvature of the spline for a parameter between 0 and 1 /// /// /// The local curvature of the spline at a specific location. /// /// /// A normalized spline parameter ([0..1]). /// public Vector3 GetCurvatureOfSpline( float param ) { if( !HasNodes ) return Vector3.zero; return GetCurvatureInternal( RecalculateParameter( param ) ); } /// /// This function returns a rotation on the spline for a parameter between 0 and 1 /// /// /// A rotation on the spline.. /// /// /// A normalized spline parameter ([0..1]). /// public Quaternion GetOrientationOnSpline( float param ) { if( !HasNodes ) return Quaternion.identity; switch( rotationMode ) { case RotationMode.Tangent: SegmentParameter sParam = RecalculateParameter( param ); Vector3 tangent = GetTangentInternal( sParam ); Vector3 normal = GetNormalInternal( sParam ); if( tangent.sqrMagnitude == 0f || normal.sqrMagnitude == 0f ) return Quaternion.identity; return Quaternion.LookRotation( tangent, normal ); case RotationMode.Node: return GetRotationInternal( RecalculateParameter( param ) ); default: return Quaternion.identity; } } /// /// This function returns an interpolated custom value on the spline for a parameter between 0 and 1. /// /// /// The control values can be set in the SplineNode inspector or in the SplineNode script. These control values will be interpolated just like /// the SplineNodes' control positions are. Depending on the used interpolation mode, the actual control values won't be elements of the set of the interpolated values. /// Such a behaviour applies to B-splines for example. Just like the B-spline doesn't necessarily contain all control positions, its interpolated /// custom values don't necessarily contain all custom control values. /// /// /// An interpolated custom value on the spline. /// /// /// A normalized spline parameter ([0..1]). /// public float GetCustomValueOnSpline( float param ) { if( !HasNodes ) return 0f; return GetValueInternal( RecalculateParameter( param ) ); } private Vector3 GetPositionInternal( SegmentParameter sParam ) { return splineInterpolator.InterpolateVector( this, sParam.normalizedParam, sParam.normalizedIndex, AutoClose, splineNodesInternal, 0 ); } private Vector3 GetTangentInternal( SegmentParameter sParam ) { return splineInterpolator.InterpolateVector( this, sParam.normalizedParam, sParam.normalizedIndex, AutoClose, splineNodesInternal, 1 ); } private Vector3 GetNormalInternal( SegmentParameter sParam ) { SplineNode n0; SplineNode n1; SplineNode n2; SplineNode n3; splineInterpolator.GetNodeData( splineNodesInternal, sParam.normalizedIndex, AutoClose, out n0, out n1, out n2, out n3 ); Vector3 normal0; Vector3 normal1; Vector3 normal2; Vector3 normal3; if( normalMode == NormalMode.UseNodeNormal ) { normal0 = n0.transform.TransformDirection( n0.normal ).normalized; normal1 = n1.transform.TransformDirection( n1.normal ).normalized; normal2 = n2.transform.TransformDirection( n2.normal ).normalized; normal3 = n3.transform.TransformDirection( n3.normal ).normalized; } else { normal0 = n0.transform.up; normal1 = n1.transform.up; normal2 = n2.transform.up; normal3 = n3.transform.up; } if( splineInterpolator is HermiteInterpolator ) { HermiteInterpolator hermiteInterpolator = splineInterpolator as HermiteInterpolator; hermiteInterpolator.RecalcVectors( this, n0, n1, ref normal2, ref normal3 ); } return splineInterpolator.InterpolateVector( sParam.normalizedParam, normal0, normal1, normal2.normalized, normal3.normalized, 0 ).normalized; } private Vector3 GetCurvatureInternal( SegmentParameter sParam ) { return splineInterpolator.InterpolateVector( this, sParam.normalizedParam, sParam.normalizedIndex, AutoClose, splineNodesInternal, 2 ); } private float GetValueInternal( SegmentParameter sParam ) { return splineInterpolator.InterpolateValue( this, sParam.normalizedParam, sParam.normalizedIndex, AutoClose, splineNodesInternal, 0 ); } private Quaternion GetRotationInternal( SegmentParameter sParam ) { return splineInterpolator.InterpolateRotation( this, sParam.normalizedParam, sParam.normalizedIndex, AutoClose, splineNodesInternal, 0 ); } /// /// This function returns a spline segment that contains the point on the spline that is defined by a normalized parameter. /// /// /// A spline segment containing the point corresponding to param. /// /// /// A normalized spline parameter ([0..1]). /// public SplineSegment GetSplineSegment( float param ) { param = Mathf.Clamp01( param ); foreach( SplineSegment segment in SplineSegments ) if( segment.IsParameterInRange( param ) ) return segment; return null; } /// /// This function converts a normalized spline parameter to the actual distance to the spline's start point. /// /// /// The actual distance from the start point to the point defined by param. /// /// /// A normalized spline parameter ([0..1]). /// public float ConvertNormalizedParameterToDistance( float param ) { return Length * param; } /// /// This function converts an actual distance from the spline's start point to normalized spline parameter. /// /// /// A normalized spline parameter based on the distance from the splines start point. /// /// /// A specific distance of a point on the spline from its starting point (must be less or equal to the spline length). /// public float ConvertDistanceToNormalizedParameter( float param ) { return (Length <= 0f) ? 0f : param/Length; } /// /// Use this function to quickly append a new SplineNode at the spline's end. /// /// /// A new GameObject that has a SplineNode-Component attached to it. /// public GameObject AddSplineNode( ) { if( splineNodesArray.Count > 0 ) return AddSplineNode( splineNodesArray[splineNodesArray.Count-1] ); else return AddSplineNode( null ); } /// /// Use this function to quickly insert a new SplineNode. /// /// /// A new GameObject that has a SplineNode-Component attached to it. /// /// /// A normalized spline parameter, that defines where the new SplineNode will be inserted. /// public GameObject AddSplineNode( float normalizedParam ) { if( SplineNodes.Length == 0 ) return AddSplineNode( ); SplineNode previousNode = null; foreach( SplineNode sNode in SplineNodes ) { if( sNode.Parameters[this].position >= normalizedParam ) return AddSplineNode( previousNode ); previousNode = sNode; } return AddSplineNode( splineNodesArray[splineNodesArray.Count - 1] ); } /// /// Use this function to quickly insert a new SplineNode. /// /// /// A new GameObject that has a SplineNode-Component attached to it. /// /// /// A reference to a SplineNode after which the new SplineNode will be inserted. /// public GameObject AddSplineNode( SplineNode precedingNode ) { GameObject gObject = new GameObject( ); SplineNode splineNode = gObject.AddComponent( ); int insertIndex; if( precedingNode == null ) insertIndex = 0; else insertIndex = splineNodesArray.IndexOf( precedingNode ) + 1; if( insertIndex == -1 ) throw( new ArgumentException( "The SplineNode referenced by \"percedingNode\" is not part of the spline " + gameObject.name ) ); splineNodesArray.Insert( insertIndex, splineNode ); UpdateSpline( ); return gObject; } /// /// Use this function to quickly remove a new SplineNode. /// /// /// A reference to the gameObject that the SplineNode is attached to. /// public void RemoveSplineNode( GameObject gObject ) { SplineNode splineNode = gObject.GetComponent( ); if( splineNode != null ) RemoveSplineNode( splineNode ); } /// /// Use this function to quickly remove a new SplineNode. /// /// /// A reference to the SplineNode that shall be removed. /// public void RemoveSplineNode( SplineNode splineNode ) { splineNodesArray.Remove( splineNode ); UpdateSpline( ); } //Recalculate the spline parameter for constant-velocity interpolation private SegmentParameter RecalculateParameter( double param ) { if( param <= 0 ) return new SegmentParameter( 0, 0 ); if( param > 1 ) return new SegmentParameter( MaxNodeIndex( ), 1 ); double invertedAccuracy = InvertedAccuracy; if( lengthData == null ) lengthData = new LengthData( ); if( lengthData.subSegmentPosition == null ) lengthData.Calculate( this ); for( int i = lengthData.subSegmentPosition.Length - 1; i >= 0; i-- ) { if( lengthData.subSegmentPosition[i] < param ) { int floorIndex = (i - (i % (interpolationAccuracy))); int normalizedIndex = floorIndex * NodesPerSegment / interpolationAccuracy; double normalizedParam = invertedAccuracy * (i-floorIndex + (param - lengthData.subSegmentPosition[i]) / lengthData.subSegmentLength[i]); if( normalizedIndex >= ControlNodeCount - 1 ) return new SegmentParameter( MaxNodeIndex( ), 1.0 ); return new SegmentParameter( normalizedIndex, normalizedParam ); } } return new SegmentParameter( MaxNodeIndex( ), 1 ); } private SplineNode GetNode( int idxNode, int idxOffset ) { idxNode += idxOffset; if( AutoClose ) return splineNodesInternal[ (idxNode % splineNodesInternal.Count + splineNodesInternal.Count) % splineNodesInternal.Count ]; else return splineNodesInternal[ Mathf.Clamp( idxNode, 0, splineNodesInternal.Count-1 ) ]; } private void ReparameterizeCurve( ) { if( lengthData == null ) lengthData = new LengthData( ); lengthData.Calculate( this ); } private int MaxNodeIndex( ) { return ControlNodeCount - NodesPerSegment - 1; } private int GetRelevantNodeCount( int nodeCount ) { int relevantNodeCount = nodeCount; if( IsBezier ) { if( nodeCount < 7 ) relevantNodeCount -= (nodeCount) % 4; else relevantNodeCount -= (nodeCount - 4) % 3; } return relevantNodeCount; } private bool EnoughNodes( int nodeCount ) { if( IsBezier ) return !(nodeCount < 4 ); else return !(nodeCount < 2); } private struct SegmentParameter { public double normalizedParam; public int normalizedIndex; public SegmentParameter( int index, double param ) { normalizedParam = param; normalizedIndex = index; } } /// /// Specifies how tangents of control points should be calculated. Note that this will only affect Hermite-Splines. /// public enum TangentMode { UseNormalizedTangents, ///< Use the normalized vector that connects the two adjacent control nodes as tangent (see UseTangents). UseTangents, ///< Use the vector that connects the two adjacent control nodes as tangent. UseNodeForwardVector ///< Use the forward vector which depends on the control node's rotation. } /// /// Specifies how normals are defined. This is very important when the spline's rotations mode is set to tangent. /// public enum NormalMode { UseGlobalSplineNormal, ///< Use the globally defined normal of the spline. (Spline.normal) UseNodeNormal, ///< Use the nodes normal. UseNodeUpVector, ///< Use the nodes local up-vector given by its transform-component. } /// /// Specifies how rotations will be interpolated over the spline. /// public enum RotationMode { None, ///< No rotation (Quaternion.identity). Node, ///< Interpolate the control nodes' orientation. Tangent ///< Use the tangent to calculate the rotation on the spline. } /// /// Specifies the type of spline interpolation that will be used. /// public enum InterpolationMode { Hermite, ///< Hermite Spline Bezier, ///< Bézier Spline BSpline, ///< B-Spline Linear, ///< Linear Interpolation CustomMatrix ///< Use a custom coefficient matrix for interpolation (if CustomMatrix hasn't been assigned to, the hermite matrix will be used) } /// /// Specifies when to update and recalculate a spline. /// public enum UpdateMode { DontUpdate, ///< Keeps the spline static. It will only be updated when the component becomes enabled (OnEnable( )). EveryFrame, ///< Updates the spline every frame. EveryXFrames, ///< Updates the spline every x frames. EveryXSeconds, ///< Updates the spline every x seconds. // WhenNodeMoved ///< Updates the spline whenever a spline node has been moved. (Will reset the nodes' transforms' hasChanged-Property to false in LateUpdate) } }