我曾经以为将动画的关键帧对齐到目标位置是无害的。然而事实并非如此。
我正在开发一款Unity动画工具,几乎一整年我都有一个demo,几乎看起来是正确的。
这个demo包含535个环绕在圆形轨道上的方块,当地面高度增加时,圆形轨道会变大,形成空气中的一股涡流。每个方块都会执行一个小弧形运动的tween,然后将其传递给下一个tween,如此反复。理论上,这应该是非常平滑的。
然而实际上并不是这样的。
每当一个tween传递给下一个tween时,方块都会产生一个微小的抖动。关闭平滑效果时,它看起来像一个锐利的后退。启用弹性平滑效果时,它会变成一个奇怪的软漂移。
同样的错误,但症状却完全不同。
https://i.redd.it/hhubt5kob05h1.gif
还有一个细节我一直以来都忽略了:tween需要精确地结束在目标位置。如果不这样做,对象可能会在随机的距离上结束,且这个距离会随着帧率的变化而变化。
因此,如果你告诉一个按钮移动到x=5和y=25,你会很失望地发现它实际上移动到x=5.03和y=25.001。tween通常不会在百分比完成达到100%时精确地结束,尤其是在帧率变动的情况下。这就是为什么显而易见的解决方案是简单地将目标位置设置为请求的精确端点:
bool done = percentDone >= 1f;
if (done)
{
inst.CurrentValue = inst.Target;
}
else
{
float t = effect.Easing.Evaluate(percentDone);
inst.CurrentValue = inst.StartValue + (t * (inst.Target - inst.StartValue));
}
基本上:如果tween完成了,直接将其设置到目标位置。
我认为这是一个合理的解决方案。甚至是安全的。
然而,这也隐藏了真正的问题。
实际的错误并不是一个错误,而是三个小的时间错误,只有当链式动画、圆形轨迹和平滑效果都存在时才会变得可见。
第一:这个“安全”的解决方案实际上是在丢弃最后一帧的有效部分。这样一来,最后一帧几乎不再贡献任何真正的运动。tween已经到达了目标位置,并且已经超出了目标位置,然后我强制它精确到达。因此,每个tween都以一个死帧的分数结束。
解决方案很简单:停止将完成视觉视为一个单独的视觉案例。将百分比限制在一个范围内,让相同的插值路径完成运动。
bool done = percentDone >= 1f;
float p = Mathf.Clamp01(percentDone);
float t = effect.Easing.Evaluate(p);
inst.CurrentValue = inst.StartValue + (t * (inst.Target - inst.StartValue));
如果 p 等于 1,lerp 将自然地落在目标位置上。无需任何特殊的对齐。
第二:手动传递会丢弃时间。
当效果A完成了一半的帧时,我驱动循环将完成A,yield,开始效果B在下一帧。
这意味着剩余的帧切片被删除。
效果A在60%的帧结束时,剩余的40%应该被效果B使用。然而,B实际上是在下一帧从零开始。
这创建了一个在链式效果之间的保证裂缝。
解决方案是立即将剩余的时间传递给下一个效果,在同一帧:
float residual = frameCurrentTime - crossTime;
if (residual > Epsilon)
{
time.PreviousCurrentTime = crossTime;
time.DeltaTime = residual;
carrying = true;
continue;
}
无需等待一帧。无丢弃的时间。
第三:当两个效果可以共享一帧时,你不能给它们全帧。
这听起来现在很明显,但在旧的流程中并不是如此。
效果A应该只接收到它完成的时间的部分。效果B应该接收到剩余的时间。如果两个效果都接收到全帧时间,时间会被双重计算。这意味着处理两个帧的同时,并且每个效果对时间的感知都不同。
这双重计算的时间创建了一个速度的突然跃升。然后弹性平滑效果恰好做了它应该做的事情:它将这个虚假的速度整合成一个可见的漂移。
因此,A的最后一步必须被限制到精确的交叉时间:
crossTime = startTime + duration;
time.CurrentTime = crossTime;
time.DeltaTime = crossTime - time.PreviousCurrentTime;
然后B只接收到剩余的残余时间。
A消耗了帧的第一部分。B消耗了帧的第二部分。他们一起加起来等于一帧。
想象一下。
https://i.redd.it/5vs950hoc05h1.gif
最有趣的部分是,原来的对齐看起来在单独的情况下是正确的。
一个tween单独使用时是无问题的。它精确地结束在目标位置。没有可见的问题。甚至是正确的。
然而,一旦我将tween连接在一起,添加了平滑效果,并且有成百上千个对象在同时进行动画时,那个“安全”的解决方案开始污染整个动画流。
我对这个问题的教训是:在动画系统支持链式动画、平滑效果、速度或传递时,最后一帧仍然很重要。不能简单地丢弃它,因为它的值足够接近。
这个错误现在已经被解决了。没有停顿,没有速度的突然跃升,没有对齐,没有漂移。
为了背景,这个问题是从我正在开发的Unity动画工具JuiceBox中出来的。有一个免费版本可用,我刚刚发布了Pro版本,并且在下几天内会有50%的折扣。
我不试图将这个转变成一个功能列表的帖子,所以我会保持在那里。主要是我觉得调试故事值得分享,因为这个错误是在我编写的代码中隐藏的,因为我确信它并不重要。
评论 (0)