Design of a squash and stretch rig in Maya

This tutorial presents a simple technique to perform squashy and stretchy deformations with a single skincluster (Linear Blending Skinning) and mainly using joint scale transformations driven by a remapValue nodes (i.e. a map function to modulate scale factor of the joint animation). So the logic is fairly simple: scaling the model sideways as its height decreases. The final rig will look like this:


 Download base model ]  Download final model ]


Two types of squashy deformations: one that completly flattens/splashes onto the ground, another one that just inflates. Hopefully tweaking the various parameters you can obtain your desired results.

Skin weight Design

First, we will design the skin weights on a simple plane model and transfer them onto our model. This is usually much easier than working on the original model.

   

The model is rigged with 3 joints, since we want the model to fully collapse as a flat plane shape, we'll design linear skin weights. Other-wise said, if the number $x$ represents some distance along the vertical axis, then the skin weights are represented with linear function such as $f(x) = ax$, I illustrate below such skin weights. You'll notice that skin weight's values vary vertically, and stay constant along the horizontal axis. Values are displayed on the side of the pictures:

Resulting deformation:

Now we can simply transfer (i.e. copy skin weights) to our model from the plane model to another object by using Maya built-in Copy Skin Weights.

Or using MEL:

copySkinWeights  -noMirror -surfaceAssociation closestPoint -influenceAssociation closestJoint -sourceSkin $src_cluster -destinationSkin $dst_cluster;

Note that you can also use a very low resolution plane, where you simply set skin weights to $0.0$ or $1.0$ at some vertices, then, Maya's copy skin weights feature will smoothly interpolate the weights across the higher resolution target model.

 Download linear skin weights (high res) ]
 Download linear skin weights (low res) ]

Note: linear skin weights will produce a discontinuous deformation (i.e. a C0 function produces a C0 deformation). This will be especially noticable for hight resolution models (coarse one will help hide the defects). Now smooth skin weights (non-linear) will avoid this issue but won't allow to perfectly flatten the model.

Mid joint animation

Let's automate how the mid joint reacts when we drag the top level joint. The goal is for the mid joint to stay in between the top and bottom joint as we move the top control:

We can achieve this purely based on geometric constraints. First we create 3 circles with curves for the controls. We parent constraint top and bottom joints to the top and bottom circles. As for the mid joint things are slightly more involved. We create a "locator" object:

We are going to constraint this locator so that it always stays between the top and bottom controls. In other words: $locator.translate = top.translate \times 0.5 + bottom.translate \times 0.5$. In Maya this is achieved with a position constraint with top and bottom curves as targets of the locator object (make sure to not use offset) .


Now, with this setup the locator will always lie exactly in the middle but our mid joint is not necessarily at the same location at rest pose. This may cause some issues later on so it's best to play with the associated barycentric weight of the top control (locator1_pointConstraint1.TopControlW1) so that the locator actually aligns with the mid joint art rest pose:

Here we set W0 = 1.3 which moves up the locator towards the mid joint. I call this tweak "barycentric weight adjustment". We will compare results throughout the tutorial with and without this barycentric weight adjustment.


Next we want the mid joint to follow the locator, so that when we move down the top joint, the mid joint automatically follows.
We'd also like the ability to tweak the mid joint location with a rig controller, and the tweak should be preserved as we move down the top joint and mid joint follows the locator. The gif below illustrate the disered behavior:

We do as follows: we parent constraint (with offset) the mid joint to the mid rig control. Now we can tweak mid joint location but it won't react as we move down the top joint. So in addition to that, we place the mid rig control as the child of the locator. Once this is don don't forget to freeze the controller's transformation. The rig control now encodes the tweak (i.e user offset) while the locator drives the overal motion:

Recall that the mid joint is most likely not exactly centered between the top and bottom joint, so the locator won't be aligned with the mid joint unless we adjusted the barycentric weights of the point constraint. Below is an example without adjustment, we can see the mid joint stays higher than the top joint as move it down:

Barycentric weight adjustments to align locator and mid joint, gives a better result:

 Model with adjusted barycentric weights ]


Alternatively, we could clamp the Y translation of the mid joint to maintain its position between top and bottom joints. This can be done with an expression node using the following expression:

mid_joint.translateY = clamp(0, max(0, top_joint.translateY), mid_joint_parentConstraint1.constraintTranslateY );

So we make sure that the output of the parent constraint from the mid control (i.e. height of the mid control) is clamped between [0, (top_joint.translateY)] before feeding it to mid_joint.translateY

Some caveats though. The mid joint is handled but the mid control does not reflect this clamping. That being said, I feel like constraining at the controller level would be unnecessarily complexify the rig. In addition, this computation takes for granted the root joint defines the bottom (0 position of the bottom, mid and top joints). More scripting would be needed to handle more general use case.


/*
    Simply select the top, bottom and middle joints (in this order)
    and run this function.

    Both point constraint adjusted weights and clamping with expression nodes are 
    present (you can play with the hard coded ifs)
*/
global proc build3pointsSquashyRigControls()
{
    string $joints[] = `ls -selection -objectsOnly -long`;

    if( size($joints) != 3 ){
        warning("Please select 3 joints, top, bottom and middle joint");
        return;
    }

    string $joint1   = $joints[0];
    string $joint2   = $joints[1];
    string $midJoint = $joints[2];

    // Create rig controls
    addCircle($joint1);
    rename "topControl";
    string $ctrl1 = active_selection();

    addCircle($joint2);
    rename "bottomControl";
    string $ctrl2 = active_selection();

    addCircle($midJoint);
    rename "midControl";
    string $midCtrl = active_selection();
    setOverrideColorIndex($midCtrl, 22/* yellow */);


    float $seg1        = distance_between($joint1  , $midJoint);
    float $full_height = distance_between($joint1  , $joint2  );
    float $seg2        = distance_between($midJoint, $joint2  );

    float $w1 = $seg1 / $full_height;
    float $w0 = $seg2 / $full_height;

    // Constrain locator pos = lerp(ctrl1, ctrl2, 0.5)
    string $res[] = `spaceLocator -p 0 0 0`;
    string $locatorName = $res[0];
    string $pointCstrName[] = `pointConstraint -offset 0 0 0 -weight 1 $ctrl1 $ctrl2 $locatorName`;
    
    if(true){
        // For convenience we adjust the weights so that $w1 always equals 1.0
        $w0 = (1.0 - $w1) + $w0;
        $w1 = 1.0;

        // Make sure the locator is aligned with the mid joint:
        setAttr ($pointCstrName[0] + "." + to_short_name($ctrl1) + "W0") $w0;
        setAttr ($pointCstrName[0] + "." + to_short_name($ctrl2) + "W1") $w1;
    }

    // mid controller follows locator:
    parent $midCtrl $locatorName;
    $midCtrl = $locatorName + "|" + $midCtrl;

    makeIdentity -apply true -t 1 -r 1 -s 1 -n 0 -pn 1 $midCtrl;
    // mid controller drives mid joint position:
    $res = `parentConstraint -maintainOffset -weight 1 $midCtrl $midJoint`;
    string $midJointConstraint = $res[0];


    // top and bottom rig controls drive top and bottom joint positions:
    parentConstraint -maintainOffset -weight 1 $ctrl1 $joint1;
    parentConstraint -maintainOffset -weight 1 $ctrl2 $joint2;

    // setup the expression node 
    // This node will force mid joint position to be between top and bottom joint positions.
    // the expr node will stand between
    // the parent constraint of the mid joint and the mid joint itself:
    if(false)
    {
        // midJoint.posY = clamp(0, max(0, topJoint.posY), midJointConstraint.poseY );
        string $expr = $midJoint+".translateY = clamp(0, max(0, "+$joint1+".translateY), "+$midJointConstraint+".constraintTranslateY );";

        // create node:
        string $expression = `createNode "expression" -name "midJointYClamp"`;

        connectAttr -force ($expression+".output[0]") ($midJoint+".translateY");
        connectAttr ($joint1+".translateY") ($expression+".input[0]");
        connectAttr ($midJointConstraint+".constraintTranslateY") ($expression+".input[1]");

        setAttr ($expression+".expression") -type "string" $expr;
    }

    group -name "squashRigControls" $ctrl1 $ctrl2 $locatorName;
    //  parent it to the parent of rootscale_suffix.
    string $list_relatives[] = `listRelatives -parent "rootscale_suffix"`;

    if( size($list_relatives) > 0 )
        parent "squashRigControls" $list_relatives[0];

}

// -----------------------------------------------------------------------------------------
// HELPER FUNCTIONS FOR THE ABOVE
// -----------------------------------------------------------------------------------------

proc float distance_between(string $obj1, string $obj2)
{
    vector $vobj1 = `xform -query -translation -worldSpace $obj1`;
    vector $vobj2 = `xform -query -translation -worldSpace $obj2`;
    return mag($vobj2 - $vobj1);
}

