Rubik's Cube Rigging

Modelling -- Texturing -- Rigging -- Animating -- Files

Note: Instructions in bold indicate using Maya's menus.

It's been pointed out that the Mental Ray render doesn't handle this rig well. I'm working on an update to the rig to work around that.

Rubik's Cube Principles

There are two tricks to Rubik's Cubes:
1) Of the 26 blocks in the Rubik's Cube, 6 are "axis" blocks, and stay in place -- they just rotate.
2) The other 20 blocks continually move around the rest of the cube. They constantly switch what axis block they rotate around.
To build this into a rig, we have to recrate both of these.

Rig Construction

This is the base of the rig (Fig A.). Under the cubeGroup node there are 7 sub groups. One is for each "axis block", and one is for all the rest of the blocks. The 7th one is the "bucket" where all of the non-axis blocks are parented most of the time. In order to make the blocks rotate properly, they need to pivot around the right pivot points -- which happen to be the pivot points of the axis blocks on each of the rotating faces.

The goal is to dynamically re-parent the "moving" blocks under each of the axis blocks as they rotate. In order to make this easier, we'll put extra transform groups underneath the axis blocks.

In addition, two attributes are needed on the top node. (Fig. A1) The Start Frame attribute sets which frame on which the rig will automatically reset itself (see below in the expression section). To avoid this auto-reset, change this attribute to a frame that is not in your playback range (i.e. -1000). The Previous Frame attribute is just for bookkeeping, to let the cube work properly whether you're playing the scene forwards or backwards. Playing backwards is important for a useful technique discussed at the bottom of the animating page.

When a block is rotating around a certain axis, it can be parented under the bucket group under that axis block. (Fig. B.) That makes the reparenting scripting easier.

Rather than parent the moving blocks directly underneath the axis block (i.e. right1) rendering geometry, the render geometry can also be stuck one more level down (right2). This allows the render geometry to be replaced without affecting how the rig operates.

There is a boolean bookkeeping attribute on the axis block transform called Is Turning. This is used by the scripts to keep track of whether a specific axis block is at a rest rotation (and has no sub-blocks parented underneath it) or is in a turning position (and has several sub-blocks parented underneath it). (Fig. B1)

In the same way, each block under the cubeBucket has another level of transform (block9), so that the sub block render geometry (block9Geom) can be replaced without too much trouble, too. (Fig. C.)

The 20 blocks in the cubeBucket will usually be out of numerical order.

While the cube is animating, the Maya selection will change a lot as the reparenting takes place.

In order to add attributes, select the node on which to add the attribute, then choose Modify -> Add Attribute. Choose the attribute type (integers or booleans for all of the attributes here), and a default value (zero and 'off').

Existing added attributes can be edited or deleted by the appropriate menu item in the Modify menu.

Expression

You can view and edit expressions by choosing Window -> Animation Editors -> Expression Editor. To see the current expressions, choose Select Filter -> By Expression Name. Click on reparentExpression to view the expression for this rig.

The expression is straightforward. It's slightly more complex in order to easily extend to having multiple cubes in the same scene without interfering with each other.

1) On the start frame, it calls the script to reset the the blocks so that the cube is "solved". resetCubes("name of the cube to reset")
2) On each frame, it calls the script that does the block reparenting so that the block rotations will be correct. reparentCubes("name of the cube whose blocks may need to be reparented")

In order to support multiple cubes in one scene, add the name of the other cubes to the string array on the first line. An example of this is shown on the animating page.

It's important that the Evaluation: setting for the expression is set to Always, so it gets called at the beginning of each frame.

Script Breakdown

Because of the way MEL parses script files (like most languages), it's probably easier to read the functions in bottom-to-top order, rather than from top-to-bottom.

The flow of the script goes like this:
reparentExpression -> reparentCubes() -> updateIsTurning() -> updateBucket()
reparentExpression -> resetCubes()

The general strategy assumes that each face of the cube will normally be in a "rest" position -- with its axis block having a rotation that is a mutiple of 90 degrees. The script is made to detect whenever an axis block is about to rotate away from this rest position and when it is returning to a rest position. These are the only two times when blocks need to be reparented.

Surprisingly, MEL doesn't seem to have a rounding function. So I've added a simple one to this script.

// round()
//
//  A simple rounding function.
//
proc float round(float $val){
  if($val > 0)
    return (trunc($val + 0.5));
  else
    return (trunc($val - 0.5));
}
//  reparentCubes()
//
//  This function handles checking whether any of the blocks need to be 
//  reparented, and to where, and when.
//
//  Arguments:
//    string  $cubeName:    The top node of the cube you want to update.
//
global proc reparentCubes(string $cubeName)
{
  //print("reparentCubes " + $cubeName + "\n");
  int $forward = 1;
  
  // Use the current frame and the .previousFrame attr on the cube's top node
  // to see whether playback is going forward or backward.
  //
  int $currFrame = `currentTime -q`;
  int $previousFrame = `getAttr ($cubeName + ".previousFrame")`;
  setAttr ($cubeName + ".previousFrame") $currFrame;
  if ($currFrame < $previousFrame){
    $forward = -1;
  }
  int $nextFrame = $currFrame + $forward;
  
  // For each fo the six axis blocks, update the .isTurning attribute and
  // the sub blocks assigned to each axis block.
  //
  updateIsTurning($cubeName, $cubeName+"|right1",0,$nextFrame);
  updateIsTurning($cubeName, $cubeName+"|left1",1,$nextFrame);
  updateIsTurning($cubeName, $cubeName+"|front1",2,$nextFrame);
  updateIsTurning($cubeName, $cubeName+"|back1",3,$nextFrame);
  updateIsTurning($cubeName, $cubeName+"|top1",4,$nextFrame);
  updateIsTurning($cubeName, $cubeName+"|bottom1",5,$nextFrame);
}

updateIsTurning is where the primary piece of subtle logic happens. Since the sub blocks need to "receive" all of the rotation of an axis block when they are part of that face, then need to already be parented to that block before it starts turning. The way to do that is to check where that block will be rotated "next" frame, and do the parenting ahead of time.

Once an axis block is finished turning, its sub blocks should be returned to the main bucket.

//  updateIsTurning()
//
//  This function provides the logic to 1) update the .isTurning attribute
//  on the axis block and 2) update the bucket on that axis block, when needed.
//
//  Arguments:
//    string   $cubeName:    The top node of the cube you want to update.
//    string   $name:        The axis block whose bucket is being updated
//    int      $index:       The index into the axis and value arrays 
//                           corresponding to the axis block. 
//                           (To maximize code re-use.)
//    int      $nextTime:    The next frame in the playback after this one.
//
proc updateIsTurning(string $cubeName, string $name, int $index, int $nextTime){
  string $axis[] = {".rx", ".rx", ".rz", ".rz", ".ry", ".ry"};
  
  // Set the relevant rotation attribute for this axis block.
  //
  string $rAttr = ($name+$axis[$index]);
  
  // Check the current rotation, and get the rotation that the block will be at
  // on the next frame (whether that is forward or backward doesn't affect
  // the calculation)
  //
  float $currentRotation = `getAttr $rAttr`;
  float $nextRotation = `getAttr -t $nextTime ($name+$axis[$index])`;
  
  // Used for checking if the next rotation is at a rest position.
  //
  int $nextRest = fmod($nextRotation,90);

  if(`getAttr ($name+".isTurning")` == 1)
  {
    // This is currently turning
    //
    
    int $currRest = fmod($currentRotation,90);
    // if this has stopped turning this frame, 
    // and isn't turning next frame, put blocks back
    // into the main bucket
    //
    if( ($currRest == 0) && ($nextRest == 0) ){
       updateBucket($cubeName, $name,$index,1);
       setAttr ($name+".isTurning") 0;
    }
  }else{
    // This is not turning now.
    //
    
    // If this is not turning now, but is turning next frame,
    // take blocks from the main bucket
    //
    if($nextRest != 0){
      updateBucket($cubeName, $name,$index,0);
      setAttr ($name+".isTurning") 1;
    }
  }
}

updateBucket() is the engine that decides which sub blocks should be reparented, and reparents them. This makes use of the listRelatives command to get a list of the blocks that are presently in the bucket that blocks are being moved from.

Action=1 -> Moving blocks from the bucket of an axis block to the main cubeBucket is fairly simple. If any blocks are in the axis block's bucket, they are simply reparented back to the main bucket.

Action=0 -> Moving blocks from the main bucket to an axis block bucket is harder. Only the blocks that are currently members of the face around the axis block should be moved into that block's bucket. The problem is how to find those blocks.

Here we can take advantage of how Maya updates location data when transforms are reparented. Each sub block starts out with its translate set to either 1, 0, or -1 in each axis. This represents that block's current translation away from the center of its current object space. Reparenting changes that object space, so in order to preserve the block's overall transformation, Maya changes the values on that object's translate attribute to compensate.

After a block travels the circuit of going main cubeBucket -> axis block bucket -> main cubeBucket, its translate attribute changes to reflect its current rotation around the center of the block, and so its translation value along the axes of the block will accurately tell us which face of the cube that block is a part of.

It sounds complicated, but it makes for a great way to look through the cubeBucket and grab the cubes on the proper face. To make the script coding easier, the axis we need to match for each block is stored in the string attribute at the top of the script.

//  updateBucket()
//
//  Arguments:
//    string   $cubeName:    The top node of the cube you want to update.
//    string   $name:        The axis block whose bucket is being updated
//    int      $index:       The index into the axis and value arrays 
//                           corresponding to the axis block. 
//                           (To maximize code re-use.)
//    int     $action:       Specifies which direction we're moving the blocks.
//    
// Grab cubes form the bucket (action=0), or put cubes 
// back in the bucket. (action=1)
//
//  Called from the updateIsTurning() function.
//
proc updateBucket(string $cubeName, string $name, int $index, int $action){

  string $mainBucket = ($cubeName+"|cubeBucket");
  //                 right    left   front    back     top  bottom
  string $axis[6] = {".tx",  ".tx",  ".tz",  ".tz",  ".ty",  ".ty"};
  int $value[6] =   {    1,     -1,      1,     -1,      1,     -1};
  
  if($action == 1){
    //print ("putting blocks from " + $name + " back into the Main bucket\n");
    string $blocks[] = `listRelatives -children ($name+"|bucket")`;
    string $c;
    for ($c in $blocks){
      parent ($name+"|bucket|"+$c) $mainBucket;
    }
  }else{
    //print ("taking blocks from the Main bucket for " + $name + "\n");
    string $blocks[] = `listRelatives -children $mainBucket`;
    
    string $b;
    float $v;  
    for($b in $blocks){
      string $attr = ($mainBucket + "|" + $b + $axis[$index]);
      $v = getAttr ($attr);
      
      // round the value to an integer to avoid numerical difficulties when
      // matching values.
      //
      $v = round($v);   
      
      // If the current translation value for this block along the axis
      // of the current axis-block matches the value (1 or -1) of the 
      // actual axis block, then this sub-block is on the same face as the
      // axis block, so it should be taken from the main bucket for this
      // rotation.
      //
      if ($v == $value[$index]){
        // To prevent numerical dreg from accumulating, we set the translate
        // value to an exact integer.
        //
        setAttr ($mainBucket + "|" + $b + $axis[$index]) $v;
        
        // Move the sub block from the main bucket to the axis block's bucket.
        parent ($cubeName+"|cubeBucket|"+$b) ($name+"|bucket");
      }
    }
  }
}

resetCubes() is not called on each frame, but only on the Start Frame for the main cube. This can also be called independently to reset the cube at a different point of the animation. This is part of a useful technique discussed on the animating page.

// resetCubes()
//
// This function resets the sub-blocks to their "solved" position.
// It does not affect the axis blocks.  Those should be animated anyway, so
// it would be pointless to reset them here.
// The function assumes that multiple cubes may be in the same scene, 
// so you need to pass in the name of the top node of the cube you want 
// to reset.
//
global proc resetCubes(string $cubeName)
{
  
  string $b;
  string $mainBucket = $cubeName + "|cubeBucket";
  string $bucket;
  string $blocks[];
  
  // First, search through all of the axis buckets and return cubes to
  // the main bucket.
  //
  $bucket = ($cubeName + "|right1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  $bucket = ($cubeName + "|left1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  $bucket = ($cubeName + "|front1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  $bucket = ($cubeName + "|back1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  $bucket = ($cubeName + "|top1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  $bucket = ($cubeName + "|bottom1|bucket");
  $blocks = `listRelatives -children $bucket`;
  for ($b in $blocks){
    parent ($bucket+"|"+$b) $mainBucket;
  }
  
  // Reset each cube to its proper rotation and translation
  //
  string $bucket = ($cubeName + "|cubeBucket|");
  int $i;
  for ($i = 1; $i <= 20; $i++){
    rotate -a 0 0 0 ($bucket + "block" + $i);
  }
  
  move -os -a 1 1 1 ($bucket + "block1");
  move -os -a 1 1 -1 ($bucket + "block2");
  move -os -a -1 1 1 ($bucket + "block3");
  move -os -a -1 1 -1 ($bucket + "block4");
  move -os -a 0 1 1 ($bucket + "block5");
  move -os -a 1 1 0 ($bucket + "block6");
  move -os -a 0 1 -1 ($bucket + "block7");
  move -os -a -1 1 0 ($bucket + "block8");
  
  move -os -a 1 0 1 ($bucket + "block9");
  move -os -a 1 0 -1 ($bucket + "block10");
  move -os -a -1 0 1 ($bucket + "block11");
  move -os -a -1 0 -1 ($bucket + "block12");
  
  move -os -a 1 -1 1 ($bucket + "block13");
  move -os -a 1 -1 -1 ($bucket + "block14");
  move -os -a -1 -1 1 ($bucket + "block15");
  move -os -a -1 -1 -1 ($bucket + "block16");
  move -os -a 0 -1 1 ($bucket + "block17");
  move -os -a 1 -1 0 ($bucket + "block18");
  move -os -a 0 -1 -1 ($bucket + "block19");
  move -os -a -1 -1 0 ($bucket + "block20");     
  
}

Continue

1) Modelling
2) Texturing
3) Rigging
4) Animating
5) File Download

The Rubik's Cube is copyright, I think, of Seven Towns, Ltd., who retains all rights, etc., that they haven't licensed to other folks. The use of the name here is without permission. If the appropriate people are annoyed by this, I'll make the appropriate modifications.




 
tony@kaap.us | WebSite by KaapFamily.net  |  Valid CSS  |  Valid HTML