Hermite Spline Controller

From Unify Community Wiki
Revision as of 13:37, 14 January 2009 by Benblo (Talk | contribs)

Jump to: navigation, search

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).

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

JavaScript - SplineController.js

<javascript> 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 extends 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>

JavaScript - SplineInterpolator.js

<javascript> 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>

JavaScript - MathUtils.js

<javascript> 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;

}

}

</javascript>

Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox