Advertisement

Skinning Animation - Weights Destroy Mesh

Started by June 20, 2020 07:01 PM
4 comments, last by aromans 4 years, 7 months ago

– SOLUTION IS IN BOTTOM OF COMMENTS –

I am in the process of writing an animation system with my own Collada parser and am running into an issue that I can't wrap my head around.

I have collected my mesh/skin information (vertices, normals, jointIds, weights, etc), my skeleton information (joints, their local transforms, inverse bind position, hierarchy structure), and my animation (keyframe transform position for each joint, timestamp).

My issue is that with everything calculated and then implemented in the shader (the summation of weights multiplied by the joint transform and vertex position) - I get the following:

When I remove the weight multiplication, the mesh remains fully intact - however the skin doesn't actually follow the animation. I am at a lost as I feel as though the math is correct, but very obviously I am going wrong somewhere. Would someone be able to shine light on the aspect I have misinterpreted?

Here is my current understanding and implementation:

  • After collecting all of the joint's localTransforms and hierarchy, I calculate their inverse bind transfromation matrix. To do this I multiple each joints localTransform with their parentLocalTransform to get a bindTransform. Inverting that bindTransform results in their inverseBindTransform. Below is my code for that:
	// Recursively collect each Joints InverseBindTransform - 
    // root joint's local position is an identity matrix. 
    // Function is only called once after data collection.
    void Joint::CalcInverseBindTransform(glm::mat4 parentLocalPosition)
    {
        glm::mat4 bindTransform = parentLocalPosition * m_LocalTransform;
        m_InverseBindPoseMatrix = glm::inverse(bindTransform);

        for (Joint child : Children) {
            child.CalcInverseBindTransform(bindTransform);
        }
    }
  • Within my animator during an animation, for each joint I take the two JointTransforms for the two frame's my currentTime is in between and I calculate the interpolated JointTransform. (JointTransform simply has a vec3 for position and quaternion for rotation). I do this for every joint and then apply those interpolated values to each Joint by again recursively muliplying the new frameLocalTransform by their parentLocalTransform. I take that bindTransform and multiply it by the invBindTransform and then transpose the matrix. Below is the code for that:
   std::unordered_map<int, glm::mat4> Animator::InterpolatePoses(float time) {

        std::unordered_map<int, glm::mat4> poses;
        if (IsPlaying()) {

            for (std::pair<int, JointTransform> keyframe : m_PreviousFrame.GetJointKeyFrames()) {

                JointTransform previousFrame = m_PreviousFrame.GetJointKeyFrames()[keyframe.first];
                JointTransform nextFrame = m_NextFrame.GetJointKeyFrames()[keyframe.first];
                JointTransform interpolated = JointTransform::Interpolate(previousFrame, nextFrame, time);
                poses[keyframe.first] = interpolated.getLocalTransform();
            }
        }
        return poses;
    }

    void Animator::ApplyPosesToJoints(std::unordered_map<int, glm::mat4> newPose, Joint* j, glm::mat4 parentTransform) 
    {
        if (IsPlaying()) {

            glm::mat4 currentPose = newPose[j->GetJointId()];
            glm::mat4 modelSpaceJoint = parentTransform * currentPose;

            for (Joint child : j->GetChildren()) {
                ApplyPosesToJoints(newPose, &amp;amp;amp;amp;amp;amp;amp;child, modelSpaceJoint);
            }

            modelSpaceJoint = glm::transpose(j->GetInvBindPosition() * modelSpaceJoint);
            j->SetAnimationTransform(modelSpaceJoint);
        }
    }
  • I then collect all the newly AnimatedTransforms for each joint and send them to the shader:
    void AnimationModel::Render(bool&amp; pass)
    {
                    [...]
        std::vector<glm::mat4> transforms = GetJointTransforms();
        for (int i = 0; i < transforms.size(); ++i) {
            m_Shader->SetMat4f(transforms[i], ("JointTransforms[" + std::to_string(i) + "]").c_str());
        }
                    [...]
    }

    void AnimationModel::AddJointsToArray(Joint current, std::vector<glm::mat4>&amp;amp;amp;amp;amp;amp;amp; matrix)
    {
        glm::mat4 finalMatrix = current.GetAnimatedTransform();
        matrix.push_back(finalMatrix);
        for (Joint child : current.GetChildren()) {
            AddJointsToArray(child, matrix);
        }
    }
  • In the shader, I simply follow the summation formula that can be found all over the web when researchiing this topic:
        for (int i = 0; i < total_weight_amnt; ++i) {
            mat4 jointTransform = JointTransforms[jointIds[i]];

            vec4 newVertexPos = jointTransform * vec4(pos, 1.0);
            total_pos += newVertexPos * weights[i];
                        [...]
  • For calculating the weights - I loop through all preadded weights in the vector, and if I find a weight that is less than the weight I'm looking to add - I replace that weight in that position. Otherwise, I append the weight onto the end of the vector. If there are less weights in my vector than my specified max_weights (which is 4) - I fill in the remaining weights/jointIds with 0.
					[...]
		for (int j = 0; j < weights.size(); ++j) {
			if (weight > weights[j]) {
				jntsAffected[j] = jointIndex;
				weights[j] = weight;
				added_weight = true;
				break;
			}
		}

		if (!added_weight) {
			jntsAffected.push_back(jointIndex);
			weights.push_back(weight);
		}
					[...]
					[...]
		if (weights.size() < max) {
			while (jntsAffected.size() < max) {
				jntsAffected.push_back(0);
				weights.push_back(0.f);
			}
		}

I know this isn't much help, but having a simple mesh may help you reason about things. Like, make a cube with 2 bones in blender and assign weights intuitively (eg 1 face to one bone, the other face to another bone, and one last face to both bones). Then try loading that in your engine and see how it behaves.

My tutorials on youtube: https://www.youtube.com/channel/UC9CQOdT1A9JlAks0-PF5vvw
Latest Tutorials:
A simple and intuitive ray triangle intersection algorithm https://youtu.be/XgUhgSlQvic

Möller Trumbore ray triangle intersection explained visually https://youtu.be/fK1RPmF_zjQ

Setting up OpenAL c++ visual studio https://youtu.be/WvND0djMcfE

Advertisement

@mattstone2728 Thanks for the advice, I think that was very helpful. I did as you advised as well as took a peak at how Assimp imports and uses all the data sent in from a Collada file. I ended up make a rectangular prism with only two bones - and with a few changes to some of my math I ended up with the following image. It seems there are two vertices that are being affected way too much. 

Just an update here for anyone who has issues with this in the future as well - I have gotten a lot closer, and feel a lot more confident in the math. However, there is still something that is being missed and I'm almost curious if its something the Collada parser is giving me that I'm not adding to my formula. I wouldn't of gotten here without rendering the simpler model above, so I greatly appreciate Matt's advice. My current status looks like this:

I'm currently only trying to render my model in Bind Pose before moving onto the animation section to keep things simple.

Collada provides a BindShapeMatrix, a Bind Pose matrix, and the inverse bind matrix which I use to calculate my final InverseBindMatrix to use to go from model space to joint space in bind pose.

this->m_JointSpaceTransform = localTransform;

m_offsetMatrix = invBindTransform * m_SkeletonBindShapeMatrix * bind_matrix;

I then calculate the “GlobalPose” or BindPose for each Joint relative to their parent joint.

glm::mat4 globalTransform = m_JointSpaceTransform * parentLocalPosition;

j->SetGlobalPose(globalTransform);

for (Joint&amp; child : Children) {
	child.CalcInverseBindTransform(globalTransform, &amp;child);
}

return j;

I then calculate the final BindPose matrix in model space by multiplying the offsetmatrix by the global pose.

glm::mat4 finalBindPoseLocation = glm::transpose(j->GetOffsetMatrix() * j->GetGlobalPose());
j->SetAnimationTransform((finalBindPoseLocation));

I'll update when I finally figure out what I am missing to help future googlers.

– SOLUTION –

I understand when something is going wrong in skinning animations, there can be alot of different areas the problem is occuring. As such, for future googlers experiencing the same issue I am - take this as more of a list of suggestions of what you could be doing wrong rather than absolutely doing wrong.

For my problem - I had the right idea but wrong approach in a lot of minor areas. Which brought me fairly close but, as they say, no cigar.

  • I had no need to calculate the Inverse Bind Pose myself, Collada's Inverse Bind Pose (sometimes/often declared as an "offsetMatrix") is more than perfect. This wasn't a problem more as I was just doing unnecessary calculations.
  • In a Collada file, they often provide you more "joints" or "nodes" in the hierarchy than what is needed for the animation. Prior to the start of your actual animated "joints", there is the scene and an initial armature "node" type. The scene is typically an identity matrix that was manipulated based on your "up axis" upon reading in the Collada file. The Node type will determine the overall size of each joint in the skeleton - so if it wasn't resized, its probably the identity matrix. Make sure your hierarchy still contains ALL nodes/joints listed in the hierarchy. I very much was not doing so - which greatly distorted my globalPosition (BindPose).
enter image description here
  • If you are representing your Joint's transforms rotation through quaternions (which is highly recommended), make sure the resulted quaternion is normalized after interpolating between two rotated positions. On the same note - when combining the Rotation and Transform into your final matrix - make sure your order of multiplication and the final output is correct.
  • Finally - your last skinning matrix is comprised of your joints InvBindMatrix * GlobalPosition * GlobalInverseRootTransform (<- this is the inverse of the local transfrom from your "scene" node mentioned in (1), remember?). Based on your prior matrix multiplications up to this point, you may or may not need to transpose this final matrix.

And with that - I was able to successfully animate my model!

One final note - my mesh and animation files are added in separately. If your animations are in separate files from your mesh, make sure you collect the skinning/joint information from the files with an animation rather than the file with the mesh. I list my steps for loading in a model and then giving it multiple animations through different files:

  1. Load in the Mesh (This contains Vertices,Normals,TexCoords,JointIds,Weights)
  2. Load in the animation file (This gives Skeleton, InverseBindPositions, and other needed info to bind skeleton to mesh) - Once skeleton and binding info is collected, gather first animation info from that file as well.
  3. For another animation, the above Skeleton should work fine for any other animation on the same mesh/model - just read in the animation information and store in your chosen data structure. Repeat step 3 til happy.

Cheers!

This topic is closed to new replies.

Advertisement