Pages

6.6.13

How To : Rig Accurate Working Springs in Maya

I've seen a few piston or damper rigs, and each one works well until a spring is added. All too often they use a scale system that kind of works ok for a casual inspection, but under close scrutiny falls apart because the scaling produces a noticeable flattening of the spring's cross section under extreme compression.

Here I'll discuss two other ways of achieving this effect with near complete accuracy, and along the way we'll create a couple of rigs in order to demonstrate these techniques.

This gif on the right  is what we're aiming for, a self contained rig that can be used almost anywhere, and will not exceed it's limits. The first method I'll discuss involves Maya's blendshapes.


Method #1 Using Deformers


This is probably the most common of the two methods as it uses a fairly well known deformer, Maya's blendshape deformer. We'll use it to morph between two meshes and control the blend of this morph with the value of difference in height of these two meshes. We'll do this by creating two topographically identical springs, one where the coils are fully compressed and the other where it's fully extended. Then we'll create a blendshape from these meshes and lastly, and probably the most difficult part, is to create a node network that will work together with contraints and rig controls to adjust the extension and compression of this spring.

So we'll need two springs, named compressed and uncompressed and created using the polygon Helix command. It's critical that we need to know the heights of both springs, as we'll need that later when we construct a node network. Create two helixes with the following values, one with height=2 and one with height=10. Name them compressed and uncompressed respectively.


 Or run the code...

import maya.cmds as mc

springCompressed = mc.polyHelix(c=10,h=2,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=0)
springUncompressed = mc.polyHelix(c=10,h=10,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=0)


The polygon Helix command creates the meshes with the pivot at the centre of the geometry. This is no good for us as later on we'll need to use contraints to help position and animate it in a meaningful way. To make life easier for ourselves, we'll relocate each mesh's pivot to the bottom of each spring, taking care to position it at the same Y value as the centre of the bottom most coil. Whilst were doing this, we'll also move both springs up so they're on the world origin. As we know the height of each spring, it's no effort to find the correct value.

mc.xform(springCompressed, pivots = (0,-1,0), p=True)
mc.xform(springUncompressed, pivots = (0,-5,0), p=True)

mc.move(0,1,0,springCompressed,relative=True, worldSpaceDistance=True,objectSpace=True)
mc.move(0,5,0,springUncompressed,relative=True, worldSpaceDistance=True,objectSpace=True)

Next we need to create the blendshape, and we might as well delete the unneeded spring (we've got the deformation values from it in the blendshape node, so we don't really need this anymore).

Select the compressed spring, then the uncompressed spring then run the Blendshape command from the Create Deformers menu. Make sure to name this node springBlend.

Then delete the uneeded uncompressed spring.

Or enter the following code..

mc.blendShape(springUncompressed,springCompressed,origin='world', name='springBlend')
mc.delete(springUncompressed)


If you select the 'springBlend' node, and look in the Channel Box you'll see a channel that controls the blend and called 'uncompressed'. If you change the value (0 < value < 1) of this hopefully the spring will now compress or extend.

We now need to find a way to control this blend value in a useful way. We know that the range of this blend value is from 0 (compressed) to 1 (uncompressed), and we know that the heights of the springs are 2 (compressed) and 10 (uncompressed). If we map these differences in height to the value of the blend, we would get a direct relationship of the physical mesh size to the blend between them. Fortunately, Maya has a utility node that will do just this, the 'setRange' node. This will take a value and a range and calculate a new value based on a different range.

So first we need a value to map into this node. This will be the distance we need the spring to represent and we'll use two locators, lower_LOC and upper_LOC, and we'll position them at the lower and upper positions of the spring.

locLower = mc.spaceLocator(name='lower_LOC')[0]
locUpper = mc.spaceLocator(name='upper_LOC')[0]
mc.move(0,2,locUpper,absolute=True, worldSpaceDistance=True,objectSpace=True)


To make the rig more dynamic and useful, we'll also point constrain the spring to the lower_LOC, and aim constrain the spring to the upper_LOC.

mc.pointConstraint(locLower,springCompressed)
mc.aimConstraint(locUpper,springCompressed,aimVector=(0,1,0))


To get the distance for our value, we'll sample the distance between these two locators by creating a distanceBetween node, and connect the two locator's worldMatrix attributes to the input matrix channels of this distanceBetween node. The output value of the distanceBetween node will be later connected to the setRange node.



distanceNode = mc.createNode('distanceBetween')
mc.connectAttr('{0}.worldMatrix[0]'.format(locUpper),'{0}.inMatrix1'.format(distanceNode),  force=True)
mc.connectAttr('{0}.worldMatrix[0]'.format(locLower), '{0}.inMatrix2'.format(distanceNode), force=True)

And now for the final part. We need something to take this distances, and map it into a value between ) and 1 so we can use it to drive the blend value between the two states of the spring.

We'll create a setRange node, connect the distanceBetween node's output value to it, and then to complete the rig we'll connect the output of the setRange node to the blend value. As the distance is a scalar, we're using just the X value attributes of the setRange node for this set up.

setRangeNode = mc.createNode('setRange')

mc.connectAttr('{0}.distance'.format(distanceNode),'{0}.valueX'.format(setRangeNode),  force=True)
mc.connectAttr('{0}.outValueX'.format(setRangeNode),'{0}.{1}'.format(blendNode,springUncompressed),  force=True)



The last step is to set the min and max attributes to reflect the relationship of the spring's height to the associated value of the blend.

minX and maxX relate to the blend value (0 and 1)
oldMinX and oldMaxX are the height of the compressed and uncompressed spring (2 and 10)



mc.setAttr('{0}.minX'.format(setRangeNode),0)
mc.setAttr('{0}.maxX'.format(setRangeNode),1)
mc.setAttr('{0}.oldMinX'.format(setRangeNode),2)
mc.setAttr('{0}.oldMaxX'.format(setRangeNode),10)

If you just want to see it in action, here's the full script. Cut and paste it into Maya's scripts editor window and move the locators about to see it work.

import maya.cmds as mc

springCompressed = mc.polyHelix(c=10,h=2,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=0, name='compressed')[0]
springUncompressed = mc.polyHelix(c=10,h=10,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=0, name='uncompressed')[0]

mc.xform(springCompressed, pivots = (0,-1,0), p=True)
mc.xform(springUncompressed, pivots = (0,-5,0), p=True)

mc.move(0,1,0,springCompressed,relative=True, worldSpaceDistance=True,objectSpace=True)
mc.move(0,5,0,springUncompressed,relative=True, worldSpaceDistance=True,objectSpace=True)

blendNode = mc.blendShape(springUncompressed,springCompressed,origin='world', name='springBlend')[0]
mc.delete(springUncompressed)

locLower = mc.spaceLocator(name='lower_LOC')[0]
locUpper = mc.spaceLocator(name='upper_LOC')[0]
mc.move(0,2,locUpper,absolute=True, worldSpaceDistance=True,objectSpace=True)

mc.pointConstraint(locLower,springCompressed)
mc.aimConstraint(locUpper,springCompressed,aimVector=(0,1,0))

distanceNode = mc.createNode('distanceBetween')
mc.connectAttr('{0}.worldMatrix[0]'.format(locUpper),'{0}.inMatrix1'.format(distanceNode),  force=True)
mc.connectAttr('{0}.worldMatrix[0]'.format(locLower), '{0}.inMatrix2'.format(distanceNode), force=True)

setRangeNode = mc.createNode('setRange')

mc.connectAttr('{0}.distance'.format(distanceNode),'{0}.valueX'.format(setRangeNode),  force=True)
mc.connectAttr('{0}.outValueX'.format(setRangeNode),'{0}.{1}'.format(blendNode,springUncompressed),  force=True)

mc.setAttr('{0}.minX'.format(setRangeNode),0)
mc.setAttr('{0}.maxX'.format(setRangeNode),1)
mc.setAttr('{0}.oldMinX'.format(setRangeNode),2)
mc.setAttr('{0}.oldMaxX'.format(setRangeNode),10)


Method #2 Using Construction History


This method exploits Maya's construction history, in particular the polyHelix node that we use to construct the spring. In this case we'll only need one mesh rather than the two that we used in the deformation technique above. We'll essentially build the same network, only this time it's much simpler as we only need to connect the distanceBetween node to the correct input of the polyHelix node. We'll not be needing the setRange node in this rig.

This time we'll only need one spring, compressed, but this time we won't relocate it's origin.

import maya.cmds as mc

springCompressed = mc.polyHelix(c=10,h=2,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=1, name='compressed')


Note, now we've turned history on, we need the original return from the polyHelix command, so springCompressed is a now a list. This is because the polyHelix command returns it's contruction node at index 1 which we'll need access to in a bit. It also uses an integral centred pivot point, which is why we don't relocate it but work with it.

We'll create the distance node and the locators with their constraints exactly as before.

locLower = mc.spaceLocator(name='locLower')[0]
locUpper = mc.spaceLocator(name='locUpper')[0]
mc.move(0,2,locUpper,absolute=True, worldSpaceDistance=True,objectSpace=True)

mc.pointConstraint(locLower, locUpper,springCompressed[0])
mc.aimConstraint(locUpper,springCompressed[0],aimVector=(0,1,0))

distanceNode = mc.createNode('distanceBetween')
mc.connectAttr('{0}.worldMatrix[0]'.format(locUpper),'{0}.inMatrix1'.format(distanceNode),  force=True)
mc.connectAttr('{0}.worldMatrix[0]'.format(locLower), '{0}.inMatrix2'.format(distanceNode), force=True)


Here's the last step; we connect the distance value to the height input of the polyHelix node.
mc.connectAttr('{0}.distance'.format(distanceNode),'{0}.height'.format(springCompressed[1]),  force=True)

That's it. All done.

If you move the locators about, it'll operate the same as the blendshape method only there's no limits imposed on the system. You could argue that's a bad thing and I'm sure we could use another setRange or a clamp node to limit this behaviour but using construction history is a very real option here, especially during the visual design stage.

See what happens when you change other values on the polyHelix node, number of coils, radius or direction. The rig will still work provided you maintain this history.

Try it.
import maya.cmds as mc

springCompressed = mc.polyHelix(c=10,h=2,w=2,r=0.1,sa=24,sco=24,sc=0,d=1,ax=(0,1,0),rcp=0,cuv=3,ch=1, name='compressed')

locLower = mc.spaceLocator(name='locLower')[0]
locUpper = mc.spaceLocator(name='locUpper')[0]
mc.move(0,2,locUpper,absolute=True, worldSpaceDistance=True,objectSpace=True)

mc.pointConstraint(locLower, locUpper,springCompressed[0])
mc.aimConstraint(locUpper,springCompressed[0],aimVector=(0,1,0))

distanceNode = mc.createNode('distanceBetween')
mc.connectAttr('{0}.worldMatrix[0]'.format(locUpper),'{0}.inMatrix1'.format(distanceNode),  force=True)
mc.connectAttr('{0}.worldMatrix[0]'.format(locLower), '{0}.inMatrix2'.format(distanceNode), force=True)

mc.connectAttr('{0}.distance'.format(distanceNode),'{0}.height'.format(springCompressed[1]),  force=True)


Conclusion


Both do the job admirably. The deformer method requires slightly more techincal skill and is less flexible. However, most game engines support morph targets so it's the only real choice if that's you're platform.

However, the construction history method has a lot going for it, it's simpler, more elegant, easily remembered and mostly of all it let's you experiment and alter the mesh in situ. For r&d it's great, not only for springs but other mechanically driven rigs.

Have fun.

8.5.13

How To : Create Hoeken's Linkages in Maya

Hoeken's Linkage is quite a special thing, really, it is. It sounds stuffy and technical and yes it is, but trust me it's also quite amusing. No doubt you've seen it before and is remarkably simple.

This linkage is a special case of a four bar linkage system where it converts rotational motion into constant linear motion in such a way as to produce an approximate stepping motion. This motion could be used to provide the mechanism for a walk, a conveyor belt, or other types of modified stepped motion.



We'll look at creating a rig in Maya that will produce a convincing representation from the above principle.

This system is all about proportions, all taken from the radius of the crank. The crank is the armature that desribes the initial circular motion and it is used to drive the whole system. We'll call the length of this crank R, which is the radius of the driving wheel. We can now express the other lengths of the components in multiples of R.



Connected to the crank is the floating link. This connects the input to the output of the system and is 5R in length.

Connected to the middle of the floating link is the grounded link. This will be pinned to the same space at the crank at a horizontal distance of 2R from it and is used to constrain the floating crank in a desirable way. The grounded link is of length 2.5R and is connected to the mid point of the floating link. Incidentally, the fourth bar of the linkage is the overextended floating link, but in this example it is considered as part of the floating link.

