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 控制器,包含五个保留名称 —— 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会中断并立即切换。loop只对自定义动画有效。内置状态无论你传入什么,都使用它自己的循环设置。- 当一个非循环的自定义动画播放完毕时,实体会回到最后提交的内置状态(也就是它之前正在做的事),如果没有则回到
idle。 - 当名称既不匹配任何已注册状态、也不匹配模型中的任何动画时,
playAnimation返回false。 - 当模型有
idle时,stopCurrentAnimations()会转入idle;否则它会退出当前状态,让模型不再有活动动画。 - 在自定义动画运行期间,对
attack、attack_melee或attack_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_version5 及更新版本会翻转 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 的效果轨道(声音、粒子以及时间轴指令动画器)。任何类型不是
bone或null_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。
它们是如何配合的:
- 同时具有
ik_source(骨链根骨骼)和ik_target(末端骨骼或定位器)的空对象定义了一条骨链。骨链是通过从目标沿层级向上走到源来发现的。 - 只有空对象的位置关键帧会被读取。它们成为相对于控制器静止位置的逐帧目标偏移;空对象上的旋转和缩放轨道会被忽略。
- 每个 tick 都会应用当前帧的目标偏移并求解骨链。在没有 IK 数据的帧上,骨链的 IK 旋转会被清除。
- 空对象上的
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_modeled 为 true 时可用) |
关于周边接口,参见 API 与开发者指南和 Lua:道具与物品 API。
故障排除
我的动画从来不会自动播放。
只有 spawn、idle、walk、attack 和 death 会自行触发。其他一切都需要显式调用 playAnimation / play_animation。
我的模型完全没有反应。
它多半既没有 spawn 也没有 idle 动画,因此创建时不会进入任何状态。加一个 idle。
我的动画在播放,但什么都没动。 检查动画长度。零长度动画会被视为静态姿势,按设计被静默跳过。
旋转被镜像了。
检查 .bbmodel 中的 meta.format_version。FMM 对格式版本 5 及更新版本翻转 X/Y 旋转符号;一个声明了某个版本却携带另一个版本数据的文件会呈现镜像效果。
控制台显示 Malformed animation timeline for model ...。
该动画中的某条轨道无法被读取或插值。该警告每个动画只触发一次,并会指出涉及的骨骼或 IK 控制器;该模型的其他动画仍会正常转换。
我在 Blockbench 时间轴里的声音和粒子毫无反应。 效果轨道不会被导入。请从 Lua 脚本或你自己的插件触发它们。