I'm writing an exporter, but I can't figure out how to covert the key and tangent values from Maya into the equivalent Hermite or Bezier curves to use in my own code. I've studied the documentation for the MFnAnimCurve class, but something isn't adding up. Has anyone successfully taken key and tangent data out of Maya and used it in another package/program?
I've included a MEL script below which creates a simple scene (a null animated on translateY with keys on frames 1, 10, and 20), and then tries to solve the animation curve for frame 16. Tangents are unweighted. The code follows the procedures laid out in the documentation, but the values just don't match up. So what is missing in the code (and documentation) that allows one to solve the curves exactly.
I know that tangent values vary based on the current UI units (both linear and fps), but none of the UI values match the calculated values. I've tried to do these calcuations both through the API and through MEL, but I thought releasing a MEL sample would be more general and easier to test.
I've searched for other threads on this topic, but all the responses simply refer the original poster to the documentation for MFnAnimCurve. Well, I've implemented the docs and it still doesn't work. Anyone know what's wrong with these equations?
Thanks for any and all help,
Michael Duffy
(can't find any attachment commands, so just cut the following text, put it in a file called TestTangents.mel, and then run it. Results are printed out to the MELScript window.)
CODE
//------------------------------------------------------------------------------
// This sample code is used to test the equations used to solve animation
// curves in Maya. Run TestTangents () in order to create the
// objects in Maya and to run the Test_SolveCurve code that tries to calculate
// the value of the animation curve at a given frame (frame 16 by default)
// according to the documentation in the Maya API for MFnAnimCurve
//
// The results differ between the documentation and the actual Maya program.
// According to the equations in the documentation, the value for frame
// 16 should be 2.192126032, but Maya calculates it as 2.4799998404 .
//
// What needs to be done to the calculations to make the script
// calculated value match the Maya calculated value?
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
global proc TestTangents ()
{
// create a new scene
file -f -new;
// create an object
group -em -n "null";
// set the animation range
playbackOptions -min 1 -ast 1 -max 20 -aet 20;
// set some keyframes. Make sure default settings of in and out tangents are spline,
// and weighted tangents are turned off.
setKeyframe -t 1 -v 0 "null.ty";
setKeyframe -t 10 -v 5 "null.ty";
setKeyframe -t 20 -v 0 "null.ty";
// Decide which frame to test. Must be between keyframes 1 and 2 (value 10 - 20)
int $KeyToTest = 16;
// now run the test
Test_SolveCurve ($KeyToTest);
// go to the tested frame so we can look at the value
currentTime $KeyToTest;
select "null";
};
//------------------------------------------------------------------------------
global proc Test_SolveCurve (int $KeyToTest)
{
// frame 16 gives a value of 2.48. Let's try to reproduce this from just
// the key values and tangents.
// first query and display our in and out tangent values.
float $afInTanX [] = keyTangent -q -ix null.ty
;
float $afInTanY [] = keyTangent -q -iy null.ty
;
float $afOutTanX [] = keyTangent -q -ox null.ty
;
float $afOutTanY [] = keyTangent -q -oy null.ty
;
print ("--ix---\n"); print ($afInTanX);
print ("--iy---\n"); print ($afInTanY);
print ("--ox---\n"); print ($afOutTanX);
print ("--oy---\n"); print ($afOutTanY);
// Frame 16 lies between keys 1 and 2 (0-based indexing). For the cubic
// curve solving, we know that the X axis is time, and the Y axis is
// value. P1 and P4 are the endpoints of the cubic segment, and
// either the bezier points of P2 and P3 are used to solve the
// cubic equation, or normals N1 and N4 are used in the case of
// hermite curves.
//
// P1 = (10, 5)
// P4 = (20, 0)
float $P1X = 10;
float $afP1Value [] = keyframe -t $P1X -q -eval "null.ty"
;
float $P1Y = $afP1Value [0];
float $P4X = 20;
float $afP4Value [] = keyframe -t $P4X -q -eval "null.ty"
;
float $P4Y = $afP4Value [0];
// From the above keyTangent calls, we can get our normals
//
// N1 = (1, 0)
// N4 = (0.000833, -1)
float $N1X = $afOutTanX [1];
float $N1Y = $afOutTanY [1];
float $N4X = $afInTanX [2];
float $N4Y = $afInTanY [2];
print "\n";
print ("P1 (" + $P1X + "," + $P1Y + ")\n");
print ("P4 (" + $P4X + "," + $P4Y + ")\n");
print ("N1 (" + $N1X + "," + $N1Y + ")\n");
print ("N4 (" + $N4X + "," + $N4Y + ")\n");
// The MFnAnimCurve docs say the normals are vectors stored as N1 = 3*(P2-P1)
// and N4 = 3*(P4-P3). From this we can solve for P2 and P3.
// N1=3(P2-P1) --> P2 = (N1 + 3P1) / 3
// N4=3(P4-P3) --> P3 = (3P4 - N4) / 3
float $P2X = ($N1X + 3.0 * $P1X) / 3.0;
float $P2Y = ($N1Y + 3.0 * $P1Y) / 3.0;
float $P3X = (3.0 * $P4X - $N4X) / 3.0;
float $P3Y = (3.0 * $P4Y - $N4Y) / 3.0;
print ("P2 (" + $P2X + "," + $P2Y + ")\n");
print ("P3 (" + $P3X + "," + $P3Y + ")\n");
print "\n";
// The next step to solving the equation is to find out where frame 16 falls.
// To accomplish this the MFnAnimCurve docs say you solve F(u) for X (time)
// and then plug that into F(u) to find Y (value)
//
// F(u) = [ u^3 u^2 u 1 ] * B * | P1 | , 0 <= u <= 1
// | P2 |
// | P3 |
// | P4 |)
//
// where B is the Bezier Basis matrix | -1 3 -3 1 |
// | 3 -6 3 0 |
// | -3 3 0 0 |
// | 1 0 0 0 |
// we will use piecewise approximation to solve for 'u'. Basically the
// question is, what 'u' value to we plug into the Bezier basis matrix
// in order for it to kick out the X that equals frame 16.
float $LeftU = 0.0;
float $RightU = 1.0;
float $TargetX = $KeyToTest; // frame 16
float $MidpointU;
int $iCurrStep;
float $LeftX = SolveBezierMatrix ($LeftU, $P1X, $P2X, $P3X, $P4X); // should be 10
float $RightX = SolveBezierMatrix ($RightU, $P1X, $P2X, $P3X, $P4X); // should be 20
print ("LeftX should be 10 : " + $LeftX + "\n");
print ("RightX should be 20 : " + $RightX + "\n");
// 10 steps of recursion should get us close enough.
for ($iCurrStep = 0; $iCurrStep < 10; ++$iCurrStep)
{
$MidpointU = ($LeftU + $RightU) / 2.0;
float $MidpointX = SolveBezierMatrix ($MidpointU, $P1X, $P2X, $P3X, $P4X);
if ($MidpointX > $TargetX)
{
// midpoint is to the right of our target
$RightX = $MidpointX;
$RightU = $MidpointU;
}
else
{
// midpoint is to the left of our target
$LeftX = $MidpointX;
$LeftU = $MidpointU;
};
};
// Our right and left U values are close enough around the target U that
// we can now LERP the values and our margin of error should be very small.
float $T = ($TargetX - $LeftX) / ($RightX - $LeftX);
float $TargetU = $LeftU + (($RightU - $LeftU) * $T);
float $ActualX = SolveBezierMatrix ($TargetU, $P1X, $P2X, $P3X, $P4X);
// we have the U value for frame 16 now.
print ("U value for frame " + $KeyToTest + " is " + $TargetU + " (Actual X value " + $ActualX + ")\n");
// we plug this U value into the bezier equation for Y, and we should get the
// value of the curve at frame 16
float $FinalY = SolveBezierMatrix ($TargetU, $P1Y, $P2Y, $P3Y, $P4Y);
float $fActualY = getAttr -t $KeyToTest "null.ty"
;
print ("The curve at frame " + $KeyToTest + " evaluates to " + $FinalY + " but Maya calculates a value of " + $fActualY + "\n");
};
//------------------------------------------------------------------------------
global proc float SolveBezierMatrix (float $U,
float $P1,
float $P2,
float $P3,
float $P4)
{
float $U2 = $U * $U;
float $U3 = $U2 * $U;
// B is the Bezier Basis matrix | -1 3 -3 1 |
// | 3 -6 3 0 |
// | -3 3 0 0 |
// | 1 0 0 0 |
float $H1 = (-1.0 * $U3) + ( 3.0 * $U2) + (-3.0 * $U) + 1.0;
float $H2 = ( 3.0 * $U3) + (-6.0 * $U2) + ( 3.0 * $U) + 0.0;
float $H3 = (-3.0 * $U3) + ( 3.0 * $U2) + ( 0.0 * $U) + 0.0;
float $H4 = ( 1.0 * $U3) + ( 0.0 * $U2) + ( 0.0 * $U) + 0.0;
return (($P1 * $H1) +
($P2 * $H2) +
($P3 * $H3) +
($P4 * $H4));
};