API: Custom Spline IK (or Motion Path)

705 views
Skip to first unread message

Roy Nieterau

unread,
Nov 12, 2012, 5:00:31 AM11/12/12
to python_in...@googlegroups.com
Hey Guys,

I'm looking into creating a custom Stretchy Spline IK setup that doesn't pop (over & under extends in extreme situations).
But I seem to be unable to wrap my head around how to calculate orientations on points on the curve.
Since the points on the curve don't have any orientation (well, they do have tangents or something like that?) I have no clue on how to actually start with this.

My main references of tools in Maya would be the Motion Path and the Spline IK (with Advanced Twist controlled by a start and end object).
If anybody has any starting points for this it would be greatly appreciated!

Thanks in advance.

Regards,
Roy

John Patrick

unread,
Nov 12, 2012, 10:08:48 AM11/12/12
to python_in...@googlegroups.com, python_in...@googlegroups.com
Hey Roy,

I've done a sort of 'manual' setup like this by writing a 'pointAtArclen' node and then wiring the rest with existing nodes.  In my case I used aim constraints for each joint, but you can do something similar within your solver- get the orientation by figuring out where each joint needs to be on the curve and what the vector would be if you were aiming each joint at the next.

-JP

Sent from my iPhone

Roy Nieterau

unread,
Nov 12, 2012, 10:13:00 AM11/12/12
to python_in...@googlegroups.com
Hey JP,

Actually I know how to get the point on the curve. Aiming at the next joint is also a relatively easy step.

On the other hand knowing/calculating the up vector for the aim is something I'm unaware of how to do it. How did you solve/calculate the up vector for the aim constraint?

I'm especially interested in allowing a twisting motion along the curve.

-Roy


CC: python_in...@googlegroups.com
From: jspa...@gmail.com
Subject: Re: [Maya-Python] API: Custom Spline IK (or Motion Path)
Date: Mon, 12 Nov 2012 07:08:48 -0800
To: python_in...@googlegroups.com

John Patrick

unread,
Nov 12, 2012, 10:33:46 AM11/12/12
to python_in...@googlegroups.com
I would think about it like this:  for each point/joint along the curve, you should have some sort of 'up vector'.  You could get that a number of ways - a start/end 'twist' amount, an array of 'up' target objects for each joint, etc.  The end result is that each point along the curve should have a vector to some other target to help determine the twist.  

Next, you'll need to know what your joint's aim-axis is.  The built-in spline ik node assumes an 'x' aim axis (which blows), but you could have an input attr for the aim axis.

Zero out whichever axis is the aim-axis in your 'up vector', then convert that vector into an MMatrix.  Now you have a matrix you can multiply your existing aimed-joint-matrix by to twist it.

I think it would be nice to have an array of world matrices for up-objects, which you would plug the controls for the spline into.  You could get the closest point along the curve for each and treat a particular axis as the 'up vector' for that point along the curve, then interpolate between them.

Roy Nieterau

unread,
Nov 12, 2012, 10:51:21 AM11/12/12
to python_in...@googlegroups.com
Hey John,

Thanks for the quick replies. I'm not sure what you mean with the following:


Zero out whichever axis is the aim-axis in your 'up vector', then convert that vector into an MMatrix.  Now you have a matrix you can multiply your existing aimed-joint-matrix by to twist it.

Why would that be helpful or what would those steps produce? If I zero out the up vector and convert that into an MMatrix ain't I creating a zero matrix? (Default matrix)

-Roy

John Patrick

unread,
Nov 12, 2012, 11:57:06 AM11/12/12
to python_in...@googlegroups.com
That was a lot of shorthand on my part.

For instance, lets say you're using some sort of 'target' object for the up-axis.  And let's say the aim-axis of the joint is y, and the up-axis is x.  Pretend your joint is at position [0,3,0] and the 'target' object is at [1,4,1], so the aim vector is [1,1,1]. If your target is perfectly on-plane with your joint, ie it lies on the x-z plane of your joint, then you don't need any work; but usually, a target object might be 'offset' from this plane, as is the case here.  You need to remove the 'y' part of this vector so that you don't wobble the joint - so the aim vector becomes [1,0,1].

aimVec = OM.MVector(1,0,1)

basisVec = OM.MVector(1,0,0) #since the aim-axis is x

quat = basisVec.rotateTo(aimVec)

twistMatrix = quat.asMatrix()


newJointMatrix = aimMatrix * twistMatrix


There are other things you'll need to take into account - rotation orders, parent matrices, inverse scale compensation, etc, to get the final euler rotation values of each joint.  Hopefully this gets you pointed in the right direction.  Good luck!

Roy Nieterau

unread,
Nov 13, 2012, 2:17:41 PM11/13/12
to python_in...@googlegroups.com
So I've been trying hard to understand how to perform the 'lookAt' stuff with the API, which I think is the basics of what I need to understand to achieve what I want.
This is what I got so far. Use it in a scene with a sphere and two cubes with their default names. ;)

###########

import maya.cmds as mc
import maya.OpenMaya as om

def getDependNodeByName( name ):
    selList = om.MSelectionList ()
    selList.add (name)
    node = om.MObject()
    selList.getDependNode (0, node)
    return node

srcNode = getDependNodeByName("pSphere1")
targetNode = getDependNodeByName("pCube1")
upNode = getDependNodeByName("pCube2")

fnSrc = om.MFnTransform(srcNode)
fnTarget = om.MFnTransform(targetNode)
fnUp = om.MFnTransform(upNode)

srcTransformMatrix = fnSrc.transformation()
srcTranslate = srcTransformMatrix.getTranslation(om.MSpace.kWorld)
srcQuatRotation = srcTransformMatrix.rotation()

targetTransformMatrix = fnTarget.transformation()
targetTranslate = targetTransformMatrix.getTranslation(om.MSpace.kWorld)

upTransformMatrix = fnUp.transformation()
upTranslate = upTransformMatrix.getTranslation(om.MSpace.kWorld)

# Aim Axis (hardcoded to x)
basisAimVec = om.MVector(0,0,0)
basisAimVec.x = 1
basisAimVec = basisAimVec.rotateBy(srcQuatRotation)

aimVec = targetTranslate - srcTranslate
quat = basisAimVec.rotateTo(aimVec)

srcTransformMatrix.addRotationQuaternion( quat.x, quat.y, quat.z, quat.w, om.MSpace().kTransform )
srcQuatRotation = srcTransformMatrix.rotation()

# Up Axis (hardcoded to y)
"""
basisUpVec = om.MVector(0,0,0)
basisUpVec.y = 1
basisUpVec = basisUpVec.rotateBy(srcQuatRotation)

upVec = upTranslate - srcTranslate
quat = basisUpVec.rotateTo(upVec)

srcTransformMatrix.addRotationQuaternion( quat.x, quat.y, quat.z, quat.w, om.MSpace().kTransform );
"""

# Set the adjusted matrix
fnSrc.set(srcTransformMatrix)
mc.refresh()

###########

So I haven't been able to implement the up axis in this. Any tips would be greatly appreciated.
I also had to refresh at the end otherwise Maya ended up not updating the viewport, anybody knows what that's all about?

Love to get into this more so thanks in advance for any help!

Regards,
Roy


From: jspa...@gmail.com
Date: Mon, 12 Nov 2012 08:57:06 -0800

Subject: Re: [Maya-Python] API: Custom Spline IK (or Motion Path)

Roy Nieterau

unread,
Nov 14, 2012, 12:14:16 PM11/14/12
to python_in...@googlegroups.com
And again I've made some progress. Or at least got some type of lookAt algorithm working.
Still not sure on how to actually use this for calculating the twist around the curve.
I'm not so sure about the used algorithm (just searched around on the internet and rewrote it for Maya) since I'm not really understanding how it works, got to look into Quaternions some more before I understand what's going on here.

Here's the code:
Testing on a scene with object's names ("pCube1", "pCube2", "pSphere1")


########

import maya.cmds as mc
import maya.OpenMaya as om
from math import sqrt


def getDependNodeByName( name ):
    selList = om.MSelectionList ()
    selList.add (name)
    node = om.MObject()
    selList.getDependNode (0, node)
    return node

       
def gsOrthonormalize( normal, tangent ):
    # Gram-Schmidt Orthonormalization
    normal.normalize()
    proj = normal * ( tangent * normal ) # normal * dotProduct(tangent,normal)
    tangent = tangent - proj
    tangent.normalize()
   
    return normal, tangent
       
def lookAt( lookDirection=om.MVector(0,0,0), upDirection=om.MVector(0,0,0) ):
    # Returns a quaternion that aims the Y-axes to lookDirection and Z-axes (orthogonal to Y) to upDirection
   
    forward = lookDirection
    up = upDirection
    up, forward = gsOrthonormalize(forward, up)
    right = lookDirection ^ upDirection # cross product
    right.normalize()
   
    quat = om.MQuaternion()
    quat.w = sqrt(1.0 + right.x + up.y + forward.z ) * 0.5
    w4_recip = 1.0 / (4.0 * quat.w)
    quat.x = (up.z - forward.y) * w4_recip
    quat.y = (forward.x - right.z) * w4_recip
    quat.z = (right.y - up.x) * w4_recip
    return quat
   
def getTranslationByTransformName( name ):
    node = getDependNodeByName( name )
    fnTransform = om.MFnTransform( node )
    transformMatrix = fnTransform.transformation()
    return transformMatrix.getTranslation(om.MSpace.kWorld)
   
def aimObject( object, aimToObject, aimUpObject ):
    srcTranslate = getTranslationByTransformName(object)
    targetTranslate = getTranslationByTransformName(aimToObject)
    upTranslate = getTranslationByTransformName(aimUpObject)

   
    aimVec = targetTranslate - srcTranslate
    upVec = upTranslate - srcTranslate
    quat = lookAt( aimVec, upVec )
   
    srcNode = getDependNodeByName(object)
    fnTransform = om.MFnTransform(srcNode)
    srcTransformMatrix = fnTransform.transformation()
    srcTransformMatrix.setRotationQuaternion( quat.x, quat.y, quat.z, quat.w, om.MSpace().kTransform )

   
    # Set the adjusted matrix
    fnTransform.set(srcTransformMatrix)
    mc.refresh()

aimObject("pSphere1","pCube1","pCube2")

#########

Looking forward to any tips from you guys on this guy. It's all giving me a headache.

-Roy


From: roy_ni...@hotmail.com
To: python_in...@googlegroups.com
Subject: RE: [Maya-Python] API: Custom Spline IK (or Motion Path)
Date: Tue, 13 Nov 2012 20:17:41 +0100

cedric bazillou

unread,
Nov 14, 2012, 12:48:20 PM11/14/12
to python_in...@googlegroups.com
Hello Roy,

just a quick tip:
look at http://circecharacterworks.files.wordpress.com/2012/07/quatermainnode.pdf the most basic use of quaternion to build an aim target node...( this one use the X axis, and i also doesn't bother to support other rotation order  ).

To retrieve your up vector just use myUpvector * quatMatrix and the work is done( there a  lot of math in your code: the API provide  basic operation like dot/cross product: in my opinion its quicker to use what is already provided, )

Roy Nieterau

unread,
Nov 14, 2012, 4:53:07 PM11/14/12
to python_in...@googlegroups.com
Hey Cedric,

Thanks for the reply. ;) (Also for your guidance over at CGtalk)

I seem to be unable to download the pdf file, maybe the link is incorrect? Don't know really.




Date: Wed, 14 Nov 2012 09:48:20 -0800
From: cedricb...@gmail.com
To: python_in...@googlegroups.com
Subject: [Maya-Python] Re: API: Custom Spline IK (or Motion Path)

cedric bazillou

unread,
Nov 14, 2012, 7:06:15 PM11/14/12
to python_in...@googlegroups.com
Strange, it is python file that I rename into a pdf in order to post it on my blog( it can be found in the download section...)

the meat of this plugin is:

    def compute(self,Plug,Data):
        targetPosition_Hdle = Data.inputValue(self.targetPosition)
        outputHandle = Data.outputValue(self.outRotate )

        theTargetVector = targetPosition_Hdle.asVector().normal()
        referenceVector = OpenMaya.MVector(1,0,0)

        aimQuaternion = OpenMaya.MQuaternion()
        aimQuaternion = referenceVector.rotateTo(theTargetVector)
        eulerRotationValue = aimQuaternion.asEulerRotation()

        outputHandle.set3Double( eulerRotationValue.x,eulerRotationValue.y,eulerRotationValue.z )
           
        Data.setClean(Plug)
( the euler value is in radians and the output attribute is directly 3 MAngle value in order to prevent maya to insert a unitconversion node( which is logical ))
This node expect the Driven object to be at position 0 0 0 under an offset group.



Le lundi 12 novembre 2012 11:00:31 UTC+1, Roy Nieterau a écrit :
quatermainNode.py

Roy Nieterau

unread,
Nov 15, 2012, 8:12:59 PM11/15/12
to python_in...@googlegroups.com
Awesome Cedric! Thanks for the help. Figured it out with a little less code this time. ;)

The next step would be figuring out how to use this technique for points on a curve. The idea is to have a curve and a start Object with a certain orientation and an end object with a Certain orientation and along the curve blend between these two orientations. Similar to the Maya Spline IK Advanced Twist.

Here's my lookAt / aim script version supporting a source object, aimTarget object and upTarget object. (Supporting the up target was quite some experimenting and thinking it through for me.)
Also supporting any x, y, z aim axis and up axis as long as they are orthogonal.


####

import maya.cmds as mc
import maya.OpenMaya as om

def getDependNodeByName( name ):
    selList = om.MSelectionList ()
    selList.add (name)
    node = om.MObject()
    selList.getDependNode (0, node)
    return node
   
def getDagPathByName( name ):

    selList = om.MSelectionList ()
    selList.add (name)
    node = om.MDagPath()
    selList.getDagPath (0, node)

    return node
   
def gsOrthonormalize( normal, tangent ):
    # Gram-Schmidt Orthonormalization
    normal.normalize()
    proj = normal * ( tangent * normal ) # normal * dotProduct(tangent,normal)
    tangent = tangent - proj
    tangent.normalize()
   
    return normal, tangent

srcNode = getDagPathByName("pSphere1")
targetNode = getDagPathByName("pCube1")
upNode = getDagPathByName("pCube2")

fnTransform = om.MFnTransform()

fnTransform.setObject(srcNode)
srcTransformMatrix = fnTransform.transformation()
srcPos = srcTransformMatrix.getTranslation(om.MSpace.kWorld)

fnTransform.setObject(targetNode)
aimTargetTransformMatrix = fnTransform.transformation()
aimTargetPos = aimTargetTransformMatrix.getTranslation(om.MSpace.kWorld)

fnTransform.setObject(upNode)
upTargetTransformMatrix = fnTransform.transformation()
upTargetPos = upTargetTransformMatrix.getTranslation(om.MSpace.kWorld)

"""
    Step one: Aim the aimAxis
"""
basisAimVec = om.MVector(0,0,1)

aimVec = aimTargetPos - srcPos
upVec = upTargetPos - srcPos
quat = basisAimVec.rotateTo(aimVec) # give the quaternion rotation so that x points to basisAimVec

# Maybe if we addRotationQuaternion we can support beyond 360? (so it will aim to the closest value?)
# Because the MQuaternion and MTransformMatrix classes support information beyond 360 degrees
srcTransformMatrix.setRotationQuaternion( quat.x, quat.y, quat.z, quat.w, om.MSpace().kWorld )

"""
    Step two: Aim the upAxis (twisting around the aimAxis)
"""
mat = srcTransformMatrix.asMatrix()
basisUpVec = om.MVector(1,0,0)
basisUpVecObject = basisUpVec * mat

"""
# Option 1: Gram-Schmidt Orthonormalization
"""
tempVec,upVec = gsOrthonormalize(aimVec,upVec)

"""
# Option 2: Remove any distance in aimAxis from upAxis so vector becomes orthogonal to aimAxis
Remove the rotation we have from the lookAt from the upVector (from upTargetPos-srcPos)
Then remove any translation in the aimAxis (so we exclude changing that when calculating the quaternion)
upVec = upVec * mat.inverse()
upVec.x = 0 # currently requires to hard-code which axis is the aim axis, could be done otherwise though
upVec = upVec * mat
"""

quat = basisUpVecObject.rotateTo(upVec)
srcTransformMatrix.addRotationQuaternion( quat.x, quat.y, quat.z, quat.w, om.MSpace().kWorld );


# Set the adjusted matrix
fnTransform.setObject(srcNode)
fnTransform.set(srcTransformMatrix)
mc.refresh()

###

Let me know if you guys see anything that can be done easier or better.

Really appreciate the help.

-Roy

cedric bazillou

unread,
Nov 16, 2012, 4:57:55 AM11/16/12
to python_in...@googlegroups.com
Hard , to tell much i have build my nodes differently:
The core concept was that the curve is a temporal system: there is a start and an end ( you will often see in SIGGRAPH paper the u parameter named as the variable t ).
I dont try to bundle all element in one node: some data can be store and update upstream.
so my node compute a matrix at a curve parameter value and the end object is used to extract a twist value.
This extraction is done by a separate node . it doesn't matter if you have 7 full spin on the twist axis: your motion path node drive your joint  and even with 180/-180 degree range the orientation will be correct


Le lundi 12 novembre 2012 11:00:31 UTC+1, Roy Nieterau a écrit :

Roy Nieterau

unread,
Nov 16, 2012, 5:18:42 AM11/16/12
to python_in...@googlegroups.com
Sounds exactly like what I would need.

So one would for example:
1. Get the point on curve (MFnNurbsCurve.getPointAtParam)
2. Create the matrix for that point (where do you get the rotation from?, since the tangent and normal give unpredictable results, although I haven't seen them working the way I actually wanted)

We know that the matrix would always aim towards a next point on the curve (aiming along the curve). The upVector would be the twist, yet there's no default rotation to which we can add twisting rotation along the aimVector (changing the upVector and the orthogonal vector of both upVector and aimVector).

3. End object extract a twist value: by extracting it's rotation along a certain local axis, right?
4. Apply the twist
5. Done.

As you can see I'm having a hard time visualing or thinking through points 2 and 3. Maybe I'm too much of a beginner for this, hehe. :)

Thanks again!

-Roy


Date: Fri, 16 Nov 2012 01:57:55 -0800

From: cedricb...@gmail.com
To: python_in...@googlegroups.com
Subject: [Maya-Python] Re: API: Custom Spline IK (or Motion Path)

cedric bazillou

unread,
Nov 16, 2012, 7:19:01 AM11/16/12
to python_in...@googlegroups.com
I use the tangent and it is really robust: its the curve normal that can break with sharp turn. But my node drives helper/cross section joint and i have better result with tangent than point at the next point on the curve.
Perhaps try to use a nurbssurface ribbon and than once you are comfortable with that you can start massaging vector and matrix?


Le lundi 12 novembre 2012 11:00:31 UTC+1, Roy Nieterau a écrit :
Reply all
Reply to author
Forward
0 new messages