Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Безье, кривые и пути
Кривые Безье (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);
}
И тогда выход будет двигаться с постоянной скоростью: