Question about rotation sequence exported by ColladaMax

Hi guys,
I implemented a Collada dae importer for my engine, and after a carefully test with the dae files provided by Collada DOM SDK, I thought the importer is correct. But when I test
the importer with data exported by ColladaMax, I found rotation sequence generated by ColladaMax is not consistent with the original 3DS Max rotation sequence, for example:

I create a bone node with keyframe animation, it’s a simple rotation about Y axis(Y_UP),
the animation source is:
<source id=“Bone01-node-ry_Bone01-node_RotY.ANGLE-output”>
<float_array id=“Bone01-node-ry_Bone01-node_RotY.ANGLE-output-array” count=“31”>-90.971 -90.8312 -90.4245 -89.7699 -88.8866 -87.7935 -86.5098 -85.0545
-83.4466 -81.7053 -79.8497 -77.8987 -75.8714 -73.7869 -71.6643 -69.5227 -67.381
-65.2584 -63.1739 -61.1467 -59.1957 -57.34 -55.5987 -53.9909 -52.5355 -51.2518
-50.1588 -49.2754 -48.6208 -48.2141 -48.0743</float_array>
<technique_common>
<accessor source="#Bone01-node-ry_Bone01-node_RotY.ANGLE-output-array" count=“31” stride=“1”>
<param name=“ANGLE” type=“float”/>
</accessor>
</technique_common>
</source>

the node is:
<node id=“Bone01-node” name=“Bone01” type=“JOINT”>
<translate>0 0 0</translate>

    &lt;rotate sid="RotZ"&gt;0 0 1 -180&lt;/rotate&gt;
    &lt;rotate sid="RotY"&gt;0 1 0 -89.029&lt;/rotate&gt;
    &lt;rotate sid="RotX"&gt;1 0 0 -90.0001&lt;/rotate&gt;

    
    &lt;!--&lt;rotate sid="RotZ"&gt;0 0 1 0.0&lt;/rotate&gt;
    &lt;rotate sid="RotY"&gt;0 1 0 -90.971&lt;/rotate&gt;
    &lt;rotate sid="RotX"&gt;1 0 0 90.0&lt;/rotate&gt;--&gt;
    
  &lt;/node&gt;

Note that the rotate channel “RotY” is the only channel with keyframe animation, now the point is, if I use the animation source data directly, the result is not correct.
Instead, if I recompute the animation data by using -(180 + animation source data), then the result is correct. But there is no extra information indicate that
“-(180 + animation source data)” is a general solution.

That appears to be a winding problem; afaik COLLADA is per-default a right-handed system (“RHS”) whilst i don’t know about 3Ds, but here teh data seems to originate from a left-handed system (“LHS”).

Let’s approximate the angles of “~” +/- 90° with “=” +/-90°.
Let “ex”, “ey”, “ez” denote the canonical unit vectors.
Let e.g. “v:X*90->w” denote “vector v yields vector w after a 90° rotation around the x-axis”

RHS : +ex:Z*-180->-ex:Y*-90->-ez:X*-90->-ey
LHS : +ex:Z0->+ex:Y-90->+ez:X*90->-ey

RHS : +ey:Z*-180->-ey:Y*-90->-ey:X*-90->+ez
LHS : +ey:Z0->+ey:Y-90->+ey:X*90->+ez

RHS : +ez:Z*-180->+ez:Y*-90->-ex:X*-90->-ex
LHS : +ez:Z0->+ez:Y-90->-ex:X*90->-ex

Thus, both rotation sequences describe the same operation, but mapped to their respect coord-systems.

Best regards,

“D”

P.S.: Btw, your term “-(180 + animation source data)” would not yield the correct result
for the x angle mapping:

-(180° + -90°) = -90° is not equal to 90°.

Well, I shouldn’t post before lunch it appears… :wink:

Relying on “windedness” of course yields no fresh insight, but this check is valid:


/*
* rot_checker.cpp
*
* Authors: [email]dungeoneer@freenet.de[/email] (dun)
* Copyright: GPL
*
* Compares two fixed sets of rotations arond the cordinate axes
* using quaternions:
*
*      v'  = qx * (qy * (qz * v * _qz) * _qy) * _qx
*          = (qx * qy * qz) * v * (_qz * _qy * _qx)
*
* Rotation precedence is Z->Y->X
*
* Compilation:
*      g++ rot_checker.cpp -o rot_checker
*
* PLEASE NOTE:
* Neither accuracy nor fitness for any particular purpose
* is expresed nor implied.
*
* Version 1.0: May 29th 2010 (dun)
*
* TODO: Make the datasets a runtime parameter
*/
#include <iostream> // for std::cout, std::endl
#include <boost/math/quaternion.hpp>
#include <cmath>   // for sin(x), cos(x)     

typedef float RealType;
const RealType DEG2RAD = M_PI/180.0;

///////////////////////////////////////////////////////////
// Data Sets
const RealType Collada_set[3] = {-90.0001, -89.029, -180.0};
const RealType ThreeDS_set[3] = { 90.0,    -90.971,    0.0};
///////////////////////////////////////////////////////////

typedef boost::math::quaternion<RealType> QuatType;
typedef float AngleType;

enum AxisType {
    X = 0,
    Y = 1,
    Z = 2
};

QuatType make_quat(const AxisType& axis, const AngleType& angle) {

    QuatType q(cos(angle * DEG2RAD / 2.0), 0.0, 0.0, 0.0);
    RealType* p = reinterpret_cast<RealType*>(&q);
       
    p[(int)axis + 1] = sin(angle * DEG2RAD / 2.0);
    return q;
}

int main() {

    QuatType qCollada(1.0, 0.0, 0.0, 0.0),   qThreeDS(1.0, 0.0, 0.0, 0.0);

    /*
     * Testing the conjugates is redundant due to quaternionic algebra,
     * but added for the sake of instructiveness
     */
    QuatType _qCollada(1.0, 0.0, 0.0, 0.0), _qThreeDS(1.0, 0.0, 0.0, 0.0);

    for (int i = 0; i <= 2; ++i) {
        qThreeDS *= make_quat((AxisType)i, ThreeDS_set[i]);
        qCollada *= make_quat((AxisType)i, Collada_set[i]);
    }
    std::cout << "Combined lhs Quaternion for COLLADA rotations: "
              << qCollada << std::endl;
    std::cout << "Combined lhs Quaternion for 3DS rotations:     "     
              << qThreeDS  << std::endl;

    for (int i = 2; i >= 0; --i) {
        _qThreeDS *= boost::math::conj(make_quat((AxisType)i, ThreeDS_set[i]));
        _qCollada *= boost::math::conj(make_quat((AxisType)i, Collada_set[i]));
    }
    std::cout << "Combined rhs Quaternion for COLLADA rotations: "
              << _qCollada << std::endl;
    std::cout << "Combined rhs Quaternion for 3DS rotations:     "     
              << _qThreeDS  << std::endl;
    return 0;
}


Running this little snippet gives:


#me@localhost: g++ -Wall ./rot_checker.cpp -o ./rot_checker
#me@localhost: ./rot_checker
Combined lhs Quaternion for COLLADA rotations: (0.495746,0.495745,-0.504219,-0.504218)
Combined lhs Quaternion for 3DS rotations:     (0.495745,0.495745,-0.504219,-0.504219)
Combined rhs Quaternion for COLLADA rotations: (0.495746,-0.495745,0.504219,0.504218)
Combined rhs Quaternion for 3DS rotations:     (0.495745,-0.495745,0.504219,0.504219)

Thus, both rotation sequences are equivalent. QED.

Best regards,

D.

Hi Dungeoneer,
Thanks for your reply! I have received an offical email from Feeling software.
It seems that is a bug of ColladaMax, here is what Josh(Director of Marketing of Feeling software) said:
“Thank you for the detailed report; indeed, it appears this is a bug, and I will file it. This seems to be an issue where we use a different representation of the angles, but
the animation was not affected, so the animated result will be wrong.”

In fact, what I mentioned is not a winding problem, Collada supports three type of configuration of right-handed system:
// Y_UP:
// | Y
// |
// | X
// O--------
// /
// Z /
//
// Z_UP:
// | Z
// | /
// | / Y
// |/
// O--------
// X
//
// X_UP:
// | X
// |
// Y |
// -------O
// /
// / Z
//
ColladaMax exporter uses “Z_UP” mode by default, which is consistent with 3DS Max’s coordinate system. To achieve the same goal of an orientation transform, there could be a lot of rotation sequences. For example, a sequence generated by 3DS Max: (1)X 90.0 (2)Y -90.971(3)Z 0.0, is equal to this sequence:(1)X -90.0001 (2)Y -89.029 (3)Z -180, So these two “static part of data” are the same:

<rotate sid=“RotZ”>0 0 1 -180</rotate>
<rotate sid=“RotY”>0 1 0 -89.029</rotate>
<rotate sid=“RotX”>1 0 0 -90.0001</rotate>

<!–<rotate sid=“RotZ”>0 0 1 0.0</rotate>
<rotate sid=“RotY”>0 1 0 -90.971</rotate>
<rotate sid=“RotX”>1 0 0 90.0</rotate>–>

But take a look at the “dynamic part of data”(animation channel source data), keyframe 0 is “-90.971” instead of “-89.029”, just as Josh said:
“This seems to be an issue where we use a different representation of the angles, but the animation was not affected, so the animated result will be wrong.”

Hi jazzboysc!

I) I assume you had not read my second post of today when you filed your reply.
My code snippet showed that both 3-tuples of rotations yield the very same quaternion, thus both ARE equivalent. So there is hardly a bug.

II) Precedence of rotations was not a major issue yet, but from your code it is to be assumed that
Z->Y->X
is the sequence used.

There is also a certain conventional reason for that, because the most commonly used combined rotation matrix, see e.g. http://www.gmat.unsw.edu.au/snap/gps/gps_survey/chap11/1114.htm is based that very sequence, as gathered from the rules for associativity of matrix multiplication.

Of course swapping the order is at disposal of the programmer; but the matrix (or its transpose) for the above order is seen in ~9/10 code samples.

III)

-89.029 + -90.971 = -180.0 

That is hardly coincidence. :wink:

Best regards,

D.

@Moderators: Is there any Tex/Latex installed here? Would be helpful :wink:

[admin: no, just bbcode]

Hi Dungeoneer,
I had read your second post, I agree with your code very much. You didn’t understand what I was trying to say, Maybe because my English is poor, sorry for that. I didn’try to point out that theses two sequences would generate different result. These two are the “static part of source data”, and just as you said, they are perfectly equal.
Now please let me try to explain the problem again…
ColladaMax generates a different angle expression for the static part of data, and they are perfectly equal to the original 3DS Max static part of data. And if I just use this static part, the result is correct.
But if I want to use the dynamic part of data(animation source channel) generated by ColladaMax, then I should fetch the float array of keyframe angles, and substitute an angle back into the static part of data, which means:
for keyframe 0:
The oirginal static part(the result is equal to 3DS Max static part) is:

<rotate sid=“RotZ”>0 0 1 -180</rotate>
<rotate sid=“RotY”>0 1 0 -89.029</rotate>
<rotate sid=“RotX”>1 0 0 -90.0001</rotate>
And it represents the rotation of keyframe 0 of the node.
But after a substitution of float array element 0(which is -90.971), the rotation sequence now is:
<rotate sid=“RotZ”>0 0 1 -180</rotate>
<rotate sid=“RotY”>0 1 0 -90.971</rotate>
<rotate sid=“RotX”>1 0 0 -90.0001</rotate>

You can see that

<rotate sid=“RotZ”>0 0 1 -180</rotate>
<rotate sid=“RotY”>0 1 0 -89.029</rotate>
<rotate sid=“RotX”>1 0 0 -90.0001</rotate>

is not equal to

<rotate sid=“RotZ”>0 0 1 -180</rotate>
<rotate sid=“RotY”>0 1 0 -90.971</rotate>
<rotate sid=“RotX”>1 0 0 -90.0001</rotate>

So I was trying to say that the animation source channel array generated by ColladaMax is not consistent with the rotation sequence generated by it.In fact, the animation source channel is consistent with the original data of 3DS Max, but the rotation sequence is not, after a substitution, the final animated result will be wrong.
If I recompute the animation source data, for example, -(180 + (-90.0001)) = -89.029, then substitute -89.029 back into the rotation sequence, the result is correct, and all the animation source data should be recomputed in this way.

Hi jazzboysc!

I’m in a little hurry, so just in brief:

This

originates from the bind pose of that bone

(I added indentation to the source)

The rotations to be applied for the animation are not generated by just replacing

bind_pose_y <- keyframe_y_angle

but by the formula given
on p. 33, chapter “Skinning equations” of the COLLADA 1.4.1 Specification 2nd Edition March 2008
and on p.38, same chapter heading, of the COLLADA 1.5.0 Specification 2nd Edition April 2008

outv = Sum [from i=0 to i=n] (((v * BSM) * IBMi * JMi)* JW)

The respective part of your COLLADA data which you rightfully labeled “static”, is the “IBMi” matrix.
Thus, what would “happen” is

first “taking back” the bone bind pose rotation to (0.0, 0.0, 0.0):

v' = v'' * IBMi

and then applying your animation data

v''' = v'' * JMi

(v’, v’’ … are meant to denote the temporary vertex-vector after subsequently post-applying the matrices of the “outv”-formula above.)

Thus, because both bind pose matrices are equivalent, as we already achieved consensus upon, applying their inverse would also yield the same, thus subsequently post-applying “JMi” will yield the same vector.

Have a nice weekend!
I’ll be back online here on Wednesday.

Best regards,

D.

pls. scratch:

v' = v'' * IBMi

pls. put:

v'' = v' * IBMi

Hi Dungeoneer,
Thanks for your kindness advice: )
But there is really nothing to do with the <skin> element or Inverse bind matrix source data.
I have read the offical Collada programming book <<COLLADA - Sailing the gulf of 3D digital content creation>> more then three times.
The example I mentioned just contains one <node> element which is animated, and no skinning is applied there, the <node> element could be a bone node, or an instance geometry element. The animation source data generated by ColladaMax remains same for both cases.
Since Feeling software(the vendor of ColladaMax) has sent me an email to indicate that it is a bug of ColladaMax, I think my question has been answered quite well.

So you haven’t yet skinned it?

Have you even had a look at the resulting animation or just the figures?