Skip to main content

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 nameLoopsWhen it plays
spawnnoOnce, when the model is created. Falls through to idle when it ends
idleyesStationary horizontal movement; switches to walk when X or Z velocity is nonzero
walkyesHorizontal movement; switches to idle when grounded and both X and Z velocity are zero
attacknoWhen triggered; returns to idle when it ends
deathnoOn removeWithDeathAnimation()

Rules that follow from how the state machine is built:

  • The starting state is spawn if the model has one, otherwise idle. 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 walk but no idle never transitions back out of walk on 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 here

JUMP 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()
  • blend does not cross-fade. true queues the animation to start after the current state's tick completes; false interrupts 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=false in that case.
  • loop applies 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 idle if 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.
  • playAnimation returns false for 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 to idle when 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, or attack_ranged is 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 from 0 to duration - 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 modeBedrock 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 typeBehavior in FMM
linearStraight linear interpolation
catmullromSmoothed interpolation (ease in/out)
bezierApproximated with fixed 0.42 / 0.58 control points — FMM does not read per-keyframe bezier handles
stepSnaps 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_version 5 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, translation 0,0,0, scale 1,1,1.
  • Keyframe data points may be written as strings in the .bbmodel. FMM parses them as plain numbers — an empty string becomes 1 for scale and 0 otherwise, and anything unparseable logs Failed to parse supposed number value ... and becomes 0. 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 hitbox bone. 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:

  1. A null object with both ik_source (chain root bone) and ik_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.
  2. 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.
  3. 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.
  4. lock_ik_target_rotation is 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_length is the duration in seconds, floored at 0.05 so 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 idle entry so the Bedrock entity definition stays valid.
  • Geometry excludes hitbox, generated fmm_nametag_bone_* nametag bones and m_ mount-point bones; authored tag_ 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_effects timeline, so Bedrock clients play them natively. See Particles.

See Resource Pack Output for where the bundle lands on disk.

Playing Animations From Other Systems​

CallerEntry point
Plugin (Java)ModeledEntity#playAnimation(String, boolean blend, boolean loop) / #stopCurrentAnimations() / #hasAnimation(String)
Prop Lua scriptcontext.prop:play_animation(name, blend, loop) / context.prop:stop_animation()
Any Lua entity tableentity.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.