跳到主要内容

FreeMinecraftModels 动画

FreeMinecraftModels 从 .bbmodel 和 .fmmodel 文件导入动画。本页介绍保留名称、帧时间、插值、循环模式与逆向运动学(IK)。

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

五个状态动画​

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

动画名称是否循环何时播放
spawn否模型被创建时播放一次。结束后转入 idle
idle是水平方向静止时;X 或 Z 速度不为零时切换到 walk
walk是水平移动时;接地且 X、Z 速度均为零时切换到 idle
attack否被触发时播放;结束后回到 idle
death否调用 removeWithDeathAnimation() 时

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

  • 如果模型有 spawn,起始状态就是 spawn,否则是 idle。两者都没有的模型没有当前状态,因此在显式播放某个动画之前不会有任何动作。
  • 只有模型中存在的动画才会获得一个状态。一个有 walk 但没有 idle 的模型永远不会自行从 walk 转出。
  • idle/walk 切换读取底层实体的水平速度;单纯的垂直移动不会开始行走。静态实体与玩家伪装没有供此功能使用的底层实体,静止道具也没有水平移动。其他动画通过脚本、API 或 FMM 伪装控制器驱动。
这里的 jump 只是枚举值

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

玩家伪装使用另一组名称​

玩家伪装增加了一个逐 tick 控制器,通过相同的播放引擎请求动画。它的五个保留名称是 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 会把动画排队到当前状态的这一 tick 完成之后再开始;false 会中断并立即切换。
  • 队列只有一个槽位,后来的排队请求会替换前一个。没有当前状态时,自定义动画立即开始。排队的内置状态无法从空的当前状态继续运行;此时请使用 blend=false。
  • loop 只对自定义动画有效。内置状态无论你传入什么,都使用它自己的循环设置。
  • 自定义动画和 hasAnimation 查询区分大小写,请使用准确名称。内置状态的播放请求接受不同大小写,但注册状态仍要求模型中使用小写名称。
  • 非循环自定义动画结束时,会请求保存的最后提交的内置状态;没有记录时请求 idle。这是管理器之前离开的最后一个内置状态,可能不是自定义动画开始前正在运行的状态。请求的返回状态不存在时,自定义状态仍被选中,但不再更新帧。
  • 对于未知名称,playAnimation 返回 false,但下述被抑制的攻击请求除外。返回成功表示请求已接受,不代表已经播放了可见帧。
  • 当模型有 idle 时,stopCurrentAnimations() 会转入 idle;否则它会退出当前状态,让模型不再有活动动画。
  • 在自定义动画运行期间,对 attack、attack_melee 或 attack_ranged 的请求会被吞掉,以免脚本化的序列被日常战斗打断。
  • 死亡是共享状态机的终止状态:新请求返回 false,排队的切换被丢弃,停止动画也不会改变该状态。不过,公开的停止方法还会向 Bedrock 单独发送停止请求,因此不要用它管理两个客户端上的死亡动画。

时间与时长​

  • Blockbench 以秒为单位存储动画长度。FMM 使用 ceil(seconds x 20) 进行转换,因此时长永远是整数个 tick,短动画向上取整而不是向下。
  • 每个动画在导入时都会被烘焙成一个扁平的逐 tick 帧数组。播放是每 tick 一次数组查表,而不是实时插值。
  • 循环动画用 counter % duration 取索引;非循环动画则钳制在最后一帧,之后不再渲染任何变化。
  • 关键帧时间保留小数 tick 位置(20 x time,不取整),因此 0.37 秒的关键帧也会参与 tick 之间的插值。帧在 0 到 duration - 1 的整数 tick 上采样;恰好位于声明结束时间的关键帧会影响插值,但本身不会被采样。如果必须在可见帧中达到最终姿势,请将其放在这个边界之前。
  • 如果同一通道上的两个关键帧落在完全相同的时间,文件顺序中靠后的那个胜出。
  • 时间为非有限值的关键帧会中止该轨道,并为每个动画产生一条 Malformed animation timeline for model ... 警告,而该模型的其他动画照常转换。

零长度动画是合法的​

长度为 0(或负数)的动画会被视为有意为之的静态姿势 —— 这是家具和其他需要一个具名「无动画」条目的道具中常见的制作选择。它会被静默跳过、不给出警告,并且不贡献任何帧。

循环模式​

Blockbench 的循环设置控制导出的 Bedrock 动画:

Blockbench 循环模式Bedrock 导出
loop"loop": true
once"loop": false
hold"loop": "hold_on_last_frame"

Java 播放使用内置状态的循环规则或自定义动画传入的 loop 参数,不使用这个 Blockbench 字段决定是否循环。因此设置不一致时,Java 与 Bedrock 的播放可能不同。

插值类型​

每个关键帧都带有自己的插值类型,而进入某个关键帧的那一段使用该关键帧的类型。支持四种:

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 ... 并变成 0。Molang 表达式不会被求值。
  • 每个关键帧只会读取第一个数据点,因此 Blockbench 在 step 关键帧上分别设置的 pre/post 值会被压缩成一个。

哪些内容不会被动画化​

  • hitbox 骨骼。指向它的动画轨道会被直接跳过。
  • Blockbench 效果轨道中的声音和时间轴指令关键帧。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. 只有空对象的位置关键帧驱动 IK。每帧偏移会加到目标骨骼或定位器的静止位置上。运行时求解器不会使用已保存的控制器静止位置作为目标基点,旋转与缩放轨道也不驱动 IK。
  3. 每个 tick,当前动画所关联的 IK 骨链接收目标偏移并求解。关联骨链缺少该帧数据时会被清除。每次切换动画以及调用 stopCurrentAnimations() 时,都会清除所有骨链的 IK 旋转,因此 IK 姿势不会带入下一个动画。
  4. lock_ik_target_rotation 会被解析并保存,但当前求解器不会应用它。

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

Bedrock 导出​

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

  • 动画标识符为 animation.fmm.<model_id>.a_<hex>,其中 <hex> 是动画名称的 UTF-8 字节的十六进制表示。因此,两个仅在 Bedrock 不允许的字符上有所不同的名称会得到各自独立的标识符。
  • animation_length 是以秒为单位的时长,下限为 0.05,这样只有一个 tick 的动画依然有效。
  • 循环模式按循环模式表中所示进行映射。
  • 没有任何动画的模型仍会得到一个空操作的 idle 条目,以保证 Bedrock 实体定义依然有效。
  • 几何体排除 hitbox、生成的 fmm_nametag_bone_* 名牌骨骼及 m_ 挂载点,保留作者创建的 tag_ 锚点。动画导出器写入烘焙轨道时不会应用相同的可见骨骼过滤,所以不要指望被排除的挂载骨骼在播放动画后出现可见几何体。
  • Bedrock 动画导出使用烘焙的骨骼旋转、位置和缩放帧,不会将运行时 IK 求解结果写入这些轨道。依赖 IK 的模型需要在 Bedrock 上单独验证。
  • 每个动画会生成一个动画控制器,由一个实体属性进行切换,这正是 FMM 在 Bedrock 客户端上播放特定动画的方式。
  • 粒子关键帧会成为该动画的 particle_effects 时间轴,因此 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。

Lua 默认参数不同:context.prop:play_animation(name) 默认为 blend=true, loop=true,而 entity.model:play_animation(name) 默认为 false, false。如果这个区别会影响行为,请明确传入两个布尔值。

故障排除​

我的动画从来不会自动播放。 共享状态机识别小写的 spawn、idle、walk、attack 和 death。移动驱动 idle/walk 切换,攻击与死亡仍需对应的运行时触发。除了伪装控制器额外选择的名称,其他名称都需要明确调用 playAnimation / play_animation。

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

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

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

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

我在 Blockbench 时间轴里的声音毫无反应。 声音关键帧不会被导入。请从 Lua 脚本或你自己的插件触发声音。粒子关键帧会被导入;如果它们没有显示,参见粒子。