Animating ASSIMP models

Started by
9 comments, last by uglybdavis 10 years, 11 months ago

I'm using open asset importer to try and animate a model (Trying CPU animation).

While the file is being parsed, i build up a skeleton:


for (int i = 0; i < m_vSkeleton.size(); ++i) {
        Joint& joint = m_vSkeleton;
        
        if (joint.Parent() == -1) {
            joint.Working() = BuildMatrix(joint.Rotation(), joint.Translation());
        } else {
            joint.Working() = BuildMatrix(joint.Rotation(), joint.Translation()) * m_vSkeleton[joint.Parent()].Working();
        }
    }

At this point if i render the skeleton it displays fine. I then load the translation and rotation frames


for (int i = 0; i < scene->mNumAnimations; ++i) {
        aiAnimation* animation = scene->mAnimations;
        for (int j = 0; j < animation->mNumChannels; ++j) {
            aiNodeAnim* channel = animation->mChannels[j];
            Joint& joint = m_vSkeleton[skeletonMap[channel->mNodeName.data]];
            
            for (int k = 0; k < channel->mNumRotationKeys; ++k) {
                aiQuatKey* rotationKey = &channel->mRotationKeys[k];
                joint.RotationTrack().push_back(RotationFrame());
                RotationFrame& frame = joint.RotationTrack()[joint.RotationTrack().size() - 1];
                frame.time = float(rotationKey->mTime);
                frame.rotation = Quaternion(rotationKey->mValue.w, rotationKey->mValue.x, rotationKey->mValue.y, rotationKey->mValue.z);
            } // k
            for (int k = 0; k < channel->mNumPositionKeys; ++k) {
                aiVectorKey* positionKey = &(channel->mPositionKeys[k]);
                joint.TranslationTrack().push_back(TranslationFrame());
                TranslationFrame& frame = joint.TranslationTrack()[joint.TranslationTrack().size() - 1];
                frame.time = float(positionKey->mTime);
                frame.translation = Vector(positionKey->mValue.x, positionKey->mValue.y, positionKey->mValue.z);
                frame.translation.w = 1.0f;
                
           } // k
        } // j
        totalDuration += float(animation->mDuration);
        numFrames += float(animation->mDuration * animation->mTicksPerSecond);
        playbackSpeed += float(animation->mTicksPerSecond);
    } // i

Finally, i animate the skeleton:


void Animation::ArticulateSkeletonToClip(float deltaTime) {    
    if (m_nPlayback > m_pCurrentAnimation->m_nDuration) {
        if (m_pCurrentAnimation->m_nPlaybackMode == AnimationClip::kPlaybackLoop) {
            m_nPlayback -= m_pCurrentAnimation->m_nDuration;
        }
    } else m_nPlayback += deltaTime;
    m_nPlayback = 0.0f;
    
    for (int i = 0; i < m_vSkeleton.size(); ++i) {
        Joint& joint = m_vSkeleton;
        
        if (joint.TranslationTrack().size() == 0 && joint.RotationTrack().size() == 0)
            continue;
        
        Matrix localTransform = BuildMatrix(joint.Rotation(), joint.Translation());

        Vector animatedTranslation; Quaternion animatedRotation;
        // Find the translation frame number
        int frame = m_pCurrentAnimation->m_nStartFrame;
        while (frame < m_pCurrentAnimation->m_nEndFrame &&
               joint.TranslationTrack()[frame].time < m_nPlayback) ++frame;
        if (frame > m_pCurrentAnimation->m_nEndFrame) frame = m_pCurrentAnimation->m_nEndFrame;
        
        // Interpolate translation
        if (frame == m_pCurrentAnimation->m_nStartFrame) { // No prev frame
            animatedTranslation = joint.TranslationTrack()[m_pCurrentAnimation->m_nStartFrame].translation;
        } else if (frame == m_pCurrentAnimation->m_nEndFrame) { // No "this" frame
            animatedTranslation = joint.TranslationTrack()[frame - 1].translation;
        } else  { // Do the actual interpolation
            TranslationFrame& curFrame = joint.TranslationTrack()[frame];
            TranslationFrame& prevFrame = joint.TranslationTrack()[frame - 1];
            
            float timeDelta = curFrame.time - prevFrame.time;
            float interpValue = (float)((m_nPlayback - prevFrame.time) / timeDelta);
            
            animatedTranslation = Lerp(prevFrame.translation, curFrame.translation, interpValue);
        }
        
        // Find the rotation frame
        frame = 0;
        while (frame < m_pCurrentAnimation->m_nEndFrame &&
               joint.RotationTrack()[frame].time < m_nPlayback) ++frame;
        if (frame > m_pCurrentAnimation->m_nEndFrame) frame = m_pCurrentAnimation->m_nEndFrame;
        
        // Interpolate rotation
        if (frame == m_pCurrentAnimation->m_nStartFrame) { // No prev frame
            animatedRotation = joint.RotationTrack()[m_pCurrentAnimation->m_nStartFrame].rotation;
        } else  if (frame == m_pCurrentAnimation->m_nEndFrame) { // No "this" frame
            animatedRotation = joint.RotationTrack()[frame - 1].rotation;
        } else { // Do actual interpolation
            RotationFrame& curFrame  = joint.RotationTrack()[frame];
            RotationFrame& prevFrame = joint.RotationTrack()[frame - 1];
            
            float timeDelta = curFrame.time - prevFrame.time;
            float interpValue = (float)((m_nPlayback - prevFrame.time) / timeDelta);
            
            animatedRotation = Slerp(prevFrame.rotation, curFrame.rotation, interpValue);
        }
        
        // Animate the bone in local space
        Matrix animationTransform = BuildMatrix(animatedRotation, animatedTranslation);
        localTransform = animationTransform * localTransform;
        
        if (joint.Parent() == -1) {
            joint.Working() = localTransform;
        } else {
            joint.Working() = localTransform * m_vSkeleton[joint.Parent()].Working();
        }
    }
}

However, rendering at this point the skeleton looks way wrong. I think the rotation / translation tracks are not working the way i think they are. Maybe they are the absolute world position, or something like that? I'm having a super hard time finding any information on the topic. Does anyone have any experience with this?

Advertisement
  1. Are you sure wxyz is the right order? Usually it's xyzw.
    
    Quaternion(rotationKey->mValue.w, rotationKey->mValue.x, rotationKey->mValue.y, rotationKey->mValue.z);
  2. Have you tried to set deltaTime = 0, so that it doesn't actually do any interpolation. Do you get correct results?
  3. It seems you're using loop instead of recursion, are you sure parent's transform gets evaluated before children's?
  4. Do you multiply matrices in correct order?
  5. Are you sure it's not an issue with sending matrices to your shader?

1, yes, the constructor is w, x, y, z

2, my delta is currently always at 0 and i getthe wrong results

3, i built the hierarchy in a manner that the parrent of each child always comes first to avoid recursion

4, yes i multiply the matrices in the correct order (left handed column major, conversion from assimps right handed matrices is accounted for)

5, in order to simplify this, there are no shaders. Just glEnableClientSide to draw the data

I went ahead and made a raw milkshape3d animator with my math library that uses roughly the same interpolation steps and things render correctly.

I'm trying to load the same milkshape file using assimp, and during the import step the keyframe tracks have wildly different values. This is where i assume the problem is, i just can't find it. Don't focus on the milkshape part, i only did that because i could code it up quickly for a test, the only difference between the two sets of data i've loaded seems to be those damn tracks. Not sure what to do to get the assimp version to behave.

Might have something to do with the offset matrix i'm not accounting for....

To elaborate on that, from the position track, these are the last few values my milkshape loader reports


Translation(64579): -1.66472e-07, 4.81555e-07, -1.35772e-07
Translation(64580): 1.13734e-07, 1.59688e-07, -1.30933e-07
Translation(64581): -2.85417e-08, 1.75542e-07, -8.92287e-08
Translation(64582): 1.25417e-07, 3.07335e-07, -1.47357e-07
Translation(64583): -5.89408e-08, -1.21651e-07, -7.68669e-08

And this is what assimp has


2.00228, 1.83964, -9.1567e-08
2.00228, 1.83964, -8.67287e-08
2.00228, 1.83964, -4.50241e-08
2.00228, 1.83964, -1.03153e-07
2.00228, 1.83964, -3.26623e-08

Those x and y values look rather suspicious....

Edit, from what sparse documentation i could find it looks like the rotation and translations are somehow not in joint local space... Which is what i assumed they where. Can anyone confirm this? Also, how would i move them into joint local space, multiplying by the ivnerse of the offset matrix does not do it :(

Dont know if anyone cares, i noticed in the source of as simp tht the rotation / translation frames are already multiplied by the joints local transform


if ((*it).rotFrames.size()) {
				nd->mRotationKeys = new aiQuatKey[(*it).rotFrames.size()];
				for(std::vector<TempKeyFrame>::const_iterator rot = (*it).rotFrames.begin(); rot != (*it).rotFrames.end(); ++rot) {
					aiQuatKey& q = nd->mRotationKeys[nd->mNumRotationKeys++];

					q.mTime = (*rot).time*animfps;

					// XXX it seems our matrix&quaternion code has faults in its conversion routines --
					// aiQuaternion(x,y,z) seems to besomething different as quat(matrix.fromeuler(x,y,z)).
					q.mValue = aiQuaternion(aiMatrix3x3(aiMatrix4x4().FromEulerAnglesXYZ((*rot).value)*
						aiMatrix4x4().FromEulerAnglesXYZ((*it).rotation)).Transpose());
				}
			}

			if ((*it).posFrames.size()) {
				nd->mPositionKeys = new aiVectorKey[(*it).posFrames.size()];

				aiQuatKey* qu = nd->mRotationKeys;
				for(std::vector<TempKeyFrame>::const_iterator pos = (*it).posFrames.begin(); pos != (*it).posFrames.end(); ++pos,++qu) {
					aiVectorKey& v = nd->mPositionKeys[nd->mNumPositionKeys++];

					v.mTime = (*pos).time*animfps;
					v.mValue = (*it).position + (*pos).value;
				}
			}

So i try to account for that in my code with:


for (int i = 0; i < scene->mNumAnimations; ++i) {
        aiAnimation* animation = scene->mAnimations;
        for (int j = 0; j < animation->mNumChannels; ++j) {
            aiNodeAnim* channel = animation->mChannels[j];
            Joint& joint = m_vSkeleton[skeletonMap[channel->mNodeName.data]];
            
            for (int k = 0; k < channel->mNumRotationKeys; ++k) {
                aiQuatKey* rotationKey = &channel->mRotationKeys[k];
                joint.RotationTrack().push_back(RotationFrame());
                RotationFrame& frame = joint.RotationTrack()[joint.RotationTrack().size() - 1];
                frame.time = float(rotationKey->mTime);
                frame.rotation = Quaternion(rotationKey->mValue.w, rotationKey->mValue.x, rotationKey->mValue.y, rotationKey->mValue.z);
                
                frame.rotation *= joint.Rotation().Inverse();
            } // k
            for (int k = 0; k < channel->mNumPositionKeys; ++k) {
                aiVectorKey* positionKey = &(channel->mPositionKeys[k]);
                joint.TranslationTrack().push_back(TranslationFrame());
                TranslationFrame& frame = joint.TranslationTrack()[joint.TranslationTrack().size() - 1];
                frame.time = float(positionKey->mTime);
                frame.translation = Vector(positionKey->mValue.x, positionKey->mValue.y, positionKey->mValue.z);
                frame.translation.w = 1.0f;
                
                frame.translation -= joint.Translation();
           } // k
        } // j
        totalDuration += float(animation->mDuration);
        numFrames += float(animation->mDuration * animation->mTicksPerSecond);
        playbackSpeed += float(animation->mTicksPerSecond);
    } // i

The skeleton is now recognizable, beit still wrong. But looks like this is the right path.

Well, closer still! The current iteration of the code:


for (int k = 0; k < channel->mNumRotationKeys; ++k) {
                aiQuatKey* rotationKey = &channel->mRotationKeys[k];
                joint.RotationTrack().push_back(RotationFrame());
                RotationFrame& rFrame = joint.RotationTrack()[joint.RotationTrack().size() - 1];
                rFrame.time = float(rotationKey->mTime);
                rFrame.rotation = Quaternion(rotationKey->mValue.w, rotationKey->mValue.x, rotationKey->mValue.y, rotationKey->mValue.z);
                
                aiVectorKey* positionKey = &(channel->mPositionKeys[k]);
                joint.TranslationTrack().push_back(TranslationFrame());
                TranslationFrame& tFrame = joint.TranslationTrack()[joint.TranslationTrack().size() - 1];
                tFrame.time = float(positionKey->mTime);
                tFrame.translation = Vector(positionKey->mValue.x, positionKey->mValue.y, positionKey->mValue.z);
                
                Matrix frame = BuildMatrix(rFrame.rotation.Inverse(), tFrame.translation);
                frame = frame * localJoint.Inverse();
                
                rFrame.rotation.FromMatrix(frame);
                tFrame.translation = Vector(frame.f3, frame.f7, frame.f11);
                tFrame.translation.w = 1.0f;
                
                if (k == channel->mNumRotationKeys - 1)
                    cout << "\nTrans: " << tFrame.translation.x << ", " << tFrame.translation.y << ", " << tFrame.translation.z << "\n";
            }

Works pretty well. The animation appears on screen, and the world is good as it should be. It is still however swimming. Almost like as simp put an extra joint in there... (an extra joint does render) i'm gonna try to null it out.

The Assimp documentation says that the data from the animation tracks replaces the node's local transformation. So yes, the animation data is "premultiplied" with the joint transformation. My suggestion is to not transform the animation data into local joint space.

----------
Gonna try that "Indie" stuff I keep hearing about. Let's start with Splatter.

I tried to compare to my code and I couldn't find where you use offset matrix.

My bone matrices are calculated like this:


transform = parent.transform * Matrix(interpolated_rotation, interpolated_translate) * offset;

@schrompf

I spent all day coming to that conclusion. And i just glanced at the docs again, NO IDEA how i missed that. I feel like a complete retard.

I'm planning to use assimp as a "converter" to a custom format, which does not have pre-multiplied tracks.

The displaying of animation bit is just to make sure i'm loading everything into my data structures correctly

@Zaoshi Kaba

I have two vertex buffers, a "data" buffer and a "scratch" buffer.

At the beginning of the program i take the data buffer and fill it with the skin position of every vertex.

At this point it can be used to render a static mesh, the scratch buffer is empty.

Then the first time an animation is attached to the mesh i multiply every vertex in the data buffer by the offset matrix.

From now on the data buffer is never rendered, only the scratch buffer is.

Every frame i update the skeleton, and multiply the the data buffer by the world position of every joint INTO the scratch buffer

Then i render the scratch buffer

The format i'm planning to export to does not store the premultiplied offset, but rather relies on the user to figure that out at load time; so the matrix is not in any of my data structures.

Like i said, the animations are playing correctly now, there just seems to be some swimming going on. Nothing a few more man-hours shouldn't be able to fix.

In case anyone is wondering what kind of ass-backwards format i'm trying to export to, it's for extra credit for a class ;) Pretty sure the format was made up to make my life hell.

But yeah, if anyone has any ideas about the swimming before i waste another three hours please let me know.

It looks like it has something to do with the data still, i'm pretty sure that i've correctly removed the joint transform from the animation tracks.

ALMOST THERE!

So, this works! Correct animation is playing, no swimming, looking good.


for (int i = 0; i < scene->mNumAnimations; ++i) {
        aiAnimation* animation = scene->mAnimations;
        for (int j = 0; j < animation->mNumChannels; ++j) {
            aiNodeAnim* channel = animation->mChannels[j];
            Joint& joint = m_vSkeleton[skeletonMap[channel->mNodeName.data]];
            Matrix localJoint = BuildMatrix(joint.Rotation(), joint.Translation());
            Matrix invLocal = localJoint.Inverse();
            Quaternion invQuat; invQuat.FromMatrix(invLocal);
            Quaternion localRot; localRot.FromMatrix(localJoint);
            
            for (int k = 0; k < channel->mNumRotationKeys; ++k) {
                aiQuatKey* rotationKey = &channel->mRotationKeys[k];
                joint.RotationTrack().push_back(RotationFrame());
                RotationFrame& frame = joint.RotationTrack()[joint.RotationTrack().size() - 1];
                frame.time = float(rotationKey->mTime);
                frame.rotation = Quaternion(rotationKey->mValue.w, rotationKey->mValue.x, rotationKey->mValue.y, rotationKey->mValue.z);
                
                frame.rotation = frame.rotation.Inverse() * invQuat;
            } // k
            for (int k = 0; k < channel->mNumPositionKeys; ++k) {
                aiVectorKey* positionKey = &(channel->mPositionKeys[k]);
                joint.TranslationTrack().push_back(TranslationFrame());
                TranslationFrame& frame = joint.TranslationTrack()[joint.TranslationTrack().size() - 1];
                frame.time = float(positionKey->mTime);
                frame.translation = Vector(positionKey->mValue.x, positionKey->mValue.y, positionKey->mValue.z);
                frame.translation.w = 1.0f;
                
                frame.translation = invLocal * frame.translation;
                frame.translation = localRot.ToMatrix() * frame.translation;

            } // k
        } // j
        totalDuration += float(animation->mDuration);
        numFrames += float(animation->mDuration * animation->mTicksPerSecond);
        playbackSpeed += float(animation->mTicksPerSecond);
    } // i

Problem is, there is a crazy ammount of jitter. Anyone know a better way to go about this? Seems like the issue spawns somewhere in the rotation code....

This topic is closed to new replies.

Advertisement