что такое твининг в анимации
Твины за рамками анимации
Оглавление
Вступление
Эта статья про твины (tween, tweenline, tween animation) и их нестандартное использование. Обычно о твинах вспоминают когда нужно что-то анимировать, будь то объект в игре или всплывающее меню на сайте. Но область их применения гораздо шире.
В статье будут приведены примеры кода — они будут написаны на языке C#.
Что такое твины
Твин (Tween) — аббревиатура для «In-between». В анимации твины описывают движение объекта (или изменение его свойства) между ключевыми кадрами.
Твины используются тогда, когда у вас есть следующие исходные данные:
Применять твины можно не только к положению объекта, но и к любым другим его свойствам, будь то: цвет, размер, прозрачность и т.д.
Проблема
Если уйти от анимации — в общем случае твины будут полезны также тогда, когда вам нужно синхронизировать несколько продолжительных действий во времени или одно действие в зависимости от другого.
Наша команда занимается разработкой различных интерактивных и вспомогательных элементов для российского телевидения. Один из текущих проектов над которым мы работаем это робот-оператор. Вместо того, чтобы находится на съёмочной площадке и ловить планы в видоискатель руками — оператор может сидеть в офисе на удобном кресле и управлять камерой с джойстика ориентируясь по картинке на мониторе.
План зависит от следующих параметров — ориентации и наезда (zoom) камеры.
На репетициях в софт вносятся основные планы которым уделяется 90% времени. Между этими планами нужны переходы — был план `A`, нам нужно аккуратно перейти на план `B` за время `T`.
Управление ориентацией камеры производится с помощью робота-манипулятора, а её наезд управляется через API объектива. Роботом и объективом нужно управлять синхронно.
Робот имеет команду «Перевести камеру из положения `A` в положение `B` за `T` миллисекунд», а вот объектив имеет только команду «Установить наезд в значение `Z`».
Сотрудник, который занимался этим функционалом, столкнулся с проблемой — рассинхрон в 1-2 кадра (40-80 мс.) между тем когда робот занимает финальное положение и тем когда на объективе выставляется финальный наезд. При том рассинхрон был как в одну сторону, так и в другую, т.е. то объектив наводился раньше чем заканчивалось движение, либо наоборот.
Вот упрощенный пример кода который делал переход из плана A в план B:
Основная проблема была в том, что Thread.Sleep не всегда делает задержку именно на заданное количество миллисекунд. В основном это зависело от того, что в определенные моменты времени у системы есть более приоритетные задачи и наш поток каждый раз мог спать немного больше положенного времени, но за 100+ вызовов набиралось до 80 мс. отставания, что является 2 кадрами телевизионной съёмки (25 кадров в секунду, 40мс на кадр). Из-за этого было отставание наезда от положения камеры. Если из-за чего-либо происходил большой лаг, допустим в пол секунды — фокусировка производилась как минимум на полсекунды позже того как робот занимал финальное положение.
Также Thread.Sleep всегда делает задержку не равную указанному значению, а +- рядом. Если замерять сколько времени прошло между вызовами Thread.Sleep более точными инструментами — разброс будет +-3 мс. от заданной задержки, что и давало нам случаи, когда наезд происходил слегка быстрее изменения положения камеры.
Рассмотрим эту проблему графически.
Состояние робота описывается его ориентацией в пространстве, это шесть значений: `X`, `Y`, `Z`, `RX`, `RY`, `RZ`. Первая тройка отвечает за положение объектива относительно центра робота, а вторая за направление в котором направлен объектив. Для упрощения, чтобы не указывать все значения — будем рассматривать только первую тройку отвечающую за положение — `X`, `Y` и `Z`. Эти значения не зависят друг от друга и могут меняться по отдельности, но чтобы переход был плавным — их изменение должно как начинаться, так и заканчиваться в одно время. Функционал робота из коробки уже позволяет сделать это одной командой.
Так выглядит график перехода робота из состояния A в состояние B:
Координаты начинают изменяться в начале перехода, а заканчивают ровно через выделенное на переход время.
А так выглядит переход наезда:
Наезд не успевает достичь требуемого значения за отведенное на переход время, и продолжает меняться уже после запланированной точки завершения перехода.
Анимированные примеры правильно перехода:
И не правильного перехода:
В обоих случаях начальное конечное состояния систем одинаковые, но вот достигаются они по-разному.
Решение
Как я упоминал выше — твины отлично подходят для синхронизации различных действий во времени, в чем и является наша проблема!
Нам нужно чтобы два перехода начавшихся в одно время — в одно время и завершились.
Для этого опишем два простых класса:
Первый класс `Tween` — абстрактный класс который описывает общий интерфейс всех твинов и принимает в конструкторе начало и конец перехода.
Второй класс `LinearDoubleTween` — класс который унаследован от `Tween` и реализует линейный переход для вещественных чисел.
В этом коде интересен только один момент — функция `GetValueAtProgress`. Вместо того, чтобы каждый шаг высчитывать на сколько значение должно было измениться — мы будем считать каким значение должно быть в эту единицу времени.
У нас уже есть значения `start` и `end` для функции `GetValueAtProgress` — это начальный и конечный наезды (`A.Zoom` и `B.Zoom`) соответственно, что же такое `progress`?
Для пояснения возьмём `start` и `end` равными каким-либо константам `A` и `B` соответственно (Для упрощения допустим что `A` всегда меньше `B`). Расположив значения `start` и `end` на числовой прямой мы получим простой отрезок:
Так как этот отрезок представляет переход из значения `start` в значение `end` мы можем представить эти значения в новой системе отсчета как 0 и 1 соответственно:
Так вот, `progress` — это точка на отрезке между 0 и 1 включительно в нашей новой системе отсчёта. Это какой-то момент внутри перехода между значениями `start` и `end`, началу перехода соответствует 0, середине 0.5, а окончанию 1. Взяв любую точку на этом отрезке, например 0.7, мы можем получить значение которое должно быть на изначальной числовой прямой в этот момент перехода:
В нашем случае ограничение на то что `progress` должен быть в границах от нуля до единицы — сделано для упрощения. Возможны такие твины, которые работают на бесконечной числовой прямой прогресса, но это уже будет не твин перехода, а циклический твин (НПример — функция синуса).
Чтобы получить значение `progress` в текущий момент времени нужно сделать следующее: сразу после начала перехода мы будем запоминать текущее время Tstart. И зная сколько должен занимать переход `T` — мы сможем перевести в прогресс перехода любую временную метку Tcurrent между началом перехода Tstart и его окончанием Tstart + `T` включительно, по следующей формуле: `progress` = (Tcurrent — Tstart) / `T`.
Вот упрощенный пример кода который делает переход из плана A в план B с использованием твинов:
Всё! Теперь независимо от задержек вызываемых Thread.Sleep — посылаемое на объектив значение всегда будет соответствовать прогрессу перехода.
Заключение
В этой статье мы рассмотрели только самое базовое использование твинов — линейная зависимость от времени, но твины становятся намного интереснее когда они становятся сложнее. Внеся небольшие изменения в функцию `GetValueAtProgress` мы можем сделать переход, например:
Если скомбинировать несколько твинов в зависимости от одного значения `progress` — мы получим таймлайн. Если мы запустим несколько объектов по одному твину, но с разными значениями `progress`, когда каждый следующий объект отстаёт от предыдущего на некоторое значение — мы получим змейку и т.д.
Твины являются очень простым инструментом для создания зависимостей от чего угодно — времени, расстояния, уровня заряда батареи и т.п. Их удобно использовать и заменять, т.к. все твины унаследованы от одного родительского класса — для изменения зависимости вам достаточно заменить используемый экземпляр твина с одного класса на другой и вы получите совсем другой эффект, т.к. независимо от того какой класс используется — то твин всегда в завершении оставит вам конечное значение `end` (если класс написан правильно).
Интерполяция — мать анимации — Твинеры в Unity
Но есть и у этого инструмента слабое место. Все анимации жестко ограничены, они представляют собой заранее описанный сценарий, который просто воспроизводится на иерархии объектов. Впрочем, это не совсем так. Как уже было отмечено выше, аниматор это чрезвычайно сложный инструмент, во всех его UI можно найти множество кнопочек и настроек, каждая из которых, как ни удивительно, выполняет какую-то функцию. К примеру, Avatar Mask позволяет сделать так, что некоторый слой анимации будет управлять отдельными частями тела, и персонаж будет махать рукой сидя на лошади, хотя отдельной такой анимации на Mixamo не нашлось =( Аниматор просто отыгрывает 2 анимации параллельно на тех костях, которые были заданы масками.
Хорошо, с третьей попытки, постараемся все таки сформулировать мысль таким образом, чтобы не скатиться вновь в дифирамбы очередной функции аниматора. Прокрутив в голове конец первого абзаца как дисклеймер, начнем.
Допустим, нам нужна анимация, которая будет активно взаимодействовать с окружением. Например, при контакте с персонажем монетка должна подпрыгнуть, сверкнуть, и улететь в карман нашего героя. То есть, сценарий нашей анимации зависит от контекста своего выполнения. Или нам просто не нужен аниматор на конкретном объекте. Например, анимируя UI нужно очень постараться, чтобы аниматор не заставлял Canvas перерисовываться на каждом кадре. А также, аниматор ничем не сможет помочь, если мы работаем с несовместимыми интерфейсами. Например, если мы захотим сделать анимацию накопления при подсчете очков или двигать вершины меша(Может быть, даже взаимодействуя с окружением! Например, плавно помять корпус авто в точке удара). Здесь нам поможет только описание логики анимации с помощью кода. Наконец-то мы подобрались к основной теме данной статьи.
Если вы загуглите слово «Твинер«, без указания контекста найти что-то может оказаться не так то просто, результат потонет в более релевантных синонимах. Правильным запросом будет Inbetweening, сокращенно Tweening. Полная версия термина звучит уже более осмысленно и лучше говорит о сути происходящего.
Англоязычная википедия отсылает нас к старой доброй «аналоговой» анимации, когда промежуточные кадры рисовались на бумаге путем наложения соседних кадров на световом столе и рисования кадра «посередине «.
Твинер не справа и не слева. Он посередине
Думаю, ни для кого не секрет, что анимации хранятся в памяти не покадрово, в виде бесконечно плотной последовательности значений, для этого используются системы функций, определяющих значение параметра в зависимости от времени. Для гибкости настройки, за очень редкими исключениями, анимации не хранятся в виде чистых функций вроде синусоиды или многочлена. Разве что, в отдельных случаях можно столкнуться с кривыми Безье. Но чаще используются B-Сплайны. Кривые, построенные по опорным точкам, изгиб каждого фрагмента которых можно изменить меняя значение промежуточных точек. Таким образом можно двигать отдельные точки, и добиваться желаемого результата, не меняя общую картину.
вместо уверенного роста, функция изменила свое значение на интервале, чтобы соответствовать касательной, которая проходит через пару промежуточных точек
Здесь мы видим, что DOTween позволяет вызовом одной функции запустить анимацию того или иного параметра стандартных Unity-компонентов. Очень просто, удобно и красиво.
А вот более эффективный способ его использования, что отмечают и сами разработчики DOTween в документации. Здесь мы можем обратиться к полям любого типа и описать функцию, по которой должно изменяться их значение с течением времени.
Но на мой взгляд, единственный необходимый и самый полезный способ работы с DOTween выглядит вот так:
Это максимально абстрактное описание твинера, позволяющее наглядно и эффективно описать любую задачу и, как самые внимательные уже заметили, может существенно сократить сигнатуру до DOTween.To(duration, MyMethod) вызываемой функции и улучшить читаемость кода. Для большей понятности посмотрим, что может находиться MyMethod:
Проще говоря, вместо того, чтобы привязываться к конкретным типам, параметрам и длительности анимации, мы просто вызываем функцию, линейно прогоняя значение ее аргумента от 0 до 1. Внутри нее мы можем использовать функции плавности, чтобы некоторые из параметров менялись нелинейно, создавая более динамичную картинку. Например, label из примера пролетит увеличиваясь от нижней границы экрана и остановится в центре с небольшим заносом, цвет будет меняться линейно, а накопление счетчика будет происходить таким образом, чтобы последние цифры тикали дольше, создавая приятный визуальный эффект. Конечно, 3 действия из 4 можно было сделать не менее красиво, а вот чего-то более сложного код уже не будет таким лаконичным. Таким образом, мы создаем очень простую и понятную абстракцию, которую легко читать и комфортно использовать вне зависимости от контекста.
Как уже сказал, в Dotween есть возможность объявлять цепочки анимаций. После завершения предыдущего твина будет выполняться следующий или разовый Callback, позволяющий сообщить о завершении анимации и отобразить кнопку «Далее»
Здесь проблема DOTween прежняя. Как только мы выходим из зоны комфорта, начинаются бесконечные страдания и нестерпимая боль. Как будет выглядеть подобная цепочка?
А еще где-то должна быть реализация. Я предпочитаю использовать лямбды в описании твинера.
В целом, вроде бы не так и страшно, но с аргументами после лямбды крайне неудобно работать. Несколько хелперов позволяют найти золотую середину и получить удобный, производительный и гибкий твинер, совместимый с любыми видами объектов. Единственная оговорка в том, что твинер без использования лямбда-функций можно реализовать куда эффективнее, храня в отдельных структурах лишь ссылку на изменяемое значение и параметры анимации вместо лямбд, захватывающих контекст. Но мы теряем всю гибкость, так что едва ли игра стоит свеч.
К слову, о захвате контекста. Это больное место всех известных мне твинеров, включая и DOTween. Смена сцены в процессе анимации или уничтожение анимируемых объектов ломает его работу. Issue был открыт в марте 2016, и судя по комментариям, проблема есть по сей день. Вероятно, предполагается, что это настолько очевидно, что здесь даже не нужна защита от дурака. В конце концов, всегда можно подписаться на смену сцены и прерывать их исполнение. Или проверять существование объекта в процессе выполнения, потеряв львиную долю производительности и рискуя создать утечку памяти, захватив в зацикленном твинере ссылку на объект, но не затрагивая при этом выгруженный компонент, откуда мы эту ссылку получили. В этой проблеме кроется одна из главных причин, по которым я использую собственный твинер, и единственная, по которой я взялся его писать.
Проблема захвата контекста решается следующим образом: По умолчанию, анимация создается в основной категории, которая очищается при смене сцены. Но если мы анимируем объект, помеченный как DontDestroyOnLoad, при создании твинера мы явно это указываем. В таком случае он попадет во вторую группу и не будет выгружен при смене сцены. Интерфейс заточен под использование линейной интерполяции, и поэтому выглядит следующим образом:
Стандартная длительность анимации в 0.3 секунды была подсмотрена в гайдлайнах Material Design и очень хорошо зарекомендовала себя на практике. Это та длительность, при которой анимация заметна, ощущается ее плавность и эффект от Easing-функции, но не заставляет пользователя ждать. Обычно я указываю бóльшую длительность для каких-то особых анимаций, которые сопровождают значимое действие, но для 90% анимаций это аргумент, который удобнее будет скрыть.
Ниже фрагмент кода, позволяющий заанимировать экран победы с показом результата:
Эта статья задумывалась ни в коем случае не как сравнение различных реализаций или реклама своей(Которая, хоть и служит мне верой и правдой не первый год, никогда не претендовала на богатый функционал или хорошую производительность. Это наивная реализация очевидного алгоритма). В первую очередь, я хотел поднять проблему излишнего перехода от общего к частному в твинерах. Инструменты, которые должны обеспечивать эффективный и плавный переход в пределах интервала, всего лишь прогонять третий аргумент в функции Lerp, соревнуются в том, как много сахара они смогут насыпать в ваши проекты. А изначально необходимую функциональность приходится достигать путем двойной конвертации в виде аргументов функции DOTween.To. Спрятав Lerp в черный ящик они, пусть и упростили твинеры для пользования детьми, превратили их в бесполезные игрушки. Впрочем, как мы убедились выше, если очень постараться, DOTween можно использовать по назначению. И мыши плакали, кололись, но продолжали есть кактус.
Разумеется, было бы глупо исключать вероятность, что автор не прав, ожидания к твинерам завышены и не в ту степь, поэтому прикрепляю 2 опроса и буду рад вашим историям в комментарий: Что самое необычное вы анимировали с помощью твинера? Свой самый необычный кейс я упомянул в статье: плавная деформация корпуса авто в области коллизии путем перемещения вершин.
cgmichael
Терминология по анимации
Профессиональные термины, которые обязательно употребляются в работе.
Их необходимо знать, чтобы понимать людей на разных этапах производства.
• Конвейер (pipeline) – все этапы производства: раскадровка, концепты, модели,текстуры, анимация, эффекты, рендер, композитинг.
• Раскадровка (storyboard) – это статичный прототип мультфильма, состоящий из серии набросков, где текстом и стрелками указаны происходящее в кадре.
• Тайминг (от англ. time “время”) – расчет времени, за которое должно выполняться определенное действие. Аниматор, должен умело использовать тайминг в анимации, так как можно один и тот же жест показать с разным смыслом. К примеру взмах руки может быть энергичен и полон решительности, но если увеличить время на выполнение взмаха, то он будет выглядеть как вялый и вельможный. В результате получится совсем не то, что требовалось. Поэтому необходимо внимательно следить за таймингом.
• Спейсинг (от англ. spacing “интервал, промежуток”) – это определение интервалов между ключевыми кадрами /анимационными ключами на линейке времени. В зависимости от способа расстановки ключей, физика движения объекта может меняться. Начиная с определенного кадра скорость движения может замедляться при увеличении интервала между ключевыми кадрами или ускоряться при более плотной их расстановке. Также объект может двигаться равномерно, если ключи на линейке выставлены с одинаковым интервалом.
Анимация мячей с одинаковым таймингом в 7 кадров, но разным спейсингом.
• Блокинг (от англ. blocking “разделение на блоки”) – это определение основных поз или ключевых моментов, определяющих движение объекта.
Без них движение не понятно. Главный аниматор задает основные позы движения, затем фазовщик между ключевыми позами вставляет промежуточные позы, то есть создает промежуточные фазы движения по расчетам главного аниматора. Так получается черновая анимация.
• Фазовка – вставка промежуточных кадров, пропущенных при раскадровке, для создания эффекта плавного перехода между ключевыми кадрами сцены.
• Подхват — движение, начатое в сцене, должно быть продолжено в следующей, иначе ломается плавность повествования.
• Липсинк (от англ. lip sync) – синхронизация движения губ с фонограммой и действием персонажа. В реплике нужно обращать внимание на движения персонажа в целом, речевые акценты (ударные моменты) и элементы выразительности: брови, глаза, рот, поза и жесты.
• Твининг (от англ. tweening “вставка промежуточных кадров”) – слово происходит от традиционного термина анимации «in betweening», который означает построение промежуточных изображений в процессе превращения исходного объекта в целевой или проще говоря заполнение промежутков между двумя ключевыми кадрами. Компьютерные программы для работы с анимацией часто сами вычисляют переход изображения из одного положения в другое посредством генерации промежуточных кадров между ними. В результате создается впечатление, что первое изображение постепенно превращается во второе.
• Перелистывание/флипинг (от англ. flipping) – смена страниц, каждая страница – новый кадр.
ДВИЖЕНИЕ
• Подготовка или упреждение (замах) – предварительный набор энергии в виде упреждение действия. Действие в обратном направлении перед самим действием. Так для броска руку с камнем отводят в назад, душевному подъему предшествует спад, и т.д.
• Брейкдаун (breakdown “разбивка” или passing position “пронос”) – средний рисунок или поза между двумя ключами, требует тонкой подгонки, выражает эмоции.
• Плавный вход и Плавный выход (Ease In & Ease out) — ключи (компоновки) наиболее выразительны, поэтому промежуточные фазы группируются вокруг компоновок (как бы смягчая), замедляя на них скорость движения персонажа. Или Слоу ин и Слоу аут – (англ. Slow in and Slow out – медленный вход и медленный выход).
• Нацеливание (aiming) – плавный акцент начинающегося движения и устремление внимания зрителя на нужный элемент. Можно сопровождать следящим взглядом персонажа. Этот настрой зрителя на то, что должно произойти. Создаёт непрерывность восприятия.
• Сжатие и растяжение (squash и stretch) – живое тело в движении то сжимается, то растягивается, то расширяется, важное правило при этом – неизменность общего “объема” персонажа. Без сквоша и стретча тело персонажа как бы каменеет.
• Оверлэпинг (от англ. overlapping – “перекрытие”) – действия идущие с определенным запозданием от основного: тело, затем волосы, хвост, одежда… Даёт анимации плавность и жизненную текучесть. В жизни, все движения имеют разную скорость, а объекты разную инерционность.
• Овершут (от англ. overshoot – “проскок”) – персонаж стремиться к позе, чтобы застыть в ней, но силы инерции заставляют его сначала проскочить эту позу и только потом занять нужное положение.
КАМЕРА
• PAN – Панорамирование (реже «панорама») — вращение камеры, творческий приём в киносъёмке, осуществляемый поворотом камеры вокруг вертикальной или горизонтальной оси при непрерывной съемке большого пространства.