Hermite Spline Controller

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
m (Text replace - "</javascript>" to "</syntaxhighlight>")
Line 160: Line 160:
  
 
// We need this to sort GameObjects by name
 
// We need this to sort GameObjects by name
class NameComparer extends IComparer  
+
class NameComparer implements IComparer  
 
{
 
{
 
function Compare(trA : Object, trB : Object) : int {
 
function Compare(trA : Object, trB : Object) : int {

Revision as of 07:08, 23 June 2012

Contents

Download

Download SplineController.zip

C# version

Download SplineController C#.zip by Benoit FOULETIER

Just transcribed everything to C# without any functional change.
Only tweak: the SplineInterpolator was added at runtime, or during OnDrawGizmos using new SplineInterpolator(), which is incorrect => it's now added at design-time (added RequireComponent to simplify things).
Note: that tweak causes the routine to keep resetting the SplineInterpolator, which prevents it from functioning correctly in the game (at least, in the iPhone version of the Unity IDE this is true). You will also need to add "DestroyImmediate(interp)" as the end of OnDrawGizmos.

Unity iPhone C# version (no generics)

Download SplineController C# for iPhone.zip by Matt Maker

Tweaked, as minimally as I could figure out, to allow this to run in Unity iPhone. It's not pretty; feel free to refactor it!

Description

A hermite spline interpolator. It allows you to move your GameObject smoothly through an easily defined path, interpolating both position and orientation.

It features two orientation modes:

  • NODE : It directly takes the orientation from the control point
  • TANGENT : Calculates the orientation based on the tangent to the curve in the control point.

It has a feature called "Auto Close", which makes automatically both starting and end points and tangents coincident.

It also has two playback modes, "ONCE" and "LOOP".

Usage

  • Add to your GameObject the script "Components/Splines/Spline Controller"
  • Create a GameObject which is going to be the parent of all the control points.
  • Create children GameObjects, which are your spline control points. They will be read in alphabetical order.
  • Asign the parent of the control points to your moving GameObject in the "Spline Parent" slot.


That's all.

A picture is worth a thousand words:

SplinePicture.png

C# codefix?

I was running into problems with the transform list being a null object when OnDrawGizmos was accessing it, here's a fix:

<csharp > /// <summary> /// Returns children transforms, sorted by name. /// </summary> Transform[] GetTransforms() { if (SplineRoot == null) return new Transform[] { };

List<Component> components = new List<Component>(SplineRoot.GetComponentsInChildren(typeof(Transform))); List<Transform> transforms = components.ConvertAll(c => (Transform)c);

transforms.Remove(SplineRoot.transform); transforms.Sort(delegate(Transform a, Transform b) { return a.name.CompareTo(b.name); });

return transforms.ToArray(); } </csharp >


JavaScript - SplineController.js

enum eOrientationMode { NODE = 0, TANGENT }
 
var SplineParent : GameObject;
var Duration : float = 10.0;
var OrientationMode : eOrientationMode = eOrientationMode.NODE;
var WrapMode : eWrapMode = eWrapMode.ONCE;
var AutoStart : boolean = true;
var AutoClose : boolean = true;
var HideOnExecute : boolean = true;
 
 
private var mSplineInterp : SplineInterpolator = null;
private var mTransforms : Array = null;
 
@script AddComponentMenu("Splines/Spline Controller")
 
function OnDrawGizmos()
{
	var trans : Array = GetTransforms();
 
	if (trans.length < 2)
		return;
 
	var interp = new SplineInterpolator();
	SetupSplineInterpolator(interp, trans);
 
	interp.StartInterpolation(null, false, WrapMode);
 
	var prevPos : Vector3 = trans[0].position;
	for (c=1; c <= 100; c++)
	{
		var currTime:float = c * Duration / 100.0;
		var currPos = interp.GetHermiteAtTime(currTime);
		var mag:float = (currPos-prevPos).magnitude * 2.0;
		Gizmos.color = Color(mag, 0.0, 0.0, 1.0);
		Gizmos.DrawLine(prevPos, currPos);
		prevPos = currPos;
	}
}
 
 
function Start()
{
	mSplineInterp = gameObject.AddComponent(SplineInterpolator);
 
	mTransforms = GetTransforms();
 
	if (HideOnExecute)
		DisableTransforms();
 
	if (AutoStart)
		FollowSpline();
}
 
 
function SetupSplineInterpolator(interp:SplineInterpolator, trans:Array) : void
{
	interp.Reset();
 
	if (AutoClose)
		var step : float = Duration / trans.length;
	else
		step = Duration / (trans.length-1);
 
	for (var c:int = 0; c < trans.length; c++)
	{
		if (OrientationMode == OrientationMode.NODE)
		{
			interp.AddPoint(trans[c].position, trans[c].rotation, step*c, Vector2(0.0, 1.0));
		}
		else if (OrientationMode == OrientationMode.TANGENT)
		{
			if (c != trans.length-1)
				var rot : Quaternion = Quaternion.LookRotation(trans[c+1].position - trans[c].position, trans[c].up);
			else if (AutoClose)
				rot = Quaternion.LookRotation(trans[0].position - trans[c].position, trans[c].up);
			else
				rot = trans[c].rotation;
 
			interp.AddPoint(trans[c].position, rot, step*c, Vector2(0.0, 1.0));
		}
	}
 
	if (AutoClose)
		interp.SetAutoCloseMode(step*c);
}
 
 
// We need this to sort GameObjects by name
class NameComparer implements IComparer 
{
	function Compare(trA : Object, trB : Object) : int {
		return trA.gameObject.name.CompareTo(trB.gameObject.name);
	}
}
 
 
//
// Returns children transforms already sorted by name
//
function GetTransforms() : Array
{
	var ret : Array = new Array();
 
	if (SplineParent != null)
	{
		// We need to use an ArrayList because there´s not Sort method in Array...
		var tempTransformsArray = new ArrayList();	
		var tempTransforms = SplineParent.GetComponentsInChildren(Transform);
 
		// We need to get rid of the parent, which is also returned by GetComponentsInChildren...
		for (var tr : Transform in tempTransforms)
		{
			if (tr != SplineParent.transform)
				tempTransformsArray.Add(tr);
		}
 
		tempTransformsArray.Sort(new NameComparer());
		ret = Array(tempTransformsArray);
	}
 
	return ret;
}
 
 
 
//
// Disables the spline objects, we generally don't need them because they are just auxiliary
//
function DisableTransforms() : void
{
	if (SplineParent != null)
	{
		SplineParent.SetActiveRecursively(false);
	}
}
 
 
//
// Starts the interpolation
//
function FollowSpline()
{	
	if (mTransforms.length > 0)
	{
		SetupSplineInterpolator(mSplineInterp, mTransforms);
		mSplineInterp.StartInterpolation(null, true, WrapMode);
	}
}

JavaScript - SplineInterpolator.js

enum eEndPointsMode { AUTO = 0, AUTOCLOSED, EXPLICIT }
enum eWrapMode { ONCE = 0, LOOP }
 
private var mEndPointsMode = eEndPointsMode.AUTO;
 
class SplineNode
{
	var Point   : Vector3;
	var Rot     : Quaternion;
	var Time    : float;
	var EaseIO  : Vector2;
 
	function SplineNode(p:Vector3, q:Quaternion, t:float, io:Vector2) { Point=p; Rot=q; Time=t; EaseIO=io; }
	function SplineNode(o : SplineNode) { Point=o.Point; Rot=o.Rot; Time=o.Time; EaseIO=o.EaseIO; }
}
 
private var mNodes : Array = null;
private var mState : String = "";
private var mRotations : boolean = false;
 
private var mOnEndCallback:Object;
 
 
 
function Awake()
{
	Reset();
}
 
function StartInterpolation(endCallback : Object, bRotations : boolean, mode : eWrapMode)
{	
	if (mState != "Reset")
		throw "First reset, add points and then call here";
 
	mState = mode == eWrapMode.ONCE? "Once" : "Loop";
	mRotations = bRotations;
	mOnEndCallback = endCallback;
 
	SetInput();
}
 
function Reset()
{
	mNodes = new Array();
	mState = "Reset";
	mCurrentIdx = 1;
	mCurrentTime = 0.0;
	mRotations = false;
	mEndPointsMode = eEndPointsMode.AUTO;
}
 
function AddPoint(pos : Vector3, quat : Quaternion, timeInSeconds : float, easeInOut : Vector2)
{
	if (mState != "Reset")
		throw "Cannot add points after start";
 
	mNodes.push(SplineNode(pos, quat, timeInSeconds, easeInOut));
}
 
 
function SetInput()
{	
	if (mNodes.length < 2)
		throw "Invalid number of points";
 
	if (mRotations)
	{
		for (var c:int = 1; c < mNodes.length; c++)
		{
			// Always interpolate using the shortest path -> Selective negation
			if (Quaternion.Dot(mNodes[c].Rot, mNodes[c-1].Rot) < 0)
			{
				mNodes[c].Rot.x = -mNodes[c].Rot.x;
				mNodes[c].Rot.y = -mNodes[c].Rot.y;
				mNodes[c].Rot.z = -mNodes[c].Rot.z;
				mNodes[c].Rot.w = -mNodes[c].Rot.w;
			}
		}
	}
 
	if (mEndPointsMode == eEndPointsMode.AUTO)
	{
		mNodes.Unshift(mNodes[0]);
		mNodes.push(mNodes[mNodes.length-1]);
	}
	else if (mEndPointsMode == eEndPointsMode.EXPLICIT && (mNodes.length < 4))
		throw "Invalid number of points";
}
 
function SetExplicitMode() : void
{
	if (mState != "Reset")
		throw "Cannot change mode after start";
 
	mEndPointsMode = eEndPointsMode.EXPLICIT;
}
 
function SetAutoCloseMode(joiningPointTime : float) : void
{
	if (mState != "Reset")
		throw "Cannot change mode after start";
 
	mEndPointsMode = eEndPointsMode.AUTOCLOSED;
 
	mNodes.push(new SplineNode(mNodes[0] as SplineNode));
	mNodes[mNodes.length-1].Time = joiningPointTime;
 
	var vInitDir : Vector3  = (mNodes[1].Point - mNodes[0].Point).normalized;
	var vEndDir  : Vector3  = (mNodes[mNodes.length-2].Point - mNodes[mNodes.length-1].Point).normalized;
	var firstLength : float = (mNodes[1].Point - mNodes[0].Point).magnitude;
	var lastLength  : float = (mNodes[mNodes.length-2].Point - mNodes[mNodes.length-1].Point).magnitude;
 
	var firstNode : SplineNode = new SplineNode(mNodes[0] as SplineNode);
	firstNode.Point = mNodes[0].Point + vEndDir*firstLength;
 
	var lastNode : SplineNode = new SplineNode(mNodes[mNodes.length-1] as SplineNode);
	lastNode.Point = mNodes[0].Point + vInitDir*lastLength;
 
	mNodes.Unshift(firstNode);
	mNodes.push(lastNode);
}
 
private var mCurrentTime = 0.0;
private var mCurrentIdx = 1;
 
function Update ()
{	
	if (mState == "Reset" || mState == "Stopped" || mNodes.length < 4)
		return;
 
	mCurrentTime += Time.deltaTime;
 
	// We advance to next point in the path
	if (mCurrentTime >= mNodes[mCurrentIdx+1].Time)
	{
		if (mCurrentIdx < mNodes.length-3)
		{
			mCurrentIdx++;
		}
		else
		{		
			if (mState != "Loop")
			{
				mState = "Stopped";
 
				// We stop right in the end point
				transform.position = mNodes[mNodes.length-2].Point;
 
				if (mRotations)
					transform.rotation = mNodes[mNodes.length-2].Rot;
 
				// We call back to inform that we are ended
				if (mOnEndCallback != null)
					mOnEndCallback();
			}
			else
			{
				mCurrentIdx = 1;
				mCurrentTime = 0.0;
			}
		}
	}
 
 	if (mState != "Stopped")
 	{
 		// Calculates the t param between 0 and 1
		var param : float = (mCurrentTime - mNodes[mCurrentIdx].Time) / (mNodes[mCurrentIdx+1].Time - mNodes[mCurrentIdx].Time);                                                                                                                                                                                                                                                                                                                                                
 
		// Smooth the param
		param = MathUtils.Ease(param, mNodes[mCurrentIdx].EaseIO.x, mNodes[mCurrentIdx].EaseIO.y);
 
		transform.position = GetHermiteInternal(mCurrentIdx, param);
 
		if (mRotations)
		{
			transform.rotation = GetSquad(mCurrentIdx, param);
		}
 	}
}
 
function GetSquad(idxFirstPoint : int, t : float) : Quaternion
{
	var Q0 : Quaternion = mNodes[idxFirstPoint-1].Rot;
	var Q1 : Quaternion = mNodes[idxFirstPoint].Rot;
	var Q2 : Quaternion = mNodes[idxFirstPoint+1].Rot;
	var Q3 : Quaternion = mNodes[idxFirstPoint+2].Rot;
 
	var T1 : Quaternion = MathUtils.GetSquadIntermediate(Q0, Q1, Q2);
	var T2 : Quaternion = MathUtils.GetSquadIntermediate(Q1, Q2, Q3);
 
	return MathUtils.GetQuatSquad(t, Q1, Q2, T1, T2);
}
 
 
 
function GetHermiteInternal(idxFirstPoint : int, t : float) : Vector3
{
	var t2 = t*t; var t3 = t2*t;
 
	var P0 : Vector3 = mNodes[idxFirstPoint-1].Point;
	var P1 : Vector3 = mNodes[idxFirstPoint].Point;
	var P2 : Vector3 = mNodes[idxFirstPoint+1].Point;
	var P3 : Vector3 = mNodes[idxFirstPoint+2].Point;
 
	var tension : float = 0.5;	// 0.5 equivale a catmull-rom
 
	var T1 : Vector3 = tension * (P2 - P0);
	var T2 : Vector3 = tension * (P3 - P1);
 
	var Blend1 : float =  2*t3 - 3*t2 + 1;
	var Blend2 : float = -2*t3 + 3*t2;
	var Blend3 : float =    t3 - 2*t2 + t;
	var Blend4 : float =    t3 -   t2;
 
	return Blend1*P1 + Blend2*P2 + Blend3*T1 + Blend4*T2;
}
 
 
function GetHermiteAtTime(timeParam : float) : Vector3
{	
	if (timeParam >= mNodes[mNodes.length-2].Time)
		return mNodes[mNodes.length-2].Point;
 
	for (var c:int = 1; c < mNodes.length-2; c++)
	{
		if (mNodes[c].Time > timeParam)
			break;
	}
 
	var idx:int = c-1;
	var param : float = (timeParam - mNodes[idx].Time) / (mNodes[idx+1].Time - mNodes[idx].Time);                                                                                                                                                                                                                                                                                                                                        
	param = MathUtils.Ease(param, mNodes[idx].EaseIO.x, mNodes[idx].EaseIO.y);
 
	return GetHermiteInternal(idx, param);
}

JavaScript - MathUtils.js

class MathUtils
{
	//
	//
	//
	static function GetQuatLength(q : Quaternion) : float
	{
		return Mathf.Sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);	
	}
 
	//
	//
	//
	static function GetQuatConjugate(q : Quaternion) : Quaternion
	{
		return Quaternion(-q.x, -q.y, -q.z, q.w);
	}
 
	//
	// Logarithm of a unit quaternion. The result is not necessary a unit quaternion.
	// 
	static function GetQuatLog(q : Quaternion) : Quaternion
	{
		var res : Quaternion = q;
		res.w = 0;
 
		if (Mathf.Abs(q.w) < 1.0)
		{		
			var theta : float = Mathf.Acos(q.w);
			var sin_theta : float = Mathf.Sin(theta);
 
			if (Mathf.Abs(sin_theta) > 0.0001)
			{
				var coef:float = theta/sin_theta;
      			res.x = q.x*coef;
      			res.y = q.y*coef;
      			res.z = q.z*coef;
   			}
		}
 
   		return res;
 
	}
 
	//
	// Exp
	//
	static function GetQuatExp(q : Quaternion) : Quaternion
	{
 
		var res : Quaternion = q;
 
 
    	var fAngle:float = Mathf.Sqrt(q.x*q.x + q.y*q.y + q.z*q.z);    
		var fSin:float = Mathf.Sin(fAngle);
 
 
    	res.w = Mathf.Cos(fAngle);
 
 
 
    	if (Mathf.Abs(fSin) > 0.0001)
 
    	{
 
        	var coef:float = fSin/fAngle;
 
        	res.x = coef*q.x;
        	res.y = coef*q.y;
        	res.z = coef*q.z;
 
    	}
 
 
		return res;
	} 
 
	//
	// SQUAD Spherical Quadrangle interpolation [Shoe87]
	//
	static function GetQuatSquad (t : float, q0 : Quaternion, q1 : Quaternion, a0 : Quaternion, a1 : Quaternion)
 
	{
 
		var slerpT:float = 2.0*t*(1.0-t);
 
    	var slerpP = Slerp(q0, q1, t);
 
    	var slerpQ = Slerp(a0, a1, t);
 
 
    	return Slerp(slerpP, slerpQ, slerpT); 
 
	}
 
	static function GetSquadIntermediate (q0:Quaternion, q1:Quaternion, q2:Quaternion)
 
	{
 
    	var q1Inv : Quaternion = GetQuatConjugate(q1);
 
    	var p0 = GetQuatLog(q1Inv*q0);
 
    	var p2 = GetQuatLog(q1Inv*q2);
    	var sum : Quaternion = Quaternion(-0.25*(p0.x+p2.x), -0.25*(p0.y+p2.y), -0.25*(p0.z+p2.z), -0.25*(p0.w+p2.w));
 
 
 
    	return q1*GetQuatExp(sum);
 
	}
 
	//
	// Smooths the input parameter t. If less than k1 ir greater than k2, it uses a sin. Between k1 and k2 it uses 
	// linear interp.
	//
	static function Ease(t : float, k1 : float, k2 : float) : float
	{ 
  		var f:float; var s:float; 
 
  		f = k1*2/Mathf.PI + k2 - k1 + (1.0-k2)*2/Mathf.PI;
 
  		if (t < k1) 
  		{ 
    		s = k1*(2/Mathf.PI)*(Mathf.Sin((t/k1)*Mathf.PI/2-Mathf.PI/2)+1); 
  		} 
  		else 
  		if (t < k2) 
  		{ 
    		s = (2*k1/Mathf.PI + t-k1); 
  		} 
  		else 
  		{ 
    		s= 2*k1/Mathf.PI + k2-k1 + ((1-k2)*(2/Mathf.PI))*Mathf.Sin(((t-k2)/(1.0-k2))*Mathf.PI/2); 
  		} 
 
  		return (s/f); 
	}
 
	//
	// We need this because Quaternion.Slerp always does it using the shortest arc
	//
	static function Slerp (p : Quaternion, q : Quaternion, t : float) : Quaternion
 
	{
		var ret : Quaternion;
 
 
    	var fCos : float = Quaternion.Dot(p, q);
 
    	if ((1.0 + fCos) > 0.00001)
    	{
    		if ((1.0 - fCos) > 0.00001)
    		{
    			var omega : float = Mathf.Acos(fCos);
    			var invSin : float = 1.0/Mathf.Sin(omega);
    			var fCoeff0  : float = Mathf.Sin((1.0-t)*omega)*invSin;
    			var fCoeff1  : float = Mathf.Sin(t*omega)*invSin;
    		}
    		else
    		{
    			fCoeff0 = 1.0-t;
    			fCoeff1 = t;
    		}
 
    		ret.x = fCoeff0*p.x + fCoeff1*q.x;
   		    ret.y = fCoeff0*p.y + fCoeff1*q.y;
   		    ret.z = fCoeff0*p.z + fCoeff1*q.z;
   		    ret.w = fCoeff0*p.w + fCoeff1*q.w;    		
    	}
    	else
    	{
    		fCoeff0 = Mathf.Sin((1.0-t)*Mathf.PI*0.5);
    		fCoeff1 = Mathf.Sin(t*Mathf.PI*0.5);
 
    		ret.x = fCoeff0*p.x - fCoeff1*p.y;
   		    ret.y = fCoeff0*p.y + fCoeff1*p.x;
   		    ret.z = fCoeff0*p.z - fCoeff1*p.w;
       		ret.w =  p.z;
    	}
 
 
    	return ret;
 
	}
 
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox