В этой серии статей мы познакомимся с инверсной кинематикой в видеоиграх. Перед началом нашего путешествия я расскажу о нескольких играх, в которых используются процедурные анимации, и о том, чем они отличаются от традиционных, основанных на ресурсах, анимаций.
GIF
Серия будет состоять из следующих частей:
- Часть 1. Введение в процедурную анимацию
- Часть 2. Математика прямой кинематики
- Часть 3. Реализация прямой кинематики
- Часть 4. Введение в градиентный спуск
- Часть 5. Инверсная кинематика для робота-манипулятора
- Часть 6. Инверсная кинематика щупалец
Часть 7. Инверсная кинематика лап паука
Часть 1. Введение в процедурную анимацию
В большинстве игр анимации персонажей являются «статичными». Когда персонаж двигается на экране, художник создаёт конкретное движение. Они изготавливаются вручную или записываются с помощью захвата движений. Анимации в этом случае — это заранее созданные ресурсы. Когда персонажу нужно выполнить другое действие, требуется другая анимация. Такой способ реализации движения персонажей довольно стандартен для индустрии игр. Существуют большие и подробные коллекции анимации, в которых есть наиболее часто используемые поведения, такие как ходьба, прыжки и стрельба. Традиционные анимации доминируют в игровой индустрии, однако у них есть равноценные альтернативы. Я хочу познакомить вас с концепцией процедурных анимаций.
Главная мысль здесь заключается в том, что моменты состояния персонажа можно генерировать процедурно. В одной из самых стандартных техник генерирования процедурных анимаций используется симуляция физики. Поэтому её часто называют физической анимацией (Wikipedia). Типичный пример — это вода. Можно анимировать её вручную или использовать анимацию, учитывающую динамику жидкостей.
Ниже мы обсудим очень специфический подвид физических анимаций, в котором применяется симуляция твёрдых тел (rigid body simulation). Такой же тип симуляции обычно используется в игровых движках, например в Unity и Unreal. Давайте посмотрим, как этот простой принцип используется в играх для создания физических анимаций.
Ragdoll-физика
В самой основе физических анимаций лежит принцип возможности симуляции движения персонажей. Воссоздавая процессы и ограничения, управляющие человеческим телом, можно приблизиться к созданию реалистичных поведений. Один из простейших, но эффективных способов создания процедурных анимаций — применение физики тряпичной куклы (рэгдолла, ragdoll)(Wikipedia). Идея заключается в создании гуманоидного тела и соединения всех его звеньев сочленениями (joints) для воссоздания степеней свободы, демонстрируемых реальным прототипом. Просто используя физику твёрдых тел и ограничения сочленений, можно симулировать падение тела человека. Это не только позволяет сэкономить деньги на «анимацию смерти». Также это позволяет создавать персонажей, реалистично падающих и взаимодействующих с окружениями. Подобную задачу почти невозможно решить с помощью только готового набора анимаций, вне зависимости от его точности.
Важнейший недостаток рэгдоллов — их огромная непредсказуемость, которая часто приводит к очень забавным поведениям.
GIF
Сегодня рэгдоллы очень привычны в играх. В Unity есть простой инструмент Ragdoll Wizard, позволяющий быстро превратить гуманоидную модель в рэгдолл.
Симуляции твёрдого тела
Основная проблема рэгдоллов — отсутствие управления движениями. Если соединить части тела сочленениями, то персонаж не сможет ни ходить, ни прыгать. Он будет только падать. Однако бывают ситуации, в которых можно использовать смешанный подход.
В статье How Grow Home Uses Maths To Generate Personality игровой журналист Алекс Уилтшир (Alex Wiltshire) разговаривает с представителями Ubisoft об игре Grow Home. Одна из основных особенностей игры — способ движения главного героя, Бада (BUD). В игре нет готовых анимаций, по крайней мере, в традиционном смысле. Когда игрок двигается, положение ног и рук управляется кодом. Части тела подвержены тем же ограничениями, что и у рэгдолла, что заставляет их создавать убедительные анимации.
Подобный принцип также активно используется в Rain World. Каждое животное в игре имеет тело, состоящее из нескольких коллайдеров. Некоторые из них управляются кодом, другие контролируются сочленениями. Это можно заметить на анимации внизу. Конечные точки крыльев хищной птицы движутся программно, остальные кости соединены шарнирами. Управление конечными точками автоматически создаёт плавную анимацию, которую иначе достигнуть бы не удалось.
GIF
И в Grow Home, и в Rain World процедурные анимации используются для повышения реалистичности персонажей. Однако контроллеры не полагаются на эти анимации. В игре Gang Beasts эта концепция развита ещё дальше. Игра полностью одобряет разболтанные движения, возникающие в результате использования ragdoll-физики. В результате получились забавные персонажи с непредсказуемыми движениями.
GIF
Инверсная кинематика
Симуляции твёрдого тела позволяют упростить создание анимаций. Мы указываем, где должны находиться руки и ноги Бада, а всё остальное делает за нас физический движок. Этот очень простой подход работает с простыми персонажами, но ему часто недостаёт реализма. В симуляциях твёрдого тела принимаются в расчёт только такие параметры, как гравитация и масса, но им не хватает знания контекста. Во многих случаях требуется создать что-то, действующее не только под воздействием ограничений, обусловленных гравитацией и соединениями.
Следующий шаг в создании процедурных анимаций известен как инверсная кинематика. Для рэгдолла любого типа инверсная кинематика вычисляет, как его нужно двигать, чтобы достичь нужной цели. В Grow Home и Rain World физика сама определяет, как должны двигаться подверженные воздействию гравитации соединения. Инверсная кинематика заставляет их двигаться в нужных фазах.
Одной из первых инди-игр, в которых активно использовалась эта концепция, стала The Majesty Of Color студии Future Proof Games. В ней игрок управляет щупальцем морского существа. В отличие от крыла птицы из Rain World, это щупальце не просто управляется шарнирами. Каждый сегмент поворачивается таким образом, чтобы конечная точка щупальца достигла нужной точки. Если бы в этой анимации использовалась только симуляция твёрдого тела, то щупальце казалось бы «приколотым» к этой точке, как кусок верёвки.
GIF
Инверсную кинематику можно использовать для решения множества задач. Наиболее стандартные — это естественное движение гуманоидных персонажей к определённым объектам. Вместо использования заранее заданных анимаций, разработчики просто указывают цель, которой должна достичь рука. Всё остальное делает инверсная кинематика, находящая наиболее естественный способ движения соединений руки. Если бы использовалась только симуляция твёрдого тела, то движение было бы судорожным, казалось бы, что части тела просто перетаскивают.
В редакторе анимаций Unity под названием Mechanim есть инструмент (справка Unity), позволяющий разработчику использовать для гуманоидных персонажей инверсную кинематику.
В оставшейся части этой серии статей я сосредоточусь на решении проблемы инверсной кинематики. Мы разберёмся, как можно управлять роботом-манипулятором или щупальцем монстра.
GIF
Часть 2. Математика прямой кинематики
Теперь мы начнём путешествие в мир инверсной кинематики. Есть множество способов решения этой проблемы, но все они начинаются с прямой кинематики.
Инверсная кинематика берёт точку в пространстве и сообщает нам, как нужно двигать рукой, чтобы достичь её. Прямая кинематика решает противоположную двойственную задачу. Зная, как мы будем двигать руку, она сообщает нам, какой точки пространства достигнет рука.
Робот-манипулятор
Инверсная кинематика изначально применялась для управления роботами-манипуляторами. Поэтому в этой серии статей будут использоваться допущения и терминология робототехники. Однако это никак не ограничивает возможные варианты применений инверсной кинематики. Вы можете использовать её для человеческих рук, лап пауков и щупалец.
Во-первых, давайте начнём с демонстрации того, что мы понимаем под термином «робот-манипулятор»:
На изображении выше показан стандартный робот-манипулятор, изготовленный из «звеньев» (limbs), соединённых «сочленениями» (joints). Так как показанный на изображении робот имеет пять независимых сочленений, то считается, что у него есть пять степеней свободы. Каждое сочленение управляется двигателем, позволяющим перемещать присоединённое к сочленению звено на определённый угол.
Чтобы рассмотреть более общий пример, мы можем начертить схему сочленений. В этой статье мы примем, что каждое сочленение может поворачиваться только по одной оси.
Инструмент, прикреплённый к концу робота-манипулятора, называется конечным звеном. В зависимости от контекста он может считаться или не считаться одной степенью свободы. В этой статье конечное звено не будет учитываться, потому что мы сосредоточимся только на движении для достижения нужной точки.
Прямая кинематика
В этом примере каждое сочленение может поворачиваться по одной оси. Поэтому состояние каждого сочленения измеряется как угол. Поворачивая каждое сочленение на определённый угол, мы позволяем конечному звену достигать разных точек в пространстве. Определение того, где находится конечное звено при известных углах сочленений, называется прямой кинематикой.
Прямая кинематика — это «простая» проблема. Она означает, что для каждого множества углов существует один единственный результат, который можно вычислить без всяких неопределённостей. Определение того, как робот-манипулятор двигается в зависимости от передаваемых нами данных — это необходимый шаг для нахождения обратной проблемы инверсной кинематики.
Геометрическая интерпретация
Прежде чем начать писать код, нам нужно понять математические построения, стоящие за прямой кинематикой. Но прежде всего нам нужно разобраться, что она значит пространственно и геометрически.
Поскольку визуализировать повороты в 3D не так просто, давайте начнём с простого манипулятора в двухмерном пространстве. Робот-манипулятор имеет «исходное положение» — это конфигурация, в которой все сочленения повёрнуты на их «нулевой угол».
На схеме выше показан манипулятор с тремя степенями свободы. Каждое сочленение повёрнуто в положение его нулевого угла, то есть робот находится в исходном положении. Мы можем увидеть, как изменяется такая конфигурация при повороте сочленения на градусов. Он приводит к соответствующему перемещению всей цепочки сочленений и звеньев, прикреплённых к .
Важно заметить, что двигатели, прикреплённые к другим сочленениям, пока не двигались. Каждое сочленение вносит свой вклад в локальный поворот прямой цепочки связей. На схеме ниже показано изменение конфигурации при повороте второго сочленения на градусов.
Положение определяет только , а на воздействуют уже и , и . Система координат поворота (красная и синяя стрелки) ориентирована в соответствии с суммой поворотов более ранней цепи соединений, к которой она присоединена.
Математика
Из предыдущих схем очевидно, что для решения проблемы прямой кинематики нам нужно вычислить положение вложенных (подчинённых) объектов при их повороте.
Давайте посмотрим, как его вычислить на примере двух сочленений. Найдя решение для двух элементов, мы можем повторять этот процесс для решения цепей любой длины.
Начнём с простого случая, когда первое сочленение находится в своём начальном положении. Это значит, что , как на схеме ниже:
Это значит, что:
Когда не равен нулю, нам нужно просто повернуть вектор расстояния в точке опоры вокруг на градусов:
Математически это можно записать как:
Ниже мы узнаем, как можно использовать функцию
AngleAxis
(документация Unity) без возни с тригонометрией.Воспроизводя ту же логику, мы можем получить уравнение для :
И, наконец, общее уравнение:
В следующей части статьи мы увидим, как это уравнение удобно реализовать в коде на C#.
Прямая кинематика в 2D
Если вы знакомы с вращением в 2D, то это можно сделать тригонометрически:
О выводе уравнения можно почитать в моей статье A Gentle Primer on 2D Rotations.
О выводе уравнения можно почитать в моей статье A Gentle Primer on 2D Rotations.
А как насчёт матрицы Денавита-Хартенберга?
Если вы обладаете инженерными знаниями, то могли решать эту проблему иначе. Проблемы прямой и обратной кинематики широко известны, и для их решения существует несколько стандартизированных подходов. Один из них — привязка к каждому сочленению четырёх параметров, называемых параметрами Денавита-Хартенберга (Wikipedia). С ними удобно работать в матричном формате и они отлично подходят для аналитического решения проблемы инверсной кинематики.
Однако в этой статье мы их не используем. Решение матрицы Денавита-Хартенберга требует больше математики, чем многие программисты захотят разбираться. Выбранный мной подход использует градиентный спуск, который является более общим алгоритмом оптимизации.
Однако в этой статье мы их не используем. Решение матрицы Денавита-Хартенберга требует больше математики, чем многие программисты захотят разбираться. Выбранный мной подход использует градиентный спуск, который является более общим алгоритмом оптимизации.
Часть 3. Реализация прямой кинематики
В этой части мы продолжим решать проблему прямой кинематики. Найдя в прошлой части математическое решение, теперь мы узнаем, как реализовать его в коде на C# для Unity. В следующей части, «Введение в градиентный спуск», мы наконец покажем теоретические обоснования для решения проблемы инверсной кинематики.
Введение
В предыдущей части мы формализировали движение робота-манипулятора. Мы начали с простого примера из трёх сочленений. В своих исходных положениях они имеют изображённую ниже конфигурацию:
Разные на схеме представляют собой декартовы координаты, или -тое сочленение. Локальные углы, определяющие поворот относительно исходных положений, помечены как .
При повороте сочленений мы наблюдаем следующую картину:
Поведение этой системы можно суммировать следующими утверждениями:
- Поворот. Глобальный поворот сочленения — это сумма поворотов всех предыдущих сочленений:
- Положение. Глобальное положение сочленения задаётся как:
С учётом всего вышесказанного, мы можем начать придумывать возможный способ реализации этих поведений в Unity.
Иерархия GameObject
В Unity уже есть способ реализации всех вышеупомянутых требований: система родительских компонентов (parenting). Если сделать игровой объект дочерним по отношению к другому, он автоматически наследует положение, поворот и масштаб.
Если вам знаком риггинг, то вас это не удивит. Кости, представляющие собой сочленения гуманоидного персонажа, тоже имеют систему родителей, при которой повороты и движения наследуются. На изображении из Unity Animation 3: Character Setup Майкла Эрбетнота показан очевидный пример этого.
При создании иерархии соединений необходимо убедиться, что, когда все локальные углы Эйлера равны нулю, то робот-манипулятор находится в исходном положении. Для гуманоидного персонажа это обычно стандартная T-образная поза, показанная на изображении выше.
Реализация
Возможность создания дочерних компонентов в Unity де-факто решает проблему прямой кинематики. К сожалению, этого недостаточно. В следующей части серии статей мы увидим, что нам на самом деле нужен способ проверки положения конечного звена без перемещения робота-манипулятора. Это заставит нас по-своему реализовать эту базовую функцию Unity.
Первый шаг — это сохранение информации о каждом из сочленений робота-манипулятора. Этого можно достичь при помощи скрипта, например
RobotJoint
из следующего примера:using UnityEngine;
public class RobotJoint : MonoBehaviour
{
public Vector3 Axis;
public Vector3 StartOffset;
void Awake ()
{
StartOffset = transform.localPosition;
}
}
Для упрощения вычислений примем, что каждое сочленение может поворачиваться только по одной своей локальной оси: X, Y ил Z. Мы обозначим это переменной
Axis
, которая принимает значение 1
для координаты относительно оси вращения. Если сочленение поворачивается по оси Y, то Axis
будет иметь вид (0,1,0)
. Мы увидим, как это позволит нам избавиться от конструкций с IF
.Давайте создадим функцию
ForwardKinematics
. Она получает массив angles
чисел типа float
. Имя говорит само за себя: angles[i]
содержит значение локального поворота i-того сочленения. Функция возвращает положение конечного звена в глобальных координатах.public Vector3 ForwardKinematics (float [] angles)
{
...
}
Код является простой реализацией на C# показанного выше уравнения положения. Функции
rotate
реализуются через удобную функцию Quaternion.AngleAxis
.Vector3 prevPoint = Joints[0].transform.position;
Quaternion rotation = Quaternion.identity;
for (int i = 1; i < Joints.Length; i++)
{
// Выполняет поворот вокруг новой оси
rotation *= Quaternion.AngleAxis(angles[i - 1], Joints[i - 1].Axis);
Vector3 nextPoint = prevPoint + rotation * Joints[i].StartOffset;
prevPoint = nextPoint;
}
return prevPoint;
Нужна помощь с кватернионами?
Повороты в Unity часто описываются через углы Эйлера. Это три числа, соотвествующие повороту объекта по осям X, Y и Z. Углы Эйлера обозначают крен (roll), тангаж (pitch) и рыскание (yaw) объекта в пространстве. Однако с математической точки зрения использование углов Эйлера может привести к довольно неприятным проблемам.
Работать с углами удобнее с помощью кватернионов (quaternions). Кватернионы — это математические объекты, которые можно использовать для описания поворотов. В отличие от них, углы Эйлера описывают ориентацию. Квартернион описывает путь, который нужно пройти из одной ориентации в другую. С технической точки зрения, это слишком большое упрощение, но для нашей статьи его более чем достаточно.
Кватернион можно представить как поворот. Вращение объекта в пространстве — это, с математической точки зрения, аналог умножения его положения на кватернион. Для создания вращения вокруг неподвижной точки в Unity можно использовать функцию
При умножении двух кватернионов создаётся новый кватернион, включающий в себя оба поворота. При каждой итерации цикла
Наконец, кватернионы используются в этой строке:
Она полностью соответствует следующей записи:
Произведение кватерниона и вектора применяет поворот.
Работать с углами удобнее с помощью кватернионов (quaternions). Кватернионы — это математические объекты, которые можно использовать для описания поворотов. В отличие от них, углы Эйлера описывают ориентацию. Квартернион описывает путь, который нужно пройти из одной ориентации в другую. С технической точки зрения, это слишком большое упрощение, но для нашей статьи его более чем достаточно.
Вращения ⇔ кватернионы
Кватернион можно представить как поворот. Вращение объекта в пространстве — это, с математической точки зрения, аналог умножения его положения на кватернион. Для создания вращения вокруг неподвижной точки в Unity можно использовать функцию
Quaternion.AngleAxis
. Строка Quaternion.AngleAxis(angle, axis);
создаёт кватернион, описывающий поворот вокруг оси axis
на angle
градусов. В этом контексте значение Axis
может быть равно (1,0,0)
, (0,1,0)
или (0,0,1)
, что обозначает, соответственно, X, Y или Z. Это объясняет, почему мы создали переменную Axis
в классе RobotJoint
.Добавление вращений ⇔ умножение кватернионов
При умножении двух кватернионов создаётся новый кватернион, включающий в себя оба поворота. При каждой итерации цикла
for
переменная rotation
умножается на текущий кватернион. Это значит, что она будет включать в себя все повороты всех сочленений.Кватернион * вектор = повёрнутый вектор
Наконец, кватернионы используются в этой строке:
Vector3 nextPoint = prevPoint + rotation * Joints[i].StartOffset;
Она полностью соответствует следующей записи:
Произведение кватерниона и вектора применяет поворот.
[Окончание следует. Во второй статье мы рассмотрим инверсную кинематику.]