-1

我从 learnopengl.com(链接)完成了骨骼动画教程。

当我播放另一个动画时,它会以非常不和谐的方式“跳转”到该动画的第一帧,而不是平滑过渡到它。

这是我到目前为止写的:

// 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);
}

下一个函数每帧运行:

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

其中包含:

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);
}

这就是它的样子:https ://imgur.com/a/pcxDhut

当“混合因子”为 0.0 和 1.0 时,奔跑和行走动画看起来非常好,但介于两者之间的任何东西都有一种不连续性......有整毫秒看起来他在同时抬起双脚奔跑. 我怎样才能让它们正确混合?我期待看到步行和跑步之间的平稳过渡,就像在大多数第三人称游戏中逐渐移动控制器上的模拟摇杆时一样。

动画来自 mixamo.com(与模型相同),采用 FBX 格式,使用 Assimp 5.1.0rc1 加载。我在虚幻引擎 4 中使用混合空间对它们进行了测试,它们之间的“交叉淡入淡出”看起来很棒。所以不是动画文件本身,它们是正确的。

4

1 回答 1

3

好的,我解决了:https ://imgur.com/a/DICWGCV

我四处搜索,发现这个动画示例有 3 个转换:步行 -> 慢跑 -> 跑步。如果您仔细观察,它们都同时开始和结束,几乎就像它们具有相同的持续时间。慢跑的持续时间从“速度乘数”(某种意义上的)设置为 0.6 开始,跑步动画从 0.5 开始。如果您单独播放它们,那么跑步动画当然比悠闲地散步要短。正是这些速度倍增器让它们同时发挥作用。把它们想象成“缩放”每个动画的持续时间。

那么如何获得这些速度倍增器呢?好吧,.GetDuration()Animation 类有一个成员函数。如果你将基础动画的持续时间除以“分层”动画的持续时间,你会得到一个数字。一个浮子。如果你把它分开,也是一样的。对我来说,它是:

WalkAnim Duration:  1266.67
RunAnim  Duration:   766.667

1266.67  /  766.667  =  1.6521775425315032471725012293473
 766.667 / 1266.67   =  0.6052618282583466885613458911950

要获得相同的持续时间(并最终同步),您必须通过将每个特定动画的“deltaTime”参数与上述之一相乘来按比例增加/减少每个动画的播放速度(阅读:lerp)商。换句话说,您必须同时提高步行速度和降低跑步速度,具体取决于“混合因子”设置的值(0 到 1 之间)。

为此,动画必须匹配。如果你在 Blender 中打开它们,它们应该从抬起的同一只脚开始,向左走一步,向右走一步,并以与它们开始时相同的位置和旋转结束,以实现无缝循环。并且它们的滴答速率必须匹配(通常是 30 fps 或 60 fps,但由于某种原因,Mixamo 也允许以 24 fps 下载它们)。30就好了。如果一个使用 30 而另一个使用 60,你会得到可怕的混合。

所以这里是代码。

注意:速度乘数中的* 1.0fand1.0f *可以省略,但我选择保留它们是因为它使 lerp 公式更易于识别,更易于阅读。无论如何,编译器可能会优化它们。

void Animator::BlendTwoAnimations(Animation* pBaseAnimation, Animation* pLayeredAnimation, float blendFactor, float deltaTime)
{
    // Speed multipliers to correctly transition from one animation to another
    float a = 1.0f;
    float b = pBaseAnimation->GetDuration() / pLayeredAnimation->GetDuration();
    const float animSpeedMultiplierUp = (1.0f - blendFactor) * a + b * blendFactor; // Lerp

    a = pLayeredAnimation->GetDuration() / pBaseAnimation->GetDuration();
    b = 1.0f;
    const float animSpeedMultiplierDown = (1.0f - blendFactor) * a + b * blendFactor; // Lerp

    // Current time of each animation, "scaled" by the above speed multiplier variables
    static float currentTimeBase = 0.0f;
    currentTimeBase += pBaseAnimation->GetTicksPerSecond() * deltaTime * animSpeedMultiplierUp;
    currentTimeBase = fmod(currentTimeBase, pBaseAnimation->GetDuration());

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

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


// Recursive function that sets interpolated bone matrices in the 'm_FinalBoneMatrices' vector
void Animator::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;

    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;

        m_FinalBoneMatrices[index] = globalTransformation * offset;
    }

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

而不是pAnimator->UpdateAnimation(deltaTime),我每帧都运行它:

pAnimator->BlendTwoAnimations(pVampireWalkAnim, pVampireRunAnim, animationBlendFactor, deltaTime * 30.0f); // 30.0f intentional here, otherwise they play too slowly
于 2021-11-10T17:30:24.423 回答