global proc addCircle(string $target)
{
    string $targetShape = get_shape($target);

    // Transform position:
    float $position[] = {0,0,0};
    if($target != "")
        $position = `xform -query -worldSpace -translation $target`;

    circle
        -center 0 0 0
        -normal 0 1 0
        -sweep 360 // full circle
        -radius 1
        -degree 3 //  1 - linear, 3 - cubic
        -useTolerance 0
        -tolerance 0.01
        -sections 8
        -constructionHistory 0;

    string $selections[] = `ls -selection -transforms`;
    string $controlName = $selections[0];

    if( $target != "" )
    {
        if( nodeType($target) == "joint")
            move -r ($position[0]) ($position[1]) ($position[2]);

        /* Not needed in our example
        else if( nodeType($targetShape) == "clusterHandle")
        {
            // Skin cluster handle position:
            vector $pos = getAttr( $targetShape+".origin" );

            // skin cluster handle transfo is usually identity:
            matrix $worldMatrix[4][4] = SCD_get_transform_world_matrix($target);
            $pos = SCD_multPointMatrix($pos, $worldMatrix);

            move -r ($pos.x) ($pos.y) ($pos.z);
        }
        */
    }

    scale -r 20 20 20;

    // freeze circle
    makeIdentity -apply true -t 1 -r 1 -s 1 -n 0 -pn 1;

    setOverrideColorIndex($controlName, 9);
}


proc setOverrideColorIndex(string $transform, int $index)
{
    setAttr ($transform+".overrideEnabled") 1;
    setAttr ($transform+".overrideColor") $index;
    setAttr ($transform+".overrideRGBColors") 0;
}


proc string get_shape( string $xform )
{
    if(size($xform) == 0)
        return "";

    string $shape;
    if ( "transform" == `nodeType $xform` ) {
        string $parents[] = `listRelatives -fullPath -shapes $xform`;
        if( size($parents) > 0) {
            $shape = $parents[0];
        }
    } else {
        $shape = $xform;
    }

    return $shape;
}

proc string active_selection()
{
    string $sel[] = `ls -selection -objectsOnly -long`;
    return $sel[0];
}

proc string to_short_name( string $long_name )
{
    string $items[];
    int $size = `tokenize $long_name "|" $items`;
    $short_name = "NULL";

    if ( $size > 0 )
        $short_name = $items[$size-1];

    return $short_name;
}

build3pointsSquashyRigControls();


Lateral scale

Ok, so far this was not that interesting, let's add squashing by scaling along the $x$ and $z$ axis according to the height of the top joint (along the $y$ axis)

To this end, we'll simply add a new expression node that takes in input top_joint.TranslateY and outputs mid_joint.ScaleX and mid_joint.ScaleZ with the following expression:

// midJoint.scaleX = (1 / (topJoint.Y/ bindPose(topJoint).Y ))^2

// The height is hard coded or computed with a script to generate the expression node
float $height_at_rest = 0.69f; 
// top joint height ratio:
float $height = top_joint.translateY / $height_at_rest; 
$height = 1.0/ $height;
$height = $height * $height;
mid_joint.scaleX = $height;
mid_joint.scaleZ = $height;
// Setup expression node that
// scales along the X axis the mid joint position according to
// the height of the top joint.

{
    
    float $restHeight = distance_between($joint1, $joint2);
    if( $restHeight < 0.0 ){
        warning("Somthing is wrong");
        return;
    }

    // midJoint.scaleX = (1 / (topJoint.Y/ bindPose(topJoint).Y ))^2
    string $expr = (
        "float $height_at_rest = "+$restHeight+";\n" +
        "// top joint height ratio: \n" +
        "float $height = "+$joint1+".translateY / $height_at_rest; \n" +
        "$height = 1.0/ $height; \n" +
        "$height = $height * $height; \n" +
        $midJoint+".scaleX = $height; \n"
    );

    string $expression = `createNode "expression" -name "scaleFromHeight"`;

    connectAttr -force ($expression+".output[0]") ($midJoint+".scaleX");
    connectAttr ($joint1+".translateY") ($expression+".input[0]");

    setAttr ($expression+".expression") -type "string" $expr;
}

Which results in the following:

Caveats: squashing is initiated very early when we move the top joint. Though for a standard character face that can work well, I have other plans for the slime. Now to customize the behaviour of the squash and stretch function inside the expression node, we need to re-write the expression node every time we come up with a new idea. This is cumbersome, so below we'll streamline this process.

Custom function for lateral inflation

So to better customize the relationship between vertical motion and horizontal squash and stretch, we will use a remapValue node to define a function that takes a scalar value and returns a scalar such as $map : \mathbb R \rightarrow \mathbb R $:

midJoint.scaleXY = map( topJoint.translateY)

Let's modify our expression node to simply output the normalized height of the top joint (i.e. at bind pose returns 1 and decrease towards 0 as we move down the top joint position).

// output = topJoint.Y / height_at_rest

// The height is hard coded or computed with a script to generate the expression node
float $height_at_rest = 0.69f; 
// top joint height ratio:
float $height = top_joint.translateY / $height_at_rest; 

//after creating the expr node, the connection below will be changed:
mid_joint.scaleX = $height; 

Now let's feed it to a remapValue node, we set the nodes's input max (the height) to range in $[0.0, 2.0]$. This means we only consider height values from 0 up to double the height from rest pose. As for lateral scaling, I don't want to scale more than a factor of $3$, so I set the output max to $3.0$:

