弹簧-质量系统的减震效果(或者这是ElasticEase吗?)

11

我想在代码中模拟动画效果(几乎任何语言都可以,因为它似乎是数学而不是语言)。

基本上,这是质点弹簧系统的仿真。我一直在看 WPF/Silverlight 的 ElasticEase,这似乎非常接近我要找的东西,但还不够。

首先,这就是我要找的东西 - 一个对象在旅行了一定的时间后到达某个位置,并立即放慢速度,在一定的时间内振荡以停留在阻尼被应用的相同位置。

例如,假设我有一个 600w/900h 画布,我有一个正方形从 TranslateTransform.Y 的 900px 开始动画,一直到 150px。它需要 4 秒钟才能到达 150px 的高度(每秒 187.5px),此时它立即被阻尼并只移动约 35px(每秒 87.5px)到达 115px 的高度,然后向下反弹 1 秒钟到达 163px 的高度(48px 和每秒 48px),然后向上反弹到 146px(17px 和每秒 17px),依此类推,直到振荡使其缓慢到达最终停留位置为 150px。振荡周期为 16 秒。

上面描述的示例是此处左上角的蓝色矩形:

enter image description here

这里是我预先知道的内容 - 从点 A 到点 B 需要多少像素和时间,以及振荡所需的秒数。质量之类的东西似乎并不重要。

我尝试了 ElasticEase,问题似乎是我无法让对象在没有缓动的情况下行驶 4 秒钟,然后在接下来的 16 秒钟内“弹跳”。即使我将 .Springiness 设置为非常高的数字,例如 20,它也总是太大。

ILSpy 显示其函数为:

protected override double EaseInCore(double normalizedTime)
        {
            double num = Math.Max(0.0, (double)this.Oscillations);
            double num2 = Math.Max(0.0, this.Springiness);
            double num3;
            if (DoubleUtil.IsZero(num2))
            {
                num3 = normalizedTime;
            }
            else
            {
                num3 = (Math.Exp(num2 * normalizedTime) - 1.0) / (Math.Exp(num2) - 1.0);
            }
            return num3 * Math.Sin((6.2831853071795862 * num + 1.5707963267948966) * normalizedTime);
        }

我在DropBox上打包了两个视频和一个Excel文件。我猜这个问题会随着人们提出更多的澄清问题而变得更加具体。

(免责声明:当涉及到这些事情时,我不知道我在说什么)


你的问题(正如你所怀疑的)是一个数学/物理问题。这是一个标准的物理第一课阻尼振荡问题,但需要用几百个单词(和一些数学公式,在SO上显示效果不佳)来完全解释。基本问题在于阻尼是指数级的,因此当离散化时,尾部振荡会丢失。你确定要求完整的解释,而不仅仅是http://en.wikipedia.org/wiki/Damping吗? :) - Dr. belisarius
我会尝试,但是我预见需要很长时间才能做好,也许一些物理学家/学生追逐赏金的人可以超越我 :). 顺便说一句,质量是存在的,但问题的所有其他常数(弹性常数和阻尼系数)都被除以质量,以获得一个与质量无关的问题(因为F-m*a == 0等于零,你可以将所有项除以任何常数)。 F是两个力的组合,弹性力与弹性常数k和位置成比例,阻尼力与速度和阻尼常数成比例。 - Dr. belisarius
使用自定义的 EasingFunctionBase 子类是否可接受? - Peter Taylor
运动物理学中不考虑力的影响被称为“运动学”。http://en.wikipedia.org/wiki/Kinematics - tom10
@Peter Taylor - 如果可能的话,一个自定义的EasingFunction会是理想的。 - Todd Main
显示剩余3条评论
2个回答

9
跳过物理学,直接转到方程式。
参数: “以下是我事先知道的 - 从点A到点B所需的像素距离[D]和秒数[T0],振荡的秒数[T1]。”此外,我将添加自由参数:最大振幅Amax,阻尼时间常数Tc和帧速率Rf,即何时需要新的位置值。 我假设您不想永远计算,因此我只会进行10秒的Ttotal,但有各种合理的停止条件...
代码: 这是Python中的代码。 主要是方程式,在def Y(t)中找到:
from numpy import pi, arange, sin, exp

Ystart, D = 900., 900.-150.  # all time units in seconds, distance in pixels, Rf in frames/second
T0, T1, Tc, Amax, Rf, Ttotal = 5., 2., 2., 90., 30., 10. 

A0 = Amax*(D/T0)*(4./(900-150))  # basically a momentum... scales the size of the oscillation with the speed 

def Y(t):
    if t<T0:  # linear part
        y = Ystart-(D/T0)*t
    else:  # decaying oscillations
        y = Ystart-D-A0*sin((2*pi/T1)*(t-T0))*exp(-abs(T0-t)/Tc)
    return y

y_result = []
for t in arange(0, Ttotal, 1./Rf):  # or one could do "for i in range(int(Ttotal*Rf))" to stick with ints    
    y = Y(t)
    y_result.append(y)

这个想法是先进行线性运动,然后进行衰减振荡。振荡由 "sin" 提供,衰减通过将其乘以 "exp" 来实现。当然,更改参数以获取您想要的任何距离、振荡大小等等。
注意事项: 1. 评论中的大多数人都建议采用物理方法。我没有使用这些方法,因为如果一个人指定了某种运动方式,那么从物理学开始,再到微分方程,然后计算运动,并调整参数以获得最终结果,有点过度了。不如直接去做最终的事情。除非,一个人对他们想要从中工作的物理学有直觉。 2. 在像这样的问题中,通常希望保持连续的速度(一阶导数),但是你说“立即减速”,所以我没有在这里这样做。 3. 注意,当阻尼应用时,振荡的周期和振幅可能不会完全按照规定值,但这可能比您关心的细节更详细。 4. 如果您需要将其表示为单个方程,则可以使用“Heaviside函数”来打开和关闭贡献。
冒着让这篇文章变得太长的风险,我意识到我可以在GIMP中制作一个gif,这就是它的样子:
我可以发布制作图形的完整代码,如果有兴趣的话,但基本上我只是为每个时间步长使用不同的D和T0值调用Y。如果我再次这样做,我可以增加阻尼(即减小Tc),但这有点麻烦,所以我将其保留为原样。

同意,实际上你不需要质量。拥有速度和加速度就足够了。在进行弹簧图布局时发现了这一点。天真的方法是使用速度、质量、力等进行完整的物理模拟。然后结果是你只是做了双倍的工作,带来了令人讨厌的副作用。 - gjvdkamp
多么精彩的解释啊!谢谢你!我会在接下来的一两天里尝试使用它,看看能否让它正常工作。 - Todd Main
@Otaku 看起来 D 点的导数不连续。当物体第一次到达静止位置时,你可能会感觉到速度突然变化。 - Dr. belisarius
修复突然的速度变化很容易,但正如我在注释2中所说,我认为这是OP想要的,而且在演示中似乎也有一个突然的变化。无论如何,Otaku,请告诉我是否需要连续的速度变化,因为在方程中很容易包含它。 - tom10
好的。赏金即将到期,所以我想确保您能够得到它。我会有一些小问题。 - Todd Main
显示剩余3条评论

5
我和 @tom10 想法一致(我也考虑过一个接受 IList<IEasingFunction>IEasingFunction,但从现有的函数中获取所需行为可能会很棘手)。
// Based on the example at
// http://msdn.microsoft.com/en-us/library/system.windows.media.animation.easingfunctionbase.aspx
namespace Org.CheddarMonk
{
    public class OtakuEasingFunction : EasingFunctionBase
    {
        // The time proportion at which the cutoff from linear movement to
        // bounce occurs. E.g. for a 4 second movement followed by a 16
        // second bounce this would be 4 / (4 + 16) = 0.2.
        private double _CutoffPoint;
        public double CutoffPoint {
            get { return _CutoffPoint; }
            set {
                if (value <= 0 || value => 1 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _CutoffPoint = value;
            }
        }

        // The size of the initial bounce envelope, as a proportion of the
        // animation distance. E.g. if the animation moves from 900 to 150
        // and you want the maximum bounce to be no more than 35 you would
        // set this to 35 / (900 - 150) ~= 0.0467.
        private double _EnvelopeHeight;
        public double EnvelopeHeight {
            get { return _EnvelopeHeight; }
            set {
                if (value <= 0 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _EnvelopeHeight = value;
            }
        }

        // A parameter controlling how fast the bounce height should decay.
        // The higher the decay, the sooner the bounce becomes negligible.
        private double _EnvelopeDecay;
        public double EnvelopeDecay {
            get { return _EnvelopeDecay; }
            set {
                if (value <= 0 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _EnvelopeDecay = value;
            }
        }

        // The number of half-bounces.
        private int _Oscillations;
        public int Oscillations {
            get { return _Oscillations; }
            set {
                if (value <= 0) {
                    throw new ArgumentException();
                }
                _Oscillations = value;
            }
        }

        public OtakuEasingFunction() {
            // Sensible default values.
            CutoffPoint = 0.7;
            EnvelopeHeight = 0.3;
            EnvelopeDecay = 1;
            Oscillations = 3;
        }

        protected override double EaseInCore(double normalizedTime) {
            // If we get an out-of-bounds value, be nice.
            if (normalizedTime < 0) return 0;
            if (normalizedTime > 1) return 1;

            if (normalizedTime < _CutoffPoint) {
                return normalizedTime / _CutoffPoint;
            }

            // Renormalise the time.
            double t = (normalizedTime - _CutoffPoint) / (1 - _CutoffPoint);
            double envelope = EnvelopeHeight * Math.Exp(-t * EnvelopeDecay);
            double bounce = Math.Sin(t * Oscillations * Math.PI);
            return envelope * bounce;
        }

        protected override Freezable CreateInstanceCore() {
            return new OtakuEasingFunction();
        }
    }
}

这是未经测试的代码,但如果有问题,调试起来不应该太麻烦。我不确定哪些属性(如果有)需要添加到属性中,以便XAML编辑器可以正确处理它们。


哇,彼得!这太棒了!我会在接下来的一两天里尝试一下并告诉你结果。 - Todd Main
Peter,你提供的很棒,和Tom的一样好。我会再开一个悬赏,并在两天后把点数给你。 - Todd Main

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接