Rak*_*aks 5 math animation bezier core-animation
为了实现2D动画,我正在寻找两个关键帧之间的插值,其中变化的速度由贝塞尔曲线定义.问题是贝塞尔曲线以参数形式表示,而要求是能够评估特定时间的值.
详细说来,假设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并知道每个帧的值
我面临着同样的问题:每个动画包似乎都使用 B\xc3\xa9zier 曲线来控制随时间变化的值,但没有关于如何将 B\xc3\xa9zier 曲线实现为 ay(x) 的信息功能。这就是我的想法。
\n\n二维空间中的标准三次 B\xc3\xa9zier 曲线可以由四个点P 0 =(x 0 , y 0 ) .. P 3 =(x 3 , y 3 )定义。
\nP 0和 P 3是曲线的端点,而 P 1和 P 2是影响其形状的控制柄。使用参数 t \xcf\xb5 [0, 1],可以使用以下方程确定曲线上任何给定点的 x 和 y 坐标
\nA) x = (1-t) 3 x 0 + 3t(1 -t) 2 x 1 + 3t 2 (1-t)x 2 + t 3 x 3且
\nB) y = (1-t) 3 y 0 + 3t(1-t) 2 y 1 + 3t 2 (1 -t)y 2 + t 3 y 3。
我们想要的是一个函数 y(x),给定 x 坐标,它将返回曲线相应的 y 坐标。为此,曲线必须从左向右单调移动,这样它就不会在不同的 y 位置上多次占据相同的 x 坐标。确保这一点的最简单方法是限制输入点,使x 0 < x 3和x 1 , x 2 \xcf\xb5 [x 0 , x 3 ]。换句话说,P 0必须位于 P 3的左侧,两个手柄位于它们之间。
\n\n为了计算给定 x 的 y,我们必须首先根据 x 确定 t。将 t 代入方程 B 即可简单地从 t 得到 y。
\n\n我看到有两种方法可以确定给定 y 的 t。
\n\n首先,您可以尝试对 t 进行二分搜索。从下限 0 和上限 1 开始,通过方程 A 计算 t 的这些值的 x。继续平分间隔,直到获得相当接近的近似值。虽然这应该可以正常工作,但它既不会特别快,也不会非常精确(至少不会同时两者)。
\n\n第二种方法是实际求解方程 A 的 t。这实施起来有点困难,因为方程是三次的。另一方面,计算变得非常快并产生精确的结果。
\n\n方程 A 可以重写为
\n (-x 0 +3x 1 -3x 2 +x 3 )t 3 + (3x 0 -6x 1 +3x 2 )t 2 + (-3x 0 +3x 1 )t + (x 0 -x) = 0。\n插入 x 0 ..x 3
的实际值,我们得到一个形式为a t 3 + b t 2 + c*t + d = 0的三次方程,我们知道 [0, 1 内只有一个解]。现在,我们可以使用Stack Overflow 答案中发布的算法来求解该方程。
下面是一个演示此方法的 C# 小类。它应该足够简单,可以将其转换为您选择的语言。
\n\nusing System;\n\npublic class Point {\n public Point(double x, double y) {\n X = x;\n Y = y;\n }\n public double X { get; private set; }\n public double Y { get; private set; }\n}\n\npublic class BezierCurve {\n public BezierCurve(Point p0, Point p1, Point p2, Point p3) {\n P0 = p0;\n P1 = p1;\n P2 = p2;\n P3 = p3;\n }\n\n public Point P0 { get; private set; }\n public Point P1 { get; private set; }\n public Point P2 { get; private set; }\n public Point P3 { get; private set; }\n\n public double? GetY(double x) {\n // Determine t\n double t;\n if (x == P0.X) {\n // Handle corner cases explicitly to prevent rounding errors\n t = 0;\n } else if (x == P3.X) {\n t = 1;\n } else {\n // Calculate t\n double a = -P0.X + 3 * P1.X - 3 * P2.X + P3.X;\n double b = 3 * P0.X - 6 * P1.X + 3 * P2.X;\n double c = -3 * P0.X + 3 * P1.X;\n double d = P0.X - x;\n double? tTemp = SolveCubic(a, b, c, d);\n if (tTemp == null) return null;\n t = tTemp.Value;\n }\n\n // Calculate y from t\n return Cubed(1 - t) * P0.Y\n + 3 * t * Squared(1 - t) * P1.Y\n + 3 * Squared(t) * (1 - t) * P2.Y\n + Cubed(t) * P3.Y;\n }\n\n // Solves the equation ax\xc2\xb3+bx\xc2\xb2+cx+d = 0 for x \xcf\xb5 \xe2\x84\x9d\n // and returns the first result in [0, 1] or null.\n private static double? SolveCubic(double a, double b, double c, double d) {\n if (a == 0) return SolveQuadratic(b, c, d);\n if (d == 0) return 0;\n\n b /= a;\n c /= a;\n d /= a;\n double q = (3.0 * c - Squared(b)) / 9.0;\n double r = (-27.0 * d + b * (9.0 * c - 2.0 * Squared(b))) / 54.0;\n double disc = Cubed(q) + Squared(r);\n double term1 = b / 3.0;\n\n if (disc > 0) {\n double s = r + Math.Sqrt(disc);\n s = (s < 0) ? -CubicRoot(-s) : CubicRoot(s);\n double t = r - Math.Sqrt(disc);\n t = (t < 0) ? -CubicRoot(-t) : CubicRoot(t);\n\n double result = -term1 + s + t;\n if (result >= 0 && result <= 1) return result;\n } else if (disc == 0) {\n double r13 = (r < 0) ? -CubicRoot(-r) : CubicRoot(r);\n\n double result = -term1 + 2.0 * r13;\n if (result >= 0 && result <= 1) return result;\n\n result = -(r13 + term1);\n if (result >= 0 && result <= 1) return result;\n } else {\n q = -q;\n double dum1 = q * q * q;\n dum1 = Math.Acos(r / Math.Sqrt(dum1));\n double r13 = 2.0 * Math.Sqrt(q);\n\n double result = -term1 + r13 * Math.Cos(dum1 / 3.0);\n if (result >= 0 && result <= 1) return result;\n\n result = -term1 + r13 * Math.Cos((dum1 + 2.0 * Math.PI) / 3.0);\n if (result >= 0 && result <= 1) return result;\n\n result = -term1 + r13 * Math.Cos((dum1 + 4.0 * Math.PI) / 3.0);\n if (result >= 0 && result <= 1) return result;\n }\n\n return null;\n }\n\n // Solves the equation ax\xc2\xb2 + bx + c = 0 for x \xcf\xb5 \xe2\x84\x9d\n // and returns the first result in [0, 1] or null.\n private static double? SolveQuadratic(double a, double b, double c) {\n double result = (-b + Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);\n if (result >= 0 && result <= 1) return result;\n\n result = (-b - Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);\n if (result >= 0 && result <= 1) return result;\n\n return null;\n }\n\n private static double Squared(double f) { return f * f; }\n\n private static double Cubed(double f) { return f * f * f; }\n\n private static double CubicRoot(double f) { return Math.Pow(f, 1.0 / 3.0); }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
您有几个选择:
假设您的曲线函数 F(t) 采用范围从 0 到 1 的参数 t,其中 F(0) 是曲线的起点,F(1) 是曲线的终点。
您可以通过以每单位时间的恒定变化增加 t 来制作沿着曲线的运动动画。所以 t 由函数 T(time) = Constant*time 定义
例如,如果您的帧为 1/24 秒,并且您希望以每秒 0.1 单位 t 的速率沿曲线移动,则每帧将 t 增加 0.1 (t/s) * 1/24 (秒/帧)。
这里的一个缺点是您的实际速度或每单位时间行驶的距离不会恒定。这取决于控制点的位置。
如果要沿曲线均匀缩放速度,可以修改每单位时间 t 的恒定变化。但是,如果您希望速度发生显着变化,您会发现很难控制曲线的形状。如果您希望一个端点处的速度更大,则必须将控制点移得更远,这反过来又会将曲线的形状拉向该点。如果这是一个问题,您可以考虑对 t 使用非常量函数。有多种方法和不同的权衡,我们需要了解有关您的问题的更多详细信息才能提出解决方案。例如,过去我允许用户定义每个关键帧的速度,并使用查找表将时间转换为参数 t,以便关键帧速度之间的速度存在线性变化(这很复杂)。
另一个常见的问题:如果您通过连接多条贝塞尔曲线来制作动画,并且希望在曲线之间移动时速度是连续的,那么您将需要约束控制点,使它们与相邻曲线对称。Catmull-Rom样条是一种常见的方法。