根据贝塞尔曲线插值间隔,插值之间的值

时间:2011-05-04 12:21:02

标签: math animation core-animation bezier

为了实现2D动画,我正在寻找两个关键帧之间的插值,其中变化的速度由Bezier曲线定义。问题是贝塞尔曲线以参数形式表示,而要求是能够评估特定时间的值。

详细说来,假设10和40的值将在4秒内插值,其值不是经常变化,而是由表示为0,0 0.2,0.3 0.5,0.5 1,1的贝塞尔曲线定义。 现在,如果我以每秒24帧的速度绘制,我需要评估每帧的值。我怎样才能做到这一点 ?我看了De Casteljau算法并且认为将曲线分成24 * 4片4秒将解决我的问题,但这听起来是错误的,因为时间是沿着“x”轴而不是沿着曲线。

进一步简化 如果我在平面中绘制曲线,x轴表示时间,y轴表示我正在寻找的值。我实际需要的是能够找出对应于“x”的“y”。然后我可以在24个分区中划分x并知道每个帧的值

3 个答案:

答案 0 :(得分:3)

我遇到了同样的问题:那里的每个动画包似乎都使用Bézier曲线来控制一段时间内的值,但是没有关于如何将Bézier曲线实现为y(x)函数的信息。所以这就是我想出来的。

2D空间中的标准三次Bézier曲线可以通过四个点来定义 P 0 =(x 0 ,y 0 ).. P 3 =(x 3 ,y 3
P 0 和P 3 是曲线的终点,而P 1 和P 2 是句柄影响它的形状。使用参数t∈[0,1],可以使用公式确定沿曲线的任何给定点的x和y坐标 A) x =(1-t) 3 x 0 + 3t(1-t) 2 x 1 < / sub> + 3t 2 (1-t)x 2 + t 3 x 3
B) y =(1-t) 3 y 0 + 3t(1-t) 2 y 1 < / sub> + 3t 2 (1-t)y 2 + t 3 y 3

我们想要的是一个函数y(x),给定一个x坐标,它将返回曲线的相应y坐标。为此,曲线必须从左向右单调移动,以便在不同的y位置上不会多次占用相同的x坐标。确保这一点的最简单方法是限制输入点,以便 x 0 &lt; x 3 x 1 ,x 2 ε[x 0 ,x <子> 3 ] 即可。换句话说,P 0 必须位于P 3 的左侧,并且它们之间有两个句柄。

为了计算给定x的y,我们必须首先从x确定t。从t得到y就是将t应用于等式B的简单问题。

我看到两种方法来确定给定y的t。

首先,您可以尝试二进制搜索t。以0的下限和1的上限开始,并通过等式A计算t的这些值的x。保持对该间隔进行二等分,直到得到一个相当接近的近似值。虽然这应该可以正常工作,但它既不会特别快速也不会非常精确(至少不会同时发生)。

第二种方法是实际解决方程式A的t。这有点难以实现,因为方程是立方的。另一方面,计算变得非常快,并产生精确的结果。

等式A可以改写为
( - X <子> 0 + 3×<子> 1 -3x <子> 2 + X <子> 3 )吨 3 +(3x 0 -6x 1 + 3x 2 )t 2 +( - 3x < sub> 0 + 3x 1 )t +(x 0 -x)= 0
插入x 0 .. x 3 的实际值,我们得到 a t 3 形式的三次方程式+ b t 2 + c * t + d = 0 我们知道在[0,1]内只有一个解。我们现在可以使用this Stack Overflow answer中发布的算法来解决这个等式。

以下是一个小C#课程,演示了这种方法。它应该很简单,可以将其转换为您选择的语言。

using System;

public class Point {
    public Point(double x, double y) {
        X = x;
        Y = y;
    }
    public double X { get; private set; }
    public double Y { get; private set; }
}

public class BezierCurve {
    public BezierCurve(Point p0, Point p1, Point p2, Point p3) {
        P0 = p0;
        P1 = p1;
        P2 = p2;
        P3 = p3;
    }

    public Point P0 { get; private set; }
    public Point P1 { get; private set; }
    public Point P2 { get; private set; }
    public Point P3 { get; private set; }

    public double? GetY(double x) {
        // Determine t
        double t;
        if (x == P0.X) {
            // Handle corner cases explicitly to prevent rounding errors
            t = 0;
        } else if (x == P3.X) {
            t = 1;
        } else {
            // Calculate t
            double a = -P0.X + 3 * P1.X - 3 * P2.X + P3.X;
            double b = 3 * P0.X - 6 * P1.X + 3 * P2.X;
            double c = -3 * P0.X + 3 * P1.X;
            double d = P0.X - x;
            double? tTemp = SolveCubic(a, b, c, d);
            if (tTemp == null) return null;
            t = tTemp.Value;
        }

        // Calculate y from t
        return Cubed(1 - t) * P0.Y
            + 3 * t * Squared(1 - t) * P1.Y
            + 3 * Squared(t) * (1 - t) * P2.Y
            + Cubed(t) * P3.Y;
    }

    // Solves the equation ax³+bx²+cx+d = 0 for x ϵ ℝ
    // and returns the first result in [0, 1] or null.
    private static double? SolveCubic(double a, double b, double c, double d) {
        if (a == 0) return SolveQuadratic(b, c, d);
        if (d == 0) return 0;

        b /= a;
        c /= a;
        d /= a;
        double q = (3.0 * c - Squared(b)) / 9.0;
        double r = (-27.0 * d + b * (9.0 * c - 2.0 * Squared(b))) / 54.0;
        double disc = Cubed(q) + Squared(r);
        double term1 = b / 3.0;

        if (disc > 0) {
            double s = r + Math.Sqrt(disc);
            s = (s < 0) ? -CubicRoot(-s) : CubicRoot(s);
            double t = r - Math.Sqrt(disc);
            t = (t < 0) ? -CubicRoot(-t) : CubicRoot(t);

            double result = -term1 + s + t;
            if (result >= 0 && result <= 1) return result;
        } else if (disc == 0) {
            double r13 = (r < 0) ? -CubicRoot(-r) : CubicRoot(r);

            double result = -term1 + 2.0 * r13;
            if (result >= 0 && result <= 1) return result;

            result = -(r13 + term1);
            if (result >= 0 && result <= 1) return result;
        } else {
            q = -q;
            double dum1 = q * q * q;
            dum1 = Math.Acos(r / Math.Sqrt(dum1));
            double r13 = 2.0 * Math.Sqrt(q);

            double result = -term1 + r13 * Math.Cos(dum1 / 3.0);
            if (result >= 0 && result <= 1) return result;

            result = -term1 + r13 * Math.Cos((dum1 + 2.0 * Math.PI) / 3.0);
            if (result >= 0 && result <= 1) return result;

            result = -term1 + r13 * Math.Cos((dum1 + 4.0 * Math.PI) / 3.0);
            if (result >= 0 && result <= 1) return result;
        }

        return null;
    }

    // Solves the equation ax² + bx + c = 0 for x ϵ ℝ
    // and returns the first result in [0, 1] or null.
    private static double? SolveQuadratic(double a, double b, double c) {
        double result = (-b + Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);
        if (result >= 0 && result <= 1) return result;

        result = (-b - Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);
        if (result >= 0 && result <= 1) return result;

        return null;
    }

    private static double Squared(double f) { return f * f; }

    private static double Cubed(double f) { return f * f * f; }

    private static double CubicRoot(double f) { return Math.Pow(f, 1.0 / 3.0); }
}

答案 1 :(得分:0)

您有几个选择:

假设你的曲线函数F(t)取一个从0到1的参数t,其中F(0)是曲线的起点,F(1)是曲线的终点。

您可以通过以每单位时间不变的变化递增t来为曲线上的运动设置动画。 所以t由函数T(时间)=常数*时间

定义

例如,如果您的帧是1/24秒,并且您希望以每秒0.1单位t的速率沿曲线移动,那么每帧增加t 0.1(t / s)* 1/24(秒/帧)。

这里的一个缺点是,您每单位时间的实际行驶速度或行驶距离不会是恒定的。这取决于您的控制点的位置。

如果要沿曲线均匀缩放速度,可以修改每单位时间t的常量变化。但是,如果您希望速度发生显着变化,您将发现难以控制曲线的形状。如果您希望一个端点处的速度更大,则必须将控制点进一步移开,这反过来将曲线的形状拉向该点。如果这是一个问题,您可以考虑对t使用非常数函数。有多种方法有不同的权衡取舍,我们需要了解有关您的问题的更多细节,以提出解决方案。例如,过去我允许用户定义每个关键帧的速度,并使用查找表从时间转换为参数t,这样关键帧速度之间的速度会发生线性变化(这很复杂)。

另一个常见的挂断:如果您通过连接多条贝塞尔曲线进行动画处理,并且希望在曲线之间移动时速度是连续的,那么您需要约束控制点,使它们与相邻曲线对称。 Catmull-Rom样条是一种常见的方法。

答案 2 :(得分:0)

answered a similar question here。基本上,如果您事先知道控制点,那么您可以将f(t)函数转换为y(x)函数。为了不必手动完成所有操作,您可以使用Wolfram Alpha等服务来帮助您进行数学计算。