Анимации FreeMinecraftModels
Эта страница описывает, что FreeMinecraftModels на самом деле делает с данными анимации внутри файла .bbmodel или .fmmodel: какие имена являются особыми, как запекаются ключевые кадры, какие режимы интерполяции и зацикливания учитываются и как управляется IK. Изложение намеренно консервативно — всё описанное здесь видно в конвейере импорта и во время выполнения.
Правила именования костей и остальную часть контракта импорта см. в разделе Заметки по созданию моделей.
Пять анимаций состояний
FreeMinecraftModels привязывает ровно пять имён анимаций в нижнем регистре к автоматическим состояниям времени выполнения. Всё остальное в модели — это пользовательские анимации.
| Имя анимации | Зацикливается | Когда проигрывается |
|---|---|---|
spawn | нет | Один раз, при создании модели. По завершении переходит в idle |
idle | да | Пока скорость базовой сущности не превышает 0.08 |
walk | да | Пока скорость базовой сущности выше 0.08 |
attack | нет | При срабатывании; по завершении возвращается в idle |
death | нет | При вызове removeWithDeathAnimation() |
Правила, вытекающие из устройства конечного автомата состояний:
- Начальное состояние —
spawn, если оно есть у модели, иначеidle. У модели, где нет ни того, ни другого, нет текущего состояния, поэтому ничего не анимируется, пока что-нибудь не будет запущено явно. - Состояние получают только те анимации, которые есть в модели. Модель с
walk, но безidleникогда не выйдет изwalkсамостоятельно. - Переключение idle/walk считывает скорость базовой сущности, так что это на самом деле возможность
DynamicEntity. У статических сущностей и маскировок игроков нет базовой сущности для этой цели, поэтому они просто остаются вidle; поддерживающая пропс стойка для брони не движется, поэтому пропсы тоже остаются вidle. Все три управляют своими настоящими анимациями через скрипты, API или (для маскировок) через собственный контроллер маскировок FMM.
jump здесь существует только в перечисленииJUMP присутствует в перечислении AnimationStateType, и состояние walk действительно запрашивает переход в прыжок, когда сущность отрывается от земли, — но состояние прыжка никогда не регистрируется, поэтому этот запрос ни к чему не приводит. Анимация с именем jump не мертва: она просто ведёт себя как любая другая пользовательская анимация и должна запускаться вручную. Исключение — маскировки игроков: у них отдельный контроллер, где jump подключён.
У маскировок игроков другой набор
Маскировка игрока не использует описанный выше конечный автомат. У неё свой контроллер, работающий каждый тик, с пятью зарезервированными именами — attack, jump, sneak, walk, idle, — которые проверяются именно в этом порядке приоритета, и он выводит предупреждение в консоль, если у модели нет idle. Полную таблицу и оговорку о времени одноразовых анимаций см. в разделе Маскировки игроков.
Пользовательские анимации
Любую анимацию, чьё имя не входит в пятёрку выше, всё равно можно проиграть по имени:
modeledEntity.playAnimation("open", /* blend */ true, /* loop */ false);
modeledEntity.stopCurrentAnimations();
boolean exists = modeledEntity.hasAnimation("open");
context.prop:play_animation("open", true, false)
context.prop:stop_animation()
blendне делает плавного перехода.trueставит анимацию в очередь, чтобы она началась после завершения тика текущего состояния;falseпрерывает и переключает немедленно.loopприменяется только к пользовательским анимациям. Встроенное состояние использует собственную настройку зацикливания независимо от того, что вы передали.- Когда незацикленная пользовательская анимация заканчивается, сущность возвращается к последнему зафиксированному встроенному состоянию (тому, чем она занималась раньше), или к
idle, если такого не было. playAnimationвозвращаетfalse, когда имя не соответствует ни зарегистрированному состоянию, ни анимации в модели.stopCurrentAnimations()переходит вidle, если он есть у модели; иначе выходит из текущего состояния и оставляет модель без активной анимации.- Пока выполняется пользовательская анимация, запрос
attack,attack_meleeилиattack_rangedпоглощается, чтобы скриптовая последовательность не прерывалась обычным боем.
Тайминг и продолжительность
- Blockbench хранит длину анимации в секундах. FMM преобразует её как
ceil(seconds x 20), поэтому продолжительность всегда целое число тиков, и короткие анимации округляются вверх, а не вниз. - Каждая анимация запекается при импорте в плоский массив кадров по тикам. Воспроизведение — это поиск в массиве на каждый тик, а не интерполяция в реальном времени.
- Зацикленные анимации индексируются как
counter % duration; незацикленные фиксируются на последнем кадре и затем перестают что-либо менять. - Времена ключевых кадров сохраняют дробную позицию в тиках (
20 x time, без округления), поэтому ключевой кадр на 0.37 с попадает между тиками и корректно интерполируется, а не примагничивается и не отбрасывается. Последний ключевой кадр анимации сохраняется, а не срезается округлением. - Если два ключевых кадра на одном канале попадают ровно в одно и то же время, побеждает тот, который идёт позже в файле.
- Ключевой кадр с нечисловым (не конечным) временем прерывает свою дорожку и порождает одно предупреждение
Malformed animation timeline for model ...на анимацию, при этом остальные анимации модели продолжают конвертироваться.
Анимации нулевой длины допустимы
Анимация длиной 0 (или отрицательной) считается намеренной статической позой — это распространённый приём при создании мебели и других пропсов, которым нужна именованная запись «без анимации». Она молча пропускается без предупреждения и не добавляет кадров.
Режимы зацикливания
Настройка зацикливания Blockbench считывается из анимации напрямую:
| Режим зацикливания Blockbench | Java во время выполнения | Экспорт для Bedrock |
|---|---|---|
loop | повторяется бесконечно | "loop": true |
once | проигрывается и останавливается | "loop": false |
hold | проигрывается и удерживает последний кадр | "loop": "hold_on_last_frame" |
Типы интерполяции
Каждый ключевой кадр несёт собственный тип интерполяции, и отрезок, ведущий в ключевой кадр, использует тип этого кадра. Поддерживаются четыре:
| Тип Blockbench | Поведение в FMM |
|---|---|
linear | Прямая линейная интерполяция |
catmullrom | Сглаженная интерполяция (плавный вход/выход) |
bezier | Аппроксимируется фиксированными контрольными точками 0.42 / 0.58 — FMM не читает манипуляторы Безье отдельных ключевых кадров |
step | Держит предыдущее значение до следующего ключевого кадра |
Всё, что вне этого набора, не разбирается и сообщается как некорректная временная шкала.
Анимируемые каналы
Для каждой кости запекаются три канала: вращение, позиция и масштаб. Что важно при создании модели:
- Значения позиции делятся на 16 (пиксели Blockbench в блоки).
- Значения вращения преобразуются в радианы.
- Blockbench
format_version5 и новее меняет знак вращения по X и Y и позиции по X. FMM компенсирует это автоматически на основе объявленной версии формата, поэтому не исправляйте это вручную — но и не смешивайте объявление v5 с данными в форме v4. - Кость без ключевых кадров на канале сохраняет для этого канала своё значение покоя; кость, у которой на данный тик нет кадров вообще, сбрасывается к вращению
0,0,0, смещению0,0,0и масштабу1,1,1. - Точки данных ключевых кадров могут быть записаны в
.bbmodelв виде строк. FMM разбирает их как обычные числа — пустая строка становится1для масштаба и0в остальных случаях, а всё, что не разбирается, пишет в логFailed to parse supposed number value ...и становится0. Выражения Molang не вычисляются. - Читается только первая точка данных ключевого кадра, поэтому раздельные значения «до/после» Blockbench на step-кадре схлопываются в одно.
Что не анимируется
- Кость
hitbox. Дорожки анимации, нацеленные на неё, пропускаются полностью. - Дорожки эффектов Blockbench (аниматоры звука, частиц и инструкций временной шкалы). Любой аниматор, чей тип не
boneи неnull_object, игнорируется, поэтому FMM не будет воспроизводить звуки или частицы с временной шкалы анимации. Запускайте их из Lua-скрипта или из собственного плагина. - Кости, которые не удалось найти по имени. Дорожка, указывающая на отсутствующую кость, пишет в лог
Failed to get bone <name> from model <model>!и пропускается.
Обратная кинематика (IK)
Пустые объекты (null objects) Blockbench выступают контроллерами IK. FMM решает цепочки во время выполнения методом FABRIK (Forward And Backward Reaching Inverse Kinematics) с ограничением в 10 итераций и допуском 0.001.
Как это устроено:
- Пустой объект, у которого заданы и
ik_source(корневая кость цепочки), иik_target(конечная кость или локатор), определяет цепочку. Цепочка находится обходом иерархии вверх от цели к источнику. - Читаются только ключевые кадры позиции пустого объекта. Они становятся покадровым смещением цели относительно позиции покоя контроллера; дорожки вращения и масштаба на пустом объекте игнорируются.
- Каждый тик применяется смещение цели для текущего кадра и решается цепочка. На кадре без данных IK вращения IK этой цепочки, наоборот, сбрасываются.
- Значение
lock_ik_target_rotationпустого объекта читается из модели.
Цепочки, которые не удаётся разрешить, пропускаются с именованным предупреждением в консоли — точные сообщения и ограничения при создании моделей см. в разделе Заметки по созданию моделей.
Экспорт для Bedrock
Каждая сконвертированная модель также записывает файл анимации Bedrock по пути animations/<model_id>.animation.json внутри сгенерированного бандла:
- Идентификаторы анимаций имеют вид
animation.fmm.<model_id>.<animation_name>, имена очищаются под требования Bedrock. animation_length— это длительность в секундах, но не меньше0.05, чтобы анимация длиной в один тик оставалась корректной.- Режим зацикливания отображается так, как показано в таблице Режимы зацикливания.
- Модель без анимаций всё равно получает одну пустую запись
idle, чтобы определение сущности Bedrock оставалось корректным. - Экспортируются только визуальные кости.
hitbox, автоматически создаваемые кости именных табличекfmm_nametag_bone_*и кости точек крепленияm_исключаются из геометрии, а значит и из блока костей анимации. Костьtag_, которую создаёте вы, не исключается — она экспортируется как любая другая кость (обычно как пустая, без кубов); отфильтровывается только соответствующая ей сгенерированная кость именной таблички. - На каждую анимацию генерируется один контроллер анимации, переключаемый свойством сущности, — именно так FMM проигрывает конкретную анимацию на клиенте Bedrock.
О том, куда бандл попадает на диске, см. Вывод ресурс-пака.
Запуск анимаций из других систем
| Вызывающая сторона | Точка входа |
|---|---|
| Плагин (Java) | ModeledEntity#playAnimation(String, boolean blend, boolean loop) / #stopCurrentAnimations() / #hasAnimation(String) |
| Lua-скрипт пропса | context.prop:play_animation(name, blend, loop) / context.prop:stop_animation() |
| Любая Lua-таблица сущности | entity.model:play_animation(name, blend, loop) / entity.model:stop_animations() (доступно, когда entity.is_modeled равно true) |
Об окружающих интерфейсах см. Руководство по API и для разработчиков и Lua: API пропсов и предметов.
Устранение неполадок
Моя анимация никогда не запускается автоматически.
Сами по себе срабатывают только spawn, idle, walk, attack и death. Всё остальное требует явного вызова playAnimation / play_animation.
Моя модель вообще ничего не делает.
Скорее всего, у неё нет ни анимации spawn, ни idle, поэтому при создании не входит ни в одно состояние. Добавьте idle.
Анимация проигрывается, но ничего не движется. Проверьте длину анимации. Анимация нулевой длины считается статической позой и по замыслу молча пропускается.
Вращения зеркальны.
Проверьте meta.format_version в .bbmodel. FMM меняет знак вращения по X/Y для версии формата 5 и новее; файл, который объявляет одну версию, но содержит данные другой, окажется зеркальным.
В консоли Malformed animation timeline for model ....
Одну дорожку в этой анимации не удалось прочитать или интерполировать. Предупреждение выводится один раз на анимацию и называет задействованную кость или контроллер IK; остальные анимации модели всё равно конвертируются.
Звуки и частицы на моей временной шкале Blockbench ничего не делают. Дорожки эффектов не импортируются. Запускайте их из Lua-скрипта или собственного плагина.