Перейти к основному содержимому

Анимации 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 считывается из анимации напрямую:

Режим зацикливания BlockbenchJava во время выполненияЭкспорт для 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_version 5 и новее меняет знак вращения по 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.

Как это устроено:

  1. Пустой объект, у которого заданы и ik_source (корневая кость цепочки), и ik_target (конечная кость или локатор), определяет цепочку. Цепочка находится обходом иерархии вверх от цели к источнику.
  2. Читаются только ключевые кадры позиции пустого объекта. Они становятся покадровым смещением цели относительно позиции покоя контроллера; дорожки вращения и масштаба на пустом объекте игнорируются.
  3. Каждый тик применяется смещение цели для текущего кадра и решается цепочка. На кадре без данных IK вращения IK этой цепочки, наоборот, сбрасываются.
  4. Значение 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-скрипта или собственного плагина.