Correctly blend between skeletal animations in OpenGL

  animation, assimp, c++, opengl, skeletal-animation

I finished the skeletal animation tutorial from learnopengl.com (link) and it works ok. The problem is, when I play another animation, it "jumps" to the first frame of that animation in a very jarring way instead of smoothly transitioning to it.

For example, let’s say I want to blend from a walk animation to a run animation. From what I understand, I need to interpolate between the local transforms of the bones. So in addition to the "CalculateBoneTransform()" function from the tutorial, I wrote a "CalculateBlendedBoneTransform()" function:

// Recursive function that sets interpolated bone matrices in the 'm_FinalBoneMatrices' vector
void CalculateBlendedBoneTransform(
        Animation* pAnimationBase,  const AssimpNodeData* node,
        Animation* pAnimationLayer, const AssimpNodeData* nodeLayered,
        const float currentTimeBase, const float currentTimeLayered,
        const glm::mat4& parentTransform,
        const float blendFactor)
{
    const std::string& nodeName = node->name;
    glm::mat4 nodeTransform = node->transformation;

    Bone* pBone = pAnimationBase->FindBone(nodeName);
    if (pBone)
    {
        pBone->Update(currentTimeBase);
        nodeTransform = pBone->GetLocalTransform();
    }

    glm::mat4 layeredNodeTransform = nodeLayered->transformation;
    pBone = pAnimationLayer->FindBone(nodeName);
    if (pBone)
    {
        pBone->Update(currentTimeLayered);
        layeredNodeTransform = pBone->GetLocalTransform();
    }

    // Blend two matrices
    const glm::quat rot0 = glm::quat_cast(nodeTransform);
    const glm::quat rot1 = glm::quat_cast(layeredNodeTransform);
    const glm::quat finalRot = glm::slerp(rot0, rot1, blendFactor);
    glm::mat4 blendedMat = glm::mat4_cast(finalRot);
    blendedMat[3] = (1.0f - blendFactor) * nodeTransform[3] + layeredNodeTransform[3] * blendFactor;

    const glm::mat4 globalTransformation = parentTransform * blendedMat;

    const auto& boneInfoMap = pAnimationBase->GetBoneInfoMap();
    if (boneInfoMap.find(nodeName) != boneInfoMap.end())
    {
        const int index = boneInfoMap.at(nodeName).id;
        const glm::mat4& offset = boneInfoMap.at(nodeName).offset;
        const glm::mat4& offsetLayerMat = pAnimationLayer->GetBoneInfoMap().at(nodeName).offset;
            
        // Blend two matrices... again
        const glm::quat rot0 = glm::quat_cast(offset);
        const glm::quat rot1 = glm::quat_cast(offsetLayerMat);
        const glm::quat finalRot = glm::slerp(rot0, rot1, blendFactor);
        glm::mat4 blendedMat = glm::mat4_cast(finalRot);
        blendedMat[3] = (1.0f - blendFactor) * offset[3] + offsetLayerMat[3] * blendFactor;

        m_FinalBoneMatrices[index] = globalTransformation * blendedMat;
    }

    for (size_t i = 0; i < node->children.size(); ++i)
        CalculateBlendedBoneTransform(pAnimationBase, &node->children[i], pAnimationLayer, &nodeLayered->children[i], currentTimeBase, currentTimeLayered, globalTransformation, blendFactor);
}

This next function runs every frame:

pAnimator->BlendTwoAnimations(pVampireWalkAnim, pVampireRunAnim, animationBlendFactor, deltaTime);

Which contains:

void BlendTwoAnimations(Animation* pBaseAnimation, Animation* pLayeredAnimation, float blendFactor, float dt)
{
    static float currentTimeBase = 0.0f;
    currentTimeBase += pBaseAnimation->GetTicksPerSecond() * dt;
    currentTimeBase = fmod(currentTimeBase, pBaseAnimation->GetDuration());

    static float currentTimeLayered = 0.0f;
    currentTimeLayered += pLayeredAnimation->GetTicksPerSecond() * dt;
    currentTimeLayered = fmod(currentTimeLayered, pLayeredAnimation->GetDuration());

    CalculateBlendedBoneTransform(pBaseAnimation, &pBaseAnimation->GetRootNode(), pLayeredAnimation, &pLayeredAnimation->GetRootNode(), currentTimeBase, currentTimeLayered, glm::mat4(1.0f), blendFactor);
}

And here’s what it looks like:

https://imgur.com/a/pcxDhut

The run and walk animations look perfectly fine when the "blend factor" is at 0.0 and 1.0, but anything in-between has a sort of discontinuity… There are entire milliseconds that look like he’s running with both feet raised at the same time. How can I get them to blend correctly? I was expecting to see a smooth transition between walking and running, like when you progressively move the analog stick on a controller.

The animations are from mixamo.com (same as the model), in FBX format, loaded using the latest commit of Assimp. I have tested them in Unreal Engine 4 with a Blend Space, and the "crossfading" between them looks great. So it’s not the animation files themselves, they are correct.

I’m not sure if I should be using an "inverse bind pose transform" that I see in other tutorials, since it seems to play individual animations just fine without one.

Source: Windows Questions C++

LEAVE A COMMENT