Матрицы и преобразования

Введение

Перед прочтением этого руководства мы рекомендуем вам внимательно прочитать и понять руководство Векторная математика, так как для этого руководства требуются знания векторов.

В этом руководстве мы расскажем о преобразованиях и о том, как мы представляем их в Godot с помощью матриц. Это не исчерпывающее руководство по матрицам. Преобразования чаще всего применяются в виде переноса, поворота и масштабирования, поэтому мы сосредоточимся на том, как представить их с помощью матриц.

Большая часть этого руководства посвящена 2D с использованием Transform2D и Vector2, но в 3D все работает очень похоже.

Примечание

Как упоминалось в предыдущем уроке, важно помнить, что в модели Godot ось 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

Примечание

В Godot все вращения задаются в радианах, а не в градусах. Полный оборот равен TAU или PI*2 радиан, а поворот на 90 градусов (четверть оборота) — TAU/4 или PI/2 радиан. Использование TAU обычно делает код более читаемым.

Примечание

Интересный факт: В дополнение к тому, что в Godot ось Y указывает вниз, вращение представлено по часовой стрелке. Это означает, что все математические и тригонометрические функции работают так же, как в системе с осью Y вверх и вращением против часовой стрелки (CCW), поскольку эти различия "компенсируются". Можно считать, что вращение в обеих системах происходит "от 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 вместе называются базисом матрицы преобразования. Важно знать термины "базис" и "базисные векторы".

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

Обратите внимание, что в 3D Godot имеет отдельную структуру Basis для хранения трех значений Vector3 базиса, поскольку код может стать сложным, и имеет смысл отделить ее от Transform3D (которая состоит из одного Basis и одного дополнительного Vector3 для начала координат).

Перевод матрицы преобразования

Изменение вектора origin (исходной) координат называется трансляцией матрицы преобразования. Трансляция — это, по сути, технический термин, обозначающий "перемещение" объекта, но он явно не подразумевает поворота.

Разберём пример для понимания. Начнём с единичного преобразования, как в прошлый раз, но теперь будем отслеживать вектор начала координат.

../../_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-преобразования содержит четыре числа в двух значениях Vector2, тогда как угол поворота и Vector2 масштаба — только 3 числа. Этот недостающий параметр называется перекосом (shearing).

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

Чтобы наглядно показать, как это будет выглядеть, давайте наложим сетку на логотип 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).

Внутриобъектные координаты в текстурах называются UV-координатами, поэтому давайте позаимствуем этот термин здесь. Чтобы найти положение объекта в мире по относительному положению, используется формула U * X + V * Y, где U и V — числа, а X и Y — базисные векторы.

Правый нижний угол сетки, который всегда находится в UV-координате (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). Это совпадает с нашими наблюдениями о том, где находится правый верхний угол изображения.

Надеюсь, теперь вы полностью понимаете, как матрица преобразования влияет на объект, а также взаимосвязь между базисными векторами и тем, как «UV» или «внутрикоординаты» объекта изменяют свое мировое положение.

Примечание

В Godot все вычисления преобразований выполняются относительно родительского узла. Когда мы говорим о "мировом положении", оно относилось бы к родительскому узлу, если бы у узла был родительский узел.

Если вам нужны дополнительные объяснения, посмотрите великолепное видео 3Blue1Brown о линейных преобразованиях: https://www.youtube.com/watch?v=kYB8IZa5AuE

Практические применения преобразований

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

Однако полезно понимать, как вручную вычислять необходимые значения. Мы рассмотрим, как использовать Transform2D или 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) в мировом пространстве, поскольку он масштабируется базисными векторами родителя. Аналогично, вектор origin дочернего узла устанавливается в (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, за тремя исключениями: добавление третьей оси, тип каждой оси Vector3, а также то, что Godot хранит Basis отдельно от Transform3D, поскольку математические вычисления могут быть сложными, и их разделение имеет смысл.

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

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

Если хотите, стоит поэкспериментировать с преобразованиями, чтобы понять, как они работают. Godot позволяет редактировать матрицы трёхмерных преобразований прямо из инспектора. Вы можете скачать этот проект с цветными линиями и кубами для визуализации векторов Basis и начала координат как в 2D, так и в 3D: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

Примечание

Матрицу преобразования Node2D нельзя редактировать непосредственно в инспекторе Godot 4.0. Это может быть изменено в будущих версиях Godot.

Если вам нужно дополнительное объяснение, вам следует посмотреть отличное видео от 3Blue1Brown о трехмерных линейных преобразованиях: https://www.youtube.com/watch?v=rHLEWRxRGiM

Представление вращения в 3D (расширенное)

Самое большое различие между матрицами преобразования 2D и 3D заключается в том, как представлено вращение само по себе, без базисных векторов.

В двумерном пространстве у нас есть простой способ (atan2) переключаться между матрицей преобразования и углом. В трёхмерном пространстве поворот слишком сложен, чтобы быть представленным одним числом. Существуют так называемые углы Эйлера, которые могут представлять повороты в виде набора из трёх чисел, однако их возможности ограничены и они не очень полезны, за исключением тривиальных случаев.

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

Однако если вам действительно необходимо знать, как это работает, вот несколько замечательных ресурсов, которые вы можете изучить по порядку:

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

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

https://eater.net/quaternions