Безье, кривые и пути
Кривые Безье (Bezier curves) — это математическая аппроксимация естественных геометрических фигур. Мы используем их для представления кривой с минимальным количеством информации и высокой степенью гибкости.
В отличие от более абстрактных математических понятий, кривые Безье были созданы для промышленного дизайна. Они являются популярным инструментом в индустрии графического программного обеспечения.
Они основаны на interpolation, которую мы видели в предыдущей статье, объединяя несколько этапов для создания плавных кривых. Чтобы лучше понять принцип работы кривых Безье, давайте начнём с их простейшей формы: квадратичной кривых Безье.
Квадратичная кривая Безье
Возьмем три минимальные точки, необходимые для того, чтобы квадратичная функция Безье сработала:
Чтобы нарисовать кривую между ними, мы сначала постепенно интерполируем по двум вершинам каждого из двух сегментов, образованных тремя точками, используя значения в диапазоне от 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)
private Vector2 QuadraticBezier(Vector2 p0, Vector2 p1, Vector2 p2, float t)
{
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
}
Затем мы интерполируем q0 и q1, чтобы получить единственную точку r, которая движется вдоль кривой.
var r = q0.lerp(q1, t)
return r
Vector2 r = q0.Lerp(q1, t);
return r;
Этот тип кривой называется Квадратичной кривой Безье.
(Image credit: Wikipedia)
Кубическая кривая Безье
Основываясь на предыдущем примере, мы можем получить больший контроль, выполнив интерполяцию между четырьмя точками.
Сначала мы используем функцию с четырьмя параметрами, чтобы принять на вводе четыре точки, p0, p1, p2 и p3:
func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
public Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
}
Мы применяем линейную интерполяцию к каждой паре точек, чтобы уменьшить их количество до трех:
var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
Vector2 q2 = p2.Lerp(p3, t);
Затем мы берем наши три точки и сокращаем их до двух:
var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)
Vector2 r0 = q0.Lerp(q1, t);
Vector2 r1 = q1.Lerp(q2, t);
И одному:
var s = r0.lerp(r1, t)
return s
Vector2 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
private Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
Vector2 q2 = p2.Lerp(p3, t);
Vector2 r0 = q0.Lerp(q1, t);
Vector2 r1 = q1.Lerp(q2, t);
Vector2 s = r0.Lerp(r1, t);
return s;
}
Результатом будет плавная кривая, интерполируемая между всеми четырьмя точками:
(Image credit: Wikipedia)
Примечание
Кубическая интерполяция Безье работает так же и в 3D, просто используйте Vector3 вместо Vector2.
Добавление контрольных точек
Основываясь на кубической кривой Безье, мы можем изменить способ работы двух точек, чтобы свободно управлять формой нашей кривой. Вместо p0, p1, p2 и p3 мы сохраним их как:
point0 = p0: Это первая точка, источник
control0 = p1 - p0: Это вектор относительно первой контрольной точки
control1 = p3 - p2: Это вектор относительно второй контрольной точки
point1 = p3: Это вторая точка, пункт назначения
Таким образом, у нас есть две точки и две контрольные точки, которые являются относительными векторами к соответствующим точкам. Если вы раньше использовали графические или анимационные программы, это может показаться знакомым:
Именно так графическое программное обеспечение представляет пользователям кривые Безье, а также то, как они работают и выглядят в Godot.
Curve2D, Curve3D, Path и Path2D
Существует два объекта, содержащих кривые: Curve3D и Curve2D (для 3D и 2D соответственно).
Они могут содержать несколько точек, что позволяет использовать более длинные пути. Также их можно задать узлам: Path3D и Path2D (также для 3D и 2D соответственно):
Однако их использование может быть не совсем очевидным, поэтому ниже приведено описание наиболее распространенных вариантов использования кривых Безье.
Оценка
Единственным вариантом может быть их оценка, но в большинстве случаев это не очень полезно. Большим недостатком кривых Безье является то, что если вы проходите их с постоянной скоростью от 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)
private float _t = 0.0f;
public override void _Process(double delta)
{
_t += (float)delta;
Position = CubicBezier(p0, p1, p2, p3, _t);
}
Как видите, скорость окружности (в пикселях в секунду) меняется, хотя t увеличивается с постоянной скоростью. Это затрудняет использование кривых Безье для чего-либо практического, изначально заданного.
Отрисовка
Рисование кривых Безье (или объектов, основанных на них) — очень распространённая задача, но и непростая. Практически в любом случае кривые Безье необходимо преобразовать в какие-либо сегменты. Однако обычно это затруднительно, если только не создавать их в очень большом количестве.
Причина в том, что некоторые участки кривой (в частности, углы) могут потребовать значительного количества точек, тогда как другие участки могут не потребовать:
Кроме того, если бы обе контрольные точки были 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)
private float _t = 0.0f;
public override void _Process(double delta)
{
_t += (float)delta;
Position = curve.SampleBaked(_t * curve.GetBakedLength(), true);
}
И тогда выход будет двигаться с постоянной скоростью: