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.

Матриці та перетворення

Вступ

Перш ніж читати цей підручник, ми рекомендуємо вам уважно прочитати та зрозуміти підручник Векторна математика, оскільки цей підручник вимагає знання векторів.

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

Більша частина цього посібника зосереджена на 2D і використовує class_Transform2D та class_Vector2, але перетворення в 3D працюють схожим чином.

Примітка

Як згадувалося в попередній статті, важливо пам'ятати, що в Godot, у 2D, вісь Y вказує вниз. На відміну від шкільного курсу лінійної алгебри, де вісь Y вказує вгору.

Примітка

Умовність полягає в тому, що вісь X - червона, вісь Y - зелена, а вісь Z - синя. Ця стаття буде використовувати колір відповідно до цієї умовності, але синім кольором ми також будемо малювати вектор походження.

Компоненти матриці та Матриця Ідентичності

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

../../_images/identity.png

Матриці мають рядки та стовпці, а матриця перетворення має конкретні умовності щодо того, що робить кожен з них.

На зображенні вище ми бачимо, що червоний вектор X представлений першим стовпцем матриці, а зелений вектор Y - представлений другим стовпцем. Зміна стовпців змінить ці вектори. Ми побачимо, як ними можна маніпулювати в наступних прикладах.

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

Коли ми посилаємося на таке значення, як t.x.y, це компонент Y вектора-стовпця X. Іншими словами, нижній лівий кут матриці. Подібним чином, t.x.x – це верхній лівий кут, t.y.x – верхній правий, t.y.y – нижній правий, де t є Transform2D.

Масштабування за допомогою матриці перетворення

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

../../_images/identity-godot.png

Тепер, щоб масштабувати матрицю, все, що нам потрібно зробити, це помножити кожен компонент на потрібний масштаб. Давайте збільшимо його на 2. 1 помножити на 2 стає 2, а 0 помножити на 2 стає 0, тому ми в кінцевому підсумку отримуємо це:

../../_images/scale.png

Щоб зробити це в коді, ми перемножуємо кожен із векторів:

var t = Transform2D()
# Scale
t.x *= 2
t.y *= 2
transform = t # Change the node's transform to what we calculated.

Якби ми хотіли повернути його до початкового масштабу, ми могли б помножити кожен компонент на 0,5. Це майже все, що треба знати для масштабування матриці перетворення.

Щоб обчислити масштаб об’єкта з існуючої матриці перетворення, ви можете використовувати length() для кожного з векторів стовпців.

Примітка

У реальних проектах ви можете використовувати метод scaled() для виконання масштабування.

Обертання за допомогою матриці перетворення

Почнемо з того самого логотипу Godot під матрицею ідентичності:

../../_images/identity-godot.png

Припустимо, що ми хочемо повернути наш логотип за годинниковою стрілкою на 90 градусів. Зараз вісь X вказує праворуч, а вісь Y - вниз. Якщо ми уявімо їх поворот, то зрозуміємо, що нова вісь X повинна вказувати вниз, а нова вісь Y повинна вказувати ліворуч.

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

Нам потрібно представити «вниз» і «ліворуч» у звичайних координатах, тому ми встановимо X як (0, 1) і Y як (-1, 0). Це також значення Vector2.DOWN і Vector2.LEFT. Коли ми це зробимо, ми отримаємо бажаний результат обертання об’єкта:

../../_images/rotate1.png

Якщо у вас виникають проблеми з розумінням вищесказаного, спробуйте цю вправу: виріжте квадрат паперу, намалюйте на ньому вектори X та Y, покладіть його на міліметровий папір, потім поверніть і відзначте кінцеві точки.

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

../../_images/rotate2.png

Примітка

Годо представляє всі обертання в радіанах, а не в градусах. Повний оберт — це радіани TAU або PI*2, а чверть оберту на 90 градусів — це радіани TAU/4 або PI/2. Робота з TAU зазвичай призводить до більш читабельного коду.

Примітка

Цікавий факт: На додаток до того, що Y в Godot вказує вниз, обертання представлене за годинниковою стрілкою. Це означає, що всі математичні та тригерні функції поводяться так само, як і система CCW Y-зверху, оскільки ці відмінності "скасовуються". Ви можете думати, що обертання в обох системах є "від X до Y".

Щоб здійснити поворот на 0,5 радіана (приблизно 28,65 градусів), ми додаємо значення 0,5 до формули вище та обчислюємо, щоб знайти фактичні значення:

../../_images/rotate3.png

Ось як це буде зроблено в коді (помістіть скрипт на вузол Node2D):

var rot = 0.5 # The rotation to apply.
var t = Transform2D()
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
transform = t # Change the node's transform to what we calculated.

Щоб обчислити обертання об’єкта з існуючої матриці перетворення, ви можете використовувати atan2(t.x.y, t.x.x), де t є Transform2D.

Примітка

У реальних проектах ви можете використовувати метод rotated() для виконання обертань.

Основа матриці перетворення

Поки що ми працювали лише з x і y, векторами, які відповідають за представлення обертання, масштабу та/або зсуву (додатково, розглянуто в кінці). Вектори X і Y разом називаються базисом матриці перетворення. Важливо знати терміни «базис» і «базисні вектори».

Ви могли помітити, що class_Transform2D насправді має три значення class_Vector2: x, y і origin. Значення origin не є частиною базису, але воно є частиною перетворення, і нам воно потрібне для представлення позиції. Відтепер ми відстежуватимемо вихідний вектор у всіх прикладах. Ви можете думати про походження як про інший стовпець, але часто краще думати про нього як про зовсім окремий.

Зауважте, що в 3D Godot має окрему структуру Basis для зберігання трьох значень class_Vector3 базису, оскільки код може бути складним і має сенс відокремити його від class_Transform3D (який складається з одного Basis і одного додаткового class_Vector3 для початку).

Переміщення за допомогою матриці перетворення

Зміна вектора початку називається трансляцією матриці перетворення. Переклад — це в основному технічний термін для «переміщення» об’єкта, але він явно не передбачає обертання.

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

../../_images/identity-origin.png

Якщо ми хочемо перемістити об’єкт у позицію (1, 2), нам потрібно встановити його початковий вектор на (1, 2):

../../_images/translate.png

Існує також метод translated_local(), який виконує операції, відмінні від безпосереднього додавання або зміни origin. Метод translated_local() переводитиме об’єкт відносно його власного обертання. Наприклад, об’єкт, повернутий на 90 градусів за годинниковою стрілкою, переміститься вправо, коли translated_local() з Vector2.UP. Для перекладу відносно глобального/батьківського фрейму замість цього використовуйте translated().

Примітка

Godot в 2D використовує координати, засновані на пікселях, тому в реальних проектах ви будете переміщувати на сотні одиниць.

Збираємо все це докупи

Ми збираємося застосувати все, що ми згадували досі, до одного перетворення. Щоб продовжити, створіть проект із вузлом Sprite2D і використовуйте логотип Godot як ресурс текстури.

Давайте встановимо переміщення на (350, 150), поворот на -0,5 радіан і масштаб на 3. Я опублікував скріншот, і код, для його відтворення, але закликаю вас спробувати відтворити скріншот, не заглядаючи в код!

../../_images/putting-all-together.png
var t = Transform2D()
# Translation
t.origin = Vector2(350, 150)
# Rotation
var rot = -0.5 # The rotation to apply.
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
# Scale
t.x *= 3
t.y *= 3
transform = t # Change the node's transform to what we calculated.

Перекоси за допомогою матриці перетворення (додатково)

Примітка

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

Node2D надає властивість зсуву з коробки.

Можливо, ви помітили, що перетворення має більше ступенів свободи, ніж поєднання вищезазначених дій. Основа матриці 2D-перетворення має чотири загальних числа у двох значеннях class_Vector2, тоді як значення обертання та Вектор2 для масштабу мають лише 3 числа. Концепція високого рівня з обмеження ступенів свободи дозволяє перекоси.

Як правило, базові вектори у вас завжди будуть перпендикулярні один одному. Однак перекоси можуть бути корисними в деяких ситуаціях, а їх розуміння допомагає зрозуміти, як працюють перетворення.

Щоб візуально показати вам, як це буде виглядати, давайте накладемо сітку на логотип Godot:

../../_images/identity-grid.png

Кожна точка на цій сітці є отримана за допомогою додавання основних векторів. Нижній правий кут рівний X + Y, а верхній правий кут рівний X - Y. Якщо ми змінимо вектори основи, то з ними зрушиться вся сітка, так як сітка складається з основних векторів. Всі лінії на сітці, які в даний час паралельні, залишаться паралельними, незалежно від того, які зміни ми вносимо в основні вектори.

Як приклад, давайте встановимо Y на (1, 1):

../../_images/shear.png
var t = Transform2D()
# Shear by setting Y to (1, 1)
t.y = Vector2.ONE
transform = t # Change the node's transform to what we calculated.

Примітка

Ви не можете встановити необроблені значення Transform2D у редакторі, тому ви повинні використовувати код, якщо хочете перекосити об'єкт.

Через те, що вектори більше не перпендикулярні, об'єкт перекосився. Нижній центр сітки, який (0, 1) відносно себе, у світовому положенні тепер розташований (1, 1).

Внутрішні координати об'єкта називаються текстурними координатами, тому давайте запозичимо цю термінологію сюди. Щоб знайти світову позицію з відносної позиції використовується формула - U * X + V * Y, де U і V - числа, а X і Y - базові вектори.

Нижній правий кут сітки, який в текстурних координатах завжди знаходиться на (1, 1), світових знаходиться на (2, 1), які розраховуються від X*1 + Y*1, що дорівнює (1, 0) + (1, 1), або (1 + 1, 0 + 1), або (2, 1). Це узгоджується з нашим спостереженням за тим, де знаходиться правий нижній кут зображення.

Аналогічно, верхній правий кут сітки, який в текстурних координатах завжди знаходиться на (1, -1), у світових знаходиться на (0, -1), які вираховуються з X*1 + Y*-1, що дорівнює (1, 0) - (1, 1), або (1 - 1, 0 - 1), або (0, -1). Це узгоджується з нашим спостереженням за тим, де знаходиться верхній правий кут зображення.

Сподіваємось, тепер ви повністю розумієте, як матриця трансформації впливає на об’єкт, а також зв’язок між базисними векторами та як «УФ» або «внутрішні координати» об’єкта змінюють його положення у світі.

Примітка

У Godot вся математика перетворення виконується відносно батьківського вузла. Коли ми маємо на увазі "світову позицію", то говоримо про позицію відносно батьківського вузла, якщо у вузла є батько.

Якщо ви хочете отримати додаткові пояснення, вам слід переглянути чудове відео 3Blue1Brown про лінійні перетворення: https://www.youtube.com/watch?v=kYB8IZa5AuE

Практичне застосування перетворень

У реальних проектах ви зазвичай працюватимете з перетвореннями всередині перетворень, маючи кілька вузлів class_Node2D або class_Node3D, пов’язаних один з одним.

Однак корисно зрозуміти, як вручну обчислити потрібні значення. Ми розглянемо, як ви можете використовувати class_Transform2D або class_Transform3D для ручного розрахунку перетворень вузлів.

Конвертування позицій між перетвореннями

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

Ми можемо знайти, який вектор відносно гравця буде визначений у світовому просторі за допомогою оператора *:

# World space vector 100 units below the player.
print(transform * Vector2(0, 100))

І ми можемо використовувати оператор * у протилежному порядку, щоб знайти, якою була б позиція у світовому просторі, якби вона була визначена відносно гравця:

# Where is (0, 100) relative to the player?
print(Vector2(0, 100) * transform)

Примітка

Якщо ви заздалегідь знаєте, що перетворення позиціонується в точці (0, 0), ви можете використовувати методи "basis_xform" або "basis_xform_inv", які пропускають роботу з перетворенням.

Переміщення об'єкта відносно себе

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

Оскільки базисні вектори є орієнтацією відносно батьківського, а вихідний вектор є позицією відносно батьківського, ми можемо додати кілька базисних векторів, щоб перемістити об’єкт відносно самого себе.

Цей код переміщує об'єкт на 100 одиниць праворуч:

transform.origin += transform.x * 100

Для переміщення у 3D вам потрібно замінити "x" на "basis.x".

Примітка

У реальних проектах для цього можна використовувати translate_object_local у 3D або move_local_x і move_local_y у 2D.

Застосування перетворень на перетвореннях

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

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

../../_images/apply.png

Єдині перетворення, що відбуваються тут, - це те, що батьківському вузлу була наданий масштаб (2, 1), дитині - масштаб (0,5, 0,5), і обом вузлам були надані позиції.

На всі дочірні перетворення впливають батьківські перетворення. Дочірній елемент має масштаб (0,5, 0,5), тож ви очікуєте, що це квадрат у співвідношенні 1:1, і це так, але лише відносно батька. Вектор дочірнього X в кінцевому підсумку виявляється (1, 0) у світовому просторі, тому що він масштабується базовими векторами батьків. Подібним чином вектор початку дочірнього вузла встановлюється на (1, 1), але це фактично переміщує його (2, 1) у світовому просторі завдяки базовим векторам батьківського вузла.

Щоб розрахувати перетворення дитини відносно світового простору вручну, ми можемо використовувати такий код:

# Set up transforms like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Calculate the child's world space transform
# origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
var origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin
# basis_x = (2, 0) * 0.5 + (0, 1) * 0
var basis_x = parent.x * child.x.x + parent.y * child.x.y
# basis_y = (2, 0) * 0 + (0, 1) * 0.5
var basis_y = parent.x * child.y.x + parent.y * child.y.y

# Change the node's transform to what we calculated.
transform = Transform2D(basis_x, basis_y, origin)

У реальних проектах ми можемо знайти світове перетворення дитини, застосовуючи одне перетворення до іншого за допомогою оператора *:

# Set up transforms like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Change the node's transform to what would be the child's world transform.
transform = parent * child

Примітка

При множенні матриць, порядок має значення! Не змішуйте їх.

Нарешті, застосування перетворення ідентичності завжди нічого не зробить.

Якщо ви хочете отримати додаткове пояснення, вам слід переглянути чудове відео 3Blue1Brown про матричну композицію: https://www.youtube.com/watch?v=XkY2DOUCWMU

Інвертування матриці перетворення

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

Множення інвертованого перетворення на нормальне перетворення скасовує всі перетворення:

var ti = transform.affine_inverse()
var t = ti * transform
# The transform is the identity transform.

Перетворення позиції за допомогою трансформації та її зворотного результату призводить до тієї самої позиції:

var ti = transform.affine_inverse()
position = transform * position
position = ti * position
# The position is the same as before.

Як усе це працює в 3D?

Однією з чудових переваг матриць перетворень є те, що вони дуже схожі між 2D і 3D перетвореннями. Увесь код і формули, використані вище для 2D, однаково працюють у 3D, за 3 винятками: додавання третьої осі, що кожна вісь має тип class_Vector3, а також те, що Godot зберігає Basis окремо від class_Transform3D, оскільки математика може бути складною, і має сенс її відокремити.

Всі концепції того, як працюють переміщення, обертання, масштабування і скошення в 3D, такі самі, як і в 2D. Для масштабування беремо кожен компонент і множимо його; щоб повернути, ми змінюємо, напрям кожного основного вектора; для переміщення ми маніпулюємо походженням;а для перекосів, ми змінюємо основні вектори, щоб вони були неперпендикулярними.

../../_images/3d-identity.png

Якщо хочете, можете по-експериментувати з перетвореннями, щоб зрозуміти їх роботу. Godot дозволяє редагувати матриці 3D-перетворення безпосередньо в Інспекторі. Ви можете завантажити цей проект, який має кольорові лінії та кубики, щоб допомогти візуалізувати вектори Basis та походження як у 2D, так і в 3D: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

Примітка

Ви не можете редагувати матрицю перетворення Node2D безпосередньо в інспекторі Godot 4.0. Це може бути змінено в майбутньому випуску Godot.

Якщо хочете отримати додаткове пояснення, вам слід ознайомитися з чудовим відео 3Blue1Brown про 3D-лінійні перетворення: https://www.youtube.com/watch?v=rHLEWRxRGiM

Представлення обертання в 3D (додатково)

Найбільша відмінність між матрицями перетворення 2D і 3D полягає в тому, як ви представляєте обертання саме по собі без базових векторів.

У 2D у нас є простий спосіб (atan2) перемикатися між матрицею трансформації та кутом. У 3D обертання надто складне для представлення одним числом. Існує щось, що називається кутами Ейлера, які можуть представляти обертання як набір із 3 чисел, однак вони обмежені та не дуже корисні, за винятком тривіальних випадків.

У 3D ми зазвичай не використовуємо кути, ми або використовуємо основу трансформації (яка використовується майже скрізь у Godot), або ми використовуємо кватерніони. Godot може представляти кватерніони за допомогою структури class_Quaternion. Я пропоную вам повністю ігнорувати, як вони працюють під капотом, оскільки вони дуже складні та неінтуїтивно зрозумілі.

Однак, якщо вам дійсно треба знати, як це працює, ось деякі великі ресурси, з якими ви можете ознайомитися:

https://www.youtube.com/watch?v=mvmuCPvRoWQ

https://www.youtube.com/watch?v=d4EgbgTm0Bg

https://eater.net/quaternions