Skeletal animation - I'm almost there!

Started by
5 comments, last by Pilpel 8 years, 11 months ago

So I've been struggling months on this topic (mainly because I don't have much time.. I'm in the army), and now I wrote my system and it looks pretty good yet it's still not working properly.

I'm almost sure that it's something with the matrix calculations.

I use Assimp and opengl.

I used Blender to export this one-boned model to a COLLADA file:

gzd6Z.png

The model has only one bone, called arm bone, that controls the arm mesh. All the other meshes are static.

I made several structures and classes that help me play animations. All the nodes are added to an std::vector of Node objects. each Node contains aiNode data and a toRoot matrix. The bone hierarchy is encapsulated in a Skeleton class, and the animation matrix (T * R) are updated for each bone in a class called Animation.

My Model::Draw() function is this:


void Model::draw()
{
    //iterate through all animation sets. if the animation is running, update the bones it affects.
    for(size_t i = 0; i < _animations.size(); i++)
        if(_animations[i].running())
            _animations[i].updateAnimationMatrices(&_skeleton);

    //calculate Bone::finalMatrix for each bone
    _skeleton.calculateFinalMatrices(_skeleton.rootBone());

    //iterate through the nodes and draw their meshes.
    for(size_t i = 0; i < _nodes.size(); i++)
    {
        if(!_nodes[i].hasBones()) //if the node mesh is static
            _shaderProgram.setUniform("ModelMatrix", _nodes[i].toRoot());
        else
            _shaderProgram.setUniform("ModelMatrix", glm::mat4(1));

        _nodes[i].draw();
    }
}

To get the "animationMatrix" for each bone (the TR matrix) I call Animation::updateAnimationMatrices(). Here's what it looks like:


void Animation::updateAnimationMatrices(Skeleton *_skeleton)
{
    double time = ((double)_timer.elapsed() / 1000.0);
    while(time >= _animation->mDuration) time -= _animation->mDuration;

    //iterate through aiNodeAnim (called channels) and update their corresponding Bone.
    for(unsigned int iChannel = 0; iChannel < _animation->mNumChannels; iChannel++)
    {
        aiNodeAnim *channel = _animation->mChannels[iChannel];
        Bone *bone = _skeleton->getBoneByName(channel->mNodeName.C_Str(), _skeleton->rootBone());

        //rotation
        glm::mat4 R = ... //calculate rotation matrix based on time

        //translation
        glm::mat4 T = ... //calculate translation matrix based on time

        //set animation matrix for the bone
        bone->animationMatrix = T * R;
        bone->needsUpdate = true;
    }
}

Now in order to calculate the "finalMatrix" for each bone (based on animationMatrix, offsetMatrixetc..), and upload it to the vertex shader, I call Skeleton::calculateFinalMatrices().


void Skeleton::calculateFinalMatrices(Bone *root)
{
    if(root)
    {
        Node *node = _getNodeByName(root->name->C_Str()); //get corresponding node of the bone
        if(node == nullptr) {
            std::cout << "could not find corresponding node for bone: " << root->name->C_Str() << "\n";
            return;
        }

        if(root->needsUpdate) //update only the bones that need to be updated (their animationMatrix has been changed)
        {
            root->finalMatrix = node->toRoot() * root->animationMatrix * root->offsetMatrix;

            //upload the bone matrix to the shader. the array is defined as "uniform mat4 Bones[64];"
            {
                std::string str = "Bones[";

                char buf[4] = {0};
                _itoa_s(root->index, buf, 10);

                str += buf;
                str += "]";

                _shaderProgram->setUniform(str.c_str(), root->finalMatrix);
            }

            root->needsUpdate = false;
        }

        for(unsigned int i = 0; i < root->numChildren; i++)
            calculateFinalMatrices(root->children[i]);
    }
}

Here's my bone structure, if it helps.

My glsl vertex shader is pretty standard. Here it is.

And finally, here's the result I get: (ignore the model's static legs. that must be some bug in the Blender exporter).

k5FuD.gif

And here's the result I should get: (using a 3d party software)

YxDaN.gif


It looks like there's something wrong with the bone's matrix calculation, although I don't know what.

Assimp docs clearly state that the offsetMatrix transforms from mesh space to bone space, so in order to transform a vertex from mesh space to "animated world space", I go:


bone->finalMatrix = nodeOfTheBone->toRootMatrix //back to world space
		  * animationMatrix //a TR matrix
		  * offsetMatrix; //mesh space to bone space 

(order of multiplication is bottom line to top line)

Any ideas or tips?

Thanks!!

Advertisement

bone->finalMatrix = nodeOfTheBone->toRootMatrix //back to world space
* animationMatrix //a TR matrix
* offsetMatrix; //mesh space to bone space

I'm sort of hurried at the moment, so I didn't go through your code very thoroughly. However, commonly the animation data includes the return to world space**. I.e., finalMat = animMat * offsetMat. Try getting rid of the toRootMat multiplication. That might explain your result - the arm has been translated from its animated position to a position near the root (assuming your root frame is near the bottom of the legs).

** That "return" to world space results from multiplying each bone's animation matrix by it's parent's animation matrix, and on down through the hierarchy. I see you have only a single bone. If it has no parent, you should be able to simpy examine the matrix you get from animMat * offsetMat. The translation part of that matrix should be very close to the bone's rest post position - i.e., you're already in world space.

FYI, this article describes the process. It's based on DirectX primarily so the multiplication order is reversed from OGL, but the general principles are the same.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

Collada matrices may be in a different coordinates system (like directX). So you may need to apply something like that. I wrote my own custom exporter from Blender to my own format. My final matrix for every animation frame is computed as such:

//inv_translate take the bones world position and moves it to the origin, so that we can perform the bones animation

// then I apply this matrix to deal in openGL coordinates

// invert the bind pose so that I can apply the bones matrix which is described in the bones coordinate system (not in the actual openGL world system)

// Actually I'm getting a little lost, since this code is pretty old and when you first have to write it its a bunch of matrices in different systems. and a lot of debugging.

// At the end though I then transform the bone back to its original position in world space
*(bone->Animation_Frames[j]) = bone_translate*inv_to_GL*(*bone->Animation_Frames[j])*inv_bind_pose*to_GL*inv_bone_translate;

So that may not help but the concept is, take the bone, move it to the origin so you can rotate it around its joint, apply any rotations/translations for the frame, put the bone back into position. The other stuff around that is finding out how to apply the rotations/translations for the frame, because those frames of animation are all relative to the bones bind pose, which is why with no animation the matrix is the identity matrix, and not a matrix to actually place the bone in the world. I also had to wrap a conversion matrix around my code since the frame animations were in Blender/DirectX coordinate systems. My engine is OpenGL. So by putting the conversion matrix around the matrices I want converted, they simply will be in openGL space like the other matrices I have in this equation.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

If you'd like some sample source, here is a small skeletal animation system in my educational engine.

https://github.com/jeske/SimpleScene/tree/skeletal-animation/SimpleScene/Meshes/Skeletal

And here is one of the better articles on how to do it..

http://www.3dgep.com/loading-and-animating-md5-models-with-opengl/

One way to debug this if you still are trying, Make a bone that is aligned to say the x-axis and rotate it 90 degrees on the y-axis. This way you can debug the matrices and see what coordinate systems they are and understanding the bind pose and animation frame matrices etc.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

Here another tutorial with complete source and pipeline.

Maya / 3DSMAX -> Ogre XML -> Renderer

Link ( voxels.blogspot.com )

Thanks. The problem was in Skeleton.cpp:

I gave no respect to the bone hierarchy when calculating finalMatrix. Each bone should transformation must apply after its parent bone transformation has been applied, so I created a new member in Bone called "nodeTransform":


void Skeleton::calculateFinalMatrices(Bone *root)
{
	if(root)
	{
		if(root->needsUpdate)
		{
			root->nodeTransform = ( root->parent ? root->parent->nodeTransform : glm::mat4(1) ) *
								  root->animationMatrix;

			root->finalMatrix = _parentNode->toRoot() *
								root->nodeTransform *
								root->offsetMatrix;

			std::string str = "Bones[";

			char buf[4] = {0};
			_itoa_s(root->index, buf, 10);

			str += buf;
			str += "]";

			_shaderProgram->setUniform(str.c_str(), root->finalMatrix);

			root->needsUpdate = false;
		}

		for(unsigned int i = 0; i < root->numChildren; i++)
			calculateFinalMatrices(root->children[i]);
	}
}

This topic is closed to new replies.

Advertisement