Безье, кривые и пути

Кривые Безье (Bezier curves) — это математическая аппроксимация естественных геометрических фигур. Мы используем их для представления кривой с минимальным количеством информации и высокой степенью гибкости.

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

Они основаны на interpolation, которую мы видели в предыдущей статье, объединяя несколько этапов для создания плавных кривых. Чтобы лучше понять принцип работы кривых Безье, давайте начнём с их простейшей формы: квадратичной кривых Безье.

Квадратичная кривая Безье

Возьмем три минимальные точки, необходимые для того, чтобы квадратичная функция Безье сработала:

../../_images/bezier_quadratic_points.png

Чтобы нарисовать кривую между ними, мы сначала постепенно интерполируем по двум вершинам каждого из двух сегментов, образованных тремя точками, используя значения в диапазоне от 0 до 1. Это дает нам две точки, которые перемещаются вдоль сегментов по мере того, как мы изменяем значение t от 0 до 1.

func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)

Затем мы интерполируем q0 и q1, чтобы получить единственную точку r, которая движется вдоль кривой.

var r = q0.lerp(q1, t)
return r

Этот тип кривой называется Квадратичной кривой Безье.

../../_images/bezier_quadratic_points2.gif

(Image credit: Wikipedia)

Кубическая кривая Безье

Основываясь на предыдущем примере, мы можем получить больший контроль, выполнив интерполяцию между четырьмя точками.

../../_images/bezier_cubic_points.png

Сначала мы используем функцию с четырьмя параметрами, чтобы принять на вводе четыре точки, p0, p1, p2 и p3:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):

Мы применяем линейную интерполяцию к каждой паре точек, чтобы уменьшить их количество до трех:

var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)

Затем мы берем наши три точки и сокращаем их до двух:

var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)

И одному:

var s = r0.lerp(r1, t)
return s

Вот полная функция:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)
    var q2 = p2.lerp(p3, t)

    var r0 = q0.lerp(q1, t)
    var r1 = q1.lerp(q2, t)

    var s = r0.lerp(r1, t)
    return s

Результатом будет плавная кривая, интерполируемая между всеми четырьмя точками:

../../_images/bezier_cubic_points.gif

(Image credit: Wikipedia)

Примечание

Кубическая интерполяция Безье работает так же и в 3D, просто используйте Vector3 вместо Vector2.

Добавление контрольных точек

Основываясь на кубической кривой Безье, мы можем изменить способ работы двух точек, чтобы свободно управлять формой нашей кривой. Вместо p0, p1, p2 и p3 мы сохраним их как:

  • point0 = p0: Это первая точка, источник

  • control0 = p1 - p0: Это вектор относительно первой контрольной точки

  • control1 = p3 - p2: Это вектор относительно второй контрольной точки

  • point1 = p3: Это вторая точка, пункт назначения

Таким образом, у нас есть две точки и две контрольные точки, которые являются относительными векторами к соответствующим точкам. Если вы раньше использовали графические или анимационные программы, это может показаться знакомым:

../../_images/bezier_cubic_handles.png

Именно так графическое программное обеспечение представляет пользователям кривые Безье, а также то, как они работают и выглядят в Godot.

Curve2D, Curve3D, Path и Path2D

Существует два объекта, содержащих кривые: Curve3D и Curve2D (для 3D и 2D соответственно).

Они могут содержать несколько точек, что позволяет использовать более длинные пути. Также их можно задать узлам: Path3D и Path2D (также для 3D и 2D соответственно):

../../_images/bezier_path_2d.png

Однако их использование может быть не совсем очевидным, поэтому ниже приведено описание наиболее распространенных вариантов использования кривых Безье.

Оценка

Единственным вариантом может быть их оценка, но в большинстве случаев это не очень полезно. Большим недостатком кривых Безье является то, что если вы проходите их с постоянной скоростью от t = 0 до t = 1, фактическая интерполяция не будет двигаться с постоянной скоростью. Скорость также является интерполяцией между расстояниями между точками p0, p1, p2 и p3, и не существует математически простого способа прохождения кривой с постоянной скоростью.

Давайте рассмотрим пример со следующим псевдокодом:

var t = 0.0

func _process(delta):
    t += delta
    position = _cubic_bezier(p0, p1, p2, p3, t)
../../_images/bezier_interpolation_speed.gif

Как видите, скорость окружности (в пикселях в секунду) меняется, хотя t увеличивается с постоянной скоростью. Это затрудняет использование кривых Безье для чего-либо практического, изначально заданного.

Отрисовка

Рисование кривых Безье (или объектов, основанных на них) — очень распространённая задача, но и непростая. Практически в любом случае кривые Безье необходимо преобразовать в какие-либо сегменты. Однако обычно это затруднительно, если только не создавать их в очень большом количестве.

Причина в том, что некоторые участки кривой (в частности, углы) могут потребовать значительного количества точек, тогда как другие участки могут не потребовать:

../../_images/bezier_point_amount.png

Кроме того, если бы обе контрольные точки были 0, 0 (помните, что это относительные векторы), кривая Безье была бы просто прямой линией (поэтому рисование большого количества точек было бы расточительством).

Перед построением кривых Безье требуется тесселяция (tessellation). Это часто делается с помощью рекурсивной функции или функции «разделяй и властвуй», которая разбивает кривую до тех пор, пока степень кривизны не станет меньше определённого порогового значения.

Классы Curve предоставляют это через функцию Curve2D.tessellate() (которая принимает необязательные аргументы этапы (stages) рекурсии и допуск (tolerance) угла). Таким образом, рисовать что-либо на основе кривой становится проще.

Траверс

Последний распространённый вариант использования кривых — их прохождение. Из-за того, что уже упоминалось о постоянной скорости, это также затруднительно.

Чтобы упростить задачу, кривые необходимо запечь в равноудалённых точках. Таким образом, их можно аппроксимировать обычной интерполяцией (которую можно улучшить с помощью кубической функции). Для этого достаточно использовать метод Curve3D.sample_baked() вместе с методом Curve2D.get_baked_length(). Первый вызов любого из них запечёт кривую внутри.

Тогда обход с постоянной скоростью можно осуществить с помощью следующего псевдокода:

var t = 0.0

func _process(delta):
    t += delta
    position = curve.sample_baked(t * curve.get_baked_length(), true)

И тогда выход будет двигаться с постоянной скоростью:

../../_images/bezier_interpolation_baked.gif