Animator.Update的底层流程是动画系统中每帧驱动动画播放和状态切换的核心逻辑。它负责根据当前状态机参数更新动画状态,计算骨骼变换,处理动画混合和过渡,触发事件,并最终输出当前帧的骨骼姿态。
下面是一个典型的Animator.Update
底层流程的详细分解和伪代码示例,帮助你理解其内部执行步骤。
Animator.Update 底层流程详解
1. 参数更新(Update Parameters)
- 读取或接收外部输入(如角色速度、攻击触发等)。
- 更新状态机参数(bool、int、float、trigger等)。
- 参数变化可能影响状态机过渡条件。
2. 状态机状态更新(Update State Machine)
- 根据当前状态和参数,判断是否满足过渡条件。
- 如果满足,开始状态过渡,记录过渡起始状态、目标状态和过渡时间。
- 处理过渡时间推进,计算过渡权重。
3. 动画时间推进(Advance Animation Time)
- 对当前动画剪辑或混合树推进播放时间(考虑播放速度、循环等)。
- 如果处于过渡状态,同时推进两个状态的动画时间。
4. 采样动画姿态(Sample Animation Poses)
- 根据当前时间采样动画剪辑关键帧,计算骨骼局部变换。
- 如果是混合状态,采样多个动画剪辑,准备混合。
5. 动画混合(Blend Animations)
- 根据过渡权重或混合参数,线性或多维混合多个动画姿态。
- 处理层级混合(如上半身和下半身分开混合)。
6. 计算骨骼全局变换(Calculate Global Transforms)
- 从根骨骼开始递归计算每个骨骼的全局变换矩阵。
- 结合父骨骼变换,得到最终骨骼姿态。
7. 触发动画事件(Trigger Animation Events)
- 检查当前动画时间是否经过事件触发点。
- 触发绑定的回调函数或游戏逻辑。
8. 输出骨骼姿态(Output Final Pose)
- 将计算好的骨骼全局变换传递给渲染系统或GPU。
- 准备下一帧使用。
Animator.Update 伪代码示例
void Animator::Update(float deltaTime) {
// 1. 更新参数(外部输入驱动)
UpdateParameters();
// 2. 更新状态机状态和过渡
if (isTransitioning) {
transitionTime += deltaTime;
float t = transitionTime / transitionDuration;
if (t >= 1.0f) {
currentState = nextState;
isTransitioning = false;
ResetAnimationEvents(currentState);
}
} else {
// 检查是否满足过渡条件
for (auto& transition : currentState->transitions) {
if (transition->IsConditionMet(parameters)) {
StartTransition(transition);
break;
}
}
}
// 3. 推进动画时间
currentState->AdvanceTime(deltaTime);
if (isTransitioning) {
nextState->AdvanceTime(deltaTime);
}
// 4. 采样动画姿态
Pose poseA = currentState->SamplePose();
Pose poseB;
if (isTransitioning) {
poseB = nextState->SamplePose();
}
// 5. 混合动画
Pose finalPose;
if (isTransitioning) {
float t = transitionTime / transitionDuration;
finalPose = BlendPoses(poseA, poseB, t);
} else {
finalPose = poseA;
}
// 6. 计算骨骼全局变换
CalculateGlobalTransforms(finalPose);
// 7. 触发动画事件
TriggerAnimationEvents(currentState, deltaTime);
if (isTransitioning) {
TriggerAnimationEvents(nextState, deltaTime);
}
// 8. 输出骨骼姿态
OutputPose(finalPose);
}
关键点总结
步骤 | 说明 |
---|---|
参数更新 | 读取外部输入,更新状态机参数 |
状态机更新 | 判断状态切换,处理过渡 |
时间推进 | 推进动画播放时间,支持循环和速度调整 |
采样姿态 | 关键帧插值计算骨骼局部变换 |
动画混合 | 线性或多维混合多个动画状态,支持层级混合 |
全局变换计算 | 递归计算骨骼全局变换矩阵 |
事件触发 | 根据动画时间触发绑定事件 |
输出姿态 | 将最终骨骼变换传递给渲染或GPU |
接下来我将更深入地拆解Animator.Update
流程中的关键步骤,特别是状态机过渡处理、动画混合算法、骨骼全局变换计算和动画事件触发的具体实现细节,并给出更贴近实际的伪代码示例。
1. 状态机过渡处理细节
1.1 过渡条件检测
- 每个状态维护一组过渡(Transition),每个过渡有条件(Condition)和目标状态。
- 条件通常是参数比较(如
speed > 0.1
、isJumping == true
)。 - 过渡检测时,遍历当前状态所有过渡,判断条件是否满足。
1.2 过渡启动
- 满足条件时,记录过渡起始时间,设置过渡状态。
- 过渡期间,两个状态动画同时播放,权重从0线性变化到1。
1.3 过渡结束
- 过渡时间达到设定时长,切换到目标状态,结束过渡。
1.4 伪代码示例
void Animator::CheckTransitions() {
for (auto& transition : currentState->transitions) {
if (transition->IsConditionMet(parameters)) {
StartTransition(transition);
break;
}
}
}
void Animator::StartTransition(Transition* transition) {
isTransitioning = true;
transitionTime = 0.0f;
transitionDuration = transition->duration;
nextState = transition->targetState;
// 重置目标状态动画时间
nextState->ResetTime();
}
2. 动画混合算法
2.1 线性混合(Crossfade)
- 两个动画姿态按权重线性插值。
- 对每个骨骼的旋转使用球面线性插值(Slerp),位置和缩放用线性插值。
2.2 多维混合(Blend Trees)
- 根据多个参数计算权重,混合多个动画。
- 例如二维混合使用三角插值或双线性插值。
2.3 混合伪代码示例
Pose BlendPoses(const Pose& poseA, const Pose& poseB, float t) {
Pose result;
for (int i = 0; i < poseA.bones.size(); ++i) {
result.bones[i].position = Lerp(poseA.bones[i].position, poseB.bones[i].position, t);
result.bones[i].rotation = Slerp(poseA.bones[i].rotation, poseB.bones[i].rotation, t);
result.bones[i].scale = Lerp(poseA.bones[i].scale, poseB.bones[i].scale, t);
}
return result;
}
接下来我将进一步细化Animator.Update
底层流程中的关键步骤,特别是状态机过渡处理、动画混合算法、骨骼全局变换计算和事件触发机制的实现细节,并给出更具体的伪代码示例,帮助你更深入理解。
1. 状态机过渡处理细节
1.1 过渡条件判断
- 每个状态包含若干个过渡(Transition),每个过渡有条件(Condition)列表。
- 条件类型包括:布尔值、触发器、数值比较等。
- 只有所有条件满足,过渡才会触发。
1.2 过渡启动与时间管理
- 过渡开始时,记录起始状态、目标状态、过渡时长和当前过渡时间。
- 过渡期间,两个状态动画同时播放,权重从0线性变化到1。
- 过渡结束后,切换到目标状态,清理过渡数据。
1.3 伪代码示例
bool Transition::IsConditionMet(const ParameterSet& params) {
for (auto& cond : conditions) {
if (!cond.Evaluate(params)) return false;
}
return true;
}
void Animator::StartTransition(Transition* transition) {
isTransitioning = true;
transitionTime = 0.0f;
transitionDuration = transition->duration;
nextState = transition->targetState;
// 可能需要重置目标状态动画时间
nextState->ResetTime();
}
2. 动画混合算法细节
2.1 线性混合(Crossfade)
- 两个Pose对应骨骼逐个插值。
- 位置用线性插值,旋转用球面线性插值(Slerp)。
2.2 多层混合
- 支持多个动画层,每层有权重和蒙版。
- 蒙版定义哪些骨骼受该层影响。
- 先计算基础层Pose,再叠加高层Pose。
2.3 伪代码示例
Pose BlendPoses(const Pose& poseA, const Pose& poseB, float t) {
Pose result;
for (int i = 0; i < poseA.bones.size(); ++i) {
result.bones[i].position = Lerp(poseA.bones[i].position, poseB.bones[i].position, t);
result.bones[i].rotation = Slerp(poseA.bones[i].rotation, poseB.bones[i].rotation, t);
}
return result;
}
Pose LayeredBlend(const std::vector<Pose>& poses, const std::vector<float>& weights, const std::vector<BoneMask>& masks) {
Pose basePose = poses[0];
for (int layer = 1; layer < poses.size(); ++layer) {
for (int i = 0; i < basePose.bones.size(); ++i) {
if (masks[layer].IsBoneActive(i)) {
basePose.bones[i].position = Lerp(basePose.bones[i].position, poses[layer].bones[i].position, weights[layer]);
basePose.bones[i].rotation = Slerp(basePose.bones[i].rotation, poses[layer].bones[i].rotation, weights[layer]);
}
}
}
return basePose;
}
3. 骨骼全局变换计算
3.1 局部到全局变换
- 每个骨骼的全局变换 = 父骨骼全局变换 * 当前骨骼局部变换。
- 根骨骼的全局变换通常是模型变换矩阵。
3.2 递归计算
- 从根骨骼开始递归计算所有骨骼的全局变换。
- 结果用于顶点变形和GPU上传。
3.3 伪代码示例
void CalculateGlobalTransformsRecursive(int boneIndex, const Pose& localPose, std::vector<Matrix4>& globalTransforms, const std::vector<int>& parentIndices) {
int parentIndex = parentIndices[boneIndex];
if (parentIndex < 0) {
globalTransforms[boneIndex] = localPose.bones[boneIndex].ToMatrix();
} else {
globalTransforms[boneIndex] = globalTransforms[parentIndex] * localPose.bones[boneIndex].ToMatrix();
}
// 递归计算子骨骼
for (int childIndex : skeleton.GetChildren(boneIndex)) {
CalculateGlobalTransformsRecursive(childIndex, localPose, globalTransforms, parentIndices);
}
}
void CalculateGlobalTransforms(const Pose& localPose) {
CalculateGlobalTransformsRecursive(rootBoneIndex, localPose, globalTransforms, parentIndices);
}
4. 动画事件触发机制
4.1 事件检测
- 事件绑定在动画的特定时间点(Normalized Time)。
- 每帧检测动画时间是否跨过事件时间点。
4.2 事件触发状态管理
- 防止事件重复触发,记录事件是否已触发。
- 循环动画时,重置事件触发状态。
4.3 伪代码示例
void TriggerAnimationEvents(AnimationState* state, float deltaTime) {
float prevTime = state->previousTime;
float currTime = state->currentTime;
float duration = state->clip->duration;
for (auto& e : state->clip->events) {
float eventTime = e.time * duration;
if (!e.triggered && prevTime < eventTime && currTime >= eventTime) {
e.callback();
e.triggered = true;
}
}
// 循环动画时重置事件
if (currTime < prevTime) {
for (auto& e : state->clip->events) {
e.triggered = false;
}
}
state->previousTime = currTime;
}
总结
细节模块 | 关键点 |
---|---|
状态机过渡 | 条件判断严格,过渡时间管理,状态切换清理 |
动画混合 | 位置线性插值,旋转Slerp,多层蒙版混合 |
全局变换计算 | 递归局部到全局矩阵计算,根骨骼特殊处理 |
事件触发 | 时间点检测,防止重复触发,循环动画事件重置 |