MEL How-To #108

Back · Previous · Next Maya

How do I plot equidistant points along a curve?

"arcLengthDimension" Node

The key here is to use an arcLengthDimension node to measure the distance along the curve.

The first thing is to create a curve that will be used during the course of this experiment.

// Our unsuspecting curve.
//
string $curve = `curve -d 3 -p -3.311535524 0.01465542598  0.01668581601
                            -p -3.066300435 0.72711707080 -0.29778268700
                            -p -2.575830259 2.15204036000 -0.92671969290
                            -p  1.845868694 2.12236523700 -0.94456269070
                            -p  2.788845691 0.71280207790 -0.33237598680
                            -p  3.258764000 0.00803332170 -0.02627731900
                            -k 0 -k 0 -k 0 -k 1 -k 2 -k 3 -k 3 -k 3`;

Now find the length of the entire curve using an "arcLengthDimension" node. This is achieved by specifying the U parameter for the end of the curve and querying the distance to that point.

// Determine the U range for this curve.
//
float $maxU = `getAttr ( $curve + ".maxValue" )`;
// Result: 3.0 //

// Create an arcLengthDimension node, attached downstream of this curve.
//
string $arcLD = `arcLengthDimension ( $curve + ".u[" + $maxU + "]" )`;
// Result: arcLengthDimensionShape1  //

// Query the distance to the U parameter.
//
float $arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 8.6 //

Unfortunately, there's no equivalent "specify a distance to a point along a curve and query the U parameter." This is where the work is involved.

Walkthrough

In order to find the U parameter at a particular distance (without a convenient means to query it directly), continually divide the curve (or a segment of the curve) in half, measuring at the midpoint of each subdivision. If the point lay before the desired distance, subdivide the lesser half of the curve segment and continue. If the point lay past the desired distance subdivide the greater half. When a point is found that is close enough to the target, stop the traversal. It's rather like walking an infinitely large red-black tree. However, rather than committing to find a match, the traversal stops when the measured value is within an arbitrary tolerance.

For this example we'll plot 10 equidistant points along the curve generated above. If the total length for the curve is 8.6cm then each of the 10 equidistant points will be 0.9556cm apart - this assuming that two points lie at the start (U = 0.0) and end (U = 3.0, in this example) of the curve.

Plotting the first point is easy: U = 0.0, and distance = 0.0.

// Get the world-space location for the edit point at U = 0.0.
//
float $xyz[3] = `pointOnCurve -pr 0.0 -p $curve`;

// Plot a locator there.
//
spaceLocator -p $xyz[0] $xyz[1] $xyz[2];

The next point will be 0.9557cm from the curve's head. The first pass divides the curve in half and checks the distance at the curve's halfway point (U = 1.5, in this example).

setAttr ( $arcLD + ".uParamValue" ) 1.5;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 4.099855 //

Much too far from the desired distance. Split in half between 0.0 and 1.5, and test at 0.75.

setAttr ( $arcLD + ".uParamValue" ) 0.75;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 1.802793 //

Continue...

setAttr ( $arcLD + ".uParamValue" ) 0.375;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 0.909892 //

OK, so our point is between U = 0.375 and U = 0.75.

setAttr ( $arcLD + ".uParamValue" ) 0.5625;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 1.354677 //

And now between U = 0.375 and 0.5625.

setAttr ( $arcLD + ".uParamValue" ) 0.46875;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 1.132847 //

And so on.. you get the idea. Stop when $arcLength is close enough to the target distance.

// Within 0.001cm is close enough.
//
float $epsilon = 0.001;

if ( abs( $arcLength - $distance ) < $epsilon )
{
  // Found point. Do something with it.
}

For our example curve, a satisfactory close result was found at U = 0.3940429688.

setAttr ( $arcLD + ".uParamValue" ) 0.3940429688;
$arcLength = `getAttr ( $arcLD + ".arcLength" )`;
// Result: 0.955322 //

// Accurate within +/- 1/1000 of a centimeter.
//
print ( abs( 0.955322 - 0.9557 ) );
// Result: 0.000378 // 

You may think that this method would be slow, but it's not too bad. The distance above was isolated in only 11 passes.

The Final Solution

Here's what you really came for: The MEL script that puts it all together in one convenient package:

proc float findParamAtDistance( string $curve, 
                                string $arcLD, 
                                float $distance, 
                                float $epsilon )
{
  float $u = 0.0;
  
  float $min = `getAttr ( $curve + ".minValue" )`;
  float $max = `getAttr ( $curve + ".maxValue" )`;

  setAttr ( $arcLD + ".uParamValue" ) $max;
  float $arcLength = `getAttr ( $arcLD + ".arcLength" )`;
  
  // Don't bother doing any work for the start or end of the curve.
  //
  if ( $distance <= 0.0 ) return 0.0;
  if ( $distance >= $arcLength ) return $max;

  // This is merely a diagnostic to measure the number of passes required to 
  // find any particular point. You may be surprised that the number of 
  // passes is typically quite low.
  //
  int $pass = 1;

  while ( true )
  {
    $u = ( $min + $max ) / 2.0;
    setAttr ( $arcLD + ".uParamValue" ) $u;
    $arcLength = `getAttr ( $arcLD + ".arcLength" )`;
    if ( abs( $arcLength - $distance ) < $epsilon ) break;
    if ( $arcLength > $distance ) $max = $u;
    else $min = $u;
    $pass++;
  }
  
  return $u;
}

proc string plotLocator( string $curve, float $uParam )
{
  float $p[3] = `pointOnCurve -pr $uParam -p $curve`;
  string $locator[] = `spaceLocator -p $p[0] $p[1] $p[2]`;
  
  return $locator[0];
}

global proc plotEquidistantLocatorsOnCurve( string $curve, int $count )
{
  // Even two is kind of silly, really.
  //
  if ( $count < 2 ) error( "Must plot at least two equidistant locators." );
  
  // Determine the U range for this curve.
  //
  float $maxU = `getAttr ( $curve + ".maxValue" )`;

  // Create an arcLengthDimension node for the curve.
  //
  string $arcLD = `arcLengthDimension ( $curve + ".u[" + $maxU + "]" )`;

  // Find the overall length of the curve.
  //
  float $arcLength = `getAttr ( $arcLD + ".arcLength" )`;
  
  // Span of each plot is ( $arcLength / ( $count - 1 ) )
  //
  float $span = $arcLength / ( $count - 1 );
  
  // First plot is at 0.0.
  //
  plotLocator( $curve, 0.0 );
  
  // Fill in the middle.
  //
  float $epsilon = 0.0001;
  int $i;
  for ( $i = 1; $i < ( $count - 1 ); $i++ )
  {
    float $distance = $span * $i;
    float $uParam = findParamAtDistance( $curve, $arcLD, $distance, $epsilon );
    plotLocator( $curve, $uParam );
  }

  // Final plot is at $maxU.
  //
  plotLocator( $curve, $maxU );
  
  // Delete the arcLengthDimensionNode (by its transform).
  //
  delete `listRelatives -fullPath -parent $arcLD`;
}
Example usage:
plotEquidistantLocatorsOnCurve( "curve1", 10 );

Related How-To's

20 Feb 2005