Русский
Русский
English
Статистика
Реклама

Анимация

Из песочницы Разжимаем древний формат сжатия анимаций

13.07.2020 14:10:08 | Автор: admin
image

В один день я просматривал различные видео на YouTube, связанные с персонажами программы Vocaloid (не совсем точное описание, но дальше буду называть просто вокалоидами). Одним из таких видео было так называемое PV из игры Hatsune Miku: Project DIVA 2nd. А именно песня relations из The Idolmaster, которую исполняли вокалоиды Megurine Luka и Kagamine Rin. Оба персонажа от Crypton Future Media. Порыскав по сети я понял, что никто так и не смог сконвертировать анимации из этой игры? Но почему? Об этом под катом.

Сама игра использует Alchemy Engine, который разработала Intrinsic Graphics, а позже купила Vicarious Visions. Это можно увидеть по файлам, имеющим расширение ".igb" (далее IGB), а также соответствующим строкам в них. Сами файлы бинарные. Погуглив немного я нашёл скрипт от тов. minmode для известной в определённых кругах программы Noesis. Запускаем её, с перекинутым в папку скриптом, пытаемся открыть файл анимаций и Получаем тыкву.

image

Как объяснил тов. minmode в своём посте на DeviantArt, этот скрипт не может прочитать анимацию, сжатую некоей Enbaya. В Google Patents я смог найти только подобное. Самим патентам уже лет 19-20, поэтому я и предполагаю, что сам алгоритм сжатия тоже древний. Да и сам сайт на это тоже намекает (доступен только через веб-архив). Поискав ещё немного я понял, что этот алгоритм был в составе некоего ProGATE от компании Enbaya. Но это ничего нам не даёт.

Вернёмся же к IGB. Переписав код для IGB, который я смог найти, а также воспользовавшись скриптом для Noesis, на C#, картина начала проясняться.

Ниже я приведу таблицу элементов, как она была выстроена в файлах IGB в этой игре (простите за корявость. Иначе не умею). Приведу только нужные нам элементы

Уточнение *List массив из элементов *

igAnimationDatabase--igSkeletonList---igSkeleton - Скелет, который, возможно, будет использоваться нами----igSkeletonBoneInfoList-----igSkeletonBoneInfo - Нода скелета--igAnimationList---igAnimation - Наша анимация----igAnimationBindingList-----igAnimationBinding - Ссылается на igSkeleton. Служит для линковки скелета к анимации----igAnimationTrackList-----igAnimationTrack - Сам трек анимации------igEnbayaTransformSource-------igEnbayaAnimationSource--------igData - Тут уже хранятся сырые данные EnbayaigData - Нода с данными, которые уже не являются нодами.

Таким образом я смог достать сырые данные для дальнейшего изучения. С помощью PPSSPP, Ghidra и плагина для неё я начал изучать бинарник игры. Я уже не особо помню как именно нашёл нужные функции, но приведу конкретные функции из EBOOT.BIT из ULJM05681 (в данном случае это не первая, а вторая, так называемая Bargain Version или же Project Diva 2nd#):

0x08A08050 инициализация функции декомпрессии на основе заголовка из igData
0x08A0876C запрос данных по конкретному времени (да. Enbaya работает со временем, не кадрами).

Сам код декомпилирован и выложен на GitLab. Написан он на Си. Компилируется как в Visual Studio, так и в gcc. Работает как в x86, так и в x64.

Я не стану углубляться в сам алгоритм. За меня лучше расскажет мой код.

Но если кратко, то Enbaya использует дельту для данных о перемещении и кватернионах. Дельту оно применяет, просто прибавляя/отнимая её к/от предыдущим/текущих данных. Перемещение остаётся как есть, а кватернион нормализуется для дальнейшего использования. Алгоритм позволяет вернуться назад во времени, не перезагружая файл. При этом он оперирует не частотой кадров, а семплами в секунду. Для этого он в памяти хранит два состояния предыдущий семпл и следующий, а движок сам интерполирует значение между ними. Однако в следствии того, что у нас данные в файле везде в целочисленном виде, мы должны их на что-то делить (точнее умножить. например на 0.0002), чтобы получить дробное число. Это число указывается в заголовке. Из-за этого деления (на самом деле умножения, но не суть) с каждым сложением и вычитанием точность немного уплывает.

А на этом всё. Честно говоря, мне было весело реверсить всё это. Надеюсь, что мои труды не прошли даром.

P.S. Используя данные igSkeleton мы уже можем получить готовую анимацию и экспортировать её, например в Maya. Через тот же Noesis.

Подробнее..

Лицевые анимации из двумерных видео

29.11.2020 04:09:36 | Автор: admin
КликбейтКликбейт

Лицевые анимации - часть общей анимации модели, описывающая движения частей лица модели. Хотя развитие компьютерной графики методов лицевой анимации началось в начале 1970-х годов, основные достижения в этой области являются более поздними и произошло с конца 1980 - х годов. Однако по сей день, существующие способы анимации не являются совершенными и часто требуют дополнительных конструкций или маркеров для достижения необходимого качества анимации.

Давайте попробуем собрать что-нибудь на колене так, чтобы оно ещё и ожило. Статья о том, как я лицевые анимации костылил, что из этого вышло или не вышло.

Введение

Дело в том, что существующие способы лицевой анимации требуют дополнительных программных, аппаратных или визуальных средств решения. А эти решения, в свою очередь, стоят денег. Давайте посмотрим на существующие решения и решим, что должно быть в нашем решении:

По моему маленьку расследованию, в настоящее время чуществуют следующие методы:

  • Создание маркеров на основе захвата движения по точкам или отметинам на лице исполнителя

    • Методы, использующие камеры, размещенные вокруг объекта и точки, нанесённые на лицо актёра или естественные отметины на коже для точного определения положения лица в пространстве

  • Безмаркерные методы с использованием различных типов камер

    • Методы, использующие различные способы создания карты глубин, такие, как камеры Kinect или Intel Real Sense

  • Звуко-управляемые методы

    • Методы, использующие скрытые марковские модели или нейронные сети для преобразования звуковых параметров в поток управляющих параметров для модели лица.

  • Анимация по ключевым кадрам

    • Методы, использующие классический способ анимации, при котором различные части модели лица смещаются вручную

Таким образом, проанализировав существующие методы создания лицевой анимации, мы можем определить необходимый список параметров алгоритма:

  1. Автоматизированность

  2. Отсутствие необходимости в использовании дорогостоящего оборудования

  3. Относительная точность передачи положения лица в пространства

  4. Отсутствие необходимости использования дополнительных маркеров

Выборка

Чтобы собрать что-то дельное, необходимо составить математическую модель того, что мы собираемся делать. Ну или хотя бы понять, как вообще мы будем определять качество итогового результата? Поэтому

Благо лиц в интернете достаточно. Так что в качестве тестовых данных я выбрал четырёх популярных персонажей. Важное замечание: все видео сохранены в 2х различных форматах, имеют различные условия освещения.

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

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

  • Rele - расстояние между левым и правым глазом, показывающее искажение верхней части лица

  • Ren - расстояние между правым глазом и носом, показывающее искажение правой скулы

  • Len - расстояние между левым глазом и носом, показывающее искажение левой скулы

  • Chin 1-8 - набор расстояний, состоящих из расстояний между левой и правой частью лица, показывающий общее искажение лица

Поговорим о том, что эти параметры собой представляют.

Параметр rele

Пусть (x,y,z) координаты центра левого и правого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений rele хранит в себе длину вектора AB, где A-координата центра левого глаза, B-координата центра правого глаза соответственно, а равно, расстояние между центрами левого и правого глаза. Пусть каждое значение списка rele вычисляется по следующей формуле для каждого кадра:

rele_i = \sqrt{(x_{left}-x_{right})^2+(y_{left}-y_{right})^2+(z_{left}-z_{right})^2}

Тогда значение rele для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

rele = \frac{ \sum_{i=m}^{n}{\frac{rele_i}{rele_0}} }{a}

Значение rele показывает среднее изменение длины вектора AB - расстояния между центрами правого и левого глаза. Из чего можем провести следующую зависимость:

Чем меньше значение функции rele для видеофайла, тем меньше изменялись основные пропорции лица, а равно, тем (предположительно) лучше качество конечной анимации

Параметр len

Пусть (x,y,z) координаты центра левого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений len хранит в себе длину вектораAB, где A-координата центра левого глаза, B-координата конца носа, а равно, расстояние между центром левого глаза и концом носа. Пусть каждое значение списка len вычисляется по следующей формуле для каждого кадра:

len_i = \sqrt{(x_{left}-x_{nose})^2+(y_{left}-y_{nose})^2+(z_{left}-z_{nose})^2}

Тогда значение len для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

len = \frac{ \sum_{i=m}^{n}{\frac{len_i}{len_0}} }{a}

Значение len показывает среднее изменение длины вектора AB - расстояния между центром левого глаза и носом. Из чего можем провести следующую зависимость:

Чем меньше значение функции len для видеофайла, тем меньше изменялось расстояние между левым глазом и носом, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Параметр ren будем вычислять по тем же формулам, внеся соответсвующие изменения.

Список параметров chin0-chin7

Список параметров chin0 - chin7 показывает изменение длины вектора AB между каждыми двумя точками, относящимися к нижней дуги лица (точки 0-16).

Список значений chini хранит в себе длины векторов AB, где A-точка лежащая на левой полудуге, B-точка лежащая на правой полудуге соответственно, а равно, расстояние левой и правой стороной лица в точке i. Пусть каждое значение списка chini вычисляется по следующей формуле для каждого кадра:

chin_i = \sqrt{(x_{chin-left}-x_{chin-right})^2+(y_{chin-left}-y_{chin-right})^2+(z_{chin-left}-z_{chin-right})^2}

Тогда значение chin_i будет вычисляться для каждой точки нижней дуги лица (кроме вершины) по следующей формуле, где - количество кадров анимации:

chin = \frac{ \sum_{i=m}^{n}{\frac{chin_i}{n}} }{a}

Значение chin показывает среднее изменение длины вектора AB - расстояния между двумя точками на левой и правой стороне нижней дуги лица. Из чего можем провести следующую зависимость:

Чем меньше значение функции chin{0-7} для видеофайла, тем меньше изменялось расстояние между двумя половинами лица, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Все эти метрики позволяют относительно точно отследить качество итоговой анимации не только по итогу, но ещё и покадрово как-то вот так:

Так как конечной целью было собрать скрипт/плагин для Blender, визуализация естественно проводилась в нём же. Так выглядят этапы формирования итоговых косточек для перемещения мэша лица :

Этапы формаирования косточек и анимацииЭтапы формаирования косточек и анимации

Результаты работы скрипта

Кошмар разКошмар раз
Кошмар дваКошмар два
Кошмар триКошмар триНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакаловНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакалов

Давайте подробнее рассмотрим "Кошмар три"

Алгоритм не обрезает фотографию, оставляя на ней только лицо, а сохраняет кадр целиком. Время обработки такой анимации увеличилась в 2-3 раза, однако, это позволило избежать неточностей и достоверно передать все выражения лица. Присутствуют необходимые для достоверной анимации циклы кадров. Помимо этого, она значительно превосходит своих предшественниц и показывает на сколько алгоритм был усовершенствован после проведения исследования, его настройки.

Ну и вот итог всего пути, который мы прошли. От точек в пространстве, до сносной анимации:

Подводим итоги

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

Да и исполнение занимает значительное количество времени, что в сравнении с некоторыми из способов недопустимо

Подробнее..

Перевод Создание шейдерной анимации в Unity

24.06.2020 08:22:39 | Автор: admin
Недавно я работал над анимацией респауна и спецэффектом главного героя моей игры King, Witch and Dragon. Для этого спецэффекта мне нужна была пара сотен анимированных крыс.


Создание двухсот мешей со скиннингом с анимацией ключевыми кадрами всего для одного спецэффекта это пустая трата ресурсов. Поэтому я решил использовать систему частиц, но для этого мне пришлось выбрать другой подход к анимации.

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

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

Я подробно опишу создание вот этой анимации:

Rat animation

Подготовка


Для начала я создал в Blender низкополигональную модель крысы.

Rat model

Для разделения модели на разные части (тело, хвост, лапы) я использую UV-координаты. Чтобы всё работало как нужно, модель нужно развернуть особым образом.

Rat UV

Основное движение крысы это прыжки. Чтобы все вершины анимировались плавно и синхронно, нам нужно разместить всё тело крысы вдоль одной из осей. Для удобства я разместил её вдоль горизонтальной оси U.

Глаза, внутренняя часть уха и нос должны иметь другой цвет на текстуре, поэтому я разместил их отдельно, но выполнил смещение только по оси V, не меняя горизонтальных координат. Если переместить их по горизонтальной оси, то их движение не будет синхронизировано с движением головы.

Хвост крысы занимает левую половину развёртки (координаты U от 0,0 до 0,5). Это будет нашей маской хвоста, которую мы используем для анимации хвоста.

Лапы расположены в нижней половине развёртки (координаты V от 0,0 до 0,4). Это наша маска лап. Кроме того, лапы сжаты по горизонтальной оси U, чтобы предотвратить нежелательную деформацию при движении вперёд-назад. Так как в своём проекте я использую cel-shading без детализованной текстуры, сжатые UV не вызовут проблем.

На основании этой UV-развёртки я создал диффузную текстуру. Теперь мы можем начать работу над шейдером.

Если вы хотите повторять все шаги, то можете скачать модель в формате FBX с диффузной текстурой отсюда.

Создание шейдера


Сначала я создам этот шейдер в Shader Graph движка Unity, а затем покажу текстовую версию.

Давайте создадим Unlit Graph, в качестве Preview выберем Custom Mesh, а затем выберем модель крысы.


Накладываем текстуру


Создадим новый параметр Texture 3D, это будет наша основная диффузная текстура. Создадим нод Sample Texture 2D, соединим наш новый параметр с его полем texture, а затем соединим нод с полем Color нода Master.


В дальнейшем мы будем работать только с вершинами.

Основное движение


Мы создадим его при помощи синусоиды. Чтобы настроить форму волны, нам нужны три параметра:

  • Jump Amplitude высота прыжков крысы
  • Jump Frequency частота прыжков крысы
  • Jump Speed скорость движения вдоль вертикальной оси


Два основных нода, которые позволят нам создать анимированное движение это Time и UV. В ноде UV нам нужно будет использовать каждую ось по отдельности, поэтому мы соединим его с нодом Split, который даст нам доступ к каждому из каналов.

Мы можем управлять скоростью прокрутки синусоиды, умножив нод Time на параметр Jump Speed.

Если умножить горизонтальный компонент UV на Jump Frequency, то мы сможем управлять сжатием и растягиванием синусоиды.

Сумма этих произведений даст нам синусоиду нужной формы. По умолчанию синусоида возвращает значения в интервале от -1,0 до 1,0. Если мы умножим их на Jump Amplitude, то получим траекторию прыжка.

Building sive wave

Теперь нам нужно применить результат к позициям вершин, но только к их вертикальной компоненте (локальной оси Y). Мы используем нод Position и соединим его с нодом Split. Затем прибавим значение синусоиды к компоненте Y и соберём всё вместе при помощи нода Combine. Подключим выход этого нода к полю Vertex Position нода Master.

Apply Y-offset

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

Rat sine wave

Как говорилось выше, синусоида может возвращать как положительные, так и отрицательные значения. Если мы теперь поместим крысу на поверхность, она будет нырять в неё. Кроме того, у синусоиды очень плавные экстремумы, из-за чего анимация походит на плавание.

Чтобы исправить это, мы используем нод Absolute. Эта функция отзеркаливает все отрицательные значения в положительные и выходные данные этой функции всегда положительны.

Absolute values of the sine wave

Сверху обычные значения, снизу абсолютные.

Давайте добавим этот нод к нашему графу.

Graph absolute node

Теперь анимация стала более скачущей.

Rat absolute sine wave

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

Для этого мы снова модифицируем синусоиду. Давайте переместим её вниз по вертикальной оси, а затем возьмём максимальное значение между синусом и нулём.

Sine wave vertical offset

Сверху абсолютное значение, посередине со смещением, внизу максимум между синусом и нулём.

Чтобы реализовать это в графе, нам нужен новый параметр Jump Vertical Offset, который позволит нам настраивать величину сдвига синусоиды.

Graph abs sine offset

Теперь крыса может какое-то время оставаться на земле.

Rat jump with vertical offset

Дополнительное раскачивание хвоста


В целом всё и так выглядит неплохо, но хвост всегда болтается рядом с землёй. Давайте добавим ему энергичности.

Мы используем UV-координаты для маскировки хвоста и анимирования его отдельно от тела.

Мы знаем, что хвост расположен в левой половине UV-развёртки. Создадим плавный градиент от 0,0 до 0,5 (или даже до 0,6, чтобы эффект был более плавным) по горизонтальной оси U. В 0,0 у нас будет белый цвет, в 0,6 и далее чёрный. Чем ярче пиксель градиента, тем больше дополнительного движения прикладывается к вершине. По сути, на кончик хвоста он будет влиять сильнее всего, а ближе к телу эффект будет ослабевать.

Для создания этого градиента мы используем нод Smooth Step.

Также нам потребуется новый параметр Tail Extra Swing для задания величины дополнительного движения.

Умножив этот новый параметр на выход нода Smooth Step, мы получим распределение движения вдоль хвоста. Затем мы прибавим его к параметру Jump Amplitude, чтобы получить окончательное движение тела, учитывающее дополнительное раскачивание хвоста.

Graph tail mask

Graph tail mask full

Теперь движение хвоста более заметно (Tail Extra Swing = 0.3).

Rat extra tail swing

Движение лап


Для движения лап мы используем примерно такую же структуру, что и для тела. Нам потребуется ещё пара новых параметров:

  • Legs Amplitude величина перемещения лап относительно их позиций по умолчанию
  • Legs Frequency частота движения лап

Нам не нужен параметр Legs Speed, потому что движение лап должно быть синхронизировано с движением тела, поэтому мы снова используем параметр Jump Speed. Единственное, что здесь стоит учитывать: поскольку мы используем абсолютное значение синусоиды, за один цикл получается два прыжка. Поэтому чтобы компенсировать это, мы используем Jump Speed * 2.

Лапы должны двигаться вперёд и назад (и положительное, и отрицательное смещение), поэтому в этом случае нам не понадобится нод Absolute.

Graph legs sine wave

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

Мы снова используем нод Smooth Step, но на этот раз в качестве входного параметра выберем вертикальную ось UV. Давайте зададим градиент от 0,1 до 0,4.

Почему 0,1, а не 0,0? Чтобы избежать деформации лап. Все вершины ниже уровня 0,1 будут иметь одинаковое смещение.

Нам нужно настроить значение Legs Frequency таким образом, чтобы когда передние лапы идут вперёд, задние двигались назад, и наоборот. В моём случае я задал значение 10.

Graph legs mask

Давайте прибавим результат к локальной позиции вершин по Z. Я временно отключил движение тела, чтобы отдельно посмотреть на анимацию лап.

Graph isolated legs movement

Rat isolated legs movement

Теперь скомбинируем всё вместе и посмотрим, как это выглядит. Я намеренно уменьшил скорость, чтобы легче было находить возможные проблемы.

Rat slomo

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

Чтобы решить эту проблему, создадим новый параметр Legs Phase Offset, позволяющий нам компенсировать эту разность фаз и согласовать анимации.

Чтобы это смещение фаз работало, нужно добавить его в нод Time уже после умножения на Jump Speed (чтобы не изменять скорость), но перед всеми остальными манипуляциями.

Graph phase offset

После настройки значения (я указал -1,0) анимация стала правильной.

Rat with phase offset

Вот как это выглядит при нормальной скорости.

Rat final

Готовый граф:

Complete graph

Текстовая версия шейдера


Для тех, кто пока не перешёл на URP/HDRP или просто предпочитает писать шейдеры вручную, вот версия в коде:

Shader "Unlit/Rat"{    Properties    {        _JumpSpeed("Jump Speed", float) = 10        _JumpAmplitude("Jump Amplitude", float) = 0.18        _JumpFrequency("Jump Frequency", float) = 2        _JumpVerticalOffset("Jump Vertical Offset", float) = 0.33        _TailExtraSwing("Tail Extra Swing", float) = 0.15        _LegsAmplitude("Legs Amplitude", float) = 0.10        _LegsFrequency("Legs Frequency", float) = 10        _LegsPhaseOffset("Legs Phase Offset", float) = -1        [NoScaleOffset]        _MainTex ("Texture", 2D) = "white" {}    }    SubShader    {        Tags { "RenderType"="Opaque" }        LOD 100        Pass        {            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            #include "UnityCG.cginc"            struct appdata            {                float4 vertex : POSITION;                float2 uv : TEXCOORD0;            };            struct v2f            {                float2 uv : TEXCOORD0;                float4 vertex : SV_POSITION;            };            sampler2D _MainTex;            float4 _MainTex_ST;            half _JumpSpeed;            half _JumpAmplitude;            half _JumpFrequency;            half _JumpVerticalOffset;            half _TailExtraSwing;            half _LegsAmplitude;            half _LegsFrequency;            half _LegsPhaseOffset;            v2f vert (appdata v)            {                float bodyPos = max((abs(sin(_Time.y * _JumpSpeed + v.uv.x * _JumpFrequency)) - _JumpVerticalOffset), 0);                float tailMask = smoothstep(0.6, 0.0, v.uv.x) * _TailExtraSwing + _JumpAmplitude;                bodyPos *= tailMask;                v.vertex.y += bodyPos;                float legsPos = sin(_Time.y * _JumpSpeed * 2 + _LegsPhaseOffset + v.uv.x * _LegsFrequency) * _LegsAmplitude;                float legsMask = smoothstep(0.4, 0.1, v.uv.y);                legsPos *= legsMask;                v.vertex.z += legsPos;                v2f o;                o.vertex = UnityObjectToClipPos(v.vertex);                o.uv = TRANSFORM_TEX(v.uv, _MainTex);                return o;            }            fixed4 frag (v2f i) : SV_Target            {                fixed4 col = tex2D(_MainTex, i.uv);                return col;            }            ENDCG        }    }}

Заключение


Поздравляю! Мы создали вершинный шейдер, который способен оживить простое существо без использования скелетного рига и анимаций с ключевыми кадрами.

Такой способ подойдёт для фоновых объектов, не находящихся в основном фокусе внимания игрока. Кроме того, по сравнению с рендерерами мешей со скиннингом (Skinned Mesh Renderers), он обеспечивает более высокую производительность.

Надеюсь, он был для вас интересным и/или полезным.
Подробнее..

Программа для physics-based анимации персонажей Cascadeur вышла в ранний доступ

14.04.2021 20:10:55 | Автор: admin


Спустя 10 лет разработки и 2 года бета-тестирования Cascadeur, программа для создания физически корректной персонажной анимации, вышел в ранний доступ! Пользователям доступны 4 варианта подписки, один из которых совершенно бесплатный.

Cascadeur позволяет создавать реалистичные экшн сцены минимальными средствами и без использования mocap, учитывая параметры физической модели персонажа. Программа обладает низким порогом входа, что делает ее доступной и понятной для новичков. Она также включает в себя инструменты на основе искусственного интеллекта, AutoPosing и AutoPhysics, которые значительно упрощают постановку позы и дальнейшую работу над анимацией.


Данный релиз является итогом двухлетнего бета-тестирования, в котором приняли участие более 70.000 человек из индустрии по созданию компьютерной графики и визуальных эффектов. Однако на этом разработка Cascadeur не заканчивается.

В будущем в программу планируется добавить ряд новых функций, включая поддержку циклов в анимации, возможность создавать кастомные пользовательские риги, а также Python-скриптинг, который призван облегчить создание рига и автоматизировать анимационные задачи. С планами на развитие Cascadeur можно познакомиться по ссылке.



Скачать программу можно на официальном сайте проекта cascadeur.com/ru. Здесь же вы найдете все необходимые материалы для того, чтобы начать анимировать прямо сейчас.

Узнать о Cascadeur больше:

Вложенность нейросетей инструмента автопозинга в Cascadeur
Почему 12 принципов Диснея недостаточно
Cascadeur: будущее игровой анимации
Подробнее..

Перевод Создание процедурной анимации смерти при помощи автоматов падающего песка

30.12.2020 12:20:04 | Автор: admin
В этом посте я покажу, как использовал автоматы падающего песка для генерации анимаций смерти монстров в моей игре Vagabond.



Автоматы падающего песка


Автомат падающего песка это клеточный автомат, симулирующий перемещение песчинок и создание куч песка под действием гравитации.

Правила просты:

  • Если ячейка под песчинкой пуста, то песчинка движется в пустую ячейку (см. (a)).
  • Если ячейка под песчинкой заполнена, но свободна ячейка внизу слева или внизу справа, то песчинка движется туда (см. (b)). Если свободны обе, то одна из них выбирается случайным образом.
  • В остальных случаях песчинка не движется.


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

С помощью этих простых правил можно получить вот такие анимации:


Генерирование анимаций смерти


Теперь давайте посмотрим, как можно использовать автоматы падающего песка для создания анимаций смерти монстров.

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


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

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


Чтобы получить изображение из 3D-состояния клеточного автомата, я проецирую состояние в 2D, беря для каждой пары координат (i, j) первую непрозрачную ячейку, где переменная k выполняет итеративный обход слоёв. Вот результат для трёх слоёв:


Чтобы улучшить ситуацию со скоростью, я рандомизирую количество строк, на которое падает песчинка за один шаг в интервале от 1 до $n$. На практике я использую $n = 2$ или $n = 3$. Вот результат:


При этом во время расщепления монстра при падении создаются дырки.

Далее я пропускаю некоторые кадры, чтобы анимации состояли ровно из 6 кадров. Вот готовый результат:


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

Полный скрипт выложен на GitHub.

Удобно то, что если вам не понравился результат, то можно запустить скрипт заново с другим seed, и получить новую анимацию. Вот несколько примеров анимации для летучей мыши:

Подробнее..

Как выделиться с помощью сайта? Дизайн с ощущением левитации

23.07.2020 16:12:40 | Автор: admin

Процесс разработки сайта с анимацией


Современная платежная платформа это портал между бизнесом и потребителем в любой точке мира. Как показать это в дизайне?

image

Перезапуск бренда требует создания нового сайта компании. Как выстроить работу над проектом так, чтобы агентство и заказчик понимали друг друга? Как в процессе разработки не забыть о конечном пользователе? Как с помощью дизайна рассказать о новом бренде? Что делать, когда идеи идут впереди технологий?

Мы в BeaversBrothers решили поделиться практикой в создании сайта с персонажной анимацией для RBK.money. Расскажем о зарождении идеи с парящими персонажами, подходах, сложностях и решениях для веб-проектов.

Заказчик: RBK.money

Деятельность: платёжный агрегатор

Целевая аудитория: малый и средний бизнес

Задача: создать сайт, который соответствует новому стилю и стратегии компании.

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

Мы презентовали основную идею дизайна сайта одновременно с согласованием его структуры. Это помогло нам сразу понять, что мы верно поняли задачу заказчика и видим проект одинаково.


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

1. Разработка сайта с погружением в бизнес-задачи заказчика


Сайт для RBK.money это основной канал взаимодействия с аудиторией. Новых посетителей площадка знакомит с продуктом, рассказывает о преимуществах. Клиенты компании могут войти в личный кабинет. Кроме того, на сайте можно узнать новости компании и посмотреть вакансии.

При выборе подрядчика компания ставит перед агентством два важных вопроса:

  1. Насколько результат будет соответствовать поставленным задачам?
  2. Успеет ли агентство выполнить работу в срок?

Мы должны хорошо разбираться в самом продукте, знать направление развития бренда, и, главное понимать ожидания целевой аудитории компании RBK.money. Создать сайт, который соответствует поставленным задачам, возможно только при правильной оценке стратегических целей бизнеса заказчика.

Именно поэтому работу начали с экспресс-аудита компании. Он помог нам понять продукт RBK.money и составить портрет целевой аудитории. Результатом аудита стал документ, в котором прописаны цели и задачи, описан продукт и ЦА. В нём мы предложили клиенту наше видение проекта и закрепили этапы работ. Прозрачность процессов помогла достичь взаимопонимания. Согласовали.

Как понять, когда и каким будет результат?


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

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

image

Этот документ нам помогает на всех этапах разработки сохранять фокус на конечном результате и избежать многочисленных правок в ходе проекта. А заказчик понимает, как будет построена работа над сайтом, и что в итоге получится. В документ всегда можно внести изменения, но при условии, что с корректировками согласны обе стороны.

Важно: здесь мы закрепляем срок разработки. Согласовываем.
До ВeaversВrothers несколько агентств пытались сделать нам сайт. Проблема была в том, что они предлагали шаблонные вещи (что есть у всех), не старались найти индивидуальный стиль. Причем мы даже уже заключили договор с агентством, и они сделали прототипы. А на этапе дизайна пришлось разойтись из-за того, что они просто копировали референсы, а индивидуальные иллюстрации и собственный стиль не разрабатывали. Выбрали ВeaversВrothers, потому что был уже положительный опыт по совместной разработке формы оплаты. Плюс, они с самого начала были вовлечены в специфику нашего бизнеса.

Наталья Сергиенко, директор по маркетингу RBK.money

2. Сделать удобный сайт для пользователей


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

Что помогает выстроить логику поведения пользователя на сайте?


В этом проекте заказчик обратился к нам после работы с другим агентством, поэтому у него уже были готовые карта сайта и контент. Нашему аналитику требовалось разобраться, где и какую информацию будет искать пользователь, как расположить кнопки призыва к действию. В общем, оценить всё то, что влияет на конверсию. Для этого была спроектирована Карта пользовательских сценариев.

Мы разделили целевую аудиторию на условные роли. В нашем случае их получилось три:

  • клиенты (подключение платёжной платформы),
  • партнёры,
  • персонал.

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

Создание Карты пользовательских сценариев один из самых значимых этапов разработки. Она позволяет навести порядок на страницах, разобраться с витиеватой структурой сайта, распределить контент и иллюстрации. Всё это ускоряет процесс разработки и в дальнейшем уменьшает количество правок. А готовый сайт делает удобным и понятным для пользователя.

Сложности и решения на этапе проектирования.


Первый экран сайта должен встречать пользователя правильно сформулированным и понятным УТП уникальным торговым предложением. Для платёжной системы привычное УТП это размер комиссии. Но в случае с предложением RBK.money оказалось, что вывести средний размер комиссии, которая зависит от оборотов компании, оказалось невозможным, поэтому заявленная минимальная цена будет вводить пользователя в заблуждение.

image

Решение нашлось в результатах Экспресс-аудита, который был проведён нами в начале проекта. Мы детально проанализировали критерии, по которым пользователь выбирает платёжный сервис. Один из основных удобство и скорость внедрения системы. Значит, акцент нужно делать на прогрессивности технологий, которые позволяют легко интегрировать платёжную систему в любой онлайн-бизнес. Нужно дать понять пользователю, что он не будет испытывать проблем с установкой решения RBK.money на свой сайт.

3. Дизайн может стать главной идеей сайта


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

Когда иллюстрации говорят о компании больше, чем текст

Акцент сайта мы предложили сделать на иллюстрациях с эффектом левитации. Такая идея взялась из инсайта: платёжная система портал между бизнесом и потребителем в любой точке мира.

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

image

Иллюстрации настолько удачно вписались в новый стиль компании, что команда RBK.money решила использовать персонажей не только на сайте. Мы передали файлы в исходниках.

image

Даже в формате изображения кажется, что картинки движутся. Но нам было этого мало. Мы решили усилить ощущение полёта на сайте с помощью сложной анимации.

Как анимация может заставить сайт летать


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

Сложности и решения по анимации сайта


Анимацию мы разработали в After Effects и экспортировали для веба с помощью плагина BodyMovin. Затем, мы использовали на странице lottie-web плеер, чтобы воспроизвести её на странице.

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



4. Разработка в четыре руки


Технические моменты разработки сайта

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

На этот проект мы задействовали двух наших разработчиков, чтобы вести работу в 4 руки. Мы разделили между ними страницы, чтобы вёрстка шла параллельно. Это позволило одновременно провести для заказчика два релиза, что сократило сроки разработки страниц.

Мы оптимизировали вёрстку, чтобы добиться максимальной скорости загрузки страниц.

Приёмы и результаты по производительности страниц сайта RBK.money

Мы старались сделать сайт максимально быстрым, даже при плохом интернете.

В разработке мы ориентировались на два основных показателя:

  • время загрузки первого контента страницы (когда пользователь видит не белый экран, а контент и может начать его читать и скролить);
  • время загрузки для взаимодействия (когда подгружаются скрипты и можно полноценно работать со страницей, открывать бургеры, листать слайдеры).

В нашем случае получились следующие результаты:

  • Время загрузки первого контента 0,5 0,8 с
  • Время загрузки для взаимодействия 1,6 1,9 с

Общая скорость загрузки сайта 94 балла из 100 (инструмент Lighthouse).

Как мы работали с временем загрузки

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

Все изображения и иллюстрации векторные это уменьшает их размер и отображает без искажений на Retina-экранах.

В итоге пользователь открывает страницу почти мгновенно, несмотря на большое количество скриптов, стилей и анимаций.

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

Сложности и решения в вёрстке сайта

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

Адаптивной версии уделили отдельное внимание. Если где-то вёрстка съезжала, выясняли причины у разработчиков браузера. В некоторых случаях корректировали текст, упрощали или отключали анимацию отдельных элементов. Но при этом сохраняли главную идею эффект левитации.

5. Результат разработки сайта


В назначенный день у команды RBK.money появился новый сайт. Получился отличный подарок к новому году для компании и её клиентов, ведь запуск произошёл в предновогоднюю неделю.

Наш веб-проект для RBK.money был отмечен международной наградой в области веб-дизайна Awwwards, 10 апреля после подведения итогов голосования на сайте премии мы получили специальный диплом. Также сайт RBK.money попал в подборку лучших примеров дизайна для веб-страниц от агрегатора Muzli от InVision: muz.li/inspiration/best-designed-landing-pages/.

Как сдать сайт в срок?

Чтобы сдать проект вовремя, мы работаем над процессами. Вот некоторые из тех, которые помогают нам в разработке сайта:

  1. Экспресс-аудит подготовительный этап, благодаря которому мы узнаём продукт и целевую аудиторию.
  2. Агрегация требований видим, что нужно сделать и каким будет результат.
  3. Карта пользовательских сценариев фиксируем, где и какую информацию ищет пользователь. Что нужно сделать, чтобы увеличить конверсию.
  4. Используем несколько разработчиков, которые трудятся в тесной связке в вёрстке. Процессы, которые можно вести параллельно нужно вести параллельно.
  5. Тестируем готовый проект в Pixel Perfect. Достигаем того результата, который был задуман.

Грабли проекта и мысли по их избежанию

  • Нужно закладывать больше времени на анимацию, потому что часто наши идеи бывают смелее, чем технические возможности браузера.
  • На сайте можно и нужно использовать сложную анимацию. Несмотря на то, что мало кто так делает. Тем более, потому что мало, кто так делает.


image

P. S. Продолжение? Следует!

Работаем над личным кабинетом для RBK.money. Делаем его удобным и понятным.
Подробнее..

О создании UI-анимаций в играх и почему они так важны

13.08.2020 18:07:06 | Автор: admin


Привет! Я старший UI-дизайнер Pixonic, Алексей Морев. И в этой статье речь пойдет UI-анимациях, которые каждый из нас может увидеть в играх.

Игровой интерфейс это совокупность элементов, которые помогают пользователю взаимодействовать и управлять игрой: кнопки, иконки, прогресс-бары, подложки, карточки, всплывающие окна. Для того, чтобы с элементами интерфейса было проще взаимодействовать, их анимируют.

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

Итак, начнем!


Прежде всего, давайте определимся, что такое анимация и какие ее виды существуют.

Если упрощенно, анимация это последовательность кадров, которая оживляет изображение. Она делится на полную (full animation) и упрощенную (limited animation). Примером полной анимации является любой полнометражный анимационный фильм: диснеевские Аладдин, Книга джунглей, Русалочка и пр. Примеры упрощенной анимации большинство мультсериалов, таких как Рик и Морти, Гравити Фолз, Крайний космос и пр.

В случае полной анимации рисуется 24-25 кадров на одну секунду хронометража и анимируются мелкие детали персонажа, окружения и пр. В упрощенной анимации число кадров в секунду меньше, а какие-то незначительные элементы персонажей и окружения могут и вовсе не анимироваться и оставаться статичными. Стоит отметить, что эти принципы должны соблюдаться в создании любых анимаций, будьто интерфейс или мультфильм.


Полная и упрощенная анимация на примере мультфильма Симпсоны

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

Что такое анимация, разобрались. Теперь логично перейти к следующим вопросам: какой тип анимации использовать в UI? И нужна ли она вообще?

Сперва отвечу на второй. Нужна ли нам анимация? Вроде бы все понятно: окно открылось, приз получен, кнопка нажалась. Не так уж и важно при этом использовать какую-либо анимацию, верно?

Не совсем так. Благодаря анимации можно не только разнообразить визуальную составляющую игры, но и помочь пользователю понять, что происходит на экране, куда нужно нажимать и какое действие выполнить. Это одна из основных причин использования UI-анимации в играх: чтобы привлечь внимание игрока к элементам, наиболее важным в данный момент.

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

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

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


Полная анимация


Упрощенная анимация


Минимальная анимация

Кто создает UI-анимацию и как устроен процесс разработки


Обычно в разработке анимации участвует три человека:

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

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

Создание анимации можно условно разделить на три этапа: продумывание, создание референса и реализация.

Первый этап: идея


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

Давайте поэтапно разберем процесс создания анимации на примере анимации попапа, которая показана выше.

В начале работы проведем ресерч игр по схожей тематике и стилистике, затем выберем наиболее удачные и интересные варианты, которые будем использовать в качестве референсов. Таким образом мы экономим время, не изобретая с нуля велосипед, а отталкиваясь от того, что уже было сделано до нас. Затем подробно описываем, что должно происходить с объектом анимации. Делается это в первую очередь для себя, чтобы ничего важного не забыть, а также для дальнейшего написания документации.

В качестве примера возьмем следующий попап и разберем на нем всю последовательность создания анимации.



Тезисно опишем идею:

  • Появление попапа после долго отсутствия игрока в игре;
  • Сделать плавную, но достаточно быструю анимацию;
  • Использовать Full frame анимацию;
  • Анимировать каждый элемент;
  • Анимировать пушку динозавру;
  • Добавить анимированное свечение под динозавром.

Когда цель становится ясна и понятно сформулирована, можно приступить ко второму этапу.

Второй этап: создание референса


Создаем собственный анимационный референс, который в дальнейшем будет демонстрироваться, дорабатываться и изменяться.

Для этого подойдут такие программы, как Adobe Animate, After Effects, Spine и т.д. На данном этапе для нас не имеет значения, в какой программе анимировать (исключение может составлять Spine, т.к. у этого редактора есть экспорт анимации во многие движки), поэтому выбираем, что для нас окажется удобнее и быстрее. Можно даже покадрово проанимировать элемент в Photoshop, но это уже экзотика. Я предпочитаю работать в After Effects, поскольку это наиболее гибкий редактор в плане анимаций: в нем можно реализовать практически любую идею, которая придет в голову.



Не забывайте учитывать тайминги, чтобы все UI-элементы вели себя адекватно: без рассинхронизации, задержек и в правильной последовательности.

Нельзя забывать и про длительность самой анимации. К примеру, если появление окна занимает больше одной секунды, а в игре оно появляется достаточно часто, игрок очень быстро устанет от затянутой анимации и будет раздражаться. В то же время, если появление какого-то элемента происходит довольно редко, и это событие приносит игроку радость (например, получение сундука, приза или ачивки), можно сделать анимацию появления длиннее и красочнее.


Третий этап: реализация


Здесь мы оптимизируем всю графику, использованную в референсе: нарезаем частицы, свечение, подложку, динозавра и прочие мелочи иными словами, все визуальные эффекты.


Пример оптимизированной нарезки

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

Почему нельзя сразу сделать все на движке? Ответ не очевиден, но довольно прост: когда вы создаете анимацию, к примеру, в After Effects, у вас есть только программа анимации и ее функционал. Не нужно настраивать камеры в игре, чтобы они верно отображались в UI. Не нужно оптимизировать графику и делить все на составляющие: вы легко можете заменить любой элемент, а то и вовсе переосмыслить анимацию, полностью ее переиначив. Если же сразу все создавать на игровом движке, вы потеряете много времени на оптимизацию файлов, будете зажаты в техническом плане и потратите много времени на нарезку графики для анимации на начальном этапе, хотя в конечном итоге она может оказаться совсем другой.

Заключение


Наспоследок подытожу материал и дам несколько коротких советов. При работе с анимацией продумывайте ее заранее, соблюдайте тайминги, по возможности используйте полную анимацию и не забывайте посматривать анимации из других проектов на предмет возможных референсов.

Если вам понравилась эта статья, следующий материал планируется про то, как можно использовать принципы Disney-анимации в UI. Узнать подробнее о самих принципах можно здесь.
Подробнее..

Анимация и экспорт. На примере игры Intravenous. Часть 1

09.02.2021 16:13:11 | Автор: admin

Сказ о том, как делать не стоит. Или, как я дважды сгорал на работе

Сегодня я хочу поделиться своим опытом в изготовлении анимаций для персонажей и внедрения их в игру. Естественно, с позиции художника. Каждый заказ, который попадает ко мне на почту разительно отличается от предыдущего, во множестве аспектов, и работать над чем-то новым для меня не впервые.

Но, именно данный заказ натолкнул меня на мысль, что подобным опытом стоит поделиться. Дабы, не знакомые со сферой, знали, как работает внутренняя кухня, а коллеги, как делать не стоит и почему. К тому же, перспектива Top-Down специфическая и материалов по ней практически не существует. Когда я начинал работу, никакого опыта с top-down перспективой, кроме игровой, у меня не было, что подогревало интерес.

Надеюсь, данное чтиво окажется, если и не интересным, то, по крайней мере, познавательным.


Promo-art для проектаPromo-art для проекта

Проект выполнен в жанре: Top Down Stealth - Action (уникальная смесь из серий Splinter Cell и Hotline Miami).

Движок: Love2D
Арт/Дизайн/Анимации выполнены в: Adobe Photoshop ( :) )
Художественное направление: Pixel art

Проект находится в Steam, и ознакомится с ним можно по данной ссылке: Intravenous

Скриншот из ранних версийСкриншот из ранних версийСкриншот из демо-версииСкриншот из демо-версии

Когда со мной связался заказчик (программист - Роман Гребенков), он уже имел на руках прототип с работающими механиками вроде взаимодействия с дверьми, телами врагов, подбора предметов и прочего. Выглядело крайне многообещающе.

Я изготовил для проекта UI (редизайн/арт), эффекты, персонажей, анимации, тайлсеты, объекты, портреты, promo-art.
В общем, практически всё, что вы увидите в игре. Но в данной статье, речь пойдёт именно про анимации, т.к. они стали "камнем преткновения" всей разработки.


Немного о перспективе

Enter The Gungeon - хороший пример перспективы 3/4Enter The Gungeon - хороший пример перспективы 3/4

Существует распространённое заблуждение, что "top-down" - это любой угол поворота камеры, в том числе несколько видов изометрии и, так называемая, перспектива 3/4.

Скетчи персонажей для освоения top-down перспективыСкетчи персонажей для освоения top-down перспективы

Связано это с тем, что у ряда перспектив, не существует какого-то объединяющего понятия/термина отличного от "вида сверху" т.е. "Top-Down".
Отсюда и возникающие недопонимания при обсуждении того или иного проекта.

"Top-Down" (топ-даун) - это перспектива, камера в которой привязана исключительно над головой персонажа.
Примеры: GTA 1/2, Darkwood, Hotline Miami


Анимации

Скетчи персонажей в пикселяхСкетчи персонажей в пикселях

Техническое задание (оно же: Т/З) включало в себя создание довольно внушительного числа анимаций для нескольких персонажей (с рядом исключений). В следствие чего, мне было необходимо придумать способ, как упростить этот процесс на большинстве этапов разработки.

Первые потуги анимации в top-down перспективеПервые потуги анимации в top-down перспективе

Список анимаций для всех персонажей включал в себя:

  • 5 видов основного оружия (Дробовик, Обрез, MP5, UZI, AK103, M4);

  • 5 видов второстепенного (Glock19, HS2000, P89, SW457, VP9);

  • 5 видов уникальных приспособлений (Тазер, Переносной ЭМИ глушитель, Светошумовая граната, Осколочная граната, Пустые магазины);

  • Ближний бой на всех видах оружия, в том числе и рукопашный;

  • Выбивание двери;

  • Idle анимации;

  • Анимации смерти;

  • Подбор и взаимодействие с предметами;

Наброски анимацийНаброски анимаций

Уникальные для персонажа игрока:

  • Перенос тел;

  • Оглушение или добивание персонажей;

  • Использование отмычки;

  • Лаз через 2 вида препятствий;

  • Движение ползком;

  • Бег;

Обрез. Умелый.Обрез. Умелый.

Помимо этого, существует 3 степени умения обращения с оружием (что увеличило список анимаций втрое!), которые мы условно назвали:

- Умелый; (персонаж игрока, профессиональные военные)
- Не умелый; (киллеры, наёмники)
- Абсолютно не умелый; (гангстеры, шпана)

Обрез. Неумелый.Обрез. Неумелый.

Все 3 степени отличаются геймплейно:

- точностью при стрельбе;
- скоростью перезарядки;
- скоростью реакции на события;

Что отражается визуально, через:
- наличие лишних телодвижений при перезарядке;
- положение персонажа (стойку);

Обрез. Абсолютно неумелый.Обрез. Абсолютно неумелый.

Список анимаций на каждого персонажа достигал значения 200+. Т.к. каждая анимация, даже в которой было изменён исключительно спрайт пистолета был уникальным.

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

В результате, это привело к тошнотворно рутинному производству и ряду медицинских проблем, в том числе: выгорание, нервный тик, нарушение сна и т.д.
Поэтому, если вы осознанно пойдёте на подобный риск, будьте готовы к последствиям.


Шаблон

Анимация падения и подъёмаАнимация падения и подъёма

Шаблон персонажа включал в себя:

- Голову;
- Тело;
- Руки;
- Оружие;
- Дополнительные слои (ладони/детали);
-
Ноги/нижняя половина тела (отдельно);

Из которых покадрово собирался цикл анимации.


Экспорт

Существует 2 пути экспорта шаблонных анимаций.

Способ 1:

  • Все части шаблона - отделены (слоями);

  • Оружие легко заменяется (если позволяет анимация);

  • Одежда кладётся поверх слоёв в игре;

Pixel art со скелетной анимациейPixel art со скелетной анимацией

+ Упор делается на сборку составляющих внутри движка игры;
+ Гибкость, возможность осуществлять исправления, буквально, на лету;
- Требователен к инструментарию движка;

Не совсем корректный, но отличный пример: Garage: Bad Trip.
(На самом деле, она известна своей скелетной анимацией скрещенной с Pixel-art графикой, и даже существует статья на эту тему, но я её не нашёл) ("Пес-песа" - тебя помнят!)

Способ 2:

  • Все части шаблона склеены (монолитный слой);

  • Оружие заменяется исключительно в исходнике (PSD/GIF файле);

  • Одежда склеивается вместе с частями шаблона;

Spritesheet персонажа из Hotline MiamiSpritesheet персонажа из Hotline Miami

+ Упор делается на финализацию работ перед отправкой;
+ Лёгкий импорт в движок игры;
- Многократно возрастающий объём работы;
- РУТИНА;
- Не подходит проектам, в которых используется большой размер спрайтов;

Отличный пример: Hotline Miami

Как вы уже понимаете, нами был выбран 2 вариант. Почему?
На это повлиял целый ряд причин:

  • Отсутствие инструментария для анимации (игра разрабатывалась на Love2D);

  • Необходимость разгрузки программиста от лишней работы (переизбыток задач);

  • Тримминг текстур (упаковка кадров анимации в spritesheets);

  • Малый размер спрайтов;

А теперь поподробнее.

  • Заказчику было необходимо добиться максимальной производительности, чтобы проект смог быть запущен на устройстве любой мощности;

  • Разработка инструментария для анимаций не рассматривалась вовсе, т.к. эти силы разумно было бросить на встроенный level-editor (редактор уровней) и проработку ИИ (искусственного интеллекта) врагов;

Тримминг кадров анимации и упаковкаТримминг кадров анимации и упаковка

Продолжение в части 2.

Подробнее..

SwiftUI по полочкам Анимация, часть 2

16.06.2020 00:11:35 | Автор: admin
image

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

image

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

Напомню, что идею реализовать такую анимацию я подсмотрел вот тут. Там авторы пошли по пути наименьшего сопротивления, и реализовали самый очевидный способ реализации этого концепта, анимировав shape. Принцип прост: каждая волна представляет собой контур очерченный кривой Безье, а параметр время влияет на положение правой границы этой волны.

Вот так выглядит анимация в готовом виде:

image

Для наглядности я немного разнесу волны по вертикали. Это несложно сделать, я просто добавил в конце NotchWave перемещение всего контура вниз на высоту волны, в соответствии с текущей фазой анимации:

return p.applying(.init(translationX: 0, y: height * self.phase))

Ну и еще .drowingGroup() надо не забыть отключить он не подразумевает выход изображения за границы фиксированного фрейма.



Подход простой, выдает требуемый результат но мне показалось слишком скучно. Мой путь оказался куда длиннее, и не сказал бы что конечный результат будет хоть в чем-то лучше, но зато по пути будет интересно.

Если вы еще не очень хорошо представляете себе принципы работы анимации в SwiftUI, настоятельно рекомендую прочитать предыдущую статью. А желающих покопаться в исходниках я приветствую на гитхабе.

Работа с градиентами


В самом начале, я выбрал другой подход. Я посчитал, что намного полезнее разобраться с анимацией смещения объектов, чем с анимацией форм (я по-прежнему называю shape-структуру формой). Смещение может работать с чем угодно различными фигурами, изображениями, другими View. В качестве учебной задачи, я захотел реализовать сглаженный цветовой переход от одной волны к другой. В оригинальном концепте что-то такое вроде бы такое было.

Всего SwiftIUI предлагает три возможных использования градиента линейный, круговой и угловой.



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



Линейный градиент характеризуется точкой начала и окончания градиента. Это не CGPoint точки с абсолютными координатами (x:y:), а UnitPoint точки, т.е. относительными координатами, где x:y: задаются в долях от ширины и высоты области, выделенной под данную View. Также есть предопределенные точки, соответствующие углам(.topLeading, .bottomTrailing и т.д.) и серединам сторон (.top, .trailing и т.д.).

Rectangle()    .fill(LinearGradient(         gradient: Gradient(stops: [             .init(color: self.end, location: 0),             .init(color: self.middle, location: 1 - self.middleGradientStop),             .init(color: self.start, location: 1)]),          startPoint: .leading,          endPoint: .trailing))    .frame(width: self.gradientLength)

Сегмент с линейным градиентом резиновый. Он имеет фиксированную ширину, но его высота не указана. Таким образом он заполнит весь предоставленный объем.

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

Rectangle()     .fill(RadialGradient(          gradient: Gradient(stops: [              .init(color: self.start, location: 0),              .init(color: self.middle, location: self.middleGradientStop),              .init(color: self.end, location: 1)]),          center: .bottomTrailing,          startRadius: self.topRadius,          endRadius: self.topRadius + self.gradientLength)                )    .frame(height: self.topRadius)

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

Сложнее всего применять угловой градиент. Он задается начальным и конечным углом, а так же направлением. Как можно видеть по листингу кода, работать с углами не очень удобно.
Приходится вспоминать школьный курс тригонометрии, чтобы вычислить требуемый угол, если тебе нужно начать угловой градиент в одной точке и закончить в другой.

       Rectangle()      .fill(AngularGradient(            gradient: Gradient(stops: [                  .init(color: self.end, location: 0),                  .init(color: self.middle, location: 1 - self.angularGradientMiddleStop(blockWidth: geometry.size.width)),                  .init(color: self.start, location: 1)]),            center: .bottomLeading,            startAngle: self.directionTo(gradientPart: self.start, blockWidth: geometry.size.width),            endAngle: Angle(degrees: 360)))...   func directionTo(gradientPart: Color, blockWidth: CGFloat) -> Angle{        let angleOf = gradientAngles(blockWidth: blockWidth)        var angle = Angle.zero        switch gradientPart{            case start: angle = angleOf.start            case middle: angle = angleOf.middle            case end: angle = angleOf.end            default: fatalError("there is no gradient stop with that color: \(gradientPart)")        }        return angle    }    func gradientAngles(blockWidth: CGFloat) -> (start: Angle, middle: Angle, end: Angle){        let blockHeight = self.bottomRadius        let center = CGPoint(x: 0, y: blockHeight)        let topRight = CGPoint(x: blockWidth, y: blockHeight - self.gradientLength)        let topGradientStarts = CGPoint(x: blockWidth, y: blockHeight - self.gradientLength * (1 - self.middleGradientStop))        let startAngle = center.radialDirection(to: topRight)        let middleAngle = center.radialDirection(to: topGradientStarts)        let endAngle = Angle(degrees: 360)        return (start: startAngle, middle: middleAngle, end: endAngle)    }...extension CGPoint{    func radialDirection(to point: CGPoint) -> Angle{        let deltaX =  point.x - self.x        let deltaY =  point.y - self.y        var angle = Angle(degrees: 0)        if deltaX == 0{            if deltaY > 0{                angle = Angle(degrees: 90)            }else{                angle = Angle(degrees: 270)            }        }else if deltaY == 0{            if deltaX > 0{                angle = Angle(degrees: 0)            }else{                angle = Angle(degrees: 180)            }        }else if deltaX > 0 && deltaY > 0{                angle = Angle(radians: atan(Double(deltaY / deltaX)))        }else if deltaX > 0 && deltaY < 0{                angle = Angle(degrees: 270) + Angle(radians: atan(Double(deltaX / -deltaY)))        }else if deltaX < 0 && deltaY > 0{                angle = Angle(degrees: 90) + Angle(radians: atan(Double(-deltaX / deltaY)))        }else if deltaX < 0 && deltaY < 0{                angle = Angle(degrees: 180) + Angle(radians: atan(Double(deltaY / deltaX)))        }        return angle    }}

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

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

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

Color это не то чем кажется


Есть один момент связанный с Color. Для создания эффекта накатывающей волны, я решил сделать переход между цветами не совсем линейным. Если присмотреться, можно заметить, что у самой кромки градиент резче, а чем дальше от кромки, тем плавнее он перетекает в основной цвет. Я добился этого вычислением промежуточного цвета, и созданием градиента с таким распределением:
начальный цвет 0
промежуточный цвет 0.3
конечный цвет 1

Проблема возникла с вычислением среднего двух цветов. SwiftUI подразумевает использование объекта Color для градиентов, а я и повелся. На самом деле, если вы хотите работать с цветами именно как с RGB-объектами, закладывайте изначально в свою модель использование UIColor, потому что в Color нет доступа непосредственно к цвету. Обратно в UIColor его тоже так просто не конвертируешь. Единственное (не)адекватное решение, которое я нашел вот тут подразумевает получение Mirror reflection с разбором его строкового представления. Такой себе бойлерплейт, но других вариантов пока нет.

И это не ошибка, не упущение. Смысл в том, что SwiftUI в объекте Color не дублирует функционал UIColor. Если вам нужна работа с rgb каналами используйте именно его. Color в SwiftUI это View имеющая некоторый базовый цвет, конкретное значение которого может несколько изменяться в зависимости от расположения звезд на небе конкретный rgb цвет определяется только в момент отрисовки на экране. В документации сказано
SwiftUI only resolves it to a concrete value just before using it in a given environment.
, но что имеется в виду под environment: цвет стенки за спиной пользователя, или тема оформления IOS непонятно. Если вам это не подходит, используйте UIColor изначально.

Бесконечная анимация


Анимация указывается модификатором .animation() и запускается после инициализации View в модификаторе .onAppear() путем изменения @State переменной. В результате, модификатор .rotationEffect() подписывается на получение animatableData в промежутке от было к стало, согласованных с системным таймером. Мы говорили об этом в прошлой части.

struct AnimatedRectObservedObject: View{    @State var angle: Double = 0    var body: some View{       return VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))            Spacer()       }       .onAppear{            self.angle += 90        }    }}



Запуская бесконечную анимацию таким образом, вы должны понимать, что любое последующее изменение анимированных модификаторов ее сломает. Например, если после запуска, вы по какой-то причине измените значение angle на 0, получите вот такой эффект:



Почему? Ответ в механизме анимации перехода от одной анимации к другой. В нашем примере, мы при первом появлении View на экране запустили анимированный переход от 0 к 90. View на самом деле хранит в себе только конечное значение 90, а исходное значение 0 вообще нигде не хранится. Механизм анимации знает текущее положение во времени анимации, и текущее значение AnimatableData. В точке времени 0.5 оно будет 45. Что произойдет, если в этот момент пользователь изменит значение на 0? Ответ: начнется анимация изменения значения с 45 до 0. Все так же зацикленная. Вот только визуально, цикл получается не замкнутым, а разорванным.

Кроме того, есть случаи, когда анимация так же ломается если в вашей View используется @ObservedObject, или иные параметры, вызывающие повторную отрисовку View. Для решения этой проблемы, у модификатора .animation есть параметр .animation(: value:). Передавая туда значение, мы указываем рендеру, что рестарт анимации нужен только при изменении этого значения.

Однако, давайте разбираться, как же остановить анимацию, если она нам больше не требуется. Для этого, нужно обеспечить две вещи. Очевидно, что мы должны сообщить SwiftUI что бесконечная анимация более не нужна, заменив ее обычной, конечной. Но этого недостаточно. Модификатор .animation() представляет собой инструкцию, какие тайминги нужно использовать, но само вращение описано внутри модификатора .rotationEffect(), и он уже подписан на получение новых значений угла поворота по таймеру. Для остановки вращения нам потребуется изменить еще и его значение.

struct AnimatedRectStopButton: View{    @State var angle: Double = 0    @State var animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(animation)            Spacer()            Text("toggle Animation").onTapGesture {                if self.angle == 90{                    self.angle = 0                    self.animation = .default                }else{                    self.angle = 90                    self.animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)                }            }        }        .onAppear{            self.angle = 90        }    }}



Так же как ранее, изменение значения angle приводило к старту новой анимации (которая выглядела сломанной), это изменение тоже будет анимировано, но теперь уже с таймингами обычной анимации. Именно поэтому, наш квадрат прекращает вращаться не мгновенно, а как бы с замедлением.

То же самое можно написать чуть более лаконично, если подвесить весь функционал на одну @Stateпеременную типа вкл/выкл:

struct AnimatedRectStopButton: View{    @State var isStarted = false    let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: isStarted ? 90 : 0))                .animation(isStarted ? animation : .default)            Spacer()            Text("toggle Animation").onTapGesture {                self.isStarted.toggle()            }        }        .onAppear{            self.isStarted.toggle()        }    }}

До этого момента все было относительно легко. Но обратите внимание, что все события здесь генерируются внутри View. .onAppear{} вызывается системой, а onTapGesture{}, понятно пользователем. Однако, как быть, если вы хотите инкапсулировать всю анимацию внутри одной View, передавая в нее лишь вкл/выкл? SwiftUI не предполагает возможности из родительской view каких-то методов дочерних. Теоретически, вы можете хранить дочернюю View как структуру, и вызывать ее mutating-методы, но вот @State переменные дочерних View таким образом поменять не получится, я пробовал не работает. Единственный способ сделать что-то подобное, это воспользоваться PassthroughSubject из Combine, как это и сделали в упомянутой статье.

На самом деле все намного проще. Если четко уложить по полочкам в голове, что @State это внутреннее состояние View, и не пытаться манипулировать им извне, то правильное решение окажется очень простым:

struct AnimatedRectStopButtonFromOutside: View{    var isStarted: Bool    let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: isStarted ? 90 : 0))                .animation(isStarted ? animation : .default)            Spacer()        }    }}struct AnimatedRectParentView: View{    @State var isOn = false    var body: some View{        VStack{            AnimatedRectStopButtonFromOutside(isStarted: isOn)            Text("toggle Animation").onTapGesture {                self.isOn.toggle()            }        }.onAppear(){            self.isOn = true        }    }}

Нам не нужна в данном случае @State подписка на обновление View внутри дочерней view она и так обновляется целиком при изменении внешнего для нее параметра. Иногда это не удобно. Иногда мы не хотели бы лишний раз инициализировать дочернюю view например, чтобы не сломать анимацию, или в init() происходят какие-то сложные и ресурсоемкие вычисления (запросы например). В этих случаях лучше пользоваться объектными сущностями, за изменениями которых можно следить с помощью модификатора onReceive.

Modifying state during view update


Есть еще с одним момент, достойный освещения. Мы оперировали только двумя значениями 0 и 90 градусов. Выключение анимации приводило к сбросу угла на 0. Но можно ли поставить анимацию на паузу прямо в тот момент, когда мы нажали кнопку, и снова ее продолжить с того же места при возобновлении? Давайте рассмотрим код, позволяющий это:

struct AnimatedRect: View{    @State var startTime: Date = Date()    @State var angle: Double = 0    @State var internalStarted: Bool    let externalStarted = true    var animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    init(started externalStarted: Bool){        self.externalStarted = externalStarted        self._internalStarted = State(initialValue: !externalStarted)//forse to start animation    }    var body: some View{        //thats wrong. It just hiding a problem from SwiftUI not solving it        DispatchQueue.main.async {            if self.internalStarted && self.externalStarted == false{                // print("stop animation")                 let timePassed = Date().timeIntervalSince(self.startTime)                 let fullSecondsPassed = Double(Int(timePassed))                 let currentStage = timePassed - fullSecondsPassed                 self.internalStarted = false                 let newAngle = self.angle - 90 + currentStage * 90                 self.angle = newAngle            }else if self.internalStarted == false && self.externalStarted {             //    print("start animation")                 self.startTime = Date()                 self.internalStarted = true                 self.angle += 90             }       }                return  VStack{            Rectangle()                .fill(Color.red)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(internalStarted ? animation : .default)            Spacer()        }    }}



Основная идея в том, чтобы в момент старта анимации засечь время. Спустя сколько-то времени, когда нам приспичит анимацию остановить, мы сможем проверить, сколько полных циклов анимации прошло, и предположить, какой угол поворота квадрата мы видим в данный момент. После этого, мы можем присвоить @State переменной угла поворота именно это значение.

Для работы такого подхода, нам нужно отследить момент старта и окончания анимации. Init() не подойдет. Мы не можем обращаться к уже существующим @State параметрам для получения предыдущего времени старта анимации. Это фишка @State переменных. Внутри init вы можете установить лишь начальное состояние этой переменной, но по окончании инициализации, если View уже существовала до init(), значение @State переменных будет восстановлено.

Поэтому тут реализована параметр let externalStarted (которым мы управляем извне), и внутренний параметр @State var internalStarted, с помощью которого мы управляем непосредственно анимацией.

Не хватало только одного какого-то модификатора, который бы проверял их соответствие и обновлял при необходимости, наподобие .onRecieve(), только чтобы отрабатывал при каждой отрисовке. И тут я подумал ведь body и так вызывается для каждой отрисовке, почему бы прямо в нем не делать эту проверку?

Оказалось, что SwiftUI очень ругается, если в процессе отрисовки View менять значение @State переменных, выдает
Modifying state during view update, this will cause undefined behavior.
и блокирует такое изменение. Тогда я пошел на грязный хак, и использовал DispatchQueue.main.async. Но давайте разберемся, почему же это грязный хак, и почему так делать не следует никогда?

На самом деле, проблема вот в чем. Если мы напишем внутри body какую-то очевидную глупость вроде i += 1, где i это какая-то @State переменная, то мы получим бесконечный цикл. В момент рендера мы делаем View в памяти не актуальной ведь мы изменили исходные данные для отрисовки. Значит, сразу по окончании отрисовки наша View попадет в очередь на повторный рендер, но и тогда мы тут же снова сделаем ее неактуальной. Мы своими руками создаем бесконечный цикл. Асинхронный вызов в данном случае вообще ничего не меняет. Он лишь немного сдвигает инициирование очередного витка на момент сразу после рендера. Таким образом, асинхронный вызов не решает проблему, а лишь маскирует ее, не давая SwiftUI ткнуть нас в нее носом. Это как в автомобиле лампочку check engine на приборке обрывать.

С одной стороны, да что тут такого? В любом языке программирования есть куча способов выстрелить себе в ногу, если ты не понимаешь что делаешь. Но с другой стороны, этот подход вообще не то, как должен работать SwiftUI. Работа с View в рантайме плохо. Да, вы можете бить себя пяткой в грудь, гарантируя что вы всегда пишете правильный код, который никогда не попадет в бесконечный цикл Но это как с не безопасным извлечением опционала это просто плохая практика.

Как я уже говорил, в таких случаях следует использовать Combine, передавая новое значение с помощью PassthroughSubject. Но можно и обойтись обычным ObservableObject, подписавшись на его willChange событие.

struct AnimatedRectObservedObject: View{    @State var startTime: Date = Date()    @State var angle: Double = 0    @ObservedObject var animationHandler: AnimationHandlerTest    let animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    init(animationHandler: AnimationHandlerTest){        self.animationHandler = animationHandler    }    var body: some View{        VStack{            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(animationHandler.isStarted ? animation : .default)       }.onReceive(animationHandler.objectWillChange){            let newValue = self.animationHandler.isStarted            if newValue == false{                 let timePassed = Date().timeIntervalSince(self.startTime)                 let fullSecondsPassed = Double(Int(timePassed))                 let currentStage = timePassed - fullSecondsPassed                 let newAngle = self.angle - 90 + currentStage * 90                withAnimation(.none){//not working:(((                 self.angle = newAngle                }            }else {                 self.startTime = Date()                 self.angle += 90             }        }       .onAppear{            self.angle += 90            self.startTime = Date()        }    }}

Спорный момент здесь это использование непосредственно значений самого объекта в подписке на уведомление, что объект только будет изменен. Пока, в моих примерах, я имею уже измененный объект. Видимо, уведомления приходят с некоторым лагом. Но не могу гарантировать что так будет всегда, потому советую либо использовать самописные didChange уведомления, либо PassthroughSubject, передавая новое значение через него.



Анимация изменения анимации


Как видно на гифке, анимация не останавливается мгновенно. Ей нужно некоторое время. Это происходит потому, что присваивая промежуточное значение угла, мы так же используем определенную анимацию дефолтную.

.animation(animationHandler.isStarted ? animation : .default)

Это ведет к необычному поведению. Несмотря на относительную простоту всего того, что я описываю, под капотом остается куча нюансов по взаимодействию, чтобы картинка выглядела плавно всегда, даже если безалаберный программист чего-то не указал. Поэтому дефолтная анимация это spring. Она больше всего подходит под имитацию законов физики. Причем это довольно короткая анимация, порядка 0.2 секунды. Если анимация перехода в новое состояние (вычисленный и указанный нами угло) не успевает красиво уложиться в дефолтные 0.2 секунды так, чтобы перейти от текущей скорости анимации с красивым замедлением до полной остановки, SwiftUI сама добавит еще один цикл бесконечной анимации, проигранный в ускоренном варианте.



Чтобы этого избежать, достаточно не использовать дефолтную анимацию. Для нижнего квадрата я отключил анимацию остановки вращения вот так:

                .animation(animationHandler.isStarted ? animation : Animation.linear(duration: 0))

Либо, можно более точно рассчитывать требуемое значение параметра в момент остановки, но для этого нужно уже не примерно предполагать текущий момент анимации, а точно знать его. Я покажу ниже, как это можно сделать.

Движение нескольких волн одной анимацией


Вернемся к радужной анимации. Итак, у нас есть сами волны, давайте научим их двигаться. Первое что я хотел бы сделать это запустить бесконечную анимацию. Я решил сделать следующим образом: у меня будет одна @State переменная, отвечающая за текущее положение анимации. При состоянии 0 первая волна будет в самом начале, а последняя в самом конце. Сами волны будут накладываться друг на друга. Таким образом, длина видимой части каждой волны будет зависеть от количества волн. Я реализовал это с помощью ZStack, в котором перечислены все волны, и собственным модификатором .wavePosition, внутри которого я вычисляю текущее положение каждой волны в данный момент анимации, и порядок их наложения друг на друга.

много кода
struct SharpWavePosition: AnimatableModifier {    let wave: WaveDescription    let animationHandler: AnimationHandler    var time: CGFloat    var currentPosition: CGFloat    public var animatableData: CGFloat {        get { time}        set {                self.time = newValue            let currentTime = newValue - CGFloat(Int(newValue))            self.currentPosition = SharpWavePosition.calculate(forWave: wave.ind, ofWaves: wave.totalWavesCount, overTime: currentTime)        }    }    init(wave: WaveDescription, time: CGFloat, animationHandler: AnimationHandler){        self.wave = wave        self.time = time        self.animationHandler = animationHandler        self.currentPosition = 0    }        static func calculate(forWave: Int, ofWaves: Int, overTime: CGFloat) -> CGFloat{        let time = overTime - CGFloat(Int(overTime))        let oneWaveWidth = CGFloat(1) / CGFloat(ofWaves)        let initialPosition = oneWaveWidth * CGFloat(forWave)        let currentPosition = initialPosition + time        let fullRounds = Int(currentPosition)        var result = currentPosition - CGFloat(fullRounds)        if fullRounds > 0 && result == 0{            // at the end of the round it should be 1, not 0            result = 1        } //       print("wave \(forWave) in time \(overTime) was at position \(result)")        return result    }    func body(content: Content) -> some View {        let oneWaveWidth = CGFloat(1) / CGFloat(wave.totalWavesCount)        var thisIsFirstWave = false        if currentPosition < oneWaveWidth{            thisIsFirstWave = true        }        return            Group{                content                            .offset(x: -wave.width + currentPosition * (wave.width +  wave.gradientLength),                            //to watch how waves move uncoment this                           // y: CGFloat(self.waveInd * 20))                        y:0)                    .zIndex(-Double(currentPosition))                    .transition(.identity)                    .animation(nil)                if thisIsFirstWave{                    content                        .offset(x: wave.gradientLength, y: 0)                        .zIndex(-2)                        .transition(.identity)                        .animation(nil)                }            }    }}extension View{    func positionOfSharp(wave: WaveDescription, inTime: CGFloat, animationHandler: AnimationHandler) -> some View {        return self.modifier(SharpWavePosition(wave: wave, time: inTime, animationHandler: animationHandler))    }}struct SharpRainbowView: View{    let waves: [SharpGradientBorder]    var animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    //@ObservedObject    var animationHandler: AnimationHandler    @State var rainbowPosition: CGFloat = 0    init(animationHandler: AnimationHandler,        backgroundColor: Color = .clear    ){        self.animationHandler = animationHandler        let bottomRadius = animationHandler.waveGeometry.bottomRadius        let topRadius = animationHandler.waveGeometry.topRadius        let gradientLength = animationHandler.waveGeometry.gradientLength        let rainbowColors = animationHandler.rainbowColors        guard var lastColor = rainbowColors.last else {fatalError("no colors to display in rainbow")}        var allWaves = [SharpGradientBorder]()        for color in rainbowColors{            let view = SharpGradientBorder(start: color,                                      end: lastColor,                                      bottomRadius: bottomRadius,                                      topRadius: topRadius,                                      gradientLength: gradientLength)            allWaves.append(view)            lastColor = color        }        self.waves = allWaves    }    var body: some View{        GeometryReader{geometry in            VStack{                ZStack{                    ForEach(self.waves.indices, id: \.self){ind in                        self.waves[ind]                            .positionOfSharp(wave: WaveDescription(ind: ind,                                                    totalWavesCount: self.waves.count,                                                    width: geometry.size.width,                                                    baseColor: self.waves[ind].end,                                                    gradientLength: self.waves[ind].bottomRadius + self.waves[ind].topRadius),                                             inTime: self.rainbowPosition,                                             animationHandler: self.animationHandler)                            .animation(self.animationHandler.isStarted ? self.animation : .linear(duration: 0))                    }                }     //           .clipped()            }        }        .onAppear(){            if self.animationHandler.isStarted{                self.rainbowPosition = 1            }        }        .onReceive(animationHandler.objectWillChange){             let newValue = self.animationHandler.isStarted             if newValue == false{                let newPosition = self.animationHandler.currentAnimationPosition                print("animated from \(self.rainbowPosition - 1) to \(self.rainbowPosition) stopped at \(newPosition)")                    self.rainbowPosition = newPosition             }else {                  self.rainbowPosition += 1            }        }    }} 


При первом отображении View на экране, мы меняем состояние с 0 на 1, что с учетом объявленной линейной анимации заставляет волны бесконечно бегать по экрану.



Когда край волны достигнет правого края, следующая ее позиция будет находится в самом начале, за краем экрана. Выглядеть это будет так, как будто из конца волны она просто исчезнет, обнажив фоновую подложку, чего мы хотели бы избежать. Именно для этой цели в модификаторе, в функции body() появился блок if{...}, в котором отрисовывается копия первой волны, пропавшей с экрана.



Обратите внимание, что мне пришлось передавать геометрические характеристики каждой волны в виде параметров модификатора. Проблема была в том, что мне требуется переопределять z-index каждой волны исходя из текущей фазы анимации. Я мог бы извлечь ширину видимой области, использовав внутри модификатора GeometryReader{}, однако столкнулся с тем, что он блокирует изменение порядка наложения волн. Модификатор .zIndex() работает только в контексте первого ZStack{} контейнера вверх по иерархии View.

GeometryReader обрубает эту связь, и zIndex() перестает работать. Если до этого момента вы думали, что GeometryReader это безобидный способ получить данные о размере текущей View это не совсем так.

Анимация потока


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



Решить эту проблему можно вставив модификатор .animation(nil). Таким образом, я отключаю анимацию всех модификаторов, примененных выше по тексту. Это общая логика работы любого модификатора: он меняет то, что у него на входе, а на входе у нас результат работы всех предыдущих модификаторов.

Вообще говоря, здесь работает целая иерархия разных анимаций. Дефолтной считается та, которая будет использована для отображения изменений всех модификаторов, если только вы не указали иное для вашей View. У нас есть поток исполнения (допустим, это main), в рамках которого вызывается body. Будем считать, что у него есть параметр .animation, который проверяется каждый раз, когда какой-то модификатор получает новое значение. Если модификатор поддерживает анимацию (удовлетворяет протоколу AnimatableModifier), и для потока включена анимация (используется какая-то конкретная анимация, а не .none и не nil), то изменение будет анимировано. Именно это мы делаем, заключая какой-то код по изменению @State параметров в блок withAnimation{} прописываем определенную анимацию в текущем потоке, а затем выполняем изменение какой-то @State переменной. В этом случае withAnimation{}, это своего рода эквивалент транзакции, и все изменения выполненные в этой транзакции будут анимированы. В результате, внутри этого же потока запускается цикл трансляции этих изменений во все зависимые View, модификаторы этих View получают новое значение, и подписываются на получение промежуточных значений AnimatableData.

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

Таким образом, withAnimation() это способ запустить анимацию по-умолчанию для выполняемого действия. Но для определенных View вы можете в явном виде указать собственную анимацию с помощью модификатора .animation(). В этом случае, модификаторы и формы, которые составляют контент, передаваемый в .animation() (все что перечислено в коде до применения .animation()) получают описанный вами тайминг, и игнорируют анимацию потока.

В своем модификаторе WavePosition, я использую .animation(nil) для того чтобы избавиться от встроенной анимации offset, дав таким образом указание игнорировать текущую анимацию потока.

Та же история и с концевой заглушкой. Напомню выводы из прошлой статьи. Фактически, у нас в памяти N структур-модификаторов WavePosition, по одной на каждую волну. И все они получают новое значение position по системному таймеру, вычисляя положение каждой волны в данный момент времени. Это значит, что у нас так же N концевых заглушек под каждую волну. Просто в каждый момент времени показывается только один из них, благодаря блоку if{}. Однако, этот же блок подкладывает нам свинью. Исчезновение и появление View также выполняются с анимацией потока. Это значит, что задействуется модификатор .transition для анимации появления и исчезновения View после изменения условия. Обычно, по-умолчанию используется .opacity, однако у меня почему-то вместо этого использовался .slide. Ни то ни другое мне не подходит, потому я просто отменил эту анимацию, используя .transition(.identity).

upd. Пока я готовил статью, вышел XCode 11.4, в котором, похоже, transition по-умолчанию переработали. По крайней мере, сейчас я без проблем закомментировал .transition(.identity) и не получил той проблемы, из-за которой мне пришлось его добавлять.

Тайминги анимации


И вот мы наконец подошли к самому интересному вопросу: а как заставить волны двигаться ускоренно? Как обычно, я зайду издалека.

На самом деле Animation это описание скорости данной анимации. Маршрут из пункта А в пункт Б мы можем определить внутри самого модификатора, но SwiftUI сам решает, когда и какое именно значение AnimatableData передать в него. Делает он это с помощью тайминговой кривой. Предположим, пункт А мы возьмем за начало координат, а пункт Б отметим как точку с координатами (1; 1). По горизонтали мы будем отмечать прошедшее время (движение из пункта А в пункт Б занимает ровно 1 единицу времени). По вертикали пройденное расстояние в долях единицы. При использовании линейной анимации, мы получим прямую. Если же мы хотим получить движение хоть чуть-чуть похожее на настоящее, то вначале нам нужно потратить немного времени на разгон, а в конце на торможение. Вот тут можно поиграться с разными вариантами, рисуя свою кривую и сравнивая ее анимацию движения со стандартными.

В SwiftUI, для различных типов анимации используются кривые Безье. В любом случае она должна начинаться в (0;0) и заканчиваться в (1;1). Кривизна линии определяется контрольными точками.

Вложенная тайминговая кривая внутри линейной бесконечной анимации


Так вот, что нам нужно сделать, чтобы добиться ускоренного движения каждой волны (easeIn) внутри линейной зацикленной анимации?

Самый отбитый вариант, который я смог придумать это реализовать свою собственную тайминговую кривую, и преобразовать с ее помощь линейное течение времени в подобие .easeIn. На этом примере можно будет очень хорошо разобраться, как же это работает.

Идея простая. Мы сделаем свой класс, в основе которого будет path кривая Bezier с помощью которой мы будем определять расстояние из пункта А в пункт Б, которое нужно показать в каждый определенный момент времени. На самом деле именно так и работает анимация в SwiftUI, да и в любом другом фреймворке с подобным функционалом. С помощью тайминговой кривой, реальные секунды и миллисекунды (которые чаще всего текут, все же, линейно) превращаются в доли расстояния(точнее, доли анимируемого отрезка вектора AnimatableData, который представляет собой выполненное изменение).

Вот моя первая реализация:

    func getActualPosition(of position: CGFloat) -> CGFloat{        let correctPosition = max(min(position, 1), 0) / duration        let trimmingCurve = TimingCurve.superEaseInPath        if correctPosition < 0.0000001{            let reversedCurve = Path(UIBezierPath(cgPath: trimmingCurve.cgPath).reversing().cgPath)            //trim to start point is impossible, so reverce the curve and get last point            guard let point = reversedCurve.currentPoint else{fatalError("cant get current timing curve start point")}            return point.y        }        guard let point = trimmingCurve.trimmedPath(from: 0, to: correctPosition).currentPoint else{fatalError("cant get current timing curve point at \(position)")}        return point.y * self.duration    }

Как видно, я немного схалтурил. По определению, я должен найти точку на кривой, соответствующую данному значению X, и вернуть ее Y. Мне не удалось найти какого-то стандартного встроенного метода для решения этой задачи, или популярного паттерна, потому я решил адаптировать для этих целей имеющийся метод trim(). Я решил что я вполне могу немного пересмотреть подход, и получить искомую точку с помощью получения части пути, соответствующей доли пройденного расстояния. Для нуля этот метод вернет 0, для 1 вернет 1, ну и в середине, наверное, все тоже будет примерно правильно.

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



Мало того, что точки находятся далеко от требуемой кривой, так и сам характер кривой искажается. Появляется лишний перегиб, что мне вообще не нужно. В итоге, мне все же пришлось придумывать относительно точный способ поиска точки на кривой Безье, зная только одну координату:

Мне пришлось хранить в памяти множество отрезков, концы которых лежат на заданной кривой. Поиск Y по данной X при таком подходе представляет собой поиск соответствующего отрезка, его деление пополам при необходимости, до тех пор, пока одна из границ такого отрезка не будет лежать достаточно близко к искомой точке. В этом случае, мы находим точку с требуемой координатой X, лежащую на данном отрезке, и возвращаем ее Y. Если вам известны более простые пути нахождения точки, лежащей на произвольной кривой Безье, по одной заданной координате напишите в комментариях. Думаю это может быть полезно многим.

В итоге, точки лежат довольно близко к графику:



Я сделал singlton объект для создания timing кривой по контрольным точкам и кэширования массива отрезков, с помощью которых происходит поиск Y. Таким образом, все что мне нужно для поиска конкретной точки на кривой найти отрезок, внутри которого будет лежать эта точка, поделить его попалам до тех пор, пока один из концов отрезка не будет достаточно близко к этой точке, для попадания в заданную погрешность, и затем, линейно интерполировать значение Y по заданной X, подменив часть кривой этим отрезком. Не утверждаю что это лучший способ, и что он будет работать во всех случаях. Опять же, пишите в комментариях, если знаете способы лучше.

С помощью такого инструмента, трансформация линейной анимации в нелинейную становится несложной. Все что нужно описать кривую по двум контрольным точкам, и пересчитать положение на отрезке согласно этой кривой. Если требуется анимация различной длительности, мы все равно используем единичную кривую, используя соответствующий коэффициент для X-координаты.

        self.timing = TimingCurve.superEaseIn(duration: 1)let animatedPosition = timing.getY(onX: currentPosition)

Вот так в итоге выглядит анимация вместе с нашей тайминговой кривой:



За полным кодом добро пожаловать на гитхаб, смотреть файл TimingCurveView.

Разобравшись с этим примером, вы на 100% поймете как устроены тайминги анимации. Вы, кстати, сможете использовать это знание, ведь SwiftUI позволяет создавать свою тайминговую кривую по контрольным точкам с помощью функции timingCurve(), и использовать ее как любую другую анимацию.

Transition


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

Идея анимации появления заключается в том, чтобы блок с анимацией просто возникал на месте уже включенной, но с него как бы отдергивалась шторка, как портьера в театре. Если синхронизировать скорость движения шторки со скоростью движения волн внутри анимации, и в самой шторке сделать градиент от фонового цвета к цвету текущей набегающей волны, мы получим визуальный эффект, как будто эта шторка это первая накатывающая волна. Для синхронизации скорости движения, мы просто используем ту же timingCurve что и для анимации движения волны. Благо, мы теперь знаем как это сделать.

   let animation = Animation.timingCurve(Double(TimingCurve.control.point1.x),                                Double(TimingCurve.control.point1.y),                                Double(TimingCurve.control.point2.x),                                Double(TimingCurve.control.point2.y),                                duration: 1)

Animation.timingCurve(x:y:) позволяет задать свой тайминг анимации на основе контрольных точек кривых Безье. На выходе мы получим полноценный объект Animation, как например привычный .linear(duration: 1), который можно использовать без ограничений. А учитывая, что я для анимации волн использую ровно те же контрольные точки, анимация будет синхронной.

можно было бы поудобнее организовать код...
Но я ленивая жопа.

По идее, можно было более корректно реализовать предопределенные статические объекты для тайминговых кривых, описание их контрольных точек, сделать для каждой из таких кривых индивидуальное хранилище интерполированных отрезков но мне уже стало лень, это все таки учебная задача, а не библиотека для распространения. Если кто-то захочет допилить, и взять эти механизмы в публичную библиотеку я возражать не буду.

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

Первая проблема возникла в тот момент, когда мне потребовалось получить текущее состояние анимации, чтобы подобрать цвет шторки. Для этого я создал ObservableObject, который передаю внутрь модификатора, и который я изменяю внутри сеттера AnimatableData. Здесь важно понимать, что изменение @Published свойства 60 раз в секунду это совсем не то что вам нужно. Это как изменять @State переменные внутри блока body. Нам не нужно инициировать что-либо при изменении состояния анимации. Но вот если нам потребуется, мы сможем узнать состояние анимации в любой момент.

  public var animatableData: CGFloat {        get { time}        set {            if animationHandler.isStarted{                self.time = newValue                if self.time != self.animationHandler.currentAnimationPosition{                    self.animationHandler.currentAnimationPosition = self.time                }            }            let currentTime = newValue - CGFloat(Int(newValue))            self.currentPosition = SharpWavePosition.calculate(forWave: wave.ind, ofWaves: wave.totalWavesCount, overTime: currentTime)            if currentPosition < 0.01{                animationHandler.currentWaveBaseColor = wave.baseColor            }        }

Именно поэтому я не подписывал SharpRainbowView на отслеживание изменений AnimationHendler:

   //@ObservedObject    var animationHandler: AnimationHandler

Для иллюстрации работы этих шторок, я включу отображение того, что происходит за границами View, закомментировав .clipped(). Кроме того, я сдвину шторки чуть выше анимации, для наглядности. Теперь стало понятнее, неправда ли?



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

Все дело в том, что первая волна представляет собой градиент последнего и первого цвета переданного массива. Ни один из них может не совпадать с цветом фона. Поэтому, и возникла необходимость в шторках вообще. Сделать шторку двухцветной, от фонового цвета к базовому цвету первой волны, мне помешала длина шторки. Она просто не перекрывает длинный хвост первой волны, и получается граница одного цвета, а хвост другого. Поэтому мне и пришлось делать шторку аж трехцветной сначала от фона к цвету последней волны, а затем от цвета последней волны к первой. Тогда хвост первой волны хорошо стыкуется с границей шторки.

Примерно те же причины заставили сделать замыкающую шторку так же трехцветной. Иначе граница смотрелась неестественной. Но в результате, первая волна (а теперь мы знаем, что на самом деле это шторка, описанная в Transition.truncate) получилась короткой. И хоть она движется полностью синхронно с первой волной, т.е. ускоренно, а не линейно, ее ширина не меняется со временем, хотя ширина последующих волн визуально увеличивается. Это можно победить, динамически изменяя ширину шторки, но мне откровенно было уже лень. Давайте расценим это как домашнее задание для тех, кто хочет погрузиться в эти механизмы, и раскурить код (заранее прошу у таких людей прощения, там полно следов моих экспериментов, которые могут вас запутать).

Есть одна проблема. Если вы нажмете кнопку в неудачное время, шторка закрытия едва-едва прикроет очередную волну. Выглядит это не очень. Решить проблему можно путем введения небольшой задержки сокрытия View с анимацией. У нас есть способ узнать текущее положение анимации, нам нужно всего лишь немного отложить начало transition, и последняя волна всегда будет полностью показана.

    Text("toggle animation").onTapGesture {                withAnimation(){                    var delay: Double = 0                    if self.isShown{                        let waveChangeTime: Double = Double(1) /  Double(self.animationHandler.rainbowColors.count)                        let currentTime = Double(self.animationHandler.currentAnimationPosition)                        let wavesPassed = Double(Int(currentTime / waveChangeTime))                        delay =  (wavesPassed + 1) * waveChangeTime - currentTime                        delay = max(delay - 0.05, 0)                        print("currentTime: \(currentTime); delay \(delay)")                    }                    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {                        self.isShown.toggle()                    }                }            }

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

Transition любит подкладывать свинью


Вообще, анимации появления и исчезновения, как по мне, очень прикольный инструмент. Но похоже, эта концепция нова, и нормально обрабатывается далеко не всеми инструментами.

Так, .rotation3DEffect() модификатор вполне успешно переворачивает View с какими-то сложными анимациями внутри, но вот .transition она переворачивать не умеет. Так обидно было написать лаконичный код, увидеть как твои волны зеркально разбегаются от центра экрана, а потом увидеть, что на левой View шторки так и бегают слева направо. Пришлось писать кучу бойлерплейта, дабы transition модификатор умел работать в обе стороны.



Еще, drowingGroup() модификатор, с помощью которого вы можете подключить Metal переложить на GPU отрисовку ZStack с большим количеством вложенных View, особенно View с градиентами, не умеет в transition. Он не понимает описанную вами анимацию появления и исчезновения и заменяет ее какой-то своей.

А как же мы прячем статус-бар?


Очень просто. В SwiftUI есть модификатор .statisBar(hidden:). Вот только api для управлением transition для статус-бара SwiftUI не предоставляет. Для этого нам придется воспользоваться возможностями UIKit. В файле SceneDelegate используется UIHostingController для превращения SwiftUI View в UIKit ViewController. Именно на этом этапе удобнее всего использовать какие-то глобальные функции UIKit, как то работа со статусбаром, или отключение системных жестов связанных с краем экрана (preferredScreenEdgesDeferringSystemGestures). Вы можете наследоваться от UIHostingController, переопределив значения каких-то системных свойств, и использовать этого наследника для передачи своей View в rootViewController. К сожалению, статусбар может принимать только ограниченное число transition: .fade, .slide и .none. По-умолчанию используется fade, и он сюда подходит лучше всего, так что оставим как есть. Будем надеяться, что этот функционал все же будут расширять.

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

struct StatusBarHider: View{    var isShown: Bool    @State var internalIsShown = true    var body: some View{        if isShown == false && self.internalIsShown == true{            DispatchQueue.main.asyncAfter(deadline: .now() + 0.7){                self.internalIsShown = self.isShown            }        }else if isShown == true && self.internalIsShown == false{            DispatchQueue.main.async(){                self.internalIsShown = self.isShown            }        }        return Spacer()            .statusBar(hidden: internalIsShown)            .animation(Animation.linear(duration: 0.3))    }}

По сути, здесь формируется простой Spacer(), который мне и так был нужен, но на него сверху накручен модификатор statusBar. Вообще, конечно, со стороны Apple было нелогично выводить этот функционал в модификаторы, т.к. это не относится к конкретной View, которую ты модифицируешь, но, видимо, просто не придумал как сделать это лучше. Куда интереснее другое.

Здесь используется тот же грязный хак, что мы рассматривали ранее: внутри body я выполняю какие-то действия в рантайме, меняя при этом значение @State переменной. Логика в целом та же, есть параметр структуры, который передается извне, и @State переменная, которая модифицируется спустя какое-то время, вызывая исчезновение или появление статусбара.

В данном случае, этот хак мне понадобился потому, что управление анимацией статусбара это функционал UIKit, который еще довольно плохо проработан в SwiftUI. По идее, я бы прикрутил анимацию с отложенным стартом к модификатору .statusBar(hidden:), но это не работает. Анимация скрытия и появления статусбара фиксирована, и не подлежит изменению со стороны SwiftUI.

На самом деле, мне ничего не мешало выполнить то же самое изменение асинхронно с нужной задержкой не внутри этой View, а в родительской, еще при нажатии кнопки пользователем но оставим это как памятник моим попыткам разобраться что к чему (и еще одним поводом рассказать, что так делать не следует).

А как запихнули размеры и положение статусбара в @Environment?


@Environment (не путать с @EnvironmentObject) это обертка, дающая доступ к фиксированному перечню переменных, отражающих окружение нашего приложения. Например, ориентацию экрана, или цветовую тему ОС. Эта же обертка позволяет вашим view быть подписанными на изменение этих параметров.

Этот перечень можно расширить. Я посчитал, что иметь в @Environment доступ к размеру и положению статусбара это было бы правильно. Вот как я это сделал:

struct StatusBarFrame: EnvironmentKey {    static var defaultValue: CGRect {        CGRect()    }}extension EnvironmentValues {    var statusBarFrame: CGRect{        get {            return self[StatusBarFrame.self]        }        set {            self[StatusBarFrame.self] = newValue        }    }}

Я создал структуру со статическим default значением. Затем, я расширил системную структуру EnvironmentValues, добавив в нее свое вычислимое свойство, геттер и сеттер которого ссылаются на созданный мной тип. Здесь используется именно тип, поскольку в EnvironmentValues реализован сабскрипт, в который ты передаешь тип, и получаешь хранимое значение, соответствующее этому типу. Не значение этого типа, а значение, хранимое в условном словаре, где ключом является сам тип.

Доступ к значению в @Environment осуществляется с помощью keyPath \.statusBarFrame. Например, для для передачи environment-значения всем view вниз по иерархии:

.environment(\.statusBarFrame, statusBarFrame) 

И в самих View для извлечения значения из хранилища:

@Environment(\.statusBarFrame) var statusBarframe: CGRect 

Кстати, для работы со статусбаром в объекте UIWindowScene в IOS 13 появился реквизит statusBarManager. Из него можно вытянуть некоторые параметры. А вот управлять ими теперь нельзя. Насколько я понял, раньше можно было получить доступ к ViewController-у статусбара, и добавить в него subView. Видимо, лавочку прикрыли.

Вообще говоря, я бы перенес функционал модификатора .statusBar(hidden:) именно сюда, тут он был бы более уместен, как по мне. Думаю рано или поздно, у разработчиков дойдут до этого руки.

Заключение


Что же, думаю на этом увлекательном примере экскурс в Анимацию средствами SwiftUI можно считать завершенным. На простых примерах я рассказал об основных инструментах анимации, рассказал как именно они работают под капотом, как их следует использовать и почему именно так. На более сложных примерах я показал типичные трудности, с которыми вы можете столкнуться и пути их преодоления.

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

SwiftUI активно пилится. Даже сейчас, две недели спустя написания кода к первой статье, сейчас, проверяя работу кода на текущей версии XCode, я вижу как некоторые баги, которые я вынужден был обходить, уже пофиксили. Поэтому я со статьей немного задержался. Кое что пришлось вырезать за неактуальностью, а все остальное перепроверять. И это радует. SwiftUI это определенно будущее нативной IOS разработки, и это очень познавательно, стоять у его истоков, наблюдать как он развивается.

Послесловие


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

Ретроспектива создания своего мультфильма

20.06.2020 18:10:45 | Автор: admin
Мы живём в удивительное время. То, что раньше было невероятным, сегодня у нас буквально валяется под ногами. В наши дни любой человек может сделать свой собственный мультфильм. Анимационные программы упрощают и ускоряют этот процесс настолько, что даже один человек ну будучи аниматором, может сделать настоящий анимационный фильм.
С удовольствием поделюсь полученным мною опытом. Речь будет идти о 2D-анимации, но многие моменты равно применимы и к 3D. Кому будет интересно ссылка на сам мультфильм в конце поста.

Вся работа от сценария до финального сведения всех сцен заняла 9 месяцев. Когда ты работаешь над мультфильмом один, стоимость производства это твоё время (твой основной ресурс). Именно, поэтому, авторская анимация это большое количество компромиссов. Для снижения стоимости проекта потребуется отказ от многих аспектов большого фреймрейта, от избыточной детализации, от крутых текстур, от сложных движений, от покадровости, от мультиперсонажности, от диалогов (озвучки), от саунд-дизайна (окружающие звуки), от спецэффектов, и ещё много чего. Но давайте разбираться с полученными уроками по порядку.

Анимация это сила. В анимации можно решить любую задачу. При написании сценария есть соблазн что-то не делать, потому, что на тот момент ты не представляешь как это нарисовать и, тем более, санимировать. Но когда начинаешь решать конкретную задачу находишь удивительные и простые решения. При написания сценария, не бойтесь! Ни за что не ограничивайте полёт своих мыслей.

Анимация это не самое важное. Анимация на втором месте. История важнее анимации. Она должна быть интересной, стоящей, поучительной. Анимация лишь раскрывает вашу историю.

Время растягивается. Как регулярно происходит в настоящем анимационном продакшене, в процессе производства стоимость проекта повышается. Если вы делаете свой первый мультфильм, то времени потребуется гораздо больше, чем вы предполагали. Нет, ещё больше. Одна из причин мы слишком оптимистично представляем себе сам процесс, переоцениваем свои способности, знания и навыки, а они требуются сразу из многих областей (рисование, композиция, цвет, принципы анимации, опыт тайминга, спэйсинга, ритм, строение кадра, монтаж, эффекты, свет и тень, текстуры и др.). Будьте готовы, что всплывёт отсутствие знаний, навыков сразу в ряде областей. И это тоже растянет время.

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

Ускорение развития навыков. Чередуйте, банально переключайтесь между теорией (20%) и практикой (80%). Не впадайте ни в одну, ни в другую крайность. Понятно, если вы ограничитесь теорией, то вы ничего, никогда и не сделаете. Но часто многие ограничиваются практикой, силовым методом и это тоже снижает скорость развития. Из 5 часов 1 час изучайте, 4 делайте. По-моему опыту это заметно ускоряет.

Замыливание. Не мыльте глаза. Старайтесь пересматривать свои сцены не слишком часто глаз замыливается очень быстро. Пополишуйте одну сцену и отложите в сторону на день. На следующий день увидите ей другими глазами.

Всегда понимаешь, что сценарий плох. В процессе работы над своим мультом, у вас будут появляться более лучшие идеи, решения. Стоит ли следовать изначально утверждённому плану или на ходу перелепливать пельмени? Мой совет перелепливать, в итоге получится лучше!

Название мультфильма. Лучшее название для вашего фильма появится в самом конце. Вы сразу его узнаете. Мультфильм вы начинаете делать просто с рабочим названием.

Зритель видит по-другому. Рассказывать истории очень сложно. Имеет значение всё. Зритель не увидит так как хочешь ты, он увидит так как он видит. И отсюда сразу два урока:
Чётко управлять вниманием, фокусировать на важном (прежде всего движением, в т.ч. камеры, контраст, световой/цветовой, выбивание/отличие из ряда и т.д.).
Обязательно устраивать предпоказы узнаешь как видят твою историю другие люди (зритель не смотрит туда, куда нужно посмотреть, упускает нить повествования, внимание рассеивается, расфокусируется, и т.д.).

Развитие сцены. Сцена должна развиваться. Не выкладывайте зрителю все карты сразу. Сначала покажите лампу, потом, что она может светиться, потом, что можно регулировать яркость свечения, потом, что может мигать одинаковым тактом, потом неравномерным тактом (под музыку), потом, что может светиться разными цветами и т.д. Развивайте сцены.

Недержание стиля. Сложно держать стиль. Почему так сложно выдержать его? Всё просто за полгода-год работы вы не можете не измениться, развиваются ваши навыки, представления, видение сейчас вы бы сделали по-другому, лучше. Но нужно сохранить начатый год назад стиль, чтобы произведение было цельным и не разрывалось.

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

Повышение производственной эффективности:
Банальное разбиение на сцены ускоряет работу, через вырезание/перемещение ключей, открытие проекта, пререндеринг).
Чистота структуры (легко и быстро читаемые прежде всего вам названия слоёв, объектов, кистей, темплейтов). Если вы задумались над элементом значит, название для него подобрано неудачное.
Простая система наименования сцен.
Качественное планирование (сценарий, сториборд, аниматик, Списки требуемых фонов, персонажей, пропсов, эффектов и т.д.). По умолчанию получается Анимация в воде слишком гладкая, нет динамики. Планирование в итоге просто сэкономит время, а поднимет анимацию на уровень выше.
Специализация. Делать работу по этапам (только фоны подряд или только риги или только пропсы). Без разницы в какой очерёдности.
Оптимизация количества локаций, фонов, количество персонажей не рисовать ненужное, несущественное, не значимое, не несущее ценной информации.
Стилизация картинки (упрощение) условные блики на стёклах, ограниченная цветовая схема на весь проект и т.д.
Скелетный подход. Накидать сцены в грубую, а потом дополишовывать (в итоге эффективнее, чем вылизывать каждую сцену до идеала).

Рисование фонов. Фоны нужно рисовать с учётом предполагаемого композитинга:
Каждый элемент фона на отдельном слое, чтобы можно было на этапе композитинга передвинуть объекты и использовать эффект параллакса.
Рисовать фоны важно в перспективе (Прямые анфас и профиль не интересные, всегда интересны перекрестки, углы, склоны, подъёмы).
Одинаковости в мире не существует (даже одинаковые элементы должны различаться деталями).
Аккуратно с детализацией (изляшняя проработка фона может отвлечь внимание зрителя с важных событий).

Риггинг. Риги нужно конструировать, учитывая:
Характер, особенности персонажа (какой он, какие движения ему потребуются, в каких ракурсах он нужен в сценах, планируются ли диалоги). Если не спланируете риг заранее, это может значительно затянуть время производства мультфильма.
Персонажа рисовать прежде всего стоит в ракурсе 3/4, спереди. Самый важный ракурс персонажа. Он чаще всего будет использоваться. Если вы планируете делать диалоги, то хорошо разнообразят картину 3/4 сзади собеседников персонажа.
Максимально упрощайте своего персонажа (без лишних деталей, текстур) гораздо ценнее вложить время в качественную анимацию, что в красивые риги с плохими движениями.
Наименование слоёв для быстрого ориентирования (и здесь тоже).
Важность установки осей вращения (Pivot Point) в темплейте.
Требуемый стиль (реалистичность, резиновость), возможности и ограничения техники перекладки.

Анимация покоя. Персонажи шевелятся всегда моргание, дыхание, покачивание (не существует статики в реальной жизни). Второстепенные персонажи в сцене должны двигаться номинально. Основные ярко. Правильные акценты:
Моргание не равномерно (оно отражает состояние, быстрота моргания, частота).
Микродвижения видны (дыхания, качание, волосы, зрачки и т.д.)

Спецэффекты. Все спецэффекты рисуйте в отдельных проектах. И только когда вас полностью устроит результат, переносите его на свою сцену. Это ускорит процесс создания, увеличит качество результата, вы не навредите уже готовым элементам в ваших сценах.

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

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

Вложенность нейросетей инструмента автопозинга в Cascadeur

17.06.2020 16:18:48 | Автор: admin

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

В этой статье будет рассмотрен подход, позволивший нам реализовать достаточно продвинутый функционал, используя лишь стандартные deep learning методы.

Постановка задачи


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



Использование полносвязных нейросетей предполагает фиксированные вход и выход, поэтому мы сделали несколько нейросетей с разным количеством входных точек: 6, 15, 20, 28 точек из всех 43 точек персонажа. На картинках ниже в зеленый окрашены те точки, которые подаются на вход нейросети соответствующего уровня детализации.


В чем же проблема использования уровней детализации? Если мы хотим подвинуть точку из 4-го уровня, то нам нужно подать на вход все 28 точек. Но мы не хотим заставлять пользователя ставить их все. Наша цель дать ему возможность подвинуть только несколько из них. Как в таком случае добиться хорошего результата? Наше решение предполагает вложенность входных данных, комбинирование результатов и использование физической модели.

Вложенность входных данных


Мы выбрали такие уровни детализации, которые имеют особое свойство иерархической вложенности.


Множество входных точек нейросети каждого уровня содержит в себе все точки с предыдущего уровня и добавляет к ним несколько новых. Это позволяет нам использовать выходные данные с одной сети как входные для следующей.

Комбинирование результатов


Давайте рассмотрим работу инструмента на примере: пользователь расставил все 6 основных точек и решил отредактировать ориентацию левой кисти за счет дополнительных точек кисти со второго уровня детализации.


Как только вы меняете еще одну точку, кроме основных 6, инструмент запоминает ее и начинает использовать в вычислении позиции других точек. Работа инструмента происходит в несколько этапов в зависимости от отредактированных точек. В данном случае весь процесс схематично изображен на картинке ниже.


Сперва используется сеть первого уровня она выставляет все 43 точки персонажа по 6 основным. Затем по очереди вызываются сети более детальных уровней. Каждая последующая принимает на вход все более детальные входные данные либо уточненные пользователем, либо из результата работы предыдущего уровня. Таким образом мы получаем возможность использовать нескольких нейросетей с разной детализацией одновременно.

Физическая корректность


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


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

Заключение и планы


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

Также мы исследуем возможности, которые дарят нам более универсальные deep learning подходы. Например, уже сегодня можно восстанавливать части фотографий с заданными характеристиками, а также переносить стиль и другие характеристики между изображениями. В будущем мы могли бы использовать этот метод и при создании анимации, например, чтобы добавить в нее или же в позу желаемые характеристики.

Мы продолжаем развивать наш инструмент автопозинга. Уже в ближайшее время Cascadeur войдет в стадию открытого бета-теста. Обязательно следите за новостями на cascadeur.com и в социальных сетях проекта.

Узнать больше о Cascadeur и других проектах студии Banzai Games:

Почему 12 принципов Диснея недостаточно
Cascadeur: задача о падающей кошке
Физика в Unity-проекте на примере мобильного файтинга
Cascadeur: будущее игровой анимации
Искусственный интеллект в файтинге Shadow Fight 3

В команду Banzai Games требуется Qt GUI программист. Подробнее о вакансии можно прочитать здесь.
Подробнее..

Стартовал открытый бета-тест Cascadeur

28.07.2020 18:07:12 | Автор: admin


Со времени первого анонса Cascadeur в начале 2019 года уже более 18 000 пользователей приняли участие в закрытом бета-тестировании программы. У аниматоров, занимающихся разработкой игр и созданием фильмов, было более 12 месяцев, чтобы испытать в работе наш инструментарий.

Сегодня мы рады сообщить о запуске открытого бета-теста (ОБТ) своей анимационной программы для физически корректной анимации персонажей. Благодаря ОБТ протестировать и оценить ее возможности сможет более широкий круг пользователей.

Подход Cascadeur к анимации значительно облегчает достижение точной механики тела. Теперь я уверен, что инструменты для физически корректной анимации станут ожидаемым стандартом в индустрии, поделился мнением один из первых пользователей Cascadeur, анимационный директор Polyarc Ричард Лико, ранее работавший над Destiny 2.

Опрос, проведенный издателем Nekki в апреле 2020 года, показал, что 85% бета-пользователей Cascadeur считают его инструментом, который будет играть важную роль в их будущих проектах. В январе 2020 года Nekki и Cascadeur были номинированы на премию Pocket Gamer Mobile Games в номинациях Best Innovation и Best Tool Provider, что является редким достижением для еще неизданного продукта.





Над открытой бета-версией Cascadeur мы работали в течение года. Большинство изменений могут быть невидимыми на первый взгляд, но внутри программы был сделан глобальный редизайн всей архитектуры. Основные особенности новой версии включают:

  • Новую архитектуру, которая делает Cascadeur намного быстрее и эффективнее

  • Улучшения рига, такие как способность перетаскивать или вращать центр масс без его фиксации и улучшенная интерполяция

  • Доработку инструментов создания рига

Новая архитектура Cascadeur значительно ускорит процесс разработки и оптимизации программы. Следующие шаги будут включать в себя:

  • Дальнейшее улучшение риггинга, с возможностью создания кастомных ригов

  • Поддержка Python для автоматизации процессов

  • Бета-версия инструмента Graph Editor






Чтобы сделать использование Cascadeur привлекательным для профессиональных аниматоров, Nekki разрешает бесплатное коммерческое использование бета-версии. Любая анимация, созданная в ОБТ-версии Cascadeur, может быть бесплатно использована в играх и фильмах без предварительного разрешения Nekki.

Мы хотим наглядно продемонстрировать вам основные особенности и инструменты Cascadeur в новом 5-минутном видео:



Получить более подробную информацию и скачать ОБТ-версию Cascadeur вы можете на cascadeur.com

Узнать о Cascadeur больше:

Вложенность нейросетей инструмента автопозинга в Cascadeur
Почему 12 принципов Диснея недостаточно
Cascadeur: задача о падающей кошке
Cascadeur: будущее игровой анимации
Подробнее..

Искусственный интеллект подался в баскетбол, а Анубис строить карьеру на телевидении

15.08.2020 02:16:25 | Автор: admin

Полное видео туть: youtu.be/lPfiMHQWP88

Аве, Кодер!

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

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

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

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

Баскетбол. Дриблинг. Бешеная динамика. Стойки. Обращение с мячом. Модели перемещаются быстро, часто меняя направление движения. Понадобится действительно грандиозное решение, чтобы все это динамическое добро обрабатывалось быстро, мощно и, между тем реалистично.

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

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

Казалось бы, учитывая ограничения, ИИ должен был не справиться с задачей, хотя бы, частично. Были предположения, что движения, входившие в эти три часа обучения, нейросеть будет адаптировать без проблем, а вот с синтезом новых будет посложнее и поэтому модели будут вести себя в некоторых моментах неественно, но результат превзошел все ожидания.

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

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

Пример дрибблинга:




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

Игрок также мог бросать мяч в кольцо и совершать подбор и при этом модель вела себя естественно, несмотря на то, что нейросети было предоставлено меньше семи минут материала для обучения.

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

Как видно из видеопримера, одна модель обучена двигаться, используя метод обучения основанный на Phase-Function Neural Network, а другая предложенный AI4Animation.

Сравнение двух моделей:



Когда сравнивали движения двух моделей, игроки могли заметить явное отсутствие скованности у варианта AI4Animation: плавность движений, присущая живым организмам и то, как модель управляет сторонним предметом мячом.

При дриблинге, модель обученная Phase-Function Neural Network заставляет мяч быть, как бы, приклеенным к руке игрока только ради того, чтобы ей было легче просчитывать движения модели, но и в этом случае это не приносило очевидного преимущества.

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

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

Насколько она улучшится? В каких ещё спортивных играх она найдет применение? Только лишь спортивных? Только в играх?

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

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

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

Пример с хорошим мальчиком:



А вот и Анубис решает прилунить свою мифологическую попу на различную мебель и, как бы сказала Малышева делает это естественно.

Пример с Анубисом:




Или пробует работать доставщиком черных ящиков в Что? Где? Когда?. Осталось только научить его вращать барабан

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

Ознакомиться можно вот тут: github.com/sebastianstarke/AI4Animation

Это был Ви. Заглядывайте на канал Аве, Кодер!

Аве!
Подробнее..

Перевод История создания Dragons Lair

07.11.2020 22:21:21 | Автор: admin

Немногим игровым компаниям удаётся настолько поразить любителей аркад, вынуждая их закидывать в автомат не 25 центов, а полтинник дабы убедить себя: то, что я сейчас увидел, было на самом деле. В 1983 году этой игре такое удалось.


image


Dragon's Lair или Логово Дракона это история о рыцаре, который врывается в замок с ловушками, чтобы спасти прекрасную девушку от злого дракона. Да, здесь не особо оригинальная история и игра не превратилась в культовый франчайз, однако её появление стало одним из самых запоминающихся событий в истории видеоигр.


image


И по сей день ведущий разработчик Dragon's Lair Рик Дайер получает электронные письма от фанатов, которые рассказывают, какое влияние игра оказала на их жизнь. Десятки лет спустя эта игра всё ещё продается, и это одна из немногих видеоигр, когда-либо попавших в Смитсоновский институт (двумя другими являются Pong от Atari и Pac-Man от Namco). Это была видеоигра, созданная при помощью новейшей технологии лазер-дисков. Это было завораживающе. Игроки смотрели на неё с трепетом. Дайер объясняет это законом Кларка: Любая достаточно продвинутая технология неотличима от магии.


image


Гадкий утёнок


В течение двух лет компания Дайера Advanced Microcomputer Systems работала над созданием игры в мире фэнтези. Взяв пример с Adventure (выходила на Atari 2600 в 1980 году), Дайер хотел продвинуть концепцию дальше, сделав её более наглядной. Он экспериментировал с бумагой для кассовых аппаратов, визитницей Rolodex (вращающийся каталог с карточками), и даже с кассетной декой (профессиональная версия магнитолы). Ничего не работало. Все они использовали линейный доступ (linear access) и поэтому были слишком медленными. Ему требовался произвольный доступ (random access). Ему нужен был лазерный диск (Laser disc).


Дайер разработал версию в виде слайд-шоу на лазерном диске с мгновенным доступом (instant access), но этого было недостаточно. Игру надо было анимировать. Я пошёл на просмотр Секрет Н.И.М.Х (анимационный фильм 1982 года) и указал на экран, сказав: вот кто должен взяться за анимацию в моей игре вспоминает Дайер. Затем он встретился с создателями фильма, представив Дону Блуту и его продюсерской команде Гэри Голдману и Джону Померою идею анимации интерактивного развлечения с тематикой меча и магии (swords'n'sorcery game) на лазерном диске. Поскольку у команды Блута не было никаких проектов, а их фильм провалился в прокате, они согласились.


image
Секрет Крыс или Секрет Н.И.М.Х. анимационный фильм 1982 года по книге Роберта О'Брайана


В первые четыре недели разработки работа не складывалась. Дайер и Блут боролись за место руководителя проекта. Как вспоминает Блут: Рик пытался написать рассказ, а я говорил: "Давай пойдём другим путём". Он подключил нас к своему проекту, но не собирался рассказывать, как его анимировать Он сделал несколько набросков и рисунков, на которые я посмотрел. И по моим стандартам качества я сказал: ни за что.


В конечном счёте, Дайер отказался от контроля анимации в команде Блута, и всё же обе команды работали по-своему. Дайер запер свою группу из семи дизайнеров в комнате, заставил их рисовать и критиковать работы друг друга. Звучит сурово, но для него это был единственный способ утереть нос команде Блута из 300 аниматоров. Группе Блута тоже приходилось перерисовывать сцены, что надломило их дух. Оглядываясь назад, Дайер понимает, что в коллективе царила токсичная атмосфера, но у него не было выбора: Мы анимировали игру, история которой не была дописана. Причина заключалась в том, что мы участвовали в гонке на опережение хотели быть первыми, кто выйдет на рынок с подобным проектом. Мы знали, что если запоздаем, это будет провал.


Ловушки


Dragon's Lair состоит из множества ловушек. Осознайте надвигающуюся угрозу, правильно отреагируйте с помощью джойстика или кнопки, и увидите следующий фрагмент фильма. Но как понять, что нужно нажимать? События в Dragon's Lair происходили очень быстро. Игроки не успевали понять, что нужно делать. Поэтому, в момент угрозы требовались визуальные подсказки, в виде жёлтой вспышки, сообщающей, в какую сторону направить джойстик. Это не сильно помогало. У вас есть только восемь кадров или треть секунды, для реакции. Единственный способ победить запоминать игру.


image


За создание ловушек отвечал Дайер. Внешний вид, звук и персонажи лежали на плечах Блута. Мы хотели сделать Дирка не просто рыцарем, который пытается пройти через замок мы хотели придать ему индивидуальность. Чтобы он запомнился, говорит Блут. Это означает, что игроку предстоит проникнуть в голову главному герою. А язык его тела подскажет о том, что происходит в его голове. Используя Чарли Чаплина в качестве модели, Блут сделал Дирка чем-то вроде болвана: У него иллюзия своего величия. Он мнит о себе больше, чем он есть на самом деле. И это делает его забавным. Он возьмется за то, что кажется абсолютно невозможным, потому что не понимает, что это невозможно. Дафна, цель для Дирка и приз в конце игры, обязана своей красивой фигурой и откровенным позам пятилетней коллекции журналов Playboy Гэри Голдмана.


image


Блут не мог позволить нанять талантливого актёра озвучки, поэтому попросил своего редактора, от природы забавного парня, озвучить Дирка. Дирк не разговаривал (ещё одна отсылка к Чаплину) он издавал звуки, чтобы показать, что испытывает. Блут попросил другого сотрудника озвучить Дафну. И она уже разговаривала.


Вот это поворот!


Как мне изобразить каждый небольшой эпизод в этом путешествии, в котором герою предстоит столкнуться по крайней мере с тремя-четыремя ловушками? говорит Блут. И как это сделать так, чтобы не повторяться, и было интересно перепроходить момент снова? В поисках вдохновения он пересматривал на видео старые фильмы. Блут заметил, что каждый эпизод там заканчивается интригующим моментом, например, когда уступ рушится, и герой вот-вот упадет в огненную яму (Тарзан) или, возможно, в яму, полную кальмаров (Багдадский вор). Мы расписали все страхи, которые есть у людей, а затем придумывали способы изобразить каждый из этих страхов, объясняет Блут. Эти страхи проникали в каждый эпизодический момент, превращая игру в серию хороших сюжетных твистов.


Сцены смерти


Самыми популярными сценами в игре были фрагменты, в которых Дирк умирает одним из примерно 50-ти довольно забавных способов. Он мог разбиться упав с высоты, сгореть заживо или его могло раздавить. Когда вы наблюдаете, как Дирка кусают летучие мыши: Вы не воспринимаете эту смерть слишком серьёзно, потому что это похоже на кота с девятью жизнями, говорит Блут. Вы воскресаете и пробуете ещё раз. Жизнь Дирка ограничивалась пятью.


image


Компания Advanced Microcomputer Systems протестировала игру на Гран-при Малибу в Эль-Монте, Калифорния. Когда Дайер посетил площадку мероприятия, он не мог поверить в то, что увидел: Там были сотни людей, все они стояли с открытыми ртами и вытаращенными глазами, просто глядя на нашу игру. Когда я подошёл к автомату слот для денег был забит монетами. Было так тесно, что нельзя было пошевелиться. Там было около 200 человек, и они не собирались уходить.


Через несколько минут Дайер побежал к телефону-автомату, чтобы позвонить президенту Cinematronix дистрибьютору Dragon's Lair. Тогда Cinematronix тестировала игру в Сан-Диего, Калифорния. Прежде чем Дайер успел сказать хоть слово, президент Cinematronix проговорил: Да, Рик, здесь происходит то же самое.


Успех


Хотя для Блута это был первый игровой опыт, он понимал, почему Dragon's Lair стала настолько успешной: Мне кажется, что проект вызвал такой фурор, потому что, на тот момент, не было ничего визуально схожего в развлечениях на игровых автоматах. Это были очень пиксельные проекты, с невыразительной графикой, которые не были особо сложными. Впервые игра выглядела как что-то из мира медиа-развлечений.


Окупаемость игры была под вопросом, поэтому Cinematronix пришлось установить цену в 50 центов вместо 25-ти. Один только проигрыватель лазерных дисков стоил 1000 долларов. Это был единственный способ, чтобы разместить наши автоматы в аркадных залах дать им возможность взимать с игроков 50 центов, говорит Дайер. В противном случае, никто не стал бы закупать эти автоматы. Конечно, как выяснилось позже, Dragon's Lair стала настолько успешной, что автомат окупался за одну-две недели, потому что занят он был круглосуточно.


image


Команда продолжила создание анимированных игр с технологией Laser Disc, это были Space Ace и Dragon's Lair 2. Но ни одна из этих аркад не была такой успешной, как первая. Эпоха лазерных дисков началась и закончилась с выходом Dragon's Lair. Мы просто технологически упёрлись в стену, вспоминает Дайер. Тем не менее, через 20 лет команда собралась снова, чтобы создать Dragon's Lair 3D (2002 год/ Playstation 2 и Nintendo Gamecube) видеоигру с компьютерной графикой, которая сохраняет ощущение рисованной анимации.


Оглядываясь на свой опыт, Дайер говорит: На самом деле, это было фантастически: две группы людей, которые никогда не делали ничего подобного раньше, собирают игру на лету. Это было время, когда никто не говорил, что это невозможно. Можно без конца рассуждать о невозможном, а можно попытаться воплотить это в реальности.




Это был перевод статьи из журнала Edge Special Retro 02/2003. Прикладываю скан оглавления со всем списком игр, которым был посвящён выпуск пишите в комментариях, что перевести дальше.


image


Также, советую ознакомиться с косплеем по Dragon's Lair (по персонажу Дафны).

Подробнее..

Минимализм ASCII-графики ретро-мониторы

10.11.2020 18:17:06 | Автор: admin

Символы, которые можно набрать на клавиатуре, являются мощным выразительным средством. ASCII-графика использует символы как штрихи, выполненные пером художника. И каждый такой штрих имеет как форму, так и смысловую нагрузку: буква, цифра, знак.

В данной публикации я покажу несколько своих новых ASCII-анимаций, расскажу пару неожиданных историй, связанных с ними, а также порассуждаю на тему, имеет ли право на существование ASCII-графика.

На заре компьютерной индустрии был период, когда такие средства отображения информации, как мониторы и принтеры, были алфавитно-цифровыми. Они могли отображать только буквы, цифры и несколько символов. Можно считать, что в этот период и зародился новый вид компьютерного искусства, который сейчас называют ASCII-графика. Само название ASCII-графика связано с таблицами ASCII, о которых можно прочитать на Хабре вот тут и тут.

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

Качество современной полиграфии, мониторов и экранов телефонов поражает. Имеет ли право на существование ASCII-графика во времена True Color и запредельной плотности точек на дюйм? Безусловно!

Древесный уголь использовался ещё в пещерной живописи, что не мешает рисованию углём оставаться актуальным и по сей день. Живопись с богатой палитрой красок не вытеснила черно-белую графику. Если карандаш берет в руку художник, то появляется произведение. Если ASCII-художник пододвигает к себе клавиатуру, то появляется ASCII-графика.

Позвольте, не претендуя на звание художника, показать вам еще несколько работ. Вот эксперимент с ASCII-анимацией на LCD-экране, работающем без подсветки. Изображение видно за счет отраженного света. Данный экран имеет светлую подложку, а все символы темные. Также чуть-чуть видно тень от символов на подложке т.к. жидкие кристаллы находятся на небольшом расстоянии от нее.

Стиль, в котором делаю анимации, можно назвать Low Res ASCII Art. Количество текстовых ячеек настолько мало, что замысел можно передать исключительно только с помощью формы символов. Сидящая птичка (первый кадр), например, находится в ячейке 3 на 3 и состоит из шести символов.

Анимация плотника состоит из шести кадров. Бревна - нули. Опилки - из точек и кавычек. Пила сделана символами ] и W. Сам плотник - e\'-.

Экран паузы в игре. Сам не курю, однако пауза ассоциируется с этим действом. Если вы играли в Metal Gear (MSX), то там можно было взять пачку сигарет в инвентаре и перекурить, а заодно подсветить лазер.

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

Тут изображены некие персонажи с гендером Male и Female рядом с запретным плодом. Гендер Male изображен символами XY, которые можно трактовать как соответствующий набор хромосом, а гендер Female символами XX. После публикации в группе на Фейсбуке я начал получать комментарии и сообщения с угрозами. Суть претензий была в том, что люди сейчас сами определяют свой пол вне зависимости от набора хромосом. Однако, я получил и сообщения со словами поддержки. Правда, личными сообщениями.

Следующая картинка получена из недавно рассекреченных материалов наблюдения за НЛО.

Если бы существовали автономные космические фабрики по производству фруктов, то колбы с деревьями или кустами могли бы выглядеть вот так.

Разрешите закончить статью анимацией танца на пилоне на персональном устройстве.

Рад был показать вам свои новые работы. Здорово, если вам понравилась хотя бы одна картинка! На Хабре есть еще несколько моих публикаций на эту тему с анимациями и артом:

Спасибо!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru