如何正确地将 OpenGL 中的骨骼动画从步行动画混合到 运行 动画?
How do I correctly blend between skeletal animations in OpenGL from a walk animation to a run animation?
我完成了 learnopengl.com (link) 的骨骼动画教程。
当我播放另一个动画时,它以一种非常不和谐的方式“跳”到该动画的第一帧,而不是平滑地过渡到它。
这是我到目前为止写的内容:
// 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);
}
下一个函数每帧 运行s:
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加载。我在 Unreal Engine 4 中用混合 Space 测试了它们,它们之间的“交叉渐变”看起来很棒。所以不是动画文件本身,它们是正确的。
好的,我解决了:https://imgur.com/a/DICWGCV
我四处搜索,发现 this 动画示例,其中有 3 个转换:步行 -> 慢跑 -> 运行。如果你仔细观察,它们都是同时开始和结束的,几乎就像它们具有相同的持续时间一样。慢跑的持续时间从设置为 0.6 的“速度倍增器”(某种)开始,运行 动画从 0.5 开始。如果单独播放,当然 运行 动画比悠闲的走路还要短。正是这些速度倍增器使它们可以同时播放。将它们想象成“缩放”每个动画的持续时间。
那么你是如何获得这些速度倍增器的呢?好吧,动画 class 有一个 .GetDuration()
成员函数。如果将基本动画的持续时间除以“分层”动画的持续时间,就会得到一个数字。一个花车。如果你反过来划分它也是一样的。对我来说,它是:
WalkAnim Duration: 1266.67
RunAnim Duration: 766.667
1266.67 / 766.667 = 1.6521775425315032471725012293473
766.667 / 1266.67 = 0.6052618282583466885613458911950
要具有相同的持续时间(并最终同步化),您必须通过乘以 'deltaTime' 参数 increase/decrease 每个播放速度的比例(阅读:一个 lerp)对于具有上述商数之一的每个特定动画。换句话说,您必须同时提高步行速度和降低 运行 的速度,具体取决于“混合因子”设置的值(在 0 和 1 之间)。
为此,动画必须匹配。如果您在 Blender 中打开它们,它们应该从同一只脚抬起开始,向左迈出一步,向右迈出一步,并以与开始时相同的位置和旋转结束,以实现无缝循环。并且它们的节拍率必须匹配(通常是 30 fps 或 60 fps,但 Mixamo 出于某种原因也允许以 24 fps 下载它们)。 30 没问题。如果一个使用 30 而另一个使用 60,你会得到糟糕的混合效果。
这是代码。
注意:速度乘数中的 * 1.0f
和 1.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
我完成了 learnopengl.com (link) 的骨骼动画教程。
当我播放另一个动画时,它以一种非常不和谐的方式“跳”到该动画的第一帧,而不是平滑地过渡到它。
这是我到目前为止写的内容:
// 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);
}
下一个函数每帧 运行s:
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加载。我在 Unreal Engine 4 中用混合 Space 测试了它们,它们之间的“交叉渐变”看起来很棒。所以不是动画文件本身,它们是正确的。
好的,我解决了:https://imgur.com/a/DICWGCV
我四处搜索,发现 this 动画示例,其中有 3 个转换:步行 -> 慢跑 -> 运行。如果你仔细观察,它们都是同时开始和结束的,几乎就像它们具有相同的持续时间一样。慢跑的持续时间从设置为 0.6 的“速度倍增器”(某种)开始,运行 动画从 0.5 开始。如果单独播放,当然 运行 动画比悠闲的走路还要短。正是这些速度倍增器使它们可以同时播放。将它们想象成“缩放”每个动画的持续时间。
那么你是如何获得这些速度倍增器的呢?好吧,动画 class 有一个 .GetDuration()
成员函数。如果将基本动画的持续时间除以“分层”动画的持续时间,就会得到一个数字。一个花车。如果你反过来划分它也是一样的。对我来说,它是:
WalkAnim Duration: 1266.67
RunAnim Duration: 766.667
1266.67 / 766.667 = 1.6521775425315032471725012293473
766.667 / 1266.67 = 0.6052618282583466885613458911950
要具有相同的持续时间(并最终同步化),您必须通过乘以 'deltaTime' 参数 increase/decrease 每个播放速度的比例(阅读:一个 lerp)对于具有上述商数之一的每个特定动画。换句话说,您必须同时提高步行速度和降低 运行 的速度,具体取决于“混合因子”设置的值(在 0 和 1 之间)。
为此,动画必须匹配。如果您在 Blender 中打开它们,它们应该从同一只脚抬起开始,向左迈出一步,向右迈出一步,并以与开始时相同的位置和旋转结束,以实现无缝循环。并且它们的节拍率必须匹配(通常是 30 fps 或 60 fps,但 Mixamo 出于某种原因也允许以 24 fps 下载它们)。 30 没问题。如果一个使用 30 而另一个使用 60,你会得到糟糕的混合效果。
这是代码。
注意:速度乘数中的 * 1.0f
和 1.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