跳到主要内容

FreeMinecraftModels 动画

本页记录 FreeMinecraftModels 对 .bbmodel.fmmodel 文件中的动画数据实际做了什么:哪些名称是特殊的、关键帧是如何被烘焙的、支持哪些插值和循环模式,以及 IK 是如何被驱动的。内容刻意保守 —— 这里的一切都能在导入和运行时管线中看到。

关于骨骼命名规则以及导入契约的其余部分,参见模型制作说明

五个状态动画

FreeMinecraftModels 恰好把五个小写动画名称绑定到自动的运行时状态上。模型中的其他一切都属于自定义动画。

动画名称是否循环何时播放
spawn模型被创建时播放一次。结束后转入 idle
idle底层实体的速度小于或等于 0.08
walk底层实体的速度大于 0.08
attack被触发时播放;结束后回到 idle
death调用 removeWithDeathAnimation()

由状态机的构建方式所推导出的规则:

  • 如果模型有 spawn,起始状态就是 spawn,否则是 idle。两者都没有的模型没有当前状态,因此在显式播放某个动画之前不会有任何动作。
  • 只有模型中存在的动画才会获得一个状态。一个有 walk 但没有 idle 的模型永远不会自行从 walk 转出。
  • idle/walk 的切换读取的是底层实体的速度,因此这实际上是 DynamicEntity 的特性。静态实体和玩家伪装在这方面没有底层实体,只会停留在 idle;道具背后的盔甲架不会移动,所以道具同样停留在 idle。这三者都通过脚本、API,或(对伪装而言)FMM 自己的伪装控制器来驱动它们真正的动画。
这里的 jump 只是枚举值

JUMP 存在于 AnimationStateType 枚举中,而且当实体离开地面时 walk 状态确实会请求一次 jump 转换 —— 但从未注册过任何 jump 状态,因此该请求最终什么也不会做。名为 jump 的动画并没有失效:它只是表现得和其他任何自定义动画一样,需要手动触发。玩家伪装是个例外 —— 它们使用一个独立的控制器,其中 jump 确实接上了线。

玩家伪装使用另一组名称

玩家伪装不会运行上面那个状态机。它有自己的逐 tick 控制器,包含五个保留名称 —— attackjumpsneakwalkidle —— 并按该优先级顺序求值;如果模型没有 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 会把动画排队到当前状态的这一 tick 完成之后再开始;false 会中断并立即切换。
  • loop 只对自定义动画有效。内置状态无论你传入什么,都使用它自己的循环设置。
  • 当一个非循环的自定义动画播放完毕时,实体会回到最后提交的内置状态(也就是它之前正在做的事),如果没有则回到 idle
  • 当名称既不匹配任何已注册状态、也不匹配模型中的任何动画时,playAnimation 返回 false
  • 当模型有 idle 时,stopCurrentAnimations() 会转入 idle;否则它会退出当前状态,让模型不再有活动动画。
  • 在自定义动画运行期间,对 attackattack_meleeattack_ranged 的请求会被吞掉,以免脚本化的序列被日常战斗打断。

时间与时长

  • Blockbench 以秒为单位存储动画长度。FMM 使用 ceil(seconds x 20) 进行转换,因此时长永远是整数个 tick,短动画向上取整而不是向下。
  • 每个动画在导入时都会被烘焙成一个扁平的逐 tick 帧数组。播放是每 tick 一次数组查表,而不是实时插值。
  • 循环动画用 counter % duration 取索引;非循环动画则钳制在最后一帧,之后不再渲染任何变化。
  • 关键帧时间保留其小数形式的 tick 位置(20 x time,不取整),因此位于 0.37 秒的关键帧会落在两个 tick 之间并被正确插值,而不会被吸附或丢弃。动画的最后一个关键帧会被保留,而不会因取整而被截掉。
  • 如果同一通道上的两个关键帧落在完全相同的时间,文件顺序中靠后的那个胜出。
  • 时间为非有限值的关键帧会中止该轨道,并为每个动画产生一条 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_version 5 及更新版本会翻转 X、Y 旋转以及 X 位置的符号。 FMM 会根据声明的格式版本自动补偿,因此不要手工修正 —— 但也不要把 v5 的声明和 v4 形态的数据混在一起。
  • 某个通道上没有关键帧的骨骼,在该通道上保持其静止值;在某个 tick 上完全没有帧的骨骼,会被重置为旋转 0,0,0、平移 0,0,0、缩放 1,1,1
  • 关键帧数据点在 .bbmodel 中可能被写成字符串。FMM 会把它们当作普通数字解析 —— 空字符串对缩放而言变成 1,其他情况变成 0,而任何无法解析的内容会记录 Failed to parse supposed number value ... 并变成 0Molang 表达式不会被求值。
  • 每个关键帧只会读取第一个数据点,因此 Blockbench 在 step 关键帧上分别设置的 pre/post 值会被压缩成一个。

哪些内容不会被动画化

  • hitbox 骨骼。指向它的动画轨道会被直接跳过。
  • Blockbench 的效果轨道(声音、粒子以及时间轴指令动画器)。任何类型不是 bonenull_object 的动画器都会被忽略,因此 FMM 不会从动画时间轴触发声音或粒子。请改用 Lua 脚本或你自己的插件来驱动它们。
  • 无法按名称解析到的骨骼。指向不存在骨骼的轨道会记录 Failed to get bone <name> from model <model>! 并被跳过。

反向动力学(IK)

Blockbench 的空对象(null object)充当 IK 控制器。FMM 在运行时使用 FABRIK(Forward And Backward Reaching Inverse Kinematics)求解骨链,最多 10 次迭代,容差为 0.001

它们是如何配合的:

  1. 同时具有 ik_source(骨链根骨骼)和 ik_target(末端骨骼或定位器)的空对象定义了一条骨链。骨链是通过从目标沿层级向上走到源来发现的。
  2. 只有空对象的位置关键帧会被读取。它们成为相对于控制器静止位置的逐帧目标偏移;空对象上的旋转和缩放轨道会被忽略。
  3. 每个 tick 都会应用当前帧的目标偏移并求解骨链。在没有 IK 数据的帧上,骨链的 IK 旋转会被清除。
  4. 空对象上的 lock_ik_target_rotation 会从模型中读取。

无法解析的骨链会被跳过,并给出一条带名称的控制台警告 —— 确切的消息文本和制作约束参见模型制作说明

Bedrock 导出

每个转换后的模型还会在生成的资源包内的 animations/<model_id>.animation.json 写出一个 Bedrock 动画文件:

  • 动画标识符为 animation.fmm.<model_id>.<animation_name>,其中名称会针对 Bedrock 做净化处理。
  • animation_length 是以秒为单位的时长,下限为 0.05,这样只有一个 tick 的动画依然有效。
  • 循环模式按循环模式表中所示进行映射。
  • 没有任何动画的模型仍会得到一个空操作的 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_modeledtrue 时可用)

关于周边接口,参见 API 与开发者指南Lua:道具与物品 API

故障排除

我的动画从来不会自动播放。 只有 spawnidlewalkattackdeath 会自行触发。其他一切都需要显式调用 playAnimation / play_animation

我的模型完全没有反应。 它多半既没有 spawn 也没有 idle 动画,因此创建时不会进入任何状态。加一个 idle

我的动画在播放,但什么都没动。 检查动画长度。零长度动画会被视为静态姿势,按设计被静默跳过。

旋转被镜像了。 检查 .bbmodel 中的 meta.format_version。FMM 对格式版本 5 及更新版本翻转 X/Y 旋转符号;一个声明了某个版本却携带另一个版本数据的文件会呈现镜像效果。

控制台显示 Malformed animation timeline for model ... 该动画中的某条轨道无法被读取或插值。该警告每个动画只触发一次,并会指出涉及的骨骼或 IK 控制器;该模型的其他动画仍会正常转换。

我在 Blockbench 时间轴里的声音和粒子毫无反应。 效果轨道不会被导入。请从 Lua 脚本或你自己的插件触发它们。