Procedural Animation

Overview

Procedural animation moves bones from code, using live game data, instead of playing keyframes made in Blockbench. Use it for motion that depends on what is happening right now, for example:

  • A head that tracks where the entity is looking.
  • A tail or antenna that sways faster as the entity runs.
  • A body that leans into a turn, or braces while aiming.
  • Parts that hide, show or resize based on the entity's state.

Procedural animation works alongside your normal animations. Your controllers play first, then your code adjusts the result.

Where Procedural Animation Runs

Override setCustomAnimations in your animator. AzureLib calls it every frame, for each animatable, at the end of its animation update:

  1. Molang queries are updated for the animatable.
  2. Every animation controller applies its animation to the bones, layered in the order the controllers were added.
  3. Bones that no controller moved ease back toward their default pose.
  4. setCustomAnimations runs. Anything you set here is what gets rendered this frame.
public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {
@Override
public void setCustomAnimations(ExampleEntity entity, float partialTicks) {
super.setCustomAnimations(entity, partialTicks);
// Your procedural animation goes here.
}
// ... registerControllers, getAnimationLocation, etc.
}

AzItemAnimator and AzBlockAnimator have the same method, so everything on this page also works for items and block entities.

Getting Bones

Bones are looked up by the name you gave them in Blockbench:

var head = context().boneCache().getBakedModel().getBoneOrNull("head");
if (head != null) {
// Move the head.
}

Look bones up inside setCustomAnimations each frame rather than storing them in a field. AzureLib swaps in new bone objects when the model changes, such as on a resource reload, and a stored bone would then point at the old model. A lookup by name is a fast map lookup, so there's no need to cache it.

Bone Values and Units

Each bone has a rotation, position and scale, each with an X, Y and Z value. Every setter has a matching getter, such as getRotX() for setRotX(...).

ValueMethodsUnits
RotationsetRotX, setRotY, setRotZRadians. Convert from degrees with Mth.DEG_TO_RAD
PositionsetPosX, setPosY, setPosZBlockbench pixels (16 to a block), on the same axes as Blockbench
ScalesetScaleX, setScaleY, setScaleZA multiplier, where 1 is normal size

Every bone also remembers its default pose from the model file through bone.getInitialAzSnapshot(). This matters for the next section.

Setting Values Safely

There are two ways to change a bone, and picking the wrong one is the most common source of bugs.

Set it from the default pose when your code fully controls the bone:

var initial = bone.getInitialAzSnapshot();
bone.setRotY(initial.getRotY() + angle);

Add to the animated value when you want to move a bone on top of what its animation is doing this frame:

bone.setRotY(bone.getRotY() + angle);

If a bone is sometimes animated and sometimes not (for example, a tail that your walk animation moves but your idle animation doesn't), ask the bone cache which case you're in. wasRotationAnimatedThisFrame returns true if an animation moved the bone's rotation this frame:

var boneCache = context().boneCache();
float base = boneCache.wasRotationAnimatedThisFrame(tail)
? tail.getRotY() // Animated this frame: build on the animation.
: tail.getInitialAzSnapshot().getRotY(); // Not animated: start from the default pose.
tail.setRotY(base + sway);

Position and scale have matching methods: wasPositionAnimatedThisFrame and wasScaleAnimatedThisFrame.

Example: Head Tracking

This turns the head to face where the entity is looking. It sets the head from its default pose, so it works whether or not your animations also move the head.

@Override
public void setCustomAnimations(ExampleEntity entity, float partialTicks) {
super.setCustomAnimations(entity, partialTicks);
var head = context().boneCache().getBakedModel().getBoneOrNull("head");
if (head == null) {
return;
}
// Where the entity is looking, relative to its body, in degrees.
float bodyYaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot);
float headYaw = Mth.wrapDegrees(entity.getViewYRot(partialTicks) - bodyYaw);
float headPitch = entity.getViewXRot(partialTicks);
var initial = head.getInitialAzSnapshot();
head.setRotX(initial.getRotX() + headPitch * Mth.DEG_TO_RAD);
head.setRotY(initial.getRotY() + headYaw * Mth.DEG_TO_RAD);
}

If your rig's head turns the opposite way, negate the value for that axis. If the head should stop at a limit, clamp the angle first, for example Mth.clamp(headYaw, -60, 60).

Example: Tail Sway

This sways the tail side to side, faster and wider while the entity walks. It sits on top of whatever the tail's animation is doing, using wasRotationAnimatedThisFrame from Setting Values Safely.

var tail = context().boneCache().getBakedModel().getBoneOrNull("tail");
if (tail != null) {
float time = entity.tickCount + partialTicks;
float walkSpeed = entity.walkAnimation.speed(partialTicks);
float sway = Mth.sin(time * (0.1F + walkSpeed * 0.3F)) * (0.15F + walkSpeed * 0.35F);
float base = context().boneCache().wasRotationAnimatedThisFrame(tail)
? tail.getRotY()
: tail.getInitialAzSnapshot().getRotY();
tail.setRotY(base + sway);
}

Example: Fading Motion In and Out

Procedural motion that switches on and off instantly looks like a pop. Instead, keep a weight between 0 and 1 that eases toward its target, and multiply your motion by it. Entity animators are created per entity, so fields in your animator are a safe place to keep this state.

This leans the upper body forward while the mob is aggressive:

public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {
/** How far into the lean we are, from 0 (none) to 1 (full). */
private float leanWeight;
/** The animation time when we last updated leanWeight, or -1 before the first frame. */
private double lastAnimTime = -1;
@Override
public void setCustomAnimations(ExampleEntity entity, float partialTicks) {
super.setCustomAnimations(entity, partialTicks);
// Time since the last frame, in ticks. Uses the animation timer, so it pauses with the game.
double animTime = context().timer().getAnimTime();
float deltaTicks = lastAnimTime < 0 ? 0 : (float) Math.max(0, animTime - lastAnimTime);
lastAnimTime = animTime;
// Ease toward the target over 5 ticks.
float target = entity.isAggressive() ? 1 : 0;
leanWeight = Mth.approach(leanWeight, target, deltaTicks / 5F);
var upperBody = context().boneCache().getBakedModel().getBoneOrNull("upper_body");
if (upperBody != null) {
var initial = upperBody.getInitialAzSnapshot();
upperBody.setRotX(initial.getRotX() + 20 * Mth.DEG_TO_RAD * leanWeight);
}
}
}

Using the animation timer instead of counting frames makes the fade take the same time at any frame rate.

Example: Hiding and Resizing Bones

Bones can be hidden or scaled to show state, such as armor plates, a backpack or a swelling egg sac:

var model = context().boneCache().getBakedModel();
var backpack = model.getBoneOrNull("backpack");
if (backpack != null) {
backpack.setHidden(!entity.hasBackpack());
}
var sac = model.getBoneOrNull("egg_sac");
if (sac != null) {
float pulse = 1 + Mth.sin((entity.tickCount + partialTicks) * 0.2F) * 0.05F;
float growth = entity.getGrowthProgress(); // Your own 0 to 1 value.
float scale = (0.5F + growth * 0.5F) * pulse;
sac.setScaleX(scale);
sac.setScaleY(scale);
sac.setScaleZ(scale);
}

Reading Bone Positions

Sometimes procedural animation needs to know where a bone is, for example to aim at something or spawn particles from a hand. Each bone can report its position:

  • bone.getLocalPosition(): relative to its parent bone.
  • bone.getModelPosition(): relative to the model.
  • bone.getWorldPosition(): in the world.

These positions are calculated when the model renders, so they describe the bone as it was drawn last frame. The first time you ask for one, the bone starts tracking its position from then on, so expect a single frame with no data.

Choosing an Approach

You want to...Use
Play motion made in BlockbenchA normal animation controller
Drive a keyframe from a value AzureLib already exposesA Molang query in the keyframe
Drive a keyframe from your own valueA custom Molang query
Mix authored animations (breathing over walking, upper body only)Layered controllers with weights, ADDITIVE and bone masks
React to live data with logic, limits or smoothingProcedural animation in setCustomAnimations

Troubleshooting

  1. A bone spins or drifts further every frame.

    • You're adding to the current value on a bone no animation is moving. Set it from getInitialAzSnapshot() instead, or check wasRotationAnimatedThisFrame as shown in Setting Values Safely.
  2. Nothing moves.

    • Check the bone name matches Blockbench exactly; getBoneOrNull returns null for a name that doesn't exist.
    • Make sure you call super.setCustomAnimations(...) and that your animator is the one set on the renderer with setAnimatorProvider.
    • Rotations are in radians. A value like 20 is over three full turns; use 20 * Mth.DEG_TO_RAD.
  3. Motion is choppy.

    • Interpolate game values with partialTicks.
  4. A bone pops when the motion stops.

  5. A bone turns the wrong way.

    • Negate the value for that axis. Rigs can be built facing different directions, so the right sign depends on your model.