Безьє, криві і шляхи

Криві Безьє є математичним наближенням природних геометричних фігур. Ми використовуємо їх для представлення кривої з якомога меншою кількістю інформації та з високим рівнем гнучкості.

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

Вони покладаються на інтерполяцію, яку ми бачили в попередній статті, поєднуючи кілька кроків для створення плавних кривих. Щоб краще зрозуміти, як працюють криві Безьє, почнемо з її найпростішої форми: Квадратична Безьє.

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

Візьміть три точки, це мінімум, необхідний для роботи Квадратичної Безьє:

../../_images/bezier_quadratic_points.png

Щоб намалювати криву між ними, ми спочатку поступово інтерполюємо дві вершини кожного з двох відрізків, утворених трьома точками, використовуючи значення в діапазоні від 0 до 1. Це дає нам дві точки, які рухаються вздовж сегментів, коли ми змінюємо значення t від 0 до 1.

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

Потім ми інтерполюємо q0 і q1, щоб отримати одну точку r, яка рухається вздовж кривої.

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

Цей тип кривої називається Квадратичною кривою Безьє.

../../_images/bezier_quadratic_points2.gif

(Зображення запозичене з Вікіпедії)

Кубічна крива Безьє

Опираючись на попередній приклад, ми можемо отримати більше контролю, за допомогою інтерполяції між чотирма точками.

../../_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.linear_interpolate(p1, t)
var q1 = p1.linear_interpolate(p2, t)
var q2 = p2.linear_interpolate(p3, t)

Потім беремо наші три точки і зменшуємо їх до двох:

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

І до одної:

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

Ось повна функція:

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

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

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

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

../../_images/bezier_cubic_points.gif

(Зображення запозичене з Вікіпедії)

Примітка

Кубічна інтерполяція Безьє працює так само в 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 відповідно).

Вони можуть містити кілька точок, що дозволяє прокладати довші шляхи. Також можна встановити їх на вузли шляхів: Path та 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 (пам'ятайте, що вони є відносними векторами), крива Безьє була б просто прямою лінією (тому малювання великої кількості точок було б марним).

Перед малюванням кривих Безьє потрібно виконати тесселяцію. Це часто робиться за допомогою рекурсивної, або поділяючої і підпорядковуючої, функції, яка розділяє криву, поки величина кривизни не стане меншою за певний поріг.

Класи кривих Curve забезпечують це за допомогою функції Curve2D.tessellate() (яка отримує необов'язкові аргументи рекурсії stages та кута tolerance). Таким чином, намалювати щось на основі кривої стає простіше.

Обхід

Останній поширений випадок використання кривих - це їх обхід. Через, згадувану раніше, постійну швидкість, це також важко.

Для спрощення, криві потрібно запікати в рівновіддалених точках. Таким чином, їх можна апроксимувати за допомогою регулярної інтерполяції (яку можна поліпшити далі за допомогою кубічного варіанту). Для цього достатньо скористатися методом Curve.interpolate_baked() разом з Curve2D.get_baked_length(). Перший виклик будь-якої з них буде запікати криву всередині.

Таким чином, обхід із постійною швидкістю можна виконати за допомогою такого псевдокоду:

var t = 0.0

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

І в результаті отримаємо рух з постійною швидкістю:

../../_images/bezier_interpolation_baked.gif