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

Вступ

Перед тим як читати цю статтю, радимо вам пройти урок :ref:`doc_vector_math`так як вам потрібно знати базові концепції векторів щоб зрозуміти цю статтю.

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

Більша частина цього посібника зосереджена на 2D і використовує Transform2D та 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 just 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, помістіть його на розграфлений папір, потім поверніть його і занотуйте куди вказують вектори. (Примітка перекладача: Напрям вверх має координати (0, -1), вправо - (1, 0), вниз - (0, 1), вліво - (-1, 0), при обертанні картинки її вектори змінюють напрямок у який вказують).

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

../../_images/rotate2.png

Примітка

Godot представляє всі обертання в радіанах, а не градусах. Повний поворот рівний 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 just calculated.

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

Примітка

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

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

До сих пір ми працювали тільки з векторами x і y, які відповідають за представлення обертання, масштабування та/або перекосів (висвітлені далі). Вектори X і Y разом називаються основою матриці перетворення. Терміни "основа" і "основні вектори" важливо знати.

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

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

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

Зміна вектора origin призводить до переміщення. При цьому переміщення не передбачає ніякого обертання.

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

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

Якщо ми хочемо, щоб об'єкт рухався в положення (1, 2), нам просто потрібно встановити його вектор походження origin на (1, 2):

../../_images/translate.png

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

Примітка

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

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

Ми збираємося застосувати все, про що ми досі говорили, для одного перетворення. Щоб продовжити далі, створіть простий проект з вузлом Sprite і використайте логотип 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 just calculated.

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

Примітка

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

Можливо, ви помітили, що перетворення має більше ступенів свободи, ніж поєднання вищезазначених дій. Основа матриці 2D-перетворення має чотири загальних числа у двох значеннях 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 just 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

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

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

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

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

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

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

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

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

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

Примітка

Якщо ви заздалегідь знаєте, що вузол перетворення розташований на (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 just 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 just calculated.
transform = Transform2D(basis_x, basis_y, origin)

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

# Set up transforms just 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.

Перетворення позиції на перетворення та її інверсію призводить до тої ж позиції (те ж саме для xform_inv):

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

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

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

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

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

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

Примітка

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

Примітка

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

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

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

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

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

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

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

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

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

https://eater.net/quaternions