Advertisement

Animation issue using BVH data in OpenGL

Started by December 02, 2023 04:09 PM
6 comments, last by snoken 11 months ago

I’m currently working on a project involving animation programming using OpenGL and BVH (Biovision Hierarchy) data for animation, and I am trying to get my head around it. I’ve run into some issues and could use some insight.

The problem I’m facing is that when I render my animation, it doesn’t come out as expected. I find the format of BVH quite confusing but this seems correct and it should work but the animation looks quite odd.

I am unsure if, for each rotation you need to multiply it by the parentMatrix? doing this seemed to prevent the legs going up to the up and flicking back down so I assumed it was correct?

BVH file: https://pastebin.com/4w2Qv5TD

void Skeleton::drawJoint(Mat4& viewMatrix, Mat4 parentMatrix, Joint* joint, uint32_t currFrame)
{ 
    
    vec3 offset_from_parent = vec3(
    	joint->jointOffsetX,
    	joint->jointOffsetY,
    	joint->jointOffsetZ
    );
        
    	vec3 anotherRot = vec3(
    	frames[currFrame][BVH_CHANNEL[joint->joint_channel[0]]],
    	frames[currFrame][BVH_CHANNEL[joint->joint_channel[1]]],
    	frames[currFrame][BVH_CHANNEL[joint->joint_channel[2]]]
    );
    
    auto jointPos = parentMatrix * (offset_from_parent);
        // get rotation for current joint for current frame
    auto boneRot =  parentMatrix * bone_rotations[currFrame][joint->id];
    
    // BVH stores rotation as Z,Y,X so, boneRot.x would be rotation for z
    auto z = boneRot.x;
    auto y = boneRot.y;
    auto x = boneRot.z;
    
    //z,y,x multiply like: x,y,z
    auto local = Mat4::Translate(jointPos) * (Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z));
    
    for(auto& child : joint->Children)
    {
        vec3 end = local * vec3(child.joint_offset[0], child.joint_offset[1], child.joint_offset[2]);
    	drawCylinder(viewMatrix, jointPos, end);
    
        drawJoint(viewMatrix, localMatrix, &child, scale, frame);
    }
} 

snoken said:
I am unsure if, for each rotation you need to multiply it by the parentMatrix?

Yes, but you do it very wrong. You store Euler angles in a Vec3, and then you multiply this vector by the parent matrix. But this makes not sense here, since euler angles are neither a point nor a vector.

Instead this:

auto boneRot =  parentMatrix * bone_rotations[currFrame][joint->id];
    
    // BVH stores rotation as Z,Y,X so, boneRot.x would be rotation for z
    auto z = boneRot.x;
    auto y = boneRot.y;
    auto x = boneRot.z;
    
    //z,y,x multiply like: x,y,z
    auto local = Mat4::Translate(jointPos) * (Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z));

You need something like:

auto local = Mat4::Translate(jointPos) * (Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z)); // local transfrom of the bone relative to the parent
Mat4 global = parentMatrix * local; // transforming it by the parent we are now in global space

snoken said:
I find the format of BVH quite confusing

Yes. All those animation file formats cause a lot of confusion.

One typical problem is the guessing about Euler angles order. Ideally the file specifies the order.
// BVH stores rotation as Z,Y,X
Maybe this does not mean the memory order of 3 dimensions, but actually Euler angles order, and you got it wrong. Quite likely.
To explain Euler angles order, we get a different result if we rotate x first, then y, finally z, vs. z first, then y and finally x.
So depending on convention of the data, you may need to change order in this line:

auto local = Mat4::Translate(jointPos) * (Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z));

Often you do not even know the order and have to guess it with trial and error. That's just one reason more why Euler angles suck.

Advertisement

Thanks for the insightful post! I am slightly confused you mentioned to do:

JoeJ said:
auto local = Mat4::Translate(jointPos) * (Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z)); // local transfrom of the bone relative to the parent Mat4 global = parentMatrix * localBoneRot; // transforming it by the parent we are now in global space

but the rotations after translations are the joint rotations per-frame, so what would localBoneRot be? or did you mean localBoneRot = Mat4::RotateX(x) * Mat4::RotateY(y) * Mat4::RotateZ(z)?

snoken said:
so what would localBoneRot be?

It was a typo, fixed my snippet above.
It's actually the local orientation (unsure about Euler order), combined with the translation offset of child to parent (unsure if this should be rotated by the local rotation, or if it is defined in parent space).

But as you see, i'm uncertain about everything too. Trial and error is your friend. : )

Thanks for getting back. Honestly been trying trial and error for a week to get this work. Can't seem to figure out where it's going wrong.

snoken said:
Thanks for getting back. Honestly been trying trial and error for a week to get this work. Can't seem to figure out where it's going wrong.

Well, trial and error works, but only for isolated problems. To build up something bigger than that, you can divide the problem into multiple smaller ones, solve one after another, eliminating doubts on the things which already work along the way.

I propose something like this:

Make your own data structure representing a bone, and generate a simple test model. Just 3 bones, resembling upper and lower leg and a foot, or just a snail.
Generate this test data with code. That's the only way to be certain about your data.
Figure out the math so child bones follow parent bones. Ideally understand it well, because such animation stuff is really the ideal example to get familiar with transformation matrices or related alternatives. The most value to learn is here.

Ideally make some GUI where you can edit and observe the bone rotations in real time. That's the best way to make sure it works.

At this point you have the basic functionality needed to play back animated skeletons.
The next step is to import the file, and see if you can get the skeleton rest pose right using your data structure. Your gif shows bone lengths are already right, and just the rotations are somewhat wrong. So you're close to the goal.

At this point you should be able to use your GUI to create funny poses from the imported skeleton, confirming success.
Finally you only need to figure out how to convert file rotation convention into yours. That's a lot easier if you're sure all of the above works.

Now i guess you have done most of this already, but likely not all and it might help.

Advertisement

I feel something is wrong specifically with RotationZ, when I apply this, the arms go all weird and the behaviour changes dramatically

This topic is closed to new replies.

Advertisement