Skeletal animation joint transformation question

Started by
7 comments, last by pondwater 12 years, 2 months ago
So i've been reading up on skinned/skeleton proceedural animation, using primarily the book 'Game Engine Architecture'. Im attempting to build my joint and skeleton system. No skinning as of yet, so for debugging purposes, im going to render the actual skeletons instead of the skin mesh.

The basics of my Joint class is as follows, using the SQT format for its transformations (personally im more confortable with transformation matrices, but i heard quaternions make interpolating between key frames easier...):


class Joint {

int parentIndex; // root joint is -1
Quaternion rotation;
Vector translation;
float scale; // using uniform scaling

};



Now using the term 'bind' pose as the original shape of the skeleton prior to any movement.

My question is, what stores the origin and orientation of a specific joint in the bind pose? Is this held in the SQT? I understand the the transformations are hierarchical and relative to the joints parent.

So does the translation vector represents the joints origin in relation to that of its parent? It's rotation quaternion representing its orientation relative to that of its parent?

I just want to make sure im understanding this all correctly...

So for example, in 2D, lets say:

- root joint A was centered on (0,0) in model space, therefore the models origin.
- A's child joint B has a translation vector (2,-2),
- B's child joint C has a translation vector (0,-2).

Would C's origin model space therefore be (2,-4)?

Thanks for any clarification
Advertisement
Your file will contain a list of bones, the bone's primary feature will be its transform. This is often specified in object space. All the bones are all in the same space. You may need to translate your data to this format.

So that you have this

Struct Bone
{
int parentIndex;
Matrix objSpace;
Matrix invObjSpace;
Matrix parentRel;
};


objSpace = This is the bones absolute orientation in the file.
invObjSpace = Inverse of above.
currentBone.parentRel = ParentBone( currentBone ).invObjSpace * currentBone.objSpace;

These are stored in order from such that all parents occur in the array before children.

You can then compute their animated object transforms including a parent relative delta like so:

vector<Matrix> output( bones.size( ) );
for( int i = 0; i < bones.size( ); ++i )
output[ i ] = output[ bones[ i ].parentIndex ] * bones[ i ].parentRel * boneDeltas[ i ];


You will need to work out how to store the root transform so that .parentIndex does not access the output vector out of bounds.

Skinning:
You can then run the skinning procedure with the invObjSpace transforms. Pass this inverse skinned data to the shader, the animated output vector transforms, and the vertices will be transformed back into their animated skinned position. You can modify this procedure in any way. It's just a big chain of matrix multiplications.
First of all, thank you very much for your reply!

Im much more comfortable with transform matrices vs a rotation quaternion, translation vector, and scale vector/scalar. Is there any advantage, aside possibly using less memory, to using the SQT format instead of a matrix? I've heard gimbol lock can be avoided by using quaternions... is this true?

I assume when you are refering to object space and when i refer to model space, we are refering to the same thing, that is, the entire model/objects local space?

Also, a quick question about the parentRel transfrom matrix. Your notation: ParentBone( currentBone ), is that refering to the parentBone of the currentBone?

so i can think of it as: currentBone.parentRel = parentOfCurrentBone.invObjSpace * currentBone.objSpace; ?

EDIT: How does one represent the inverse of an affine transformtion in the SQT form?
i wasn't sure what SQT meant at first.. but now i get it. Scale Quaternion Translation? We normally call it PRS, for position rotation scale (which makes no suggestion of the storage types.)

Anyways. I would not store the bone data i mentioned in PRS format. I would only store the actual animation keyframe data in PRS, since it is much easier to interpolate than a fully composed matrix. It is completely impossible to avoid gimbal lock. Matrices are likely your best defense against gimbal lock for storing absolute tranforms. Incremental transformations, and affine transformations are where it will cause problems.

Object space = model space, yes.

Also, correct on the parentRel thing. The parent access needs to be checked so that first bone, who's parent is -1, does not access bad memory. But your pseudo code is correct.

The inverse of PRS, or TQS would be 1/S 1/R 1/P, or 1/S 1/Q 1/T. (ie. inverted affine transformations in the opposite order)
How you store a 1/S 1/R 1/P into a PRS likely requires composing a full matrix and decomposing it again.

Proof: P * R * S * 1/S * 1/R * 1/P = identity;

To answer your initial question about which part of PRS would store the parent relative offset. Well in the parent relative transform, that's be the P part.

struct PRS
{
Vec3 P;
Quat R;
Vec3 S:
PRS( const Matrix& m )
{
P = m.Translation( );
R = m.Rotation( );
S = m.Scale( );
}
Matrix ToMatrix( ) const
{
return Matix( P ) * Matrix( R ) * Matrix( S );
}
PRS ToInverse( ) const
{
return PRS( Matrix( 1/S ) * Matrix( 1/R ) * Matrix( 1/P ) );
}
};


You can easily lerp this structure to determine the animated bone delta, then use the ToMatrix method to use it in your final bone pallet procedure.
[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

Scale Quaternion Translation? We normally call it PRS, [/quote]

[/font]
[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

SRT or TRS depending on your world view (although SRT is the most common).

[/font]


The inverse of PRS, or TQS would be 1/S 1/R 1/P, or 1/S 1/Q 1/T. (ie. inverted affine transformations in the opposite order)
How you store a 1/S 1/R 1/P into a PRS likely requires composing a full matrix and decomposing it again.


Nonsense. The inverse of a translation of +2 units in X, is not +0.5 units in X.

[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

EDIT: How does one represent the inverse of an affine transformtion in the SQT form? [/quote]

[/font]
[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

R' = conjugate( R )

[/font]
[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

T' = -T
S' = 1/S

[/font]

[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

[/font][color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

Anyways. I would not store the bone data i mentioned in PRS format.

[/font][color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

[/quote]

[/font]
[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

That's ok so long as you have no plans to support ragdolls/ik/etc. The only advantage matrices have over quats is that they are quicker when transforming a large set of vertices/normals. In every other case, quats are the easier representation to work with.

It is completely impossible to avoid gimbal lock.[/quote]

[/font]
If that was true, robots would not exist.

[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

[/font][color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

Matrices are likely your best defense against gimbal lock for storing absolute tranforms.

[/font][color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

[/quote]
They are 'a' defence, just not a very good one! (Quats are better!)

[/font]

I've programmed robots for many years, and there is inevitably a case where gimbal lock will be an issue, no matter how much you prepare for them.

How are quaternions better than matrices? Both store a single absolute orientation relative to their parent.

Also, we have custom ragdoll and it hardly uses TRS. The physics doesn't spit out TRS, so that seems like it would require extra work.

My bad on the 1/T thing. I was trying to choose a symbol that would be uniform across the board. So that the matrix proof line would look good.

They must be applied in the reverse order correct?
Because a rotate 1 plus translate (2,0), inverted is not rotate -1 plus translate (-2,0).


[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

EDIT: How does one represent the inverse of an affine transformtion in the SQT form?

[/font]


[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

R' = conjugate( R )[/font]



[color=#282828][font=helvetica, arial, verdana, tahoma, sans-serif]

T' = -T
S' = 1/S[/font]


[/quote]

This is not correct. If you have S = UniformScaleMatrixBy(s), R = RotationMatrix (orthonormal), T = TranslationMatrixBy(x,y,z), and you have M = T*R*S, then M^(-1)=T'*R'*S', where S' = UniformScaleMatrixBy(1/s), R' = R^t (transpose), and T' = -R^t*(x,y,z). R needs to be transposed, not conjugated (conjugation refers to taking the complex conjugates of each element), and in the inverse, the translation part is not -T.

For code, see float3x4::InverseOrthogonalUniformScale or float3x4::InverseOrthonormal in MathGeoLib. (coincidentally, I very recently reported about the exact same bug, i.e. mistaking (-x,-y,-z) for -R^t*(x,y,z) as the translation part in a library called Cinder, see the comments there for more info and the derivation of the math)

I've programmed robots for many years, and there is inevitably a case where gimbal lock will be an issue, no matter how much you prepare for them.

How are quaternions better than matrices? Both store a single absolute orientation relative to their parent.


People recommend switching from matrices to quats, or from euler angles to quats to avoid gimbal lock. This is unfortunately so wrong, because it gives a misleading impression that somehow the way the rotation is represented is what would cause the gimbal lock, but it's not.

It is the more subtle effect of having to change your logic when moving from euler angles to quats, which will help you avoid gimbal lock (to some extent, depending on what you're doing).

In the most common manner, rotations are prone to gimbal lock whenever you are operating on a sequence of three (or more) rotations R1 * R2 * R3 each constrained to happen around a fixed axis. In this logic, it can occur that the rotation performed by R2 aligns the axes of R1 and R3 so that the perform their rotations about the same axis. This causes one degree of freedom to be lost, which is the issue we all have observed.

Incidentally, Euler angles use scalar triplets a,b,c (e.g. for yaw, pitch and roll) and generate rotation matrices R_a * R _b * R_c to combine to the final rotation. This is exactly like shown above, and will be prone to gimbal lock. Switching to using a quaternion (or a matrix) instead of the euler triplets allows you to avoid gimbal lock, not due to a change in your data storage, but by forcing you to change your program logic, i.e. instead of several subsequent rotations about cardinal axes, you will only store a single full orientation, and operate on that single orientation instead.

Note that if you insisted on doing the Euler->Quat conversion poorly, and did something like

Quat yawRotation = Quat::RotateAxisAngle(WorldAxisX, yawRotation);
Quat pitchRotation = Quat::RotateAxisAngle(WorldAxisY, pitchRotation);
Quat rollRotation = Quat::RotateAxisAngle(WorldAxisZ, rollRotation);
Quat finalRotation = yawRotation * pitchRotation * rollRotation;

One might think "it's all quaternions, no matrices or euler angles, so no gimbal lock!", but that's wrong. The code is running a rotation logic exactly like shown above: a sequence of rotations constrained about fixed axes, and there are at least three of those -> prone to gimbal lock.

So, it's not the representation which itself avoids the gimbal lock, but the way your representation will let you run a different logic which avoids gimbal lock.

PS. In the field of robotics where you have a sequence of several 1DoF joints on a robot arm, you will always have to live with gimbal lock, since you can't change the physical world by changing the code from eulers/matrices to quats and switching your program logic - the joints will always be 1DoF.
Thank you all once again for the responses! I think i am beginning to understand... but i have a couple questions regarding how the transformations are done between keyframes. Here's an update example of (simplified) code that i could potentially use:


class Joint {

int parentIndex;
Matrix bindTransform;
Matrix inverseBindTransform;
Matrix parentTransform // parentTransform = parentOfCurrent.inverseBindTransform * bindTransform;
}

class Skeleton {

std::vector<Joint> joints;
std::vector<Matrix> jointTransforms; //jointTransforms = jointTransforms[joints.parentIndex] * joints.parentTransform * delta;
}

class JointPose {

Quaternion rotation;
Vector translation;
float scale;
Matrix getMatrix();
}

class SkeletonPose {

std::vector<JointPose> jointPoseArray;
float poseTime;
}


EDIT: alright so i implemented the code above for static skeletons, and it seems to be working. I instantiate my skeleton by giving the joint coords in model space, and render them using the jointTransform vector. Everything renders in the correct space.

Now i'm curious about applying transform deltas to interpolate the animation between keyframes.

Should my JointPose class hold the absolute transform from one keyframe to another? or hold the absolute transform from the bind pose to the keyframe?

This topic is closed to new replies.

Advertisement