Let's design the mapping curve, at rest height we want to output $1.0$ as our scale factor. We need to deduce a sample point $p(x,y)$ that ensures this condition. Since input max is $2.0$ the normalized rest height should be $p.x = 0.5$ (since input max $\times 0.5 = 1.0$). Similarly $p.y = 0.333 = 1/3$ ensures output scale is $1.0$ (since output max $\times 1/3 = 1$).

Two other sample points are placed, one to specify lateral scaling peak value when $height = 0$, and another to specify minimal scale factor when the top joint's height increases. With this we are getting closer to the desired splashing effect when crushing the slime, here the version with clamping of the mid joint's $y$ coordinates:

And here with the barycentric weight adjustment:

This is cool but not entirely satisfying, ideally we'd like to splash closer to the ground. One thing we can try is to tweak by hand the curve so that most of the scaling happens towards the end of the crushing motion (adjusted barycentric weights):


float $restHeight = SCD_compute_rest_pose_height_of(/*top joint:*/$joint1);
if( $restHeight < 0.0 ){
    warning("SCD_create3JointsSquashControls: could not compute rest pose height of top joint");
    return;
}

// midJoint.scaleX = (1 / (topJoint.Y/ bindPose(topJoint).Y ))^2
string $expr = (
    "float $height_at_rest = "+$restHeight+";\n" +
    "// top joint height ratio: \n" +
    "float $height = "+$joint1+".translateY / $height_at_rest; \n" +
    $midJoint+".scaleX = $height; \n"
);

// create node:
$expression = `createNode "expression" -name "scaleFromHeight"`;

connectAttr -force ($expression+".output[0]") ($midJoint+".scaleX");
connectAttr ($joint1+".translateY") ($expression+".input[0]");

setAttr ($expression+".expression") -type "string" $expr;

// Create and connect remapValue node
string $remapNode = `shadingNode -asUtility remapValue`;
// rename "remapToscaleSides"
$remapNode = `rename "remapToscaleSides"`;

connectAttr -f ($expression + ".output[0]") ($remapNode + ".inputValue");
connectAttr -f ($remapNode + ".outValue") ($midJoint + ".scaleX");

// Defining the remap curve:
setAttr ($remapNode + ".inputMax") 2.0;
setAttr ($remapNode + ".outputMax") 3.0;

setAttr ($remapNode + ".value[0].value_FloatValue") 1.0;
setAttr ($remapNode + ".value[1].value_FloatValue") 0.0;
// set interp to spline:
setAttr ($remapNode + ".value[0].value_Interp") 3;
setAttr ($remapNode + ".value[1].value_Interp") 3;

// Add a midpoint to smooth the curve:
setAttr ($remapNode + ".value[2].value_FloatValue") 0.3333333;
setAttr ($remapNode + ".value[2].value_Position") 0.500000;
setAttr ($remapNode + ".value[2].value_Interp") 3;


// set more points to tweak the curve:
setAttr ($remapNode + ".value[3].value_Position") 0.109162;
setAttr ($remapNode + ".value[3].value_FloatValue") 0.549065;
setAttr ($remapNode + ".value[3].value_Interp") 3;

setAttr ($remapNode + ".value[4].value_Position") 0.282651;
setAttr ($remapNode + ".value[4].value_FloatValue") 0.399533;
setAttr ($remapNode + ".value[4].value_Interp") 3;



Customize mid joint descent velocity

Let's refine our rig even further! Currently the mid joint travels at a constant speed. Ideally, I'd like the mid joint to rapidly descend as we press down the top joint and then decelerate as we get closer to the ground. In other words, a linear interpolation between top and bottom controls positions won't do it. We'd like another curve to control the speed of the descent of the mid joint. This will allow to concentrate most of the splashing closer to the bottom part of the model.

To achieve this, we'll hijack the barycentric weights of the point constraint driving our locator. This constraint computes the barycenter between top and bottom controls, it also associates barycentric weights $w0$ and $w1$ to top and bottom controls:

We are going to drive $W1$ so that it is initially set to $1.0$ at rest pose and increases to a huge value as the locator gets closer to the bottom joint. So using a remapValue node, we set the input height (i.e. input max) to $2.0$ and the max $W1$ (output max) to $20.0$

We also ensure at rest pose (normalized height = 1.0) to output $W1 = 1.0$ with an extra sample point:

With barycentric weight adjustment:

Here is what we get without clamping and barycentric weight adjustment:

// Create and connect remapValue node
string $remapNode = `shadingNode -asUtility remapValue`;
$remapNode = `rename "remapMidCtrlHeight"`;

connectAttr -f ($expression + ".output[0]") ($remapNode + ".inputValue");
connectAttr -f ($remapNode + ".outValue") ($pointCstrName[0] + "." + SCD_to_short_name($ctrl2) + "W1");

setAttr ($remapNode + ".inputMax") 2;
setAttr ($remapNode + ".outputMax") 20;


//customize the remap curve:

// adjust end points
setAttr ($remapNode + ".value[0].value_FloatValue") 1;
setAttr ($remapNode + ".value[1].value_FloatValue") 0;

// set mid point
setAttr ($remapNode + ".value[2].value_FloatValue") 0.05;
setAttr ($remapNode + ".value[2].value_Position") 0.5;
setAttr ($remapNode + ".value[2].value_Interp") 2; //

// smooth interp for all points
setAttr ($remapNode + ".value[0].value_Interp") 2;
setAttr ($remapNode + ".value[1].value_Interp") 2;

 

Adjustable splash area

Let's make it so that the splash area is adjustable when we squash the slime onto the ground. We add a custom attribute to the mid joint controller, select the controller and go to the channel box:

We set the minimum value to $1.0$ (no side scaling) and leave the max value without limits. Since the output max of our curve that defines side scaling is the maximum scale factor possible, we plug our splash_area attribute straight into it:

Next we need to dynamically adjust the value of the central control point of our curve (we assume there is only a central point in this scenario)

The height of that point should be $\frac{1.0}{\text{splash_area}}$ so we simply use a multiplyDivide node

// For dynamic splash area adjustment we use a much simpler curve
removeMultiInstance -break true ($remapToscaleSides+".value[4]");
removeMultiInstance -break true ($remapToscaleSides+".value[3]");

// Add custom attribute to mid control
addAttr -ln "splash_area"  -at double  -min 1.0 -dv 1 $midCtrl;
setAttr -e-keyable true ($midCtrl + ".splash_area");

// Connect to central control point of the curve
string $multNode = `shadingNode -asUtility multiplyDivide`;
setAttr ($multNode + ".operation") 2;
setAttr ($multNode + ".input1X") 1;
connectAttr -f ($midCtrl + ".splash_area") ($multNode + ".input2X");
connectAttr -f ($multNode + ".outputX") ($remapToscaleSides + ".value[2].value_FloatValue");

// Adjust max scale factor of the curve
connectAttr -f ($midCtrl + ".splash_area") ($remapToscaleSides + ".outputMax");

Allow user to specify scale

Right now, we cannot tweak scale factors of our rig controls:

Adding this for top and bottom controls is straightforward, we just need to add scale constraints:

scaleConstraint -offset 1 1 1 -weight 1 botom_control bottom_joint;
scaleConstraint -offset 1 1 1 -weight 1 top_control top_joint;

For the mid control, it's a bit more complicated. There is already some sort of automatic computation in place to infer the scale factor. So we need to mix both user tweak and automatic computation. Let's multiply the user specified scale against the automatic computation with a multiplyDivide node:

string $multNode = `shadingNode -asUtility multiplyDivide`;

connectAttr -f ($midCtrl + ".scaleX") ($multNode + ".input1X");
setAttr ($multNode + ".input1Y") 1.0;
connectAttr -f ($midCtrl + ".scaleZ") ($multNode + ".input1Z");

connectAttr -f ($remapToscaleSides + ".outValue") ($multNode + ".input2X");
setAttr ($multNode + ".input2Y") 1.0;
connectAttr -f ($remapToscaleSides + ".outValue") ($multNode + ".input2Z");

connectAttr -f ($multNode + ".outputX") ($midJoint + ".scaleX");
connectAttr -f ($multNode + ".outputY") ($midJoint + ".scaleY");
connectAttr -f ($multNode + ".outputZ") ($midJoint + ".scaleZ");

Note that for now we disallowed custom scaling of the Y axis, but we could add it with an extra connection. Here is our final rig:

{
    int $barycentricAdjustment = 1;
    int $yAxisClamping = 0;
    int $adjustDescentSpeedWithCurve = 1;
    int $adjustableSplashAreaSize = 1;
    int $rigControlsWithScale = 1;

    string $joints[] = SCD_get_selected_joints();

    if( size($joints) != 3 ){
        warning("Please select 3 joints, top, bottom and middle joint");
        return;
    }

    string $joint1   = $joints[2];
    print("top joint1: " + $joint1 + "\n");

    string $joint2   = $joints[1];
    print("bottom joint2: " + $joint2 + "\n");

    string $midJoint = $joints[0];
    print("midJoint: " + $midJoint + "\n");

    // Create rig control for each joint:
    createControlCircleAt($joint1);
    rename "topControl";
    string $ctrl1 = get_active_selection();

    createControlCircleAt($joint2);
    rename "bottomControl";
    string $ctrl2 = get_active_selection();

    SCD_createControlCircleAt($midJoint);
    rename "midControl";
    string $midCtrl = get_active_selection();
    set_overrideColorIndex($midCtrl, 22/* yellow */);

    float $seg1        = SCD_distance_node($joint1  , $midJoint);
    float $full_height = SCD_distance_node($joint1  , $joint2  );
    float $seg2        = SCD_distance_node($midJoint, $joint2  );

    float $w1 = $seg1 / $full_height;
    float $w0 = $seg2 / $full_height;


    // Constrain locator pos = lerp(ctrl1, ctrl2, 0.5)
    string $res[] = `spaceLocator -p 0 0 0`;
    string $locatorName = $res[0];
    string $pointCstrName[] = `pointConstraint -offset 0 0 0 -weight 1 $ctrl1 $ctrl2 $locatorName`;

    if($barycentricAdjustment)
    {

        // For convenience we adjust the weights so that $w1 always equals 1.0
        $w0 = $w0 / $w1;
        $w1 = 1.0;

        // Make sure the locator is aligned with the mid joint:
        setAttr ($pointCstrName[0] + "." + SCD_to_short_name($ctrl1) + "W0") $w0;
        setAttr ($pointCstrName[0] + "." + SCD_to_short_name($ctrl2) + "W1") $w1;
    }

    // mid controller follows locator:
    parent $midCtrl $locatorName;
    $midCtrl = $locatorName + "|" + $midCtrl;

    makeIdentity -apply true -t 1 -r 1 -s 1 -n 0 -pn 1 $midCtrl;
    // mid controller drives mid joint position:
    $res = `parentConstraint -maintainOffset -weight 1 $midCtrl $midJoint`;
    string $midJointConstraint = $res[0];


    // top and bottom rig controls drive top and bottom joint positions:
    parentConstraint -maintainOffset -weight 1 $ctrl1 $joint1;
    parentConstraint -maintainOffset -weight 1 $ctrl2 $joint2;

    // setup expression node that
    // forces mid joint position to be between top and bottom joint positions.
    // the expr node will stand between
    // the parent constraint of the mid joint and the mid joint itself:
    if($yAxisClamping){
        // midJoint.posY = clamp(0, max(0, topJoint.posY), midJointConstraint.poseY );
        string $expr = $midJoint+".translateY = clamp(0, max(0, "+$joint1+".translateY), "+$midJointConstraint+".constraintTranslateY );";

        // create node:
        string $expression = `createNode "expression" -name "midJointYClamp"`;

        connectAttr -force ($expression+".output[0]") ($midJoint+".translateY");
        connectAttr ($joint1+".translateY") ($expression+".input[0]");
        connectAttr ($midJointConstraint+".constraintTranslateY") ($expression+".input[1]");

        setAttr ($expression+".expression") -type "string" $expr;
    }

    // TODO: parent the whole setup under a group
    group -name "squashRigControls" $ctrl1 $ctrl2 $locatorName;
    $ctrl1   = "squashRigControls|" + $ctrl1;
    $ctrl2   = "squashRigControls|" + $ctrl2;
    $midCtrl = "squashRigControls|" + $midCtrl;

    //  parent it to the parent of rootscale_suffix.
    string $list_relatives[] = `listRelatives -parent "rootscale_suffix"`;

    if( size($list_relatives) > 0 ){
        parent "squashRigControls" $list_relatives[0];
        $ctrl1   = $list_relatives[0] + "|" + $ctrl1;
        $ctrl2   = $list_relatives[0] + "|" + $ctrl2;
        $midCtrl = $list_relatives[0] + "|" + $midCtrl;
    }


    string $expression = "";

    string $remapToscaleSides = "";

    ////////////////////////////////////////
    // Setup expression node that
    // scales along the X axis the mid joint position according to
    // the height of the top joint.
    if(false)
    {

        ///////////////////
        // Fixed function.

        print($joint1+"\n"  );
        float $restHeight = SCD_compute_rest_pose_height_of(/*top joint:*/$joint1);
        if( $restHeight < 0.0 ){
            warning("SCD_create3JointsSquashControls: could not compute rest pose height of top joint");
            return;
        }

        // midJoint.scaleX = (1 / (topJoint.Y/ bindPose(topJoint).Y ))^2
        string $expr = (
            "float $height_at_rest = "+$restHeight+";\n" +
            "// top joint height ratio: \n" +
            "float $height = "+$joint1+".translateY / $height_at_rest; \n" +
            "$height = 1.0/ $height; \n" +
            "$height = $height * $height; \n" +
            $midJoint+".scaleX = $height; \n"
        );

        // create node:
        $expression = `createNode "expression" -name "scaleFromHeight"`;

        connectAttr -force ($expression+".output[0]") ($midJoint+".scaleX");
        connectAttr ($joint1+".translateY") ($expression+".input[0]");

        setAttr ($expression+".expression") -type "string" $expr;
    }
    else if(true)
    {
        ///////////////////
        // Dynamic function (curve based)


        print($joint1+"\n"  );
        float $restHeight = SCD_compute_rest_pose_height_of(/*top joint:*/$joint1);
        if( $restHeight < 0.0 ){
            warning("SCD_create3JointsSquashControls: could not compute rest pose height of top joint");
            return;
        }

        // midJoint.scaleX = (1 / (topJoint.Y/ bindPose(topJoint).Y ))^2
        string $expr = (
            "float $height_at_rest = "+$restHeight+";\n" +
            "// top joint height ratio: \n" +
            "float $height = "+$joint1+".translateY / $height_at_rest; \n" +
            $midJoint+".scaleX = $height; \n"
        );

        // create node:
        $expression = `createNode "expression" -name "scaleFromHeight"`;

        connectAttr -force ($expression+".output[0]") ($midJoint+".scaleX");
        connectAttr ($joint1+".translateY") ($expression+".input[0]");

        setAttr ($expression+".expression") -type "string" $expr;

        // Create and connect remapValue node
        $remapToscaleSides = `shadingNode -asUtility remapValue`;
        // rename "remapToscaleSides"
        $remapToscaleSides = `rename "remapToscaleSides"`;

        connectAttr -f ($expression + ".output[0]") ($remapToscaleSides + ".inputValue");
        connectAttr -f ($remapToscaleSides + ".outValue") ($midJoint + ".scaleX");
        connectAttr -f ($remapToscaleSides + ".outValue") ($midJoint + ".scaleZ");

        // Defining the remap curve:
        setAttr ($remapToscaleSides + ".inputMax") 2.0;
        setAttr ($remapToscaleSides + ".outputMax") 3.0;

        setAttr ($remapToscaleSides + ".value[0].value_FloatValue") 1.0;
        setAttr ($remapToscaleSides + ".value[1].value_FloatValue") 0.0;
        // set interp to spline:
        setAttr ($remapToscaleSides + ".value[0].value_Interp") 3;
        setAttr ($remapToscaleSides + ".value[1].value_Interp") 3;

        // Add a midpoint to smooth the curve:
        setAttr ($remapToscaleSides + ".value[2].value_FloatValue") 0.3333333;
        setAttr ($remapToscaleSides + ".value[2].value_Position") 0.500000;
        setAttr ($remapToscaleSides + ".value[2].value_Interp") 3;


        // set more points to tweak the curve:
        setAttr ($remapToscaleSides + ".value[3].value_Position") 0.109162;
        setAttr ($remapToscaleSides + ".value[3].value_FloatValue") 0.549065;
        setAttr ($remapToscaleSides + ".value[3].value_Interp") 3;

        setAttr ($remapToscaleSides + ".value[4].value_Position") 0.282651;
        setAttr ($remapToscaleSides + ".value[4].value_FloatValue") 0.399533;
        setAttr ($remapToscaleSides + ".value[4].value_Interp") 3;
    }


    ////////////////////////////////////////
    // Adjusting descent speed of mid joint when approaching bottom joint

    if($adjustDescentSpeedWithCurve){
        // Create and connect remapValue node
        string $remapNode = `shadingNode -asUtility remapValue`;
        $remapNode = `rename "remapMidCtrlHeight"`;

        connectAttr -f ($expression + ".output[0]") ($remapNode + ".inputValue");
        connectAttr -f ($remapNode + ".outValue") ($pointCstrName[0] + "." + SCD_to_short_name($ctrl2) + "W1");

        setAttr ($remapNode + ".inputMax") 2;
        setAttr ($remapNode + ".outputMax") 20;


        //customize the remap curve:

        // adjust end points
        setAttr ($remapNode + ".value[0].value_FloatValue") 1;
        setAttr ($remapNode + ".value[1].value_FloatValue") 0;

        // set mid point
        setAttr ($remapNode + ".value[2].value_FloatValue") 0.05;
        setAttr ($remapNode + ".value[2].value_Position") 0.5;
        setAttr ($remapNode + ".value[2].value_Interp") 2; //

        // smooth interp for all points
        setAttr ($remapNode + ".value[0].value_Interp") 2;
        setAttr ($remapNode + ".value[1].value_Interp") 2;
    }


    //////////////////////////////////
    // Adjustable splash area
    if($adjustableSplashAreaSize){
        // For dynamic splash area adjustment we use a much simpler curve
        removeMultiInstance -break true ($remapToscaleSides+".value[4]");
        removeMultiInstance -break true ($remapToscaleSides+".value[3]");

        // Add custom attribute to mid control
        addAttr -ln "splash_area"  -at double  -min 1.0 -dv 1 $midCtrl;
        setAttr -e-keyable true ($midCtrl + ".splash_area");
        setAttr ($midCtrl + ".splash_area") 3.0;

        // Connect to central control point of the curve
        string $multNode = `shadingNode -asUtility multiplyDivide`;
        string $multNode = `rename "reciprocalNumber"`;
        setAttr ($multNode + ".operation") 2;
        setAttr ($multNode + ".input1X") 1;
        connectAttr -f ($midCtrl + ".splash_area") ($multNode + ".input2X");
        connectAttr -f ($multNode + ".outputX") ($remapToscaleSides + ".value[2].value_FloatValue");

        // Adjust max scale factor of the curve
        connectAttr -f ($midCtrl + ".splash_area") ($remapToscaleSides + ".outputMax");
    }


    if($rigControlsWithScale)
    {
        scaleConstraint -offset 1 1 1 -weight 1 $ctrl1 $joint1;
        scaleConstraint -offset 1 1 1 -weight 1 $ctrl2 $joint2;

        // mid controller:
        string $multNode = `shadingNode -asUtility multiplyDivide`;

        connectAttr -f ($midCtrl + ".scaleX") ($multNode + ".input1X");
        setAttr ($multNode + ".input1Y") 1.0;
        connectAttr -f ($midCtrl + ".scaleZ") ($multNode + ".input1Z");

        connectAttr -f ($remapToscaleSides + ".outValue") ($multNode + ".input2X");
        setAttr ($multNode + ".input2Y") 1.0;
        connectAttr -f ($remapToscaleSides + ".outValue") ($multNode + ".input2Z");

        connectAttr -f ($multNode + ".outputX") ($midJoint + ".scaleX");
        connectAttr -f ($multNode + ".outputY") ($midJoint + ".scaleY");
        connectAttr -f ($multNode + ".outputZ") ($midJoint + ".scaleZ");
    }
}


// -----------------------------------------------------------------------------------------
// HELPER FUNCTIONS FOR THE ABOVE
// -----------------------------------------------------------------------------------------
 
proc float distance_between(string $obj1, string $obj2)
{
    vector $vobj1 = `xform -query -translation -worldSpace $obj1`;
    vector $vobj2 = `xform -query -translation -worldSpace $obj2`;
    return mag($vobj2 - $vobj1);
}
 
global proc addCircle(string $target)
{
    string $targetShape = get_shape($target);
 
    // Transform position:
    float $position[] = {0,0,0};
    if($target != "")
        $position = `xform -query -worldSpace -translation $target`;
 
    circle
        -center 0 0 0
        -normal 0 1 0
        -sweep 360 // full circle
        -radius 1
        -degree 3 //  1 - linear, 3 - cubic
        -useTolerance 0
        -tolerance 0.01
        -sections 8
        -constructionHistory 0;
 
    string $selections[] = `ls -selection -transforms`;
    string $controlName = $selections[0];
 
    if( $target != "" )
    {
        if( nodeType($target) == "joint")
            move -r ($position[0]) ($position[1]) ($position[2]);
 
        /* Not needed in our example
        else if( nodeType($targetShape) == "clusterHandle")
        {
            // Skin cluster handle position:
            vector $pos = getAttr( $targetShape+".origin" );
 
            // skin cluster handle transfo is usually identity:
            matrix $worldMatrix[4][4] = SCD_get_transform_world_matrix($target);
            $pos = SCD_multPointMatrix($pos, $worldMatrix);
 
            move -r ($pos.x) ($pos.y) ($pos.z);
        }
        */
    }
 
    scale -r 20 20 20;
 
    // freeze circle
    makeIdentity -apply true -t 1 -r 1 -s 1 -n 0 -pn 1;
 
    setOverrideColorIndex($controlName, 9);
}
 
 
proc setOverrideColorIndex(string $transform, int $index)
{
    setAttr ($transform+".overrideEnabled") 1;
    setAttr ($transform+".overrideColor") $index;
    setAttr ($transform+".overrideRGBColors") 0;
}
 
 
proc string get_shape( string $xform )
{
    if(size($xform) == 0)
        return "";
 
    string $shape;
    if ( "transform" == `nodeType $xform` ) {
        string $parents[] = `listRelatives -fullPath -shapes $xform`;
        if( size($parents) > 0) {
            $shape = $parents[0];
        }
    } else {
        $shape = $xform;
    }
 
    return $shape;
}
 
proc string active_selection()
{
    string $sel[] = `ls -selection -objectsOnly -long`;
    return $sel[0];
}
 
proc string to_short_name( string $long_name )
{
    string $items[];
    int $size = `tokenize $long_name "|" $items`;
    $short_name = "NULL";
 
    if ( $size > 0 )
        $short_name = $items[$size-1];
 
    return $short_name;
}


 Download final model ]

 
Paypal donation Donate

No comments

(optional field, I won't disclose or spam but it's necessary to notify you if I respond to your comment)
All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.
Anti-spam question: