🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

I have a problem with skinning -- arms and legs are deattached

Started by
4 comments, last by HandleNull 3 years, 8 months ago

Hi gamedev.net,

Hope you are all well and healthy. I am working on a personal project in C++ with modern OpenGL.

I have already asked this question on r/gamdev, but I am hoping someone here might be able to help.

I have a problem I am stuck on. I am trying to implement skinning, however it is only almost right. I wondered if you could consider the code, maybe you can see where I am going wrong? I have worked on it all summer, and I am getting a little lazy or desperate at the end I suppose, if you could find the error I would be over the moon. Only if it is easy for you to see what I am doing wrong, of course.

The arms are not attached correctly, and the arms and legs are not animating correctly: https://youtu.be/7h24-j8iY9Q

It is really supposed to look like this when running: https://youtu.be/0Pr-ZEbItWU

The code is partially based on the lovely book Hands on Animation Programming: https://github.com/PacktPublishing/Hands-On-Game-Animation-Programming/blob/master/Chapter15/Sample01/Code/GLTFLoader.cpp

I have tested the book's source code, and it animates my model correctly.

animation.cpp is my code: https://gist.github.com/alanhaugen/6056be162cf92fed3cf3b2119caa7e10

The actual skinning is done in void Animation::ApplyTransform.

I am considering developing a matrix class, Mat, it is used for keyframes and the implementation is mat.cpp: https://gist.github.com/alanhaugen/1872f5950c4912f78cf6be0323614f0e

I do the inverse matrix calculation when I load the model. The animations are in their own gltf files. Here's joint.cpp: https://gist.github.com/alanhaugen/5e81a6ed6ce72c58565079a796ef2c0a

I have tried to debug the way the joint-tree is traversed, I have not found it to be incorrect.

It animates models where each joint has only one child each correctly, such as this worm: https://youtu.be/GJhMtyQInEU

When I disable scale and rotation, the arms and legs end up pointing into the air, which is strange: https://youtu.be/-5dh6gaaaIc

I might not be applying rot * scale* position in the right order in mat.cpp, although I have tried all permutations of those operations. I do the calculation in keyframe.cpp: https://gist.github.com/alanhaugen/8e439048a481878b99e5a0a055afec8c

I have looked at the animation matrices in a GPU profiler, but I don't really know what to look for. I could compare them with the animation transforms from the book code, but the shaders are a little different. Here is my GLSL skinning.vert: https://gist.github.com/alanhaugen/128b217786d310d924a9c1d5f05ed435

I hope this isn't too much to ask. If you have any ideas about what could be wrong, I would really appreciate them. Although, I will surely figure it out eventually :)

Kind regards,
Alan

Advertisement

Hello everyone,

I have found a bug. The problem seems to be how I deal with weights.

Here's a fork we made animating correctly: https://youtu.be/gqBIqWDu7bQ

Here's how the current implementation animates it: https://youtu.be/9XC5ekgzwsc

In blender, when the hand splits into fingers, only one of the next joints has weights, you can see the joints are selected in the imgur image and see the individual weights: https://imgur.com/a/j6pSNbj

This seems to work fine in most animation tools. In our implementation, it will only work correctly if we put weights on all the joints leading into the fingers.

I can't immediately see how to fix this problem. I try to set the sum of the weights per vertex to 1: https://gist.github.com/alanhaugen/556b7ce50945f9c8ab543b18cde81283

May I please receive advice from you if you can see what might help?

Alan

Hi,

I'm having the same problem, if you found a solution, could you please share?

Thank you.

I think u have a problem with the final joint matrix concatenation:

check this, the final matrix F of any given joint must convert mesh data (verts, normals, etc…):

  • from bind pose to bone space in the current animated pose (using the joint's offset matrix O)
  • from bone space in the current animated pose to root space (aka mesh space) (using the joint's global matrix G, not local matrix)
  • from this root/mesh space to world space (using the skeletal global inverse matrix Inv)

We can formulate this like so in right-to-left matrix multiplication notation, which means (apply O first then G then Inv):

F = Inv * G * O; 

Now u know this, this means you must find F for each joint. Pseudo-code:

// when u load yr skeleton, compute matrix Inv once only
Inv = math::inverse(skeleton→getrootnode( )→getlocalmatrix( ));

// animate your joints: this means find the local matrix L of each joint
// in the current animation and in the the current time or frame, which means
// calculate the new Scale matrix S, Rotation matrix R and Translation matrix T
// I leave it as an exercise for you to workout S, R, T, but effectively GIVEN THE CURRENT ANIMATION TIME you will need to workout what the current and next animtion keyframes are and interpolate between them using this ANIMATION TIME as a factor between these 2 keyframes. If you don't know how to do this, then respectfully you are not ready to finish off skeletal animation :-)
// But looking at your videos, I think you have got S,R,T right but I could be wrong as I haven't looked at your code.
// Anyway, when u have these 3 matrices then get the joint Local matrix L like so

for (joint : animated joints in current animation) // some joints may not be animated in this animation
  L[joint] = T * R * S; // again in right to left mult notation (scale first then rot then trans)
 

When u have the local L matrix of each joint, you can now compute the global G matrix of each joint. This matrix is obtained by the concatenation of the joint's parent global (not local) matrix to the joint's local matrix. And you start this concatenation by traversing the skeletal tree/hierarchy from the root node and down you go:

// pseudo

// declare it
void calcglobalmatrices(matrix parentG)
{
   G[joint] = parentG * L[joint]; // right to left notation
   
   for (joint_child ; joint)
   		calcglobalmatrices(G[joint]);
}

// call it
calcglobalmatrices(skeleton→getrootnode( )→getlocalmatrix( )) // root local is its global

// some people also call it this way
calcglobalmatrices(math::matrix4(1)); 

The difference in those 2 calls is that this 2nd one can be used in any scene hierarchy where this skeleton is inserted into, whereas the previous one is only to be called on the skeleton itself (if it is not part of a scene or anything...)

Ok, so at this point we have Inv, L, G matrices, so we need O.

Well guess what, usually O matrices come with your 3D editor exporter and this O matrix would be stored with your skeletal joints data. Why? because this matrix O is usually related your bind pose data. This is the pose where your skeleton is not animated (usually seen with arms stretched like on a cross or slightly along the body). So you need to load O[joint] from your 3D editor exported data

// Now that we have everything, we can compute the final F matrix of each joint

for (joint : skeleton)
     F[joint] = Inv * G[joint] * O[joint];

F now has all the joints matrix and can be sent to the vertex shader.

Then u can compute the animated vertex v from joints that influence it, the following snippet shows 4 joints influencing vertex v:

// You need to pass to this shader info about joint's weights and joint indices that influence this vertex v, something like this:

// 4 joint jointindex[0],[1],[2]&[3] influencing the vertex currently in the shader
layout (location = 3) in ivec4 jointindex;

// jointweight.x is joint0's weight on vert v, jointweight.y is joint1's weight on vert v, etc...
layout (location = 4) in vec4 jointweight;

....

tmp = vert_v;
animated_vert_v  = (F[ jointindex[0] influencing vert_v] * tmp) * jointweight.x; 
animated_vert_v += (F[ jointindex[1] influencing vert_v] * tmp) * jointweight.y; 
animated_vert_v += (F[ jointindex[2] influencing vert_v] * tmp) * jointweight.z;
animated_vert_v += (F[ jointindex[3] influencing vert_v] * tmp) * jointweight.w; 

// then project the animated v with PROJECTION * VIEW * WORLD, you can concat these CPU-side
// PVW = PROJECTION * VIEW * WORLD
gl_Position = PVW * animated_vert_v;

// do the same for normal
etc...

That's it. All the best.

Ok so I have made an effort to present it in a simple and easy to understand way, but when u code this, it may become quite involved, so don't try to do all at once make sure you that each section is correct in your code before u get to “That's it” ?

Hi @ddlox,

Following your code, I found the problem (and again, thanks for your help, your post helped me a lot to understand about the blend skinning process ... =))

The engine that I'm using, is something like a 'handler', where I need to handle old models to a new Model system (the old uses int values, while the new uses float).

In the conversion process, some values are rounded, and because of this, I needed to rewrite the load model to fix that.

Another thing is that the old system already computes the vertices with the inverse matrix of only one bone (since the skinning before isn't blended) and save it to the file.

Now, I've been added the inverse matrix in the Animation code, removed the inverse before and it's working fine.

This topic is closed to new replies.

Advertisement