Skip to content

Stretchy Bones Re-visted

September 21, 2013

ScaleFK Viewport

I’ve needed to revisit the stretchy bone issue again because some game engines will export spline controllers if they’re parented into the hierarchy.  Plus, sometimes you need FK stretch.

ScaleFK Schematic

I’m going to create a simple arm rig.

  • Click create -> Spacewarp -> Bones.  Create three bones.  bone L upper arm, bone L lower arm, and bone L hand.
  • Create three circle splines.  control L upper arm, control L lower arm, control L hand.
  • Align the splines to the bones
  • Parent control L lower arm to control L upper arm.  And parent control L hand to control L lower arm.
  • Animation -> Bone Tools
  • Select bone L upper arm and, in the Object Properties rollout, turn off Freeze Length.  This will allow the bone to stretch.  Do the same for bone L lower arm.
  • Select bone L upper arm.  Animation -> Constraints -> Position Constraint.  Click control L upper arm
  • Animation -> Constraints -> Orientation Constraint.  Click control L upper arm
  • Select bone L lower arm
  • Animation -> Constraints -> Orientation Constraint.  Click control L lower arm
  • Select bone L hand
  • Animation -> Constraints -> Orientation Constraint.  Click control L hand
  • Check to see if rotating both controllers will rotate the bones and moving control upper arm will move the entire setup.
  • Create two Expose Transform objects by clicking Create -> Helpers -> ExposeTM.  Call one etm L upper arm and etm L lower arm.
  • Set the display size of the expose transforms to something appropriate to your model.
  • In etm L upper arm set the expose node to control L lower arm.  Uncheck Parent.  Set the local reference node to bone L upper arm.  This will calculate the distance between the shoulder and the elbow controller.
  • In etm L lower arm set the expose node to control L hand.  Uncheck Parent.  Set the local reference node to bone L lower arm.
  • Parent etm L upper arm to control L lower arm.  Change the move mode from View to Parent. Set etm L upper arm‘s position to 0.0, 0.0, 0.0.  This should snap it to the elbow.
  • Parent etm L lower arm to control L hand.  Set the position to 0.0, 0.0, 0.0 in parent space.
  • Select bone L lowerarm
  • In the motion panel, open the Assign Controller rollout.  Select Transform -> Position -> X Position.  Click Assign Controller and assign a Float Script.
  • In the Expression Editor create a variable called TargetDistance.
  • Select TargetDistance and press Assign Track.
  • Find etm L upper arm, go into the Object section, select Distance, and press Ok.
  • In the Expression textbox type “TargetDistance”.  This will make the X axis of the bone equal to the distance of the controller from the shoulder.  Click Evaluate to check for any errors and if there are none, click close.
  • Do the same for the lower arm.  Select bone L hand.  Assign a Float script to the X Position.  Create a TargetDistance variable.  Associate it with the distance track.  Tell the bone to take the TargetDistance as its X Position and click ok.

You now should have a bone that rotates normally, is not connected to the controllers in the schematic view, and if control L lower arm is moved on the Parent coordsys along the x axis will scale in size.

You could of course stop here and let the animator go nuts, but sometimes they like interfaces that are a bit easier to use.  The only way that I know how to do this is with a MaxScript.

To start off lock the move on control L lower arm and control L hand to stop people from erroneously moving the controllers outside of parent space.

  • Select control L lower arm
  • Click the Hierarchy tab and go to Link Info.
  • Turn on the Move X/Y/Z locks.
  • Do the same for control L hand.

Open the Max Script editor window.


	-- Gets an existing Attribute holder, or creates one if it's needed.
    fn GetAttributeModifier obj =
        for i = 1 to obj.modifiers.count do
			if classof obj.modifiers[i] == EmptyModifier then
				return obj.modifiers[i]

		emptyMod = emptyModifier()
		addModifier obj emptyMod
		return emptyMod

	-- Deletes a rollout from the Attribute Holder modifier specified
	fn DeleteAttributeRollout emptymod name =
		defs = custAttributes.getDefs emptymod
		-- print ("Defs " + defs as string)
		if defs != undefined and defs.count > 0 then
			for i = defs.count to 1 by -1 do
				-- print (defs[i].name)
				if defs[i].name == #armData then
					custAttributes.delete emptymod defs[i]

	-- Adds the arm attributes to an Attribute holder on the shoulder.
    fn AddArmAttributes obj lengths emptymod =
        attributeCA = attributes armData
            parameters main rollout:paramsRollout
                upperArmLength type:#float ui:upperArmLengthUI default:1.0
                lowerArmLength type:#float ui:lowerArmLengthUI default:1.0
                originalUpperArmLength type:#float default:17.0
				originalLowerArmLength type:#float default:17.0
				scaledUpperArmLength type:#float default:17.0
                scaledLowerArmLength type:#float default:17.0

            rollout paramsRollout "Joint Scale"
                spinner upperArmLengthUI "UArm Length" type:#float
                spinner lowerArmLengthUI "LArm Length" type:#float

                on upperArmLengthUI changed val do
                    scaledX = val * originalUpperArmLength
                    scaledUpperArmLength = scaledX

                on lowerArmLengthUI changed val do
                    scaledX = val * originalLowerArmLength
                    scaledLowerArmLength = scaledX


		-- Delete the old rollout if it exists
		DeleteAttributeRollout emptymod #armData

		-- Add the new version of the rollout
        custAttributes.add emptymod attributeCA BaseObject:false

		-- Set the proper default values to the original and scaled arm lengths.
		-- you can't set them in the parameters for some reason, so do it here 
		-- before we really need to use the data.
		emptymod.armData.originalUpperArmLength = lengths[1]
		emptymod.armData.originalLowerArmLength = lengths[2]
		emptymod.armData.scaledUpperArmLength = emptymod.armData.originalUpperArmLength * emptymod.armData.upperArmLength
		emptymod.armData.scaledLowerArmLength = emptymod.armData.originalLowerArmLength * emptymod.armData.lowerArmLength

		return ""

	fn SetupLeftArm =
		obj = $control_L_upper_arm
		lengths = #($etm_L_upper_arm.distance, $etm_L_lower_arm.distance)
		select obj
		attrib = GetAttributeModifier obj
		AddArmAttributes obj lengths attrib


This script, although it looks a little complex just does some house keeping by grabbing an Attribute Holder modifier, if it exists or creates one if it’s needed.  Then inside of it, it deletes any old rollouts called armData, and generates a new one.  It exposes just what the animator needs to animate, and stores the rest of the variables in there, but hidden.  This is something I haven’t been able to replicate outside of using MaxScript.

Now that the scale variables are all hooked up, the last step is using them to control the X position of the elbow and hand controllers that we locked earlier.

  • Select Control L Upper Arm.
  • Right click and select Wire Parameter
  • Modified Object -> Attribute Holder -> Arm Data -> ScaledUpperArmLength
  • Click on control L lower arm
  • Transform -> Position -> X Position
  • Click the right arrow and then connect.
  • Right click on Control L Upper Arm and select Wire Parameter again.
  • Modified Object -> Attribute Holder -> Arm Data -> ScaledLowerArmLength
  • Click on control L hand
  • Transform -> Position -> X Position

Now when you select the players controller, you can adjust the two spinners and get the arm bones to scale.

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: