Интерполяция значений между интервалами, интерполяция по кривой Безье - PullRequest
5 голосов
/ 04 мая 2011

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

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

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

Ответы [ 3 ]

3 голосов
/ 11 апреля 2013

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

Стандартная кубическая кривая Безье в 2D-пространстве может быть определена четырьмя точками 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 для любой заданной точки вдоль кривой затем можно определить с помощью уравнений
А) х = (1-т) 3 х 0 + 3т (1-т) 2 х 1 + 3т 2 (1-т) х 2 + т 3 х 3 и
B) y = (1-т) 3 y 0 + 3т (1-т) 2 y 1 + 3т 2 (1-т) у 2 + т 3 у 3 .

Нам нужна функция y (x), которая при заданной координате x возвращает соответствующую координату y кривой. Чтобы это работало, кривая должна монотонно двигаться слева направо, чтобы она не занимала одну и ту же координату x более одного раза в разных положениях y. Самый простой способ гарантировать это - ограничить входные точки так, чтобы x 0 3 и x 1 , x 2 ϵ [x 0 , x 3 ] . Другими словами, P 0 должен быть слева от P 3 с двумя ручками между ними.

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

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

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

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

Уравнение А можно переписать как
(- 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 .
Вставив ваши фактические значения для x 0 .. x 3 , мы получим кубическое уравнение вида a t 3 + b t 2 + 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); }
}
0 голосов
/ 11 апреля 2013

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

0 голосов
/ 06 мая 2011

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

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

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

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

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

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

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

...