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

После просмотра официального гайда возникает много вопросов, поговорим про то, какие подводные камни ожидают нас при работе с этой программой на каждом этапе рабочего процесса (на примере Spine — Unity), как можно оптимизировать свою работу, а так же рассмотрим некоторые популярные фишки типа 3D эффекта. В статье будет много тяжелых гифок.

Сразу оговорюсь, для создания программной анимации в играх есть и другие решения, Dragon Bones, Spriter, Creature, Marionette studio, плагин Puppet 2D и наверняка найдутся другие, просто я как аниматор работаю в Spine.

Наглядная демонстрация принципа скелетной анимации


Схема работы


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

Общая схема работы такова:

Первым делом необходимо подготовить рабочий материал из графических редакторов. Затем ассеты (assets, текстуры) импортируются в Spine и анимируются. На выходе мы получаем Json — файл в котором записаны все ключевые кадры трансформации костей, слотов и проч. Данный файл импортируется в движок, создается специальная skeleton data, которая добавляется на сцене в Spine game object, где наша анимация визуализируется по средствам mesh renderer, запуском можно управлять с помощью кода или же стандартным unity animator.

Непосредственно рабочий процесс в спайне выглядит следующим образом:

  1. Импорт текстур
  2. Риггинг (настройка скелета)
  3. Скиннинг (настройка меша и привязка его к костям)
  4. Анимация
  5. Экспорт Json и проверка

В качестве примера попробуем сделать анимацию вот такого персонажа:

Импорт текстур


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

Типичные ошибки:

не рисовать текстуры движущихся частей где их не видно:


неправильно поставленные тени


А также недостаточное число проекций если персонаж меняет ракурс.

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

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

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

Как пользоваться скриптом layers to PNG
1) Экспортируем из фотошопа с помощью скрипта Json и PNG картинки (можно указать скеил фактор)
2) Импортируем json в спайн file-import data
3) Указываем путь к текстурам
подробнее


Однако у такого способа есть ряд недостатков — скрипт сохраняет изображения без сжатия, что непрактично. Просто пересохранив слои через file-generate-image assets мы уменьшим вес изображения приблизительно в 10 раз.

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



Если для проекта необходима покадровая анимация (напр. огонь, спецэффекты, и проч.) — материалы необходимо отрисовать заранее. Далее с помощью adobe after effects и скрипта ae_to_spine мы можем в пару кликов перенести последовательность кадров в Spine.

Как пользоваться скриптом AE to Spine
Аналогично скрипту layers to png. Подробнее


Рекомендую заранее продумать концепцию наименования, материалы часто приходят в формате «слой 1 (копия) 5», это не слишком практично, для каждой текстуры в проекте необходимо сделать уникальное название, что бы не вызвать проблемы при запаковке атласов. Вполне пригоден вариант формата CharacterVyasya_Skin_a_Hand_R_1.

Тут во многом нам может помочь инструмент find and replace который позволяет быстро переименовывать, указывать путь для большого количества объектов.

инструмент find and replace


Риггинг


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

Тут может помочь введение дополнительных костей-контролов для разбиения движения по осям X и Y (т.к. в спайне остутствует функция separate dimentions).

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



Тут же маленький типс, если вдруг чувствуется нагромождение из за излишнего количества костей — попробуйте покрутить настройку bone scale:



Смотрите примеры готовых ригов из образцов поставляемых вместе со спайном, или в сети.

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

Так же полезные материалы можно искать на behanсe, форуме, соответствующих сообществах в социальных сетях.

Скиннинг


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

При создании меша помните: чем меньше вертексов — тем проще контролировать анимацию.

Демонстрация


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

1 вариант - недостаточно точек, видны изломы на картинке:


2, 3 вариант - хорошо:



4 вариант - излишнее количество точек, которые не выполняют никакой функции, никуда не годится:


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

Лайфхак как продублировать целую иерархию

Производительность


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

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

И так, когда наш персонаж настроен можно переходить к анимации:



Анимация


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

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

3D-эффект


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

Демонстрация


Для себя я отметил несколько способов создания псевдо 3Д:

  1. Анимация непосредственно вертексов в меше
  2. Много костей в ключевых точках меша, анимируются кости
  3. Кости ставятся выборочно, а далее тщательно настраиваются веса в меше
  4. Комбинированный способ, все сведено под глобальные контролы

Наглядно принцип построения трехмерного эффекта можно рассмотреть на геометрических примитивах:

Куб
Проставляются кости в ключевых точках меша:


Анимируя кости создаем трехмерный эффект:




Немного сложнее пример со сферой:


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



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



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



То же самое, можно сделать без лишней массы костей,



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



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

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

И так, мы просидели несколько часов над ключевыми кадрами, мешами, и сделали анимацию персонажа:

Что получилось


Тестирование


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

Если ваш аниматор не дружит с движком, можно использовать Skeleton Viewer

Какие проблемы возникают наиболее часто:

  1. Вы забыли положить некоторые ассеты, или наоборот в экспорт попало лишнего (рабочие файлы, референсы, и проч) — тут все очевидно — перед тем как отдать материал тщательно проверяйте что должно быть в проекте а что нет
  2. несовпадение на стыках анимации — можно попробовать выставить в юнити длительность перехода, тем самым сгладив стык. Или поправить ключи если такое возможно
  3. текстуры отображаются некорректно
  4. после отработки анимация выглядит не так как раньше

Некорректное отображение текстур


Чаще всего такое происходит из за неправильной композиции текстуры — меш должен четко соответствовать размеру спрайта.



Иначе будет вот так.


После отработки анимация выглядит не так как раньше


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



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

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

Красным выделена иконка фильтра объектов по костям и кнопка для быстрого разворачивания иерархии:



Горячие клавиши по умолчанию:

Key Active: L
Key Selected: ctrl + L
Key Dopesheet: ctrl + shift + L
Key Translation, Key Rotation, Key Scale, Key Shear, Key Color — нужно настраивать самому

2) Использовать скрипт setToSetupPose, который как бы и заставляет проигрываться анимацию из дефолтного состояния. Но такой метод имеет существенный недостаток — Setup Pose выставляется мгновенно, а рендер меняет картинку на соответствующую только со следующего кадра. Таким образом мы имеем проскакивание лишнего кадра между анимациями, выглядит это неприятно. Соответствующее issue уже стоит на доске у разработчиков.

Setup Pose баг


JSON


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

  1. При необходимости смержить несколько проектов
  2. При необходимости скопировать иерархию костей вместе с анимацией
  3. При необходимости откатиться на более старую версию
  4. Любое другое применение, которое придет вам на ум

Мержинг проектов


Часто возникает необходимость держать анимации в одной сцене, например как тут:

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



Для этого есть как минимум два способа: руками и скриптом.

Руками

Копируем куски кода из одного json в другой, в соответствующие категории: слоты к слотам, кости к костям, анимации к анимациям, и т.д. Что бы не возникло конфликтов, важно соблюсти корректность наименования объектов, что бы названия из одного json не совпадали с названиями из другого. Проще всего этого добиться заранее в спайне присвоив всем объектам в именах свой уникальный индекс типа _skel1_ и _skel2_. Этот способ слегка затратный по времени, но меня еще не подводил.

Примеры
Предельно простой пример для демонстрации принципа, два скелета с костями в одном проекте:



Скелет 1
{
"skeleton": { "hash": "ZMTMZiuTD2M2gnBhJR0JLPQWOws", "spine": "3.4.02", "width": 0, "height": 0, "images": "" },
"bones": [
	{ "name": "root_skel1_" },
	{ "name": "bone1_skel1_", "parent": "root_skel1_", "length": 26.95, "rotation": 360, "x": 11.09, "y": -9.65, "color": "00ff00ff" },
	{ "name": "bone2_skel1_", "parent": "bone1_skel1_", "length": 26.95, "x": 26.27, "color": "00ff00ff" },
	{ "name": "bone3_skel1_", "parent": "bone2_skel1_", "length": 26.95, "x": 26.57, "color": "00ff00ff" },
	{ "name": "bone4_skel1_", "parent": "bone3_skel1_", "length": 26.95, "x": 25.97, "color": "00ff00ff" }
],
"animations": {
	"animation": {}
}
}


Скелет 2
{
"skeleton": { "hash": "osF6oBu7PH6sMNfjN7pm2EwQ8fY", "spine": "3.4.02", "width": 0, "height": 0, "images": "" },
"bones": [
	{ "name": "root_skel2_" },
	{ "name": "bone1_skel2_", "parent": "root_skel2_", "x": 19.25, "y": 26.63, "color": "fff100ff" },
	{ "name": "bone2_skel2_", "parent": "bone1_skel2_", "x": 27.14, "color": "fff100ff" },
	{ "name": "bone3_skel2_", "parent": "bone2_skel2_", "x": 25.57, "color": "fff100ff" },
	{ "name": "bone4_skel2_", "parent": "bone3_skel2_", "x": 27.14, "color": "fff100ff" }
],
"animations": {
	"animation": {}
}
}


Соединенные скелеты
{
"skeleton": { "hash": "ZMTMZiuTD2M2gnBhJR0JLPQWOws", "spine": "3.4.02", "width": 0, "height": 0, "images": "" },
"bones": [
	{ "name": "root" },
	{ "name": "root_skel1_", "parent": "root" },
	{ "name": "bone1_skel1_", "parent": "root_skel1_", "length": 26.95, "rotation": 360, "x": 11.09, "y": -9.65, "color": "00ff00ff" },
	{ "name": "bone2_skel1_", "parent": "bone1_skel1_", "length": 26.95, "x": 26.27, "color": "00ff00ff" },
	{ "name": "bone3_skel1_", "parent": "bone2_skel1_", "length": 26.95, "x": 26.57, "color": "00ff00ff" },
	{ "name": "bone4_skel1_", "parent": "bone3_skel1_", "length": 26.95, "x": 25.97, "color": "00ff00ff" },
	{ "name": "root_skel2_", "parent": "root"  },
	{ "name": "bone1_skel2_", "parent": "root_skel2_", "x": 19.25, "y": 26.63, "color": "fff100ff" },
	{ "name": "bone2_skel2_", "parent": "bone1_skel2_", "x": 27.14, "color": "fff100ff" },
	{ "name": "bone3_skel2_", "parent": "bone2_skel2_", "x": 25.57, "color": "fff100ff" },
	{ "name": "bone4_skel2_", "parent": "bone3_skel2_", "x": 27.14, "color": "fff100ff" }
],
"animations": {
	"animation": {}
}
}


В результате имеем все в одном проекте:



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

Более сложный пример с лягушкой:
(Осторожно, длинные файлы)

Жук
Лягушка
Жук+Лягушка


Skeleton Merger Tool

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

Дублирование объектов вместе с анимацией


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

Пример
Есть муравей с анимацией. Задача сделать в спайне целую цепочку бегающих друг за другом муравьев. При дублировании муравья анимация не копируется. Обратите внимание, у первого муравья в названии стоит индекс _skel_1_, у второго _skel_2_.



Экспортируем Json и открываем его текстовым редактором. Код анимации walk_1 копируем, находим там все участки _skel_1_:

"animations": {
	"walk_1": {
		"bones": {
			"US_2_skel_1_": {
				"rotate": [
					{
						"time": 0,
						"angle": -42.21,
						"curve": [ 0.25, 0, 0.75, 1 ]
					},
					{
						"time": 0.4,
						"angle": -0.92,
						"curve": [ 0.25, 0, 0.75, 1 ]
					},
					{ "time": 0.8, "angle": -42.21 }
				]
			},
....

заменяем их на _skel_2_

....
			"US_2_skel_2_": {
				"rotate": [
					{
						"time": 0,
						"angle": -42.21,
						"curve": [ 0.25, 0, 0.75, 1 ]
					},
					{
						"time": 0.4,
						"angle": -0.92,
						"curve": [ 0.25, 0, 0.75, 1 ]
					},
					{ "time": 0.8, "angle": -42.21 }
			},
...

Вставляем обратно. Таким образом мы переназначили анимацию на другие кости. Сохраняем json, и импортируем его в спайн путем file — import data. Теперь у нас два бегающих муравья.



Откатиться до старой версии


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

Руками перебить версию в json

"skeleton": { "hash": "", "spine": "3.4.02", "width": 0, "height": 0, "images": "" },

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

JsonRollback tool

Ознакомиться с принципом работы и скачать можно по ссылке.

Резюме


  • Четко обозначайте какие задачи в чем вы делаете
  • Используйте скрипты для ускорения работы
  • Делайте гибкий риг и не перебарщивайте с мешем
  • Перед отправкой тестируйте материал в рантайме
  • Не стесняйтесь залезть в json если это вам сэкономит время

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

P.S. Статья писалась аниматором по большей части для аниматоров, если вы программист и у вас «рукалицо», можете высказаться в комментариях.
Поделиться с друзьями
-->

Комментарии (6)


  1. NEFES
    23.11.2016 16:52

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


    1. AleksandrPakhomov
      23.11.2016 20:30

      Решается просто с помощью ограничителя трансформации. В режиме Setup нужно создать дополнительную кость которая будет управлять полетом(палка_2). Родительская ее допустим пусть будет root. Нажать на кость палки, которая так и остается дочерней костью руки, выбрать создание нового ограничителя трансформации, потом нужно выбрать целевую кость палка_2(ту которая будет управлять палкой). После назвать как-то ограничитель трансформации. Координаты палки примут значения координат палки_2. Это пока не нужно, потому в настройках MIX или по русски смешаное все ползунки стоит сместить в начало, выставить по нолям, и палка снова станет дочерней костью руки. После этого в режиме анимации находим место в котором нужно отсоеденить палку от руки на таймлайне, предварительно выбираем созданный ограничитель трансформации, и в mix просто меняем нужные параметры c 0 на 100. И палка в тот же момент примет координаты палки_2, желательно что бы в этот момент палка_2 была в том самом месте где будет палка, что бы не было рывков. Потом просто свободно управляем палкой_2. Рука и сам персонаж уже не влияют на палку. Если нужно вернуть обратно управление рукой просто поменять значения со 100 на 0. Надеюсь доступно написал.


      1. NEFES
        23.11.2016 23:07

        Спасибо! Попробовал способ, всё получилось. Проблема была в том, что я использую версию 2.1.27 для совместимости с GameMaker Studio. Там таких «наворотов» нет и в помине.


    1. borovikmotion
      23.11.2016 23:22

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


  1. mopsicus
    24.11.2016 14:39

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


    1. borovikmotion
      24.11.2016 15:46

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