Rotate bone by Local delta to Global Matx

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 16/08/2004 at 15:29, xxxxxxxx wrote:
User Information:
Cinema 4D Version: 8.503
Platform: Windows ;
Language(s) : C++ ;
That's what I need to do. The deltas are local axis rotations, but, of course, bones only like global settings (or they go all wonky with a vengence).I will be thumbing my 3D CG books for an answer, but maybe someone has a quick coding solution to this. The bones are parented AND fixed at this stage.
Sheesh, I haven't had to deal with matrix complexities like this since my own 3D engines. Why is it that I have to deal with them in a 3D app's SDK? ;)
Thanks,
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 17/08/2004 at 11:58, xxxxxxxx wrote:
Here's the code problematic code (three methods). To the point, this is a conversion from Poser JPs to C4D bones.
The first method, SetupBone(), creates the bones in their default position/rotation based on the Origin/Endpoint for each JP'd body part (that's all we get, folks).
The second method, TransformBone() attempts to rotate the bones from the default position into Pose/Base positions, but hasn't worked consistently yet.
The third method, Align() determines a rotation value to put the C4D bone into a known World axis alignment. It also correlates the red, green, blue axes (since they are not identical to x,y,z) to the correct channel sub type (or Poser axis) wrt the C4D bone.
Background: Poser bones (JPs) ALL start with axes coinciding with the World axes (a bone pointing down the +X axis is rotated along its length by changing the X rotation value). All rotations are in degrees and Euler angles. Each axis gets a JP 'function', as above, X would be the 'twist' axis and is thusly denoted twistX in the file. Also, rotation order is significant in Poser  usually twist/joint/joint  and denoted solely by order in the file.
What do I need to know to get this to work? There seems to be an unreconcilable difference between what I see and how I think (since there is nothing whatsoever detailing C4D bones) they should be rotating.
If noone can explain why the bones rotate contrary to weeks of observation, checking, coding, and calculating, then I will be forced to go higher than this forum and contact Maxon directly for information. This is the third time (and I've heard the same from David Mathews) requesting some sort of information on how C4D bones (PROGRAMMATICALLY!  I don't care a twat about how they work in the GUI  the control is completely different) work in order to obtain predictable results without any responses. The complexity that you see below is a result of all attempts to try to resolve this over the past three weeks. I know where the Poser bones are (and they are created/positioned/rotated properly) and I know where the C4D bones are (including exact rotation) before applying deltas.
// Setup Bone void PoserBase::SetupBone(void) { Vector r, p; Matrix m; Vector* aPts; PolygonObject* aObj; // Create axis markers if ((aObj = PolygonObject::Alloc(3,1)) == NULL) throw ErrorException(ERROR_MEMORY, NULL); baseDocument>InsertObject(aObj, boneObject, NULL, FALSE); aPts = aObj>GetPoint(); aPts[0] = Vector(1,0,0); aPts[1] = Vector(0,1,0); aPts[2] = Vector(0,0,1); // Rotate bone r = VectorToHPB(endPoint  origin); boneObject>SetMg(HPBToMatrix(r), FALSE); // Align Bone axes with World axes (X/Y/Z) r = boneObject>GetRot(); r.z = 0.0; boneObject>SetRot(r); m = boneObject>GetMg(); Vector a0 = aPts[0] * m; Vector a1 = aPts[1] * m; Vector a2 = aPts[2] * m; r = MatrixToHPB(m); r.z += AlignBone(a0, a1, a2); // Remove and delete aObj aObj>Remove(); PolygonObject::Free(aObj); // Position bone boneObject>SetMg(MatrixMove(origin) * HPBToMatrix(r) * MatrixScale(Vector(1.0)), FALSE); SetWeights(); boneObject>Message(MSG_UPDATE); } // Transform FIXED bone void PoserBase::TransformBone(void) { Bool ik = FALSE; UCHAR type, subType, i,j,k; Real scaling = tpLoader>GetSettings()>GetObjScaling(); Vector vpos, vrot, vscale, drot, tpos, trot, tscale; Matrix mg, m[3]; KeyFrame* key; Channel* chan; mg = boneObject>GetMg(); vpos = Vector(mg.off.x, mg.off.y, mg.off.z); vrot = MatrixToHPB(mg); vscale = Vector(Len(mg.v1), Len(mg.v2), Len(mg.v3)); //vrot = boneObject>GetRot(); if (ikChain) ik = ikChain>GetState() & ((ikTarget)?TRUE:FALSE); if (ik) { mg = ikTarget>GetMg(); tpos = Vector(mg.off.x, mg.off.y, mg.off.z); trot = MatrixToHPB(mg); tscale = Vector(Len(mg.v1), Len(mg.v2), Len(mg.v3)); } // Extract values from Channel keys for (chan = channel; chan != NULL; chan = (Channel* )chan>GetNext()) { type = chan>GetType(); subType = chan>GetSubType(); // rotation if (type == CHAN_TYPE_ROTATE) { if (key = chan>GetKeyFrame0()) { if (subType == raxis) drot.y = (Rad(key>GetValue()) * rsign); else if (subType == gaxis) drot.x = (Rad(key>GetValue()) * gsign); else if (subType == baxis) drot.z = (Rad(key>GetValue()) * bsign); /* if (subType == raxis) vrot.y += (Rad(key>GetValue()) * rsign); else if (subType == gaxis) vrot.x += (Rad(key>GetValue()) * gsign); else if (subType == baxis) vrot.z = (Rad(key>GetValue()) * bsign); */ } } // scale else if ((type == CHAN_TYPE_SCALE)  (type == CHAN_TYPE_PROPAGATINGSCALE)) { if (key = chan>GetKeyFrame0()) { if (subType == CHAN_SUB_NONE) vscale *= key>GetValue(); else if (subType == raxis) vscale.y *= key>GetValue(); else if (subType == gaxis) vscale.x *= key>GetValue(); else if (subType == baxis) vscale.z *= key>GetValue(); } } // translation else if ((type == CHAN_TYPE_TRANSLATE) && ik) { if (key = chan>GetKeyFrame0()) { if (subType == CHAN_SUB_X) tpos.x += (key>GetValue() * scaling); else if (subType == CHAN_SUB_Y) tpos.y += (key>GetValue() * scaling); else if (subType == CHAN_SUB_Z) tpos.z += (key>GetValue() * scaling); } } } // BoneObject: Apply Channels to Global Matrix if (axis[0]) { subType = (axis[0])>GetSubType(); if (subType == CHAN_SUB_Z) i = 0; else if (subType == CHAN_SUB_Y) j = 0; else k = 0; subType = (axis[1])>GetSubType(); if (subType == CHAN_SUB_Z) i = 1; else if (subType == CHAN_SUB_Y) j = 1; else k = 1; subType = (axis[2])>GetSubType(); if (subType == CHAN_SUB_Z) i = 2; else if (subType == CHAN_SUB_Y) j = 2; else k = 2; } else { i = 0; j = 1; k = 2; } m _= MatrixRotZ((baxis == CHAN_SUB_Z)?drot.z:((gaxis == CHAN_SUB_Z)?drot.x:drot.y)); m[j] = MatrixRotY((baxis == CHAN_SUB_Y)?drot.z:((gaxis == CHAN_SUB_Y)?drot.x:drot.y)); m[k] = MatrixRotX((baxis == CHAN_SUB_X)?drot.z:((gaxis == CHAN_SUB_X)?drot.x:drot.y)); boneObject>SetMg(MatrixMove(vpos) * m[2] * m[1] * m[0] * HPBToMatrix(vrot) * MatrixScale(vscale)); //boneObject>SetRot(vrot); boneObject>Message(MSG_UPDATE); if (ik) { ikTarget>SetMg(MatrixMove(tpos)*HPBToMatrix(trot)*MatrixScale(tscale)); ikTarget>Message(MSG_UPDATE); } } // Determine correction Bank rotation by aPts //  Returns Real correction angle Real PoserBase::AlignBone(Vector a0, Vector a1, Vector a2) { Real comp; CHAR d0, d1; // Determine raxis and gaxis world axis and directions if ((Abs(a0.x) > Abs(a0.y)) && (Abs(a0.x) > Abs(a0.z))) { if (a0.x > 0) d0 = 1; else d0 = 1; } else if ((Abs(a0.y) > Abs(a0.x)) && (Abs(a0.y) > Abs(a0.z))) { if (a0.y > 0) d0 = 2; else d0 = 2; } else if ((Abs(a0.z) > Abs(a0.x)) && (Abs(a0.z) > Abs(a0.y))) { if (a0.z > 0) d0 = 3; else d0 = 3; } if ((Abs(a1.x) > Abs(a1.y)) && (Abs(a1.x) > Abs(a1.z))) { if (a1.x > 0) d1 = 1; else d1 = 1; } else if ((Abs(a1.y) > Abs(a1.x)) && (Abs(a1.y) > Abs(a1.z))) { if (a1.y > 0) d1 = 2; else d1 = 2; } else if ((Abs(a1.z) > Abs(a1.x)) && (Abs(a1.z) > Abs(a1.y))) { if (a1.z > 0) d1 = 3; else d1 = 3; } if ((Abs(a2.x) > Abs(a2.y)) && (Abs(a2.x) > Abs(a2.z))) { if (a2.x >= 0) bsign = 1.0; else bsign = 1.0; } else if ((Abs(a2.y) > Abs(a2.x)) && (Abs(a2.y) > Abs(a2.z))) { if (a2.y >= 0) bsign = 1.0; else bsign = 1.0; } else if ((Abs(a2.z) > Abs(a2.x)) && (Abs(a2.z) > Abs(a2.y))) { if (a2.z >= 0) bsign = 1.0; else bsign = 1.0; } if (d0 > 0) rsign = 1.0; else rsign = 1.0; if (d1 > 0) gsign = 1.0; else gsign = 1.0; /* Red Green (RG:B) Red Green (RG:B) aPt[0] aPt[1] (XY:Z) (JtJt:Twist) aPt[0] aPt[1] (XY:Z) (JtJt:Twist) ========================== ========================== 1,0,0 0,1,0 0 (XY:Z) 0,1,0 1,0,0 90 (XY:Z) 1,0,0 0,0,1 90 (ZX:Y) 0,1,0 0,0,1 180 (YZ:X) 1,0,0 0,1,0 90 (YX:Z) 0,1,0 1,0,0 180 (YX:Z) 1,0,0 0,0,1 0 (XZ:Y) 0,1,0 0,0,1 90 (ZY:X) 1,0,0 0,1,0 90 (YX:Z) 0,0,1 1,0,0 90 (XZ:Y) 1,0,0 0,0,1 180 (XZ:Y) 0,0,1 0,1,0 90 (YZ:X) 1,0,0 0,1,0 180 (XY:Z) 0,0,1 1,0,0 180 (ZX:Y) 1,0,0 0,0,1 90 (ZX:Y) 0,0,1 0,1,0 180 (ZY:X) 0,1,0 1,0,0 0 (YX:Z) 0,0,1 1,0,0 0 (ZX:Y) 0,1,0 0,0,1 90 (ZY:X) 0,0,1 0,1,0 0 (ZY:X) 0,1,0 1,0,0 90 (XY:Z) 0,0,1 1,0,0 90 (XZ:Y) 0,1,0 0,0,1 0 (YZ:X) 0,0,1 0,1,0 90 (YZ:X) */ // Handle compensation // R = +X if (d0 == 1) { // G = +Y if (d1 == 2) { comp = 0.0; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_Z; } // G = +Z else if (d1 == 3) { comp = RAD_90; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Y; } // G = Y else if (d1 == 2) { comp = RAD_90; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Z; } // G = Z else if (d1 == 3) { comp = 0.0; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_Y; } } // R = X else if (d0 == 1) { // G = +Y if (d1 == 2) { comp = RAD_90; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Z; } // G = +Z else if (d1 == 3) { comp = RAD_180; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_Y; } // G = Y else if (d1 == 2) { comp = RAD_180; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_Z; } // G = Z else if (d1 == 3) { comp = RAD_90; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Y; } } // R = +Y else if (d0 == 2) { // G = +X if (d1 == 1) { comp = 0.0; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Z; } // G = +Z else if (d1 == 3) { comp = RAD_90; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_X; } // G = X else if (d1 == 1) { comp = RAD_90; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_Z; } // G = Z else if (d1 == 3) { comp = 0.0; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_X; } } // R = Y else if (d0 == 2) { // G = +X if (d1 == 1) { comp = RAD_90; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_Z; } // G = +Z else if (d1 == 3) { comp = RAD_180; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_X; } // G = X else if (d1 == 1) { comp = RAD_180; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Z; } // G = Z else if (d1 == 3) { comp = RAD_90; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_X; } } // R = +Z else if (d0 == 3) { // G = +X if (d1 == 1) { comp = RAD_90; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_Y; } // G = +Y else if (d1 == 2) { comp = RAD_90; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_X; } // G = X else if (d1 == 1) { comp = RAD_180; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Y; } // G = Y else if (d1 == 2) { comp = RAD_180; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_X; } } // R = Z else if (d0 == 3) { // G = +X if (d1 == 1) { comp = 0.0; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_X; baxis = CHAN_SUB_Y; } // G = +Y else if (d1 == 2) { comp = 0.0; raxis = CHAN_SUB_Z; gaxis = CHAN_SUB_Y; baxis = CHAN_SUB_X; } // G = X else if (d1 == 1) { comp = RAD_90; raxis = CHAN_SUB_X; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_Y; } // G = Y else if (d1 == 2) { comp = RAD_90; raxis = CHAN_SUB_Y; gaxis = CHAN_SUB_Z; baxis = CHAN_SUB_X; } } return comp; }
Thanks for any help in resolving this,
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 17/08/2004 at 16:40, xxxxxxxx wrote:
So, is it true then that the bones are rotating about their parents' systems and not their own even when setting Global rotations?
If true, then how does one add a local delta rotation to a bone using Global matrix? How do you rotate a bone 5d 'X', 10d 'Y', and 10d 'Z' (where XYZ refer to, one supposes, PHB, respectively) using this mess of a system. Does one really need to employ quaternions and EulerMatrixQuaternion conversion methods to rotate a bone (parented and fixed, of course) using the SDK?
Does anyone know how to do this? And could you please explain in detail how (code perfectly acceptable)?
Thanks,
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 18/08/2004 at 00:37, xxxxxxxx wrote:
I don't think there's any difference in how bones rotate compared to other objects. The true data is what's in the local matrix, obj>GetMl(). The rest of the functions are helpers to set and get that data. But essentially there's nothing secret to it, just the local matrix and some math (sorry, the SDK cannot remove all math from 3D programming). That's why there's nothing special in the documentation about bones.
I'd love to help you with the math, though strictly it's outside of the scope of the developer support, especially when it involves a third program. I'm not sure I understand exactly what's wrong, but I'll do my best.
Firstly, I'd try to move away from any function named HPB in C4D. Since C4D and Poser has, as it seems, different ways of doing angle based rotations, that will just make it harder. Instead you should try to build your rotation from smaller rotation matrices, like RotAxisToMatrix() or MatrixRotX().
Secondly, you would have to investigate what order and coordinate system that is used for the XYZ rotations in Poser. For example if the rotation is done XYZ or ZXY, and if the different rotations are relative to each other or to the world. Perhaps you already know this, I couldn't really tell from the code/text. The C4D rotation model is irrelevant at this point, since you'll be using matrices in C4D.
Finally, when you know how Poser does its rotations you can duplicate it by doing each step as a matrix transformation of the bone's local or global matrix in C4D. As for the parenting, it shouldn't really matter if you use global matrices.
Did any of this help? I could be more specific, for example saying that 5dX+10dY10dZ can be written "obj>SetMg(MatrixRotZ(Rad(10))^MatrixRotY(Rad(10))^MatrixRotX(Rad(5))^obj>GetMg())", but there are just too many factors that I don't know about Poser to make an educated guess which such expression is correct.
From what you describe it doesn't sound like quaternions should be necessary, unless they are used in Poser. Euler is nice for the user but, as you've seen, it's very messy for programmers. Thus my recommendation to move from euler to matrices as early as possible in the conversion.

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 18/08/2004 at 21:16, xxxxxxxx wrote:
Alright, I moved away from HPBToMatrix for the deltas, using MatrixRotX/Y/Z and placing them *before* the required HPBToMatrix for initial bone rotations (zero rotation in Poser), and worked out the rotation order required. That gets me *this* close to correct rotations. There are still some tweaks with the rotations and, especially, IK translation influences when there.
Poser just uses ordered axis rotations, but order is important which is difficult to resolve in C4D. Luckily, Poser always rotates along the bone (twist) first and C4D always rotates Z as the bone 'twist'. It was resolving the other two that has led to most of the frustration.
Thanks for bearing with me, Mikael. Now on to great things! :)
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 19/08/2004 at 17:37, xxxxxxxx wrote:
Addendum:
1. Poser rotation is straight forward (as already stated). Ordered rotations and a RightHanded coordinate system. Order and handedness are considered in my code.
2. I'm using MatrixRotX/Y/Z before rotating the bone into initial orientation.
3. Still getting unexpected results. Here's an example of the issue. There are four bones one after the other (parented) all in the same orientation (pointing in the +Y direction). All four have the same rotation order YZX (shown in this synopsis directly from the file) :
actor abdomen:1
{
jointX
jointZ
twistY
rotateY 3.62239
rotateZ 14.8229
rotateX 0.694393
}
actor chest:1
{
jointX
jointZ
twistY
rotateY 3.49671
rotateZ 7.46422
rotateX 11.9492
}
actor neck:1
{
jointX
jointZ
twistY
rotateY 29.456
rotateZ 1.4596
rotateX 0.00706232
}
actor head:1
{
jointX
jointZ
twistY
rotateY 30.2108
rotateZ 0.568758
rotateX 4.93937
}Three of them rotate as expected. Number four (head:1) has the H and P rotations swapped. Why?
There is no significant difference between these and my code sees them identically. This is what is causing me frustration. Remember that all of these bones are being rotated (MatrixRotX/Y/Z) from C4D's default bone orientation (down the +Z axis) and all are oriented afterwards as discussed before (using HPBToMatrix) using SetMg().
I do realize that this could be caused by HPBToMatrix, but there is really no way to avoid it since the initial bone orientation is given by two points (vector). There are no rotations for initial orientation. I could resolve the rotation components (theta = acos(Fx/F)), but we all know that this can lead to ambiguous results in certain situations.
But this is definitely the case. I took the local H and P values (in the Coordinates Manager) for head:1 and swapped them. Voila! Perfect alignment. Those other bones do not work similarly  they are incorrectly rotated if H and P are swapped.
So, again, explain this to me...
Thanks,
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 19/08/2004 at 23:16, xxxxxxxx wrote:
I guess it's the negative Z rotation that causes the behaviour you see. But that's just a symptom. I think you might be correct in suspecting the HPB functions; they are a bit fuzzy. I would suggest avoiding all functions with HPB in the name  don't even look at HPB coordinates in C4D  at least until you have something that works. A "VectorToMatrix()" function can be made by setting v3 first to the Vector and then filling in the rest with crossproducts.

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 19/08/2004 at 23:46, xxxxxxxx wrote:
Unfortunately, I've only used ordered XYZ angles for rotations most recently. I did something of an HPB system over ten years ago on an Amiga and the code is lost to the great bitbucket in the sky (and it was a great struggle then!). :)
Component vector resolution is definitely not working. So I'll try the method you suggest. My main issue is what kind of HPB system C4D uses. I've encountered a variety of rotation systems, but 'HPB' doesn't come up as a regular name (except for LightWave  which is useless). Almost everything else immediately digresses into quaternions! Arghhh!! ;)
Hating to impress further, could you possibly give some code for this VectorToMatrix() method (which is exactly what I've been trying to find or construct). In other words, cross products of what? :)
Thanks,
Robert

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 20/08/2004 at 00:16, xxxxxxxx wrote:
Well, essentially one way is
Matrix VectorToMatrix(const Vector& v) { Matrix m; m.v3 = !v; // Z = Z m.v1 = !(Vector(0,1,0) % m.v3); // X = "Y" % Z m.v2 = !(m.v3 % m.v1); // Y = Z % X return m; }
using the standard crossproduct rules X % Y = Z and X % Y =  Y % X, where you can permute the XYZ cyclic (X > Y > Z > X). This will make sure that the matrix's Y is towards world +Y. However, the weakness is in the dependence on the up direction (0,1,0).
There are different ways around this singularity, like choosing another up direction, but I guess the only interesting way is the exact way that Poser does it. Perhaps you could ask the Poser developer support, if there is such a thing? How would Poser handle a bone that points towards +Y?

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED
On 20/08/2004 at 10:16, xxxxxxxx wrote:
Yes, it's all coming back to me now. ;) The ole DOF situation. It has been a while since I determined the angles of a vector using crossproducts and an 'arbitrary' up vector. Will have to research fixes for the degenerate case (which seems to lean towards quaternions! again...).
As far as I know, Poser doesn't handle rotations well at all  it definitely succumbs to gimbal lock. Remember that Poser doesn't see the bone pointing up, only an axial system aligned with the world axial system. As 'bones' are rotated, rotation order is important (and probably the only saving grace). But there are cases when axes line up and you are stuck with an identical rotation on different axes.
flipCode has some very interesting solutions to this in several threads, but, of course, quaternions are still highly regarded solutions. Might be worth a go at some of the code (remembering matrix differences in handedness  yikes!).
Wish me luck!
Thank you very much,
Robert