FreeMinecraftModels Animations
FreeMinecraftModels imports animation data from .bbmodel and .fmmodel files. This page covers reserved names, frame timing, interpolation, loop modes and inverse kinematics (IK).
For bone naming rules and the rest of the import contract, see Model Authoring Notes.
The Five State Animations
FreeMinecraftModels binds exactly five lowercase animation names to automatic runtime states. Everything else in the model is a custom animation.
| Animation name | Loops | When it plays |
|---|---|---|
spawn | no | Once, when the model is created. Falls through to idle when it ends |
idle | yes | Stationary horizontal movement; switches to walk when X or Z velocity is nonzero |
walk | yes | Horizontal movement; switches to idle when grounded and both X and Z velocity are zero |
attack | no | When triggered; returns to idle when it ends |
death | no | On removeWithDeathAnimation() |
Rules that follow from how the state machine is built:
- The starting state is
spawnif the model has one, otherwiseidle. A model with neither has no current state, so nothing animates until something is played explicitly. - Only animations that exist in the model get a state. A model with a
walkbut noidlenever transitions back out ofwalkon its own. - The idle/walk switch reads the underlying entity's horizontal velocity; vertical motion alone does not start walking. Static entities and player disguises have no underlying entity for this purpose, and stationary props have no horizontal movement. Their additional animations are driven through scripts, the API, or FMM's disguise controller.
jump is enum-only hereJUMP exists in the AnimationStateType enum, and the walk state does request a jump transition when the entity leaves the ground — but no jump state is ever registered, so that request resolves to nothing. An animation named jump is not dead: it just behaves like any other custom animation and has to be triggered manually. Player disguises are the exception — they use a separate controller where jump is wired up.
Player Disguises Use A Different Set
A player disguise adds a per-tick controller that requests animations through the same playback engine. Its five reserved names are attack, jump, sneak, walk, idle, evaluated in that priority order when its one-shot countdown is inactive. It warns on the console if the model has no idle. See Player Disguises for the full table and the one-shot timing caveat.
Custom Animations
Any animation whose name is not one of the five above can still be played by name:
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()
blenddoes not cross-fade.truequeues the animation to start after the current state's tick completes;falseinterrupts and switches immediately.- There is one queued slot; a later queued request replaces the earlier one. A custom animation starts immediately if there is no current state. A queued built-in state cannot advance from a null current state; use
blend=falsein that case. loopapplies to custom animations only. A built-in state uses its own loop setting regardless of what you pass.- Use exact names for custom animations and
hasAnimation; both lookups are case-sensitive. Built-in playback requests accept different casing, but registering a built-in state still requires its lowercase name in the model. - When a non-looping custom animation finishes, it requests the stored last committed built-in state, or
idleif none was recorded. This is the last built-in state the manager previously left, which can differ from the state active immediately before the custom animation. If the requested return state does not exist, the custom state remains selected without further frame updates. playAnimationreturnsfalsefor an unknown name, except for the suppressed attack requests described below. A successful return means the request was accepted, not that a visible frame has played.stopCurrentAnimations()transitions toidlewhen the model has one; otherwise it exits the current state and leaves the model with no active animation.- While a custom animation is running, a request for
attack,attack_melee, orattack_rangedis swallowed so a scripted sequence is not interrupted by routine combat. - Death is terminal in the shared state machine: new playback requests return
false, queued transitions are discarded, and stopping animations does not change that state. The public stop method also sends a separate Bedrock stop request, so do not use it to manage a death sequence across both clients.
Timing And Duration
- Blockbench stores animation length in seconds. FMM converts it with
ceil(seconds x 20), so duration is always a whole number of ticks and short animations round up rather than down. - Every animation is baked into a flat per-tick frame array at import time. Playback is an array lookup per tick, not live interpolation.
- Looping animations index with
counter % duration; non-looping ones clamp to the last frame and then stop rendering further changes. - Keyframe times keep their fractional tick position (
20 x time, not rounded), so a keyframe at 0.37 s contributes to interpolation between ticks. Frames are sampled at integer ticks from0toduration - 1; a keyframe exactly at the declared end time influences interpolation but is not itself sampled. Place a final pose before that boundary if a rendered frame must reach it. - If two keyframes on the same channel land on exactly the same time, the later one in file order wins.
- A keyframe with a non-finite time aborts that track and produces a single
Malformed animation timeline for model ...warning per animation, while the model's other animations keep converting.
Zero-Length Animations Are Valid
An animation whose length is 0 (or negative) is treated as an intentional static pose — a common authoring choice for furniture and other props that need a named "no animation" entry. It is skipped silently with no warning, and it contributes no frames.
Loop Modes
Blockbench's loop setting controls the exported Bedrock animation:
| Blockbench loop mode | Bedrock export |
|---|---|
loop | "loop": true |
once | "loop": false |
hold | "loop": "hold_on_last_frame" |
Java playback uses the built-in state's loop policy or the loop argument passed for a custom animation. It does not select its loop policy from this Blockbench field, so Java and Bedrock playback can differ when those settings disagree.
Interpolation Types
Each keyframe carries its own interpolation type, and the segment leading into a keyframe uses that keyframe's type. Four are supported:
| Blockbench type | Behavior in FMM |
|---|---|
linear | Straight linear interpolation |
catmullrom | Smoothed interpolation (ease in/out) |
bezier | Approximated with fixed 0.42 / 0.58 control points — FMM does not read per-keyframe bezier handles |
step | Snaps to the previous value until the next keyframe |
Anything outside that set fails to parse and is reported as a malformed timeline.
Animated Channels
Three channels are baked per bone: rotation, position, and scale. Notes that matter when authoring:
- Position values are divided by 16 (Blockbench pixels to blocks).
- Rotation values are converted to radians.
- Blockbench
format_version5 and newer flips the sign of X and Y rotation and of X position. FMM compensates automatically based on the declared format version, so do not hand-correct for it — but do not mix a v5 declaration with v4-shaped data either. - A bone with no keyframes on a channel keeps its rest value for that channel; a bone with no frames at all for a given tick is reset to rotation
0,0,0, translation0,0,0, scale1,1,1. - Keyframe data points may be written as strings in the
.bbmodel. FMM parses them as plain numbers — an empty string becomes1for scale and0otherwise, and anything unparseable logsFailed to parse supposed number value ...and becomes0. Molang expressions are not evaluated. - Only the first data point of a keyframe is read, so Blockbench's separate pre/post values on a step keyframe collapse to one.
What Is Not Animated
- The
hitboxbone. Animation tracks targeting it are skipped outright. - Sound and timeline-instruction keyframes in Blockbench effect tracks. FMM imports only the particle keyframes of an effect track; see Particles. Play sounds from a Lua script or from your own plugin instead.
- Bones that could not be resolved by name. A track pointing at a missing bone logs
Failed to get bone <name> from model <model>!and is skipped.
Inverse Kinematics (IK)
Blockbench null objects act as IK controllers. FMM solves chains at runtime with FABRIK (Forward And Backward Reaching Inverse Kinematics), capped at 10 iterations with a 0.001 tolerance.
How it fits together:
- A null object with both
ik_source(chain root bone) andik_target(end bone or locator) defines a chain. Discovery checks the hierarchy and also supports sibling/downward searches; root-level sibling targets produce a source-only chain. - Only the null object's position keyframes drive IK. They become a per-frame offset added to the target bone's or locator's rest position. The runtime solver does not use the controller's stored rest position as the goal base. Rotation and scale tracks do not drive IK.
- Each tick the current animation's mapped IK chains receive their goal offsets and are solved. A mapped chain with missing frame data is cleared. Every animation switch, and
stopCurrentAnimations(), clears the IK rotations of all chains, so an IK pose does not carry over into the next animation. lock_ik_target_rotationis parsed and stored, but the current solver does not apply it.
Chains that cannot be resolved are skipped with a named console warning — see Model Authoring Notes for the exact messages and the authoring constraints.
Bedrock Export
Each converted model also writes a Bedrock animation file at animations/<model_id>.animation.json inside the generated bundle:
- Animation identifiers are
animation.fmm.<model_id>.a_<hex>, where<hex>is the animation name's UTF-8 bytes written as hexadecimal. Two names that differ only in characters Bedrock does not allow therefore get separate identifiers. animation_lengthis the duration in seconds, floored at0.05so a one-tick animation is still valid.- Loop mode is mapped as shown in the Loop Modes table.
- A model with no animations still gets a single no-op
idleentry so the Bedrock entity definition stays valid. - Geometry excludes
hitbox, generatedfmm_nametag_bone_*nametag bones andm_mount-point bones; authoredtag_anchors remain. The animation exporter writes baked bone tracks without applying that same visual-bone filter, so do not animate excluded mount bones expecting visible geometry. - Bedrock animation export uses baked bone rotation, position and scale frames. It does not bake the runtime IK solver's rotations into those tracks; verify IK-dependent models separately on Bedrock.
- One animation controller is generated per animation, switched by an entity property, which is how FMM plays a specific animation on a Bedrock client.
- Particle keyframes become the animation's
particle_effectstimeline, so Bedrock clients play them natively. See Particles.
See Resource Pack Output for where the bundle lands on disk.
Playing Animations From Other Systems
| Caller | Entry point |
|---|---|
| Plugin (Java) | ModeledEntity#playAnimation(String, boolean blend, boolean loop) / #stopCurrentAnimations() / #hasAnimation(String) |
| Prop Lua script | context.prop:play_animation(name, blend, loop) / context.prop:stop_animation() |
| Any Lua entity table | entity.model:play_animation(name, blend, loop) / entity.model:stop_animations() (available when entity.is_modeled is true) |
See the API & Developer Guide and Lua: Prop API for the surrounding surfaces.
The Lua defaults differ: context.prop:play_animation(name) defaults to blend=true, loop=true, while entity.model:play_animation(name) defaults to false, false. Pass both booleans explicitly when the distinction matters.
Troubleshooting
My animation never plays automatically.
The shared state machine recognizes lowercase spawn, idle, walk, attack and death. Movement drives idle/walk transitions; attacks and death still need their runtime trigger. Other names need an explicit playAnimation / play_animation call, except for the additional names selected by the disguise controller.
My model does nothing at all.
It probably has neither a spawn nor an idle animation, so no state is entered on creation. Add an idle.
My animation plays but nothing moves. Check the animation length. A zero-length animation is treated as a static pose and is skipped silently by design.
Rotations are mirrored.
Check meta.format_version in the .bbmodel. FMM flips X/Y rotation sign for format version 5 and newer; a file that declares one version but carries the other version's data will come out mirrored.
Console says Malformed animation timeline for model ....
One track in that animation could not be read or interpolated. The warning fires once per animation and names the bone or IK controller involved; the model's other animations still convert.
Sounds in my Blockbench timeline do nothing. Sound keyframes are not imported. Trigger sounds from a Lua script or your own plugin. Particle keyframes are imported; if they don't show, see Particles.