Интерполяция значений между интервалами, интерполяция по кривой Безье

Чтобы реализовать 2D-анимацию, я ищу интерполирующие значения между двумя ключевыми кадрами со скоростью изменения, определяемой кривой Безье. Проблема в том, что кривая Безье представлена ​​​​в параметрической форме, тогда как требование состоит в том, чтобы иметь возможность оценить значение для определенного времени.

Чтобы уточнить, скажем, значение 10 и 40 должно быть интерполировано в течение 4 секунд, при этом значение изменяется не постоянно, а в соответствии с кривой Безье, представленной как 0,0 0,2,0,3 0,5,0,5 1,1. Теперь, если я рисую со скоростью 24 кадра в секунду, мне нужно оценивать значение для каждого кадра. Как я могу это сделать ? Я посмотрел на алгоритм Де Кастельжо и подумал, что разделение кривой на 24 * 4 части за 4 секунды решит мою проблему, но это звучит ошибочно, поскольку время идет по оси «х», а не по кривой.

Для дальнейшего упрощения. Если я рисую кривую на плоскости, ось x представляет время, а ось y - значение, которое я ищу. Что мне действительно нужно, так это иметь возможность найти «y», соответствующий «x». Затем я могу разделить x на 24 деления и узнать значение для каждого кадра.


person Raks    schedule 04.05.2011    source источник


Ответы (3)


Я столкнулся с той же проблемой: кажется, что каждый пакет анимации использует кривые Безье для управления значениями во времени, но нет никакой информации о том, как реализовать кривую Безье как функцию y(x). Итак, вот что я придумал.

Стандартная кубическая кривая Безье в двумерном пространстве может быть определена четырьмя точками P0=(x0, y0) . , P3=(x3, y3).
P0 и P3 — конечные точки кривой, а P1 и P2 — маркеры, влияющие на ее форму. Используя параметр t ϵ [0, 1], координаты x и y для любой заданной точки на кривой можно определить с помощью уравнений
A) x = (1-t)3< /sup>x0 + 3t(1-t)2x1 + 3t2(1-t )x2 + t3x3 и
B) y = (1-t)< sup>3y0 + 3t(1-t)2y1 + 3t2 (1-t)y2 + t3y3.

Нам нужна функция y(x), которая по координате x возвращает соответствующую координату y кривой. Чтобы это работало, кривая должна монотонно двигаться слева направо, чтобы она не занимала одну и ту же координату x более одного раза в разных позициях y. Самый простой способ обеспечить это — ограничить входные точки таким образом, чтобы x0 ‹ x3 и x1< /sub>, x2 ϵ [x0, x3]. Другими словами, P0 должен быть слева от P3 с двумя маркерами между ними.

Чтобы вычислить y для данного x, мы должны сначала определить t из x. Получение y из t является простым применением t к уравнению B.

Я вижу два способа определения t для заданного y.

Во-первых, вы можете попробовать бинарный поиск t. Начните с нижней границы 0 и верхней границы 1 и вычислите x для этих значений для t с помощью уравнения A. Продолжайте делить интервал пополам, пока не получите достаточно близкое приближение. Хотя это должно работать нормально, оно не будет ни особенно быстрым, ни очень точным (по крайней мере, не то и другое одновременно).

Второй подход состоит в том, чтобы фактически решить уравнение A для t. Это немного сложно реализовать, потому что уравнение кубическое. С другой стороны, расчет становится очень быстрым и дает точные результаты.

Уравнение A можно переписать как
(-x0+3x1-3x2+x3< /sub>)t3 + (3x0-6x1+3x2)t2 + (-3x0+3x1)t + (x0-x) = 0.< br> Подставляя ваши фактические значения для x0..x3, мы получаем кубическое уравнение вида at3 + bt2 + c*t + d = 0, для которого, как мы знаем, существует только одно решение в пределах [0, 1]. Теперь мы можем решить это уравнение с помощью алгоритма, подобного тому, который опубликован в этом ответе на переполнение стека.

Ниже приведен небольшой класс 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); }
}
person Daniel Wolf    schedule 10.04.2013

У вас есть несколько вариантов:

Допустим, ваша функция кривой F(t) принимает параметр t в диапазоне от 0 до 1, где F(0) — начало кривой, а F(1) — конец кривой.

Вы можете анимировать движение по кривой, увеличивая t на постоянное изменение в единицу времени. Таким образом, t определяется функцией T (время) = константа * время

Например, если ваш кадр составляет 1/24 секунды, и вы хотите двигаться по кривой со скоростью 0,1 единицы t в секунду, то в каждом кадре вы увеличиваете t на 0,1 (t/s) * 1/24. (сек/кадр).

Недостатком здесь является то, что ваша фактическая скорость или расстояние, пройденное за единицу времени, не будут постоянными. Это будет зависеть от положения ваших контрольных точек.

Если вы хотите равномерно масштабировать скорость вдоль кривой, вы можете изменить постоянное изменение t в единицу времени. Однако, если вы хотите, чтобы скорости резко менялись, вам будет сложно контролировать форму кривой. Если вы хотите, чтобы скорость в одной конечной точке была намного больше, вы должны переместить контрольную точку дальше, что, в свою очередь, сдвинет форму кривой к этой точке. Если это проблема, вы можете рассмотреть возможность использования непостоянной функции для t. Существует множество подходов с различными компромиссами, и нам нужно знать больше о вашей проблеме, чтобы предложить решение. Например, в прошлом я разрешал пользователям определять скорость в каждом ключевом кадре и использовал таблицу поиска для преобразования времени в параметр t, чтобы было линейное изменение скорости между скоростями ключевых кадров (это сложно).

Еще одна распространенная ошибка: если вы анимируете, соединяя несколько кривых Безье, и хотите, чтобы скорость была непрерывной при перемещении между кривыми, вам нужно будет ограничить контрольные точки, чтобы они были симметричны соседней кривой. Сплайны Catmull-Rom являются распространенным подходом.

person Evan Rogers    schedule 06.05.2011

Я отвечал на аналогичный вопрос здесь. По сути, если вы заранее знаете контрольные точки, вы можете преобразовать функцию f (t) в функцию y (x). Чтобы не делать все это вручную, вы можете использовать такие сервисы, как Wolfram Alpha, которые помогут вам с математикой.

person David Rönnqvist    schedule 11.04.2013