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

Вступ

Перед тим як читати цю статтю, радимо вам пройти урок :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

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

In actual projects, you will usually be working with transforms inside transforms by having multiple Node2D or Spatial nodes parented to each other.

However, sometimes it's very useful to manually calculate the values we need. We will go over how you could use Transform2D or Transform to manually calculate transforms of nodes.

Converting positions between transforms

There are many cases where you'd want to convert a position in and out of a transform. For example, if you have a position relative to the player and would like to find the world (parent-relative) position, or if you have a world position and want to know where it is relative to the player.

We can find what a vector relative to the player would be defined in world space as using the "xform" method:

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

And we can use the "xform_inv" method to find a what world space position would be if it was instead defined relative to the player:

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

Примітка

If you know in advance that the transform is positioned at (0, 0), you can use the "basis_xform" or "basis_xform_inv" methods instead, which skip dealing with translation.

Moving an object relative to itself

A common operation, especially in 3D games, is to move an object relative to itself. For example, in first-person shooter games, you would want the character to move forward (-Z axis) when you press W.

Since the basis vectors are the orientation relative to the parent, and the origin vector is the position relative to the parent, we can simply add multiples of the basis vectors to move an object relative to itself.

This code moves an object 100 units to its own right:

transform.origin += transform.x * 100

For moving in 3D, you would need to replace "x" with "basis.x".

Примітка

In actual projects, you can use translate_object_local in 3D or move_local_x and move_local_y in 2D to do this.

Applying transforms onto transforms

One of the most important things to know about transforms is how you can use several of them together. A parent node's transform affects all of its children. Let's dissect an example.

In this image, the child node has a "2" after the component names to distinguish them from the parent node. It might look a bit overwhelming with so many numbers, but remember that each number is displayed twice (next to the arrows and also in the matrices), and that almost half of the numbers are zero.

../../_images/apply.png

The only transformations going on here are that the parent node has been given a scale of (2, 1), the child has been given a scale of (0.5, 0.5), and both nodes have been given positions.

All child transformations are affected by the parent transformations. The child has a scale of (0.5, 0.5), so you would expect it to be a 1:1 ratio square, and it is, but only relative to the parent. The child's X vector ends up being (1, 0) in world space, because it is scaled by the parent's basis vectors. Similarly, the child node's origin vector is set to (1, 1), but this actually moves it (2, 1) in world space, due to the parent node's basis vectors.

To calculate a child transform's world space transform manually, this is the code we would use:

# 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)

In actual projects, we can find the world transform of the child by applying one transform onto another using the * operator:

# 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

Примітка

When multiplying matrices, order matters! Don't mix them up.

Lastly, applying the identity transform will always do nothing.

If you would like additional explanation, you should check out 3Blue1Brown's excellent video about matrix composition: https://www.youtube.com/watch?v=XkY2DOUCWMU

Inverting a transformation matrix

The "affine_inverse" function returns a transform that "undoes" the previous transform. This can be useful in some situations, but it's easier to just provide a few examples.

Multiplying an inverse transform by the normal transform undoes all transformations:

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

Transforming a position by a transform and its inverse results in the same position (same for "xform_inv"):

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

How does it all work in 3D?

One of the great things about transformation matrices is that they work very similarly between 2D and 3D transformations. All the code and formulas used above for 2D work the same in 3D, with 3 exceptions: the addition of a third axis, that each axis is of type Vector3, and also that Godot stores the Basis separately from the Transform, since the math can get complex and it makes sense to separate it.

All of the concepts for how translation, rotation, scale, and shearing work in 3D are all the same compared to 2D. To scale, we take each component and multiply it; to rotate, we change where each basis vector is pointing; to translate, we manipulate the origin; and to shear, we change the basis vectors to be non-perpendicular.

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

If you would like, it's a good idea to play around with transforms to get an understanding of how they work. Godot allows you to edit 3D transform matrices directly from the inspector. You can download this project which has colored lines and cubes to help visualize the Basis vectors and the origin in both 2D and 3D: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

Примітка

Spatial's "Matrix" section in Godot 3.2's inspector displays the matrix as transposed, with the columns horizontal and the rows vertical. This may be changed to be less confusing in a future release of Godot.

Примітка

You cannot edit Node2D's transform matrix directly in Godot 3.2's inspector. This may be changed in a future release of Godot.

If you would like additional explanation, you should check out 3Blue1Brown's excellent video about 3D linear transformations: https://www.youtube.com/watch?v=rHLEWRxRGiM

Representing rotation in 3D (advanced)

The biggest difference between 2D and 3D transformation matrices is how you represent rotation by itself without the basis vectors.

With 2D, we have an easy way (atan2) to switch between a transformation matrix and an angle. In 3D, we can't simply represent rotation as one number. There is something called Euler angles, which can represent rotations as a set of 3 numbers, however, they are limited and not very useful, except for trivial cases.

In 3D we do not typically use angles, we either use a transformation basis (used pretty much everywhere in Godot), or we use quaternions. Godot can represent quaternions using the Quat struct. My suggestion to you is to completely ignore how they work under-the-hood, because they are very complicated and unintuitive.

However, if you really must know how it works, here are some great resources, which you can follow in order:

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

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

https://eater.net/quaternions