Interpolating values between interval, interpolation as per Bezier curve

后端 未结 3 1754
逝去的感伤
逝去的感伤 2021-02-03 14:36

To implement a 2D animation I am looking for interpolating values between two key frames with the velocity of change defined by a Bezier curve. The problem is Bezier curve is re

3条回答
  •  挽巷
    挽巷 (楼主)
    2021-02-03 15:04

    I was facing the same problem: Every animation package out there seems to use Bézier curves to control values over time, but there is no information out there on how to implement a Bézier curve as a y(x) function. So here is what I came up with.

    A standard cubic Bézier curve in 2D space can be defined by the four points P0=(x0, y0) .. P3=(x3, y3).
    P0 and P3 are the end points of the curve, while P1 and P2 are the handles affecting its shape. Using a parameter t ϵ [0, 1], the x and y coordinates for any given point along the curve can then be determined using the equations
    A) x = (1-t)3x0 + 3t(1-t)2x1 + 3t2(1-t)x2 + t3x3 and
    B) y = (1-t)3y0 + 3t(1-t)2y1 + 3t2(1-t)y2 + t3y3.

    What we want is a function y(x) that, given an x coordinate, will return the corresponding y coordinate of the curve. For this to work, the curve must move monotonically from left to right, so that it doesn't occupy the same x coordinate more than once on different y positions. The easiest way to ensure this is to restrict the input points so that x0 < x3 and x1, x2 ϵ [x0, x3]. In other words, P0 must be to the left of P3 with the two handles between them.

    In order to calculate y for a given x, we must first determine t from x. Getting y from t is then a simple matter of applying t to equation B.

    I see two ways of determining t for a given y.

    First, you might try a binary search for t. Start with a lower bound of 0 and an upper bound of 1 and calculate x for these values for t via equation A. Keep bisecting the interval until you get a reasonably close approximation. While this should work fine, it will neither be particularly fast nor very precise (at least not both at once).

    The second approach is to actually solve equation A for t. That's a bit tough to implement because the equation is cubic. On the other hand, calculation becomes really fast and yields precise results.

    Equation A can be rewritten as
    (-x0+3x1-3x2+x3)t3 + (3x0-6x1+3x2)t2 + (-3x0+3x1)t + (x0-x) = 0.
    Inserting your actual values for x0..x3, we get a cubic equation of the form at3 + bt2 + c*t + d = 0 for which we know there is only one solution within [0, 1]. We can now solve this equation using an algorithm like the one posted in this Stack Overflow answer.

    The following is a little C# class demonstrating this approach. It should be simple enough to convert it to a language of your choice.

    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); }
    }
    

提交回复
热议问题