So how does this translate in Maya? We'll use a joint for the crank and connect two joints to this, one for the floating link and one for the IK chain that we'll use to restrict the floating link's movement. We'll pin the IK chain to the ground and we'll add and point constrain a locator at the end of teh floating link to visualise the output. We'll then add a couple of expressions to give it some life.


Let's create the joints. Choose the front view (in a Yup World), open up the script editor, clear the python tab and type the following...

import maya.cmds as mc

crankRadius = 1.0

# Create the joints
crank_JNT = mc.joint(n='crank_JNT', p=(0, 0, 0))
floatingLink_JNT = mc.joint(n='floatingLink_JNT', p=(crankRadius, 0, 0)  )
floatingLink_NULL = mc.joint(n='floatingLink_NULL', p=(crankRadius, crankRadius * 5, 0)  )
mc.pickWalk( direction='up')
groundedLink_JNT = mc.joint(n='groundedLink_JNT', p=(crankRadius, crankRadius * 2.5, 0)  )
groundedLink_NULL = mc.joint(n='groundedLink_NULL', p=(crankRadius * 3.5 , crankRadius * 2.5, 0)  )

# Orient the Root joint
mc.joint([crank_JNT], e=True, zso=True, oj='xyz')



We've chosen a crankRadius of 1.0, this is the main variable for the system and dictates the scale of the system

Next we need to add the IK to the groundedLink_JNT, constrain it to the groundedPin_LOC. While we're at it, we'll add a locator for the 'foot'

# Orient the Root joint
mc.joint([crank_JNT], e=True, zso=True, oj='xyz')

# Add the iK
groundedLink_IK = mc.ikHandle(n='groundedLink_IK',sj=floatingLink_JNT, ee=groundedLink_NULL)

# And the grounded pin locator
groundedPin_LOC = mc.spaceLocator(n='groundedPin_LOC')
mc.move(crankRadius * 2,0,0,groundedPin_LOC) 

# Pin the leg with the constraint
mc.pointConstraint(groundedPin_LOC,groundedLink_IK[0],offset=(0,0,0), weight=1)

#add the foot locator
foot_LOC = mc.spaceLocator(n='foot_LOC')
mc.pointConstraint(floatingLink_NULL,foot_LOC,offset=(0,0,0), weight=1)



You should see the same in Maya as the image above.

Now we'll add some movement using a simple expression.

#add the control expression
exp = 'crank_JNT.rotateZ = frame * 10;'
mc.expression(string = exp,name = 'constantMotor')

You should set the timeline range to something like 500 frames. We'll create a simple expression to rotate the crank based on the frame number, nothing more sophisticated is needed as the system will do the rest. I've used a 10x multiplier but you can add what you want.


Remember I said an approximate stepping motion. The values we've chosen are optimal but not exact to achieve true linear motion, however, it's good enough.

Now for some fun. Let's breath some life into it and make it translate.

#create the motor expression
exp =  'float $prevX = `getAttr -time (frame-1) {0}.tx`;'.format(foot_LOC[0])
exp += 'float $delta =  {0}.translateX - $prevX;'.format(foot_LOC[0])
exp += 'if ({0}.translateY >{1} && {0}.translateY <{2})'.format(foot_LOC[0], (4 * crankRadius - crankRadius/100),  (4 * crankRadius + crankRadius/20))

exp += '{'
exp += '{0}.translateX = {0}.translateX - $delta;'.format(crank_JNT)
exp += '{0}.translateX = {0}.translateX - $delta;'.format(groundedPin_LOC[0])
exp += '};'

mc.expression(string = exp,name = 'crawler')

It's quite a simple calculation, we're getting the difference (delta) of the x transform from the last frame to the current frame of the foot_LOC, and applying it to the crank and the groundedPin. We only do this when the foot_LOC is at the height that it's displaying linear behaviour. This height is 4R and we're using a -ve 1% and +ve 5% tolerance to clamp it's movement.
Ok, so it's more of a drag than a walk, we could add a foot and hang it off the floatingLink_NULL so it's below the crank. Then again, I quite like it as it is. It made me smile, I bet you did too.

Try setting the timeline range from frames 15 to 20, set the playback looping to oscillate.

1.5.13

Python - Live Housekeeping in Maya.

Sometimes, there's a need to make sure things aren't saved with the scene. Things like helper geometry or specific 3rd Party plugin dependency nodes, or even maya locator or lights. And sometimes you may just want to have a bit of a clear out.

Expressions for example. I wonder if you've also come across the 80mb+ file that had nothing but simple geometry yet filled to the brim with legacy debris in the DAG from 3rd party tools that had no right to be in your pipeline. In this case, a tool one of the team used generated expressions to provide an interactive UX for splitting polys. For one reason or another it failed to cleanup after itself. Added to the fact that an artist's or animator's workflow can often propogate these tpyes of nodes by means of 'dirty' imports, it can lead to rubbish in the scenes than at the very least increases the storage footprint and load / save times, at the worst it could compromise the scene's export integrity. Expressions are one example of this 80mb 'infection'.

So we needed a way to clean the scene, but not rely on the users to action this clean up. We decided that running the cleanup script when the user saves is probably a good time to do this.

So rather than a scriptJob (iirc this doesn't catch save events), I'm using the API and OpenMaya.MSceneMessage. The kBeforeSave enumerator is used to register a callback to a function that does the cleaning up, onSave in this case.

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

def onSave(*args):
 mc.delete(mc.ls(type="expression"))

callbackID = om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeSave,onSave)

This will execture everytime the user saves. It's unobtrusive and simple.

To remove it, use the following..
def callBackOff():
 om.MMessage.removeCallback(callbackID)

This worked well to clean assets whilst the underlying issues were dealt with, but I've often thought this could be used for other live housekeeping duties. We nearly always validate for game assets exports, rarely for maya saves.

And if you had an evil streak, you could have a lot of fun with this.


28.2.12

Maya : Using Operators to flip Anim Curves in the Graph Editor

One neat trick in Maya is the ability to use MEL operators as a shorthand way to manipulate transforms, rotates and scales.

Valid operator are: += to add, -= to subtract, *= to multiply and /= to divide.

Say you wanted to move a selection of objects all up by a set amount. You could use the 'relative transform' box on the status line, if you don't use a snap that is. However, if you entered '+=5' in the translateY channel box of the selected meshes then you'd move everything selected up by 5 in the Y.

Where this is really neat is that this trick also works in the Graph Editor. You can offset curves by a fixed amount. You can scale -1 and flip curves and key frames, all by entering a operator in the value side of the stats box. Chris Lesage added that by typing a percentage in, then the existing value will be calculated to the new value. To use Chris's example, if you have '2' and you type '110%' then you get '2.2'.

4.1.12

Python - Find Items in a List that are also a String

I don't really use the filter command much, until I needed to see whether something in a string was also in a list..

myList = ['fart', 'boff', 'squit','trump']
myString = "I really couldn't help but trump and fart all night"
print filter(lambda x : x in myString, myList)
>>>['fart', 'trump']

20.12.11

Photo #3

 

Nature vs Graff

5.12.11

Maya Utility Node Listing Help

I find I'm using more utility nodes these days, and most of the time these are created from scripts. Nodes like 'distanceBetween', 'multiplyDivide' and 'clamp' nodes provide the various gubbins for rigging functions and enhancements. Up till now, I just created them and would search for them by graphing a connected node, or looking for them in the outliner. These nodes always required a little bit of effort to find them. So now when I create them, I create them as 'utility' nodes by saying so in the syntax of the creation command. Btw, this mimics how they're created by default in the hypershade.
import maya.cmds as mc

mc.shadingNode('clamp', asUtility=True, name='myClamp')
Now I can easily access all these nodes under the 'Utility' tab in the hypershade. I love it more when it's easier.

9.11.11

iSausages

Teaching geometry to a 4 year old...

How To : Animate Cranks and Pistons in Maya

A recent project I worked on required the modelling of an animating crank and a piston similar to those used by steam locomotives. Engines that use pistons as a power source need a way of converting the back and forth (reciprocating) linear piston motion into rotational motion. Cranks are used to do this and these are connected to points that are offset from the main rotational axis on what is called the crankshaft, and these in turn are connected to the pistons. The crankshaft is the part that receives the rotational energy. The system will also work in reverse, turning the rotational movement into linear.

So that's how it works in the real world, so how do we translate this into Maya?

For our purposes, we'll use the rotating crankshaft to drive the animation and there's a few ways we can go about this; using constraints, using set driven keys or using expressions. Using constraints will only get you part of the way and as you develop it, you'll find there'll be an issue with cyclic dependencies. So you'll constrain the piston to the rod, but you also need to constrain the rod to the piston and even using controllers or parenting this will still cause the same cyclic dependencies that result in evaluation errors and nasty jittering. I've tried all sorts of ways but couldn't create a viable solution. So another way would be to use set driven keys, but you'll need to use a lot of them in order to describe the motion accurately. At best you'll only achieve an approximation. However, if cheap and nasty works in the context of your requirements then fine, use whatever works. If you want to know how to do it properly, read on.

Expressions (unfortunately) are the only way we can solve this accurately. It's not cheap (you'll need to invest in some maths) but the result is a perfect system that's smooth and completely predictable.

I don't usually advocate the use of expressions and try and avoid them like the plague; they can be too rigid and restrictive in an animated workflow, locking nodes onto coded 'rails' and initiating behaviours that are hard to break away from. However, a system such as a crank and piston is ideal for this method; the system is completely closed (unless you plan to decompose the system later) and is a perfect example of how we can use expressions correctly and mathematically model the reciprocating motion. Just because I don't like using them, doesn't mean I won't and after this exercise I must say I am feeling just a little bit of love for Maya's expressions.

 So, here's what we're aiming for...

figure 1


The crankshaft is the assembly that rotates, the piston is the assembly that only moves in one axis, and the connecting rod is the linkage that connects them both together and transfers the rotational energy into linear motion.

In order to work out the position of the piston, we'll need to find out how much the connecting rod needs to be rotated by in order to keep the piston horizontal. Once we have that angle, we've got our solution, and this solution comes from our mathematical knowledge of triangles.

So here's the investment part, the maths...

figure 2



As the crankShaft_JNT rotates ($alpha), we need to work out the relative angle ($gamma) needed for the connectingRod_JNT to let the piston_JNT remain in a fixed horizontal axis. In our case, the piston_JNT can only move in the X axis. We know the crank radius and the connecting rod length, so we can use triangles and trigonometry to find $gamma by application of the Sine Rule to find $beta.

crank radius / sin $beta = connection rod length / sin $alpha

Re-arranging this equation to find $beta..

$beta = arcsin (connection rod length / (sin $alpha * crank radius))

And using another rule; pairs of angles with parallel lines (transversals), we can find $gamma(red on figure 2) from $alpha (yellow on figure 2) and $beta (blue on figure 2).

$gamma = $alpha + $beta

Note, we'll have to negate $gamma as an anticlockwise angle is needed. See the right hand grip rule for how to work out which way something will rotate.

Below we have an example script to create this system using Maya's joints and an expression which will explain it further. Cut and paste it into Maya's script window and execute it.

import maya.cmds as mc

#settings
crankRadius = 1
connectionRodLength = 4
expressionName = 'crank'

# Create the components
mc.joint(n='crankShaft_JNT', p=(0, 0, 0))
mc.joint(n='connectingRod_JNT', p=(crankRadius, 0, 0)  )
mc.joint('crankShaft_JNT', e=True, zso=True, oj='xyz')
mc.joint(n='piston_JNT', p=((connectionRodLength + crankRadius), 0, 0) )
mc.joint('crankShaft_JNT', e=True, zso=True, oj='xyz' )

#create throttle attribute on the crank
mc.addAttr('crankShaft_JNT', longName = "throttle", attributeType='double')
mc.setAttr('crankShaft_JNT.throttle', edit=True, keyable=True)
mc.setAttr('crankShaft_JNT.throttle', 200)

#expression syntax
exp = 'crankShaft_JNT.rotateY = time * crankShaft_JNT.throttle;'
exp += 'float $alpha = crankShaft_JNT.rotateY;'
exp += 'float $beta = asind((%s * sind($alpha) / %s));' % (str(crankRadius), str(connectionRodLength))
exp += '$gamma = -1 * ($beta + $alpha);'
exp += 'connectingRod_JNT.rotateY = $gamma;'

#create the expression
mc.expression(string = exp,name = expressionName);

You should see the the following result in the top view after hitting play..

figure 3

I've connected the crankShaft_JNT.rotateY to the time attribute, and together with this I've used a throttle attribute on this joint to control the speed. The expression should be simple to follow using the simple maths above as reference.

So there you have it, a completely perfect simulation of a crank and piston. Just parent constrain your geometry, group it all and move into position.

Please feel free to drop me a line if you need anyhing else from this.