• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
uglybdavis

Animating ASSIMP models

10 posts in this topic

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[i];
        
        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[i];
        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[i];
        
        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?

0

Share this post


Link to post
Share on other sites
  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

Share this post


Link to post
Share on other sites

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....

Edited by uglybdavis
0

Share this post


Link to post
Share on other sites

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 :(

Edited by uglybdavis
0

Share this post


Link to post
Share on other sites

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[i];
        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.

0

Share this post


Link to post
Share on other sites

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.

0

Share this post


Link to post
Share on other sites

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.

1

Share this post


Link to post
Share on other sites

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;
1

Share this post


Link to post
Share on other sites

@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.

Edited by uglybdavis
0

Share this post


Link to post
Share on other sites

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[i];
        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....

Edited by uglybdavis
0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0