从一个动画到另一个动画的过渡可能会不和谐。想象一下,如果一个角色正处于出拳过程中,而玩家决定要开始奔跑。如果动画只是从跳转剪辑切换到运行剪辑,过渡将是艰难和不自然的。
动画混合可以通过生成两个动画的平均中间帧来解决这个问题。这种褪色通常很短——四分之一秒或更短。这种短暂的混合产生的平滑动画过渡提供了更好的外观体验。
本章探讨如何实现动画混合和添加动画混合,以及如何设置交叉渐变控制器来管理混合队列。将涵盖以下主题:
- 姿势混合
- 交叉渐变动画
- 添加剂混合
动画混合是每个关节的局部空间中两个姿势之间的线性混合。把它想象成一个lerp
或mix
函数,但应用于整个姿势。该技术不混合动画剪辑;相反,它混合了这些片段被采样到的姿势。
混合两个姿势时,整个姿势不需要混合。假设有两个动画——一个运行周期和一个攻击。如果玩家按下攻击按钮,攻击姿势的上半部分会在短时间内混合,在整个动画中保持1
的权重,然后在接近动画结束时混合出来。
这是一个使用姿势混合来创建跑步攻击动画的例子,而不必制作攻击动画的腿部动画。攻击动画可以混合在行走动画的行走循环之上。动画混合可以用来在动画之间平滑过渡,或者将多个动画组合成一个新的动画。
在下一节中,您将为Pose
类声明一个Blend
函数。这个Blend
函数将在两个姿势之间线性插值,类似于向量lerp
的工作方式。该功能需要两个姿势和一个插值,通常表示为t
,其范围为0
至1
Blend
函数采用两个姿势——一个混合值和一个根节点——作为参数。当混合值为0
时,Blend
功能返回第一个姿势,当混合值为1
时,返回第二个姿势。对于0
和1
之间的任何值,姿势都是混合的。根节点决定第二个动画的哪个节点(及其子节点)应该混合到第一个动画中。
为了适应指定开始混合的根骨骼,需要有一种方法来检查一个节点是否在另一个节点的层次结构中。IsInHierarchy
函数取一个Pose
类,一个节点是根节点,一个节点是搜索节点。如果搜索节点是根节点的后代,函数返回true
:
bool IsInHierarchy(Pose& pose, unsigned int root,
unsigned int search);
void Blend(Pose& output,Pose& a,Pose& b,float t,int root);
混合两个姿势时,假设姿势相似。相似的姿势具有相同数量的关节,并且每个关节在姿势之间具有相同的父索引。在下一节中,您将实现Blend
功能。
为了融合工作,必须发生在局部空间,便于两个姿势之间的融合。循环遍历输入姿势中的所有关节,并在混合的两个姿势中的关节的局部变换之间进行插值。对于位置和比例,使用向量lerp
函数,对于旋转,使用四元数nlerp
函数。
要支持动画根,请检查当前变换是否是混合根的后代。如果是,进行混合。如果不是,跳过混合并保留第一个输入姿势的变换值。按照以下步骤实现层次检查和Blend
功能:
-
要检查一个关节是否是另一个关节的后代,请沿着后代关节一直向上直到根节点。如果在此层次中遇到的任何节点是您要检查的节点,则返回
true
:bool IsInHierarchy(Pose& pose, unsigned int parent, unsigned int search) { if (search == parent) { return true; } int p = pose.GetParent(search); while (p >= 0) { if (p == (int)parent) { return true; } p = pose.GetParent(p); } return false; }
-
要将两个姿势混合在一起,循环每个姿势的关节。如果当前关节不在混合根的层次结构中,请不要混合它。否则,使用您在 第 5 章实现变换中编写的
mix
功能混合Transform
对象。mix
函数考虑了四元数邻域:void Blend(Pose& output, Pose& a, Pose& b, float t, int root) { unsigned int numJoints = output.Size(); for (unsigned int i = 0; i < numJoints; ++ i) { if (root >= 0) { if (!IsInHierarchy(output, root, i)) { continue; } } output.SetLocalTransform(i, mix( a.GetLocalTransform(i), b.GetLocalTransform(i), t) ); } }
如果使用整个层次混合两个动画,则Blend
的根参数将为负。对于混合根的负关节,Blend
功能跳过IsInHierarchy
检查。在下面的部分,您将探索如何在两个动画之间渐变以实现平滑过渡。
混合动画最常见的用例是两个动画之间的交叉渐变。一个交叉渐变是从一个动画到另一个动画的快速混合。交叉渐变的目标是隐藏两个动画之间的过渡。
一旦完成交叉渐变,激活的动画需要被你正在渐变到的动画替换。如果您渐变到多个动画,它们都会被评估。最早结束的先被移除。请求的动画被添加到列表中,淡出的动画从列表中移除。
在下一节中,您将构建一个CrossFadeController
类来处理交叉渐变逻辑。这个类提供了一个简单直观的应用编程接口,只需一次函数调用就可以使动画之间的淡入淡出变得简单。
当将动画淡入已经采样的姿势时,你需要知道正在淡入的动画是什么,它的当前播放时间,淡入持续时间的长度,以及淡入的当前时间。这些值用于执行实际混合,并包含有关混合状态的数据。
创建一个新文件并命名为CrossFadeTarget.h
以实现中的CrossFadeTarget
助手类。这个助手类包含前面描述的变量。默认构造函数应该将所有内容的值设置为0
。还提供了一个方便的构造函数,用于获取剪辑指针、姿势引用和持续时间:
struct CrossFadeTarget {
Pose mPose;
Clip* mClip;
float mTime;
float mDuration;
float mElapsed;
inline CrossFadeTarget()
: mClip(0), mTime(0.0f),
mDuration(0.0f), mElapsed(0.0f) { }
inline CrossFadeTarget(Clip* target,Pose& pose,float dur)
: mClip(target), mTime(target->GetStartTime()),
mPose(pose), mDuration(dur), mElapsed(0.0f) { }
};
CrossFadeTarget
辅助类的mPose
、mClip
和mTime
变量用于每一帧中,以对要淡入的动画进行采样。mDuration
和mElapsed
变量用于控制动画应该淡入多少。
在下一节中,您将实现一个控制动画播放和淡入淡出的类。
跟踪当前播放的片段并管理渐变是一个新的CrossFadeController
类的工作。创建一个新文件CrossFadeController.h
,在其中声明新的类。这个类需要包含一个骨架、一个姿势、当前播放时间和一个动画剪辑。它还需要一个控制动画混合的CrossFadeTarget
对象的向量。
CrossFadeController
和CrossFadeTarget
类都包含指向动画剪辑的指针,但是它们没有这些指针。因为两个类都不拥有指针的内存,所以生成的构造函数、复制构造函数、赋值操作符和析构函数都可以使用。
CrossFadecontroller
类需要设置当前骨架、检索当前姿势、检索当前剪辑的函数。可以使用Play
功能设置当前动画。使用FadeTo
功能可以混合新的动画。由于CrossFadeController
类管理动画播放,它需要一个Update
函数来采样动画剪辑:
class CrossFadeController {
protected:
std::vector<CrossFadeTarget> mTargets;
Clip* mClip;
float mTime;
Pose mPose;
Skeleton mSkeleton;
bool mWasSkeletonSet;
public:
CrossFadeController();
CrossFadeController(Skeleton& skeleton);
void SetSkeleton(Skeleton& skeleton);
void Play(Clip* target);
void FadeTo(Clip* target, float fadeTime);
void Update(float dt);
Pose& GetCurrentPose();
Clip* GetcurrentClip();
};
每一帧都会评估整个mTargets
列表。每一个动画都会被评估并混合到当前正在播放的动画中。
在下一节中,您将实现CrossFadeController
类。
创建新文件,CrossFadeController.cpp
。CrossFadeController
在这个新文件中实现。按照这些步骤执行CrossFadeController
:
-
在默认构造函数中,为当前剪辑和时间设置默认的值
0
,并将骨架标记为未设置。有一个方便的构造函数接受一个框架引用。便利构造器应该调用SetSkeleton
函数:CrossFadeController::CrossFadeController() { mClip = 0; mTime = 0.0f; mWasSkeletonSet = false; } CrossFadeController::CrossFadeController(Skeleton& skeleton) { mClip = 0; mTime = 0.0f; SetSkeleton(skeleton); }
-
实现
SetSkeleton
功能,将提供的骨架复制到CrossFadeController
中。它将该类标记为设置了骨架,并将其余姿势复制到交叉渐变控制器的内部姿势中:void CrossFadeController::SetSkeleton( Skeleton& skeleton) { mSkeleton = skeleton; mPose = mSkeleton.GetRestPose(); mWasSkeletonSet = true; }
-
实现
Play
功能。该功能应清除任何活跃的交叉渐变。它应该设置剪辑和回放时间,但它也需要将当前姿势重置为骨骼的其余姿势:void CrossFadeController::Play(Clip* target) { mTargets.clear(); mClip = target; mPose = mSkeleton.GetRestPose(); mTime = target->GetStartTime(); }
-
执行
FadeTo
功能,检查请求的淡入淡出目标是否有效。淡入淡出目标只有在不是淡入淡出列表中的第一项或最后一项时才有效。假设满足这些条件,FadeTo
功能会将提供的动画剪辑和持续时间添加到渐变列表中:void CrossFadeController::FadeTo(Clip* target, float fadeTime) { if (mClip == 0) { Play(target); return; } if (mTargets.size() >= 1) { Clip* clip=mTargets[mTargets.size()-1].mClip; if (clip == target) { return; } } else { if (mClip == target) { return; } } mTargets.push_back(CrossFadeTarget(target, mSkeleton.GetRestPose(), fadeTime)); }
-
实现
Update
功能,播放激活的动画,并融入淡入淡出列表中的任何其他动画:void CrossFadeController::Update(float dt) { if (mClip == 0 || !mWasSkeletonSet) { return; }
-
将当前动画设置为目标动画,如果动画已经完成淡入淡出,则移除淡入淡出对象。每帧只移除一个目标。如果您想要移除所有淡出的目标,请将循环改为向后:
unsigned int numTargets = mTargets.size(); for (unsigned int i = 0; i < numTargets; ++ i) { float duration = mTargets[i].mDuration; if (mTargets[i].mElapsed >= duration) { mClip = mTargets[i].mClip; mTime = mTargets[i].mTime; mPose = mTargets[i].mPose; mTargets.erase(mTargets.begin() + i); break; } }
-
将淡入淡出列表与当前动画混合。需要对当前动画和淡入淡出列表中的所有动画进行采样:
numTargets = mTargets.size(); mPose = mSkeleton.GetRestPose(); mTime = mClip->Sample(mPose, mTime + dt); for (unsigned int i = 0; i < numTargets; ++ i) { CrossFadeTarget& target = mTargets[i]; target.mTime = target.mClip->Sample( target.mPose, target.mTime + dt); target.mElapsed += dt; float t = target.mElapsed / target.mDuration; if (t > 1.0f) { t = 1.0f; } Blend(mPose, mPose, target.mPose, t, -1); } }
-
用
GetCurrentPose
和GetCurrentclip
助手函数完成CrossFadeController
类的实现。这些是简单的 getter 函数:Pose& CrossFadeController::GetCurrentPose() { return mPose; } Clip* CrossFadeController::GetcurrentClip() { return mClip; }
您现在可以创建CrossFadeController
的实例来控制动画播放,而不是手动控制播放什么动画。当你开始播放新动画时,CrossFadeController
类会自动淡入新动画。在下一节中,您将探索添加动画混合。
添加动画用于通过添加额外的关节运动来修改动画。一个常见的例子是左倾。如果有一个左倾动画只是弯曲角色的脊柱,它可以添加到行走动画中,以创建左倾行走动画、跑步动画或任何其他类型的动画。
不是所有的动画都适合添加动画。附加动画通常是专门制作的。我在本章示例代码提供的Woman.gltf
文件中添加了一个Lean_Left
动画。这部动画是加性的。它只会弯曲一个脊椎关节。
附加动画通常不会根据时间播放,而是根据其他输入播放。可以把向左倾斜作为一个例子——它应该由用户的操纵杆来控制。操纵杆越靠近左边的,在动画中倾斜应该走得越远。将附加动画的回放与时间以外的内容同步是很常见的。
添加剂混合的功能在Blending.h
中声明。第一个函数MakeAditivePose
将时间点0
的附加片段采样为输出姿势。这个输出姿势是用来将两个姿势加在一起的参考。
Add
功能执行两个姿势之间的加法混合过程。添加混合公式为结果姿态 = 输入姿态 + ( 添加姿态–添加基础姿态)。前两个参数,即输出姿势和输入姿势,可以指向同一个姿势。要应用附加姿势,需要附加姿势和附加姿势的参考:
Pose MakeAdditivePose(Skeleton& skeleton, Clip& clip);
void Add(Pose& output, Pose& inPose, Pose& addPose,
Pose& additiveBasePose, int blendroot);
MadeAdditivePose
辅助函数生成Add
函数作为其第四个参数的附加基本姿势。该函数旨在初始化期间调用。在下一节中,您将实现这些功能。
在Blending.cpp
中实现MakeAdditivePose
功能。该函数仅在加载期间调用。它应该在剪辑开始时对所提供的剪辑进行采样。样本结果是基于加性姿势:
Pose MakeAdditivePose(Skeleton& skeleton, Clip& clip) {
Pose result = skeleton.GetRestPose();
clip.Sample(result, clip.GetStartTime());
return result;
}
添加混合的公式为结果姿态 = 输入姿态 + ( 添加姿态–添加基础姿态)。附加基本姿势的减法仅在动画的第一帧和当前帧之间应用附加动画的增量。正因为如此,你只能动画一个骨骼,比如说一个脊椎骨骼,并达到一个让角色向左倾斜的效果。
要实现添加混合,循环通过姿势的每个关节。与常规动画混合一样,有一个blendroot
参数需要考虑。使用每个关节的局部变换遵循提供的公式:
void Add(Pose& output, Pose& inPose, Pose& addPose,
Pose& basePose, int blendroot) {
unsigned int numJoints = addPose.Size();
for (int i = 0; i < numJoints; ++ i) {
Transform input = inPose.GetLocalTransform(i);
Transform additive = addPose.GetLocalTransform(i);
Transform additiveBase=basePose.GetLocalTransform(i);
if (blendroot >= 0 &&
!IsInHierarchy(addPose, blendroot, i)) {
continue;
}
// outPose = inPose + (addPose - basePose)
Transform result(input.position +
(additive.position - additiveBase.position),
normalized(input.rotation *
(inverse(additiveBase.rotation) *
additive.rotation)),
input.scale + (additive.scale -
additiveBase.scale)
);
output.SetLocalTransform(i, result);
}
}
重要信息
四元数没有减法运算符。要从四元数 B 中移除四元数 A 的旋转,将 B 乘以 A 的倒数。四元数的逆应用了旋转的逆,这就是为什么四元数乘以它的逆会得到恒等式。
添加动画最常用于创建新的动画变体,例如,将行走动画与蹲伏姿势混合以创建蹲伏行走。所有动画都可以与蹲伏姿势相结合,以编程方式创建蹲伏版本的动画。
在本章中,您学习了如何混合多个动画。混合动画可以混合整个层次或只是一个子集。您还构建了一个系统来管理新动画播放时动画之间的淡入淡出。我们还介绍了附加动画,当给定关节角度进行插值时,它可以用来创建新的运动。
本章的可下载材料中包含四个示例。Sample00
是书中至此的所有代码。Sample01
演示如何通过在计时器上混合行走和跑步动画来使用Blend
功能。Sample02
演示了交叉渐变到随机动画的交叉渐变控制器的使用,Sample03
演示了如何使用添加动画混合。
在下一章,你将学习反向运动学。反向运动学可以让你根据角色末端的位置来计算角色的肢体应该如何弯曲。想想把一个角色的脚踩在不平的地面上。