4
\$\begingroup\$

I use the XNA Animation Component library to perform blending between animations. It uses spherical linear interpolation between matrices.

In most cases this works without problems. However, I have one model where all of the animations are blended wrong. Weird rotation and warping of the model occurs.

I was told by a math expert that the problem may lie in the method that does the matrix interpolation. He said: "The right way to do it would be to decompose A=UP (polar decomposition), obtain quaternion from U and lerp/slerp it, while interpolating P component-wise. I'm not sure why the original author refrained from implementing the polar decomposition as its implementation is straightforward and easy."

I have included the code below.

I need some help in making the recommended changes. I am an expert in C# but know almost nothing about matrix interpolation.

private static Quaternion qStart, qEnd, qResult;
private static Vector3 curTrans, nextTrans, lerpedTrans;
private static Vector3 curScale, nextScale, lerpedScale;
private static Matrix startRotation, endRotation;
private static Matrix returnMatrix;

public static void SlerpMatrix(
        ref Matrix start, 
        ref Matrix end,
        float slerpAmount,
        out Matrix result)
    {
        if (start == end)
        {
            result = start;
            return;
        }
        // Get rotation components and interpolate (not completely accurate but I don't want 
        // to get into polar decomposition and this seems smooth enough)
        Quaternion.CreateFromRotationMatrix(ref start, out qStart);
        Quaternion.CreateFromRotationMatrix(ref end, out qEnd);
        Quaternion.Lerp(ref qStart, ref qEnd, slerpAmount, out qResult);

        // Get final translation components
        curTrans.X = start.M41;
        curTrans.Y = start.M42;
        curTrans.Z = start.M43;
        nextTrans.X = end.M41;
        nextTrans.Y = end.M42;
        nextTrans.Z = end.M43;
        Vector3.Lerp(ref curTrans, ref nextTrans, slerpAmount, out lerpedTrans);

        // Get final scale component
        Matrix.CreateFromQuaternion(ref qStart, out startRotation);
        Matrix.CreateFromQuaternion(ref qEnd, out endRotation);
        curScale.X = start.M11 - startRotation.M11;
        curScale.Y = start.M22 - startRotation.M22;
        curScale.Z = start.M33 - startRotation.M33;
        nextScale.X = end.M11 - endRotation.M11;
        nextScale.Y = end.M22 - endRotation.M22;
        nextScale.Z = end.M33 - endRotation.M33;
        Vector3.Lerp(ref curScale, ref nextScale, slerpAmount, out lerpedScale);

        // Create the rotation matrix from the slerped quaternions
        Matrix.CreateFromQuaternion(ref qResult, out result);

        // Set the translation
        result.M41 = lerpedTrans.X;
        result.M42 = lerpedTrans.Y;
        result.M43 = lerpedTrans.Z;

        // Add the lerped scale component
        result.M11 += lerpedScale.X;
        result.M22 += lerpedScale.Y;
        result.M33 += lerpedScale.Z;
    }
\$\endgroup\$
2
  • \$\begingroup\$ Have you tried Slerp while in the quaternion form? Works great for my animations. You can see an example in the source code for JMonkey 3D. (It's in Java, but I'm sure you'll get the gist of it for C#) \$\endgroup\$
    – House
    Commented Dec 20, 2012 at 22:51
  • \$\begingroup\$ Also, since it's just one model, you may want to make sure that it's not being exported in a strange way. I had a model that had one of the bones rotation set to Euler instead of Quaternion. All of its rotations were funky as a result. \$\endgroup\$
    – House
    Commented Dec 20, 2012 at 23:08

2 Answers 2

3
\$\begingroup\$

The scale computation looks wrong to me. It should be obtained by dividing, not subtracting (with additional checks for divisions by zero), then it should be applied by multiplying (not adding) each column or each row of the final rotation matrix (not just the diagonal term).

If you're fluent in C# you probably have realised that the code is very poor quality. Reading and writing to static member variables from a static method instead of local variables is a good indication of code smell.

Anyway, here is a much simpler version which makes use of the decomposition methods available in XNA:

public static void SlerpMatrix(
        ref Matrix start, 
        ref Matrix end,
        float slerpAmount,
        out Matrix result)
    {
        Quaternion qStart, qEnd, qResult;
        Vector3 curTrans, nextTrans, lerpedTrans, curScale, nextScale, lerpedScale;

        start.Decompose(out curScale, out qStart, out curTrans); 
        end.Decompose(out nextScale, out qEnd, out nextTrans); 

        Quaternion.Lerp(ref qStart, ref qEnd, slerpAmount, out qResult);
        Vector3.Lerp(ref curTrans, ref nextTrans, slerpAmount, out lerpedTrans);
        Vector3.Lerp(ref curScale, ref nextScale, slerpAmount, out lerpedScale);

        result = Matrix.CreateScale(lerpedScale)
               * Matrix.CreateFromQuaternion(qResult)
               * Matrix.CreateTranslation(lerpedTrans);
    }
\$\endgroup\$
1
  • \$\begingroup\$ This actually worked! All the weird issues went away. Amazing. Thank you so much! \$\endgroup\$
    – Rye bread
    Commented Dec 21, 2012 at 12:03
0
\$\begingroup\$

Hard to tell from just the code, but a common issue with quaternion interpolation is that 2 (antipodal) quaternions represent the same rotation. If matrix -> quaternion ever returns the 'wrong' one in one of the two cases, your lerp is going to put you at 0 which will lead to all sorts of nonsense. Also your scale calc looks bogus as Sam mentioned ;)

\$\endgroup\$
1
  • 1
    \$\begingroup\$ just to add the solution to the problem: in case the dot product of the 2 input quats is less than 0 (on different sides of the hemisphere) just negate all components of one quat, then they are on the same hemisphere again and slerp will take the shorter arc \$\endgroup\$ Commented Dec 21, 2012 at 12:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .