MEL How-To #105

Back · Previous · Next Maya

How do I align an object to a curve (without using a constraint)?

Orthogonal Basis Vectors

(Wait! Don't run away frightened at the prospect of calculus derivatives a la Eric W. Weisstein's MathWorld. I promise it won't be nearly so intense.)

The key to aligning an object to a curve is generating the orthogonal axes to which the object will be aligned. That's not as nearly as difficult as it sounds. One of the wonderful properties of matrix mathematics is that the rows of a 3x3 rotation matrix are in fact the orthogonal basis vectors for the space described by the matrix. In this How-To we apply this in reverse - calculating the basis vectors and then using them to populate a rotation matrix.

(This would be a good opportunity to the plug the matrixPlayground MEL script.)

Tangent

The first axis of interest is the tangent for the curve. Webster's defines "tangent" as:

"A line that is tangent; specifically : a straight line that is the limiting position of a secant of a curve through a fixed point and a variable point on the curve as the variable point approaches the fixed point."

Hmm.. perhaps an illustration would be better.

Tangent
Tangent

The tangent for a curve is easily queried with the "pointOnCurve" command:

float $tangent[3] = `pointOnCurve -ch off -parameter 0.0 -tangent curve1`;
// Result: 3.359381 1.5 -11.471752 //

Normal

The other two vectors are the normal and binormal. These two vectors lie at right-angles to the tangent, and to each other, and form the orthogonal basis vectors for our alignment.

The normal can also be queried with the "pointOnCurve" command:

float $normal[3] = `pointOnCurve -ch off -parameter 0.0 -normal curve1`;
// Result: -1.706138 48.378846 5.826198 //

Maya also promises a "normalized" query for this vector (that is, returned as a unit vector). However, this is unfortunately not always the case.

float $normal[3] = `pointOnCurve -ch off -parameter 0.0 -normalizedNormal curve1`;
// Result: -0.000349918 0.00992218 0.00119492 //

Do not rely on Maya's normalized result - always normalize them yourself.

Binormal

The binormal is not available as a query from "pointOnCurve"; however, once you have the tangent and normal it is simply a matter of performing a cross product - using the "cross" command - to generate the binormal. Note that you'll need to represent the tangent and normal as a (vector) to use the "cross" command.

vector $tan  = `pointOnCurve -ch off -parameter 0.0 -tangent curve1`;
// Result: <<3.359381, 1.5, -11.471752>>  //

vector $norm = `pointOnCurve -ch off -parameter 0.0 -normal curve1`;
// Result: <<-1.706138, 48.378846, 5.826198>>  //

// Normalize the vectors queried from "pointOnCurve".
//
$tan = `unit $tan`;
// Result: <<0.27885, 0.12451, -0.952229>>  //
$norm = `unit $norm`;
// Result: <<-0.0349918, 0.992218, 0.119492>>  //

// Calculate the binormal.
//
vector $bi = `cross $tan $norm`;
// Result: <<0.959697, 0.0, 0.281037>> //

Five Easy Steps

With the math in hand, let's put it to use. In five easy steps, this is how we'll perform the alignment:

  1. Query the position of the parameter point along the curve.
  2. Query the tangent and normal for the parameter point.
  3. Calculate the orthogonal binormal to the tangent and normal.
  4. Assemble these into a transformation matrix.
  5. Assign this matrix to the object.

First, let's make a curve to demonstrate:

global proc string be_make_curve()
{
  string $curve = `curve -d 3 -p -72.883868 0.0 -12.81907 
                              -p -50.487995 10.0 -89.297418 
                              -p -5.696248 50.0 -242.254114 
                              -p 15.842993 25.0 26.335076 
                              -p 33.010327 50.0 136.913809 
                              -p 112.657308 10.0 71.004563 
                              -p 140.283868 -25.0 -34.099569 
                              -p 154.097148 50.0 -86.651634 
                              -k 0 -k 0 -k 0 
                              -k 1 -k 2 -k 3 -k 4 
                              -k 5 -k 5 -k 5`;

  return $curve;
}

// Make the curve.
//
string $curve = be_make_curve();

Now plot a bunch of locators along the curve. The Z axes for these locators will be aligned to the curve's tangent, and their X axes will be aligned to the curve's normal, as returned by the "pointOnCurve" command.

global proc be_plot_locators( string $curve )
{
  float $p[3];
  float $t[3];
  float $n[3];
  vector $tan;
  vector $norm;
  vector $bi;
  string $locator[];
  matrix $m[4][4] = << 1.0, 0.0, 0.0, 0.0;
                       0.0, 1.0, 0.0, 0.0;
                       0.0, 0.0, 1.0, 0.0;
                       0.0, 0.0, 0.0, 1.0 >>;

  float $u;
  float $span = 0.1;
  float $maxU = `getAttr ( $curve + ".maxValue" )`;
  for ( $u = 0.0; $u <= $maxU; $u += $span )
  {
    // Query the position, tangent and normal.
    //
    $p = `pointOnCurve -ch off -pr $u -p $curve`;
    $t = `pointOnCurve -ch off -pr $u -nt $curve`;
    $n = `pointOnCurve -ch off -pr $u -nn $curve`;

    // Translational coordinates in a Maya matrix are always represented
    // in Maya's internal units. Convert position to (cm) units.
    // See MEL How-To #102.
    //
    $p[0] = linearToInternal( $p[0] );
    $p[1] = linearToInternal( $p[1] );
    $p[2] = linearToInternal( $p[2] );

    // Maya promises normalized tangent and normal,
    // but they really aren't.
    //
    $tan  = `unit << $t[0], $t[1], $t[2] >>`;
    $norm = `unit << $n[0], $n[1], $n[2] >>`;
    
    // Calculate the binormal.
    //
    $bi = `cross << ($tan.x),  ($tan.y),  ($tan.z)  >>
                 << ($norm.x), ($norm.y), ($norm.z) >>`;

    // Normalize our vector.
    //
    $bi = `unit $bi`;

    // Create a matrix, using normal for the X axis and 
    // tangent for the Z axis.
    //
    $m = << ($norm.x), ($norm.y), ($norm.z), 0.0;     // X axis
            ($bi.x),   ($bi.y),   ($bi.z),   0.0;     // Y axis
            ($tan.x),  ($tan.y),  ($tan.z),  0.0;     // Z axis
            $p[0],     $p[1],     $p[2],     1.0 >>;  // Position

    // Create a locator and assign its world-space matrix.
    //
    $locator = `spaceLocator`;

    xform -ws -m ($m[0][0]) ($m[0][1]) ($m[0][2]) ($m[0][3]) 
                 ($m[1][0]) ($m[1][1]) ($m[1][2]) ($m[1][3]) 
                 ($m[2][0]) ($m[2][1]) ($m[2][2]) ($m[2][3]) 
                 ($m[3][0]) ($m[3][1]) ($m[3][2]) ($m[3][3]) $locator[0];
  }
}

// Plot some locators.
//
be_plot_locators( $curve );

Preventing "Roll"

When using the tangents and normals returned from the "pointOnCurve" command, you may find that the normals "roll" around the curve. The illustration below shows the orientation for some of the locators plotted from the example above.

Rolling normals
Rolling normals

It is common that this is not the desired behavior. What is necessary, then, is to calculate a normal which better satisfies the desired orientation, rather than simply querying it from the curve. Consider how Maya offers several "up vector" options when animating an object along a motion path - these options constrain the solution in order to produce a more consistent behavior. We'll employ the same technique here.

A popular method is to use the world up-axis as a basis for the desired alignment. To calculate the desired normal you'll need to resort to some trigonometry - specifically, the use of the cross product operator. This allows us to calculate the orthogonal basis - the tangent, normal and binormal - for any point along the curve. Maya will provide the tangent, and we'll figure out the other two.

Below is a variation for plotting the locators. This one allows you to specify an up vector. As before, the locators' Z axes will be aligned to the tangent of the curve. This time, however, the Y axis (the binormal) will favour the $upVector, and the X axis (the normal) will be calculated accordingly.

global proc be_plot_no_roll_locators( string $curve, vector $upVector )
{
  // Ensure a valid up-vector. Use Y-up as default if input is bogus.
  //
  if ( `mag $upVector` < 0.001 ) $upVector = << 0.0, 1.0, 0.0 >>;

  // Ensure up-vector is normalized.
  //
  $upVector = `unit $upVector`;

  float $p[3];
  float $t[3];
  vector $norm;
  vector $tan;
  vector $bi;
  string $locator[];
  matrix $m[4][4] = << 1.0, 0.0, 0.0, 0.0;
                       0.0, 1.0, 0.0, 0.0;
                       0.0, 0.0, 1.0, 0.0;
                       0.0, 0.0, 0.0, 1.0 >>;

  float $u;
  float $span = 0.1;
  float $maxU = `getAttr ( $curve + ".maxValue" )`;
  for ( $u = 0.0; $u <= $maxU; $u += $span )
  {
    // Query the position and tangent.
    //
    $p = `pointOnCurve -ch off -pr $u -p $curve`;
    $t = `pointOnCurve -ch off -pr $u -nt $curve`;

    // Translational coordinates in a Maya matrix are always represented
    // in Maya's internal units. Convert position to (cm) units.
    // See MEL How-To #102.
    //
    $p[0] = linearToInternal( $p[0] );
    $p[1] = linearToInternal( $p[1] );
    $p[2] = linearToInternal( $p[2] );

    // Maya promises normalized tangent,
    // but it really isn't.
    //
    $tan  = `unit << $t[0], $t[1], $t[2] >>`;

    // Calculate a normal using the Y-up vector as the binormal.
    //
    $norm = `cross $tan $upVector`;

    // Calculate the orthogonal binormal.
    //
    $bi = `cross $tan $norm`;
    
    // If the binormal is pointing the wrong way,
    // negate it and the normal.
    //
    if ( `dot $upVector $bi` < 0.0 )
    {
      $bi = -$bi;
      $norm = -$norm;
    }

    // Normalize our vectors.
    //
    $norm = `unit $norm`;
    $bi = `unit $bi`;

    // Create a matrix, using normal for the X axis and 
    // tangent for the Z axis.
    $m = << ($norm.x), ($norm.y), ($norm.z), 0.0;     // X axis
            ($bi.x),   ($bi.y),   ($bi.z),   0.0;     // Y axis
            ($tan.x),  ($tan.y),  ($tan.z),  0.0;     // Z axis
            $p[0],     $p[1],     $p[2],     1.0 >>;  // Position

    // Create a locator and assign its world-space matrix.
    //
    $locator = `spaceLocator`;

    xform -ws -m ($m[0][0]) ($m[0][1]) ($m[0][2]) ($m[0][3]) 
                 ($m[1][0]) ($m[1][1]) ($m[1][2]) ($m[1][3]) 
                 ($m[2][0]) ($m[2][1]) ($m[2][2]) ($m[2][3]) 
                 ($m[3][0]) ($m[3][1]) ($m[3][2]) ($m[3][3]) $locator[0];
  }
}

// You may want to delete all of the previous locators here.

// Specify an up vector.
//  
vector $upVector = << 0.0, 1.0, 0.0 >>;

// Plot new locators, using the "no roll" method.
//
be_plot_no_roll_locators( $curve, $upVector );

And, voila. No roll.

No roll
No roll

Limitation

Try it using the X axis as the up vector:

vector $upVector = << 1.0, 0.0, 0.0 >>;
be_plot_no_roll_locators( $curve, $upVector );

Here you're going to see a roll again. You'll note that the roll occurs at the two points where the curve's tangent is aligned along the X axis - the same as the up-vector. This introduces a problem when calculating the binormal, as you cannot generate an orthogonal basis from two equal vectors. You would need to work around this by either carefully selecting your up-vector, or by adjusting the up-vector slightly when approaching this situation.


Acknowledgements


Related How-To's

19 Feb 2005