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.

Використання 3D перетворень

Вступ

Якщо ви досі ніколи не розробляли 3D ігор, то обертання у трьох вимірах, спершу, може вам здатись незрозумілим. Після 2D вам може здатись: „О! Це прямо як в 2D, але з додатковими осями!“.

Спочатку це здається легким. Для простих ігор такого способу мислення може бути навіть достатньо. Але, на жаль, це враження зазвичай оманливе.

Кути у три-вимірному просторі найчастіше називають "Кутами Ейлера".

../../_images/transforms_euler.webp

Кути Ейлера були описані математиком Леонардом Ейлером на початку 18ст.

../../_images/transforms_euler_himself.png

Такий підхід до опису 3D орієнтації був проривним для свого часу, але він має декілька недоліків коли використовується при розробці ігор (чого ще очікувати від чоловіка у смішному капелюсі). Ця стаття покликана пояснити чому так, а також описати найкращі шляхи роботи з перетвореннями 3D об'єктів.

Проблеми кутів Ейлера

Хоча й здається, що об'єкт можна просто обертати навколо кожної з осей, на ділі це практично.

Порядок осей

Головна причина цього в тому, що не існує єдиного способу отримати орієнтацію з кутів. Немає стандартної математичної функції, яка приймає всі кути й повертає реальне положення об'єкта. Єдиний спосіб отримати це положення — обертати об'єкт за цими кутами в певному порядку.

Це можна зробити, покрутивши об'єкт спершу по осі X, потім — по Y і потім — по Z. Або ж, ви можете спершу покрутити по осі Y, а потім — по Z і нарешті — по X. Будь-який спосіб підходить, але кінцевий результат не обов'язково буде однаковим. А значить — існує декілька способів знаходження обертання за трьома кутами, залежно від порядку обертань.

Нижче наведене зображення осей обертання (в порядку X, Y, Z) у вигляді карданного підвісу (з Вікіпедії). Як бачите, положення кожної з осей залежить від повороту попередньої осі:

../../_images/transforms_gimbal.gif

У вас може виникнути питання: „Як це може вплинути на мене?“. Погляньмо на приклад з практики:

Уявіть, що ви працюєте над управлінням від першої особи (як в шутерах). Рух курсора в ліво-право повинен контролювати той кут огляду, що паралельний до землі. Натомість, рухаючи курсор вгору й вниз гравець переміщує свій огляд відповідно вгору та вниз.

В такому разі, щоб досягти необхідного результату, обертати спершу потрібно вісь Y (в нашому випадку вісь „вгору“, так як Godot використовує орієнтацію „Y-Up“), а потім — вісь X.

../../_images/transforms_rotate1.gif

Якби ми спершу повернули вісь X, а потім — Y, то ми отримали б небажаний результат:

../../_images/transforms_rotate2.gif

Залежно від типу гри й результату, якого ви хочете досягти, порядок, в якому потрібно обертати об'єкт, може відрізнятись. Тому, застосовувати обертання в порядку X, Y, Z не достатньо: потрібно також знати й порядок обертання.

Інтерполяція

Ще одна проблема, яка виникає при роботі з кутами Ейлера — це інтерполяція. Уявіть, що ви хочете зробити плавний перехід між двома різними камерами чи положеннями ворогів (включно з їх орієнтаціями). Найпростіший спосіб — інтерполювати значення кута по кожній з осей незалежно. Отриманий результат повинен бути ось таким:

../../_images/transforms_interpolate1.gif

Але кути не завжди працюють так, як повинні:

../../_images/transforms_interpolate2.gif

Насправді, камера крутиться у протилежний бік!

Є декілька причин, чому могло так трапитись:

  • Обертання не були лінійно прив'язані до орієнтації, а тому інтерполяція не завжди обирає найкоротший шлях (як-от шлях від 270 до 0 градусів — це не те ж саме що шлях від 270 до 360 хоч це й однакові кути).

  • Сталось блокування обертання (перша вісь обертання вирівнялась з останньою й система втратила одну степінь свободи). Подивіться статтю по блокування обертання якщо хочете дізнатися більше.

Скажіть „Ні“ кутам Ейлера

Результатом усього цього є те, що ви не повинні використовувати властивість rotation вузлів class_Node3D у Godot для ігор. Він призначений для використання в основному в редакторі, для узгодженості з двовимірним двигуном і для простих поворотів (зазвичай лише однієї осі або навіть двох у деяких випадках). Як би у вас не було спокуси, не використовуйте його.

Натомість, існує кращий спосіб вирішити проблему обертання.

Знайомство з перетвореннями

Godot використовує тип даних class_Transform3D для орієнтації. Кожен вузол class_Node3D містить властивість transform, яка є відносною до батьківського перетворення, якщо батьківський тип є похідним від Node3D.

Також, можна отримати глобальне положення об'єкта у властивості global_transform.

Перетворення має Basis (підвластивість transform.basis), яка складається з трьох class_Vector3 векторів. Доступ до них здійснюється через transform.basis і доступ до нього можна отримати безпосередньо за допомогою transform.basis.x, transform.basis.y і transform.basis.z. Кожен вектор вказує в напрямку обертання його осі, отже вони ефективно описують загальне обертання вузла. Масштаб (якщо він рівномірний) також можна визначити за довжиною сокири. Базис також можна інтерпретувати як матрицю 3x3 і використовувати як transform.basis[x][y].

Початковий базис (незмінений) виглядає десь так:

var basis = Basis()
# Contains the following default values:
basis.x = Vector3(1, 0, 0) # Vector pointing along the X axis
basis.y = Vector3(0, 1, 0) # Vector pointing along the Y axis
basis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis

Також він являється аналогом одиничної матриці 3x3.

Відповідно до стандарту OpenGL, X це Права вісь, Y — це вісь Вгору й Z — це вісь Уперед.

Разом з базисом перетворення також має початкову точку. Це Vector3 який описує як далеко об'єкт знаходиться від початку координат (0, 0, 0). З базисом та початковою точкою, перетворення повністю описує переміщення, обертання та масштабування у просторі.

../../_images/transforms_camera.png

Щоб побачити, як працюють перетворення можна поглянути на 3D ґізмо об'єкта, перемкнуте в режим локального простору.

../../_images/transforms_local_space.png

Стрілочки на ґізмо вказують на осі базису X, Y і Z (червоного, зеленого та синього кольорів відповідно). А центр же ґізмо вказує на початкову точку об'єкта.

../../_images/transforms_gizmo.png

Якщо хочете дізнатись більше про векторну математику та перетворення, прочитайте статтю Векторна математика.

Використання перетворень

Звісно, використовувати перетворення не так просто як кути. Та й свої проблеми вони також мають.

Повернути об'єкт можна перемноживши базис його перетворення на базис іншого (це називається композицією) або використавши методи обертання.

var axis = Vector3(1, 0, 0) # Or Vector3.RIGHT
var rotation_amount = 0.1
# Rotate the transform around the X axis by 0.1 radians.
transform.basis = Basis(axis, rotation_amount) * transform.basis
# shortened
transform.basis = transform.basis.rotated(axis, rotation_amount)

Метод у Node3D спрощує це:

# Rotate the transform around the X axis by 0.1 radians.
rotate(Vector3(1, 0, 0), 0.1)
# shortened
rotate_x(0.1)

Він обертає вузол відносно батьківського вузла.

Щоб обертатись відносно власного простору об'єкта потрібно використати наступний код:

# Rotate around the object's local X axis by 0.1 radians.
rotate_object_local(Vector3(1, 0, 0), 0.1)

Вісь повинна бути визначена в місцевій системі координат об'єкта. Наприклад, щоб обертати об’єкт навколо локальних осей X, Y або Z, використовуйте Vector3.RIGHT для осі X, Vector3.UP для осі Y і Vector3.FORWARD для осі Z.

Помилки пов'язані з точністю

Виконання перетворень, з часом, спричиняє втрату точності у зв'язку з обмеженнями чисел з рухомою комою. Через це, довжини осей можуть не дорівнювати точно 1.0 та кути між ними можуть не дорівнювати точно 90 градусів.

Якщо перетворення відбуваються в кожному кадрі, то вони в решті-решт почнуть деформуватись. Уникнути цього неможливо.

Існують два способи справитись з цим. Перший — ортонормалізувати перетворення об'єкта час від часу (можливо раз на кадр, якщо воно змінюється в кожному кадрі):

transform = transform.orthonormalized()

Це зробить довжини всіх осей знову рівними 1.0 а кути між ними — рівними 90 градусів. Хоч в такому випадку масштабування об'єкта буде втрачено.

Рекомендується не масштабувати вузли, якими збираються маніпулювати; натомість масштабувати свої дочірні вузли (наприклад, MeshInstance3D). Якщо вам абсолютно необхідно масштабувати вузол, то повторно застосуйте його в кінці:

transform = transform.orthonormalized()
transform = transform.scaled(scale)

Як отримати дані

Ви можете подумати: „Добре, а як мені отримати кути з матриці перетворення?. Якщо стисло — то ніяк. Вам потрібно перестати думати про кути.

Уявіть, що вам потрібно вистрілити кулею в напрямку, куди дивиться ваш гравець. Просто використовуйте вісь вперед.

# On RigidBody3D.

# Keep in mind that -Z is forward.
bullet.transform = transform
bullet.linear_velocity = -transform.basis.z * BULLET_SPEED

А чи дивиться ворог на гравця? Використайте скалярний добуток щоб дізнатись (про скалярний добуток можете почитати у статті Векторна математика):

# Get the direction vector from player to enemy
var direction = enemy.transform.origin - player.transform.origin
if direction.dot(enemy.transform.basis.z) > 0:
    enemy.im_watching_you(player)

Зміщення вліво:

# On CharacterBody3D.

# Keep in mind that -X is left.
if Input.is_action_pressed("strafe_left"):
    velocity = -transform.basis.x * MOVE_SPEED

move_and_slide()

Стрибок:

# On CharacterBody3D.

# Keep in mind that +Y is up.
if Input.is_action_just_pressed("jump"):
    velocity.y = JUMP_SPEED

move_and_slide()

Загалом, усі дії з об'єктом можна здійснити використовуючи одні лише вектори.

Як задати дані

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

У таких випадках тримайте кути й повороти поза трансформації та встановлюйте їх у кожному кадрі. Не намагайтеся отримати та повторно використати їх, оскільки перетворення не призначене для використання таким чином.

Приклад, який дозволяє озиратись в стилі шутерів від першої особи:

# accumulators
var rot_x = 0
var rot_y = 0

func _input(event):
    if event is InputEventMouseMotion and event.button_mask & 1:
        # modify accumulated mouse rotation
        rot_x -= event.screen_relative.x * LOOKAROUND_SPEED
        rot_y -= event.screen_relative.y * LOOKAROUND_SPEED
        transform.basis = Basis() # reset rotation
        rotate_object_local(Vector3(0, 1, 0), rot_x) # first rotate in Y
        rotate_object_local(Vector3(1, 0, 0), rot_y) # then rotate in X

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

Інтерполяція з кватерніонами

Здійснити інтерполяцію між двома перетвореннями можна використавши кватерніони. Дізнатись, як працюють кватерніони можна деінде, в інтернеті. Зараз же, вам достатньо знати, що їх основна ціль — забезпечувати інтерполяцію найкоротшим шляхом. І якщо у вас є два обертання: кватерніон може плавно переходити від одного, до іншого, по найкоротшій з осей.

Кути обертання можна з легкістю перетворити в кватерніон.

# Convert basis to quaternion, keep in mind scale is lost
var a = Quaternion(transform.basis)
var b = Quaternion(transform2.basis)
# Interpolate using spherical-linear interpolation (SLERP).
var c = a.slerp(b,0.5) # find halfway point between a and b
# Apply back
transform.basis = Basis(c)

Посилання на тип class_Quaternion містить більше інформації про тип даних (воно також може виконувати накопичення трансформації, точки трансформації тощо, хоча це використовується рідше). Якщо ви інтерполюєте або застосовуєте операції до кватерніонів багато разів, майте на увазі, що їх потрібно зрештою нормалізувати. Інакше вони також страждатимуть від чисельних помилок точності.

Кватерніони корисні, якщо потрібно здійснити інтерполяцію камери, вздовж шляху, тощо. Вони дадуть правильний та гладенький результат.

Перетворення — це ваші найкращі друзі

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

Не соромтесь задавати питання на цю тему в будь-якій інтернет-спільноті Godot . І, коли ви наберетесь достатньо знань, будь ласка, допоможіть іншим!