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.
AzureLib has two ways to do this:
- Pose modifiers (recommended): small pieces of code registered on your animator that adjust the animated pose every frame. AzureLib weights them, orders them, limits them to the bones you choose, and removes their changes again before the next frame, so they can't drift or pop.
setCustomAnimations: an animator method where you set bone values directly. It's still fully supported and is the right tool for hiding bones; see Writing Bones Directly.
Built on pose modifiers, AzureLib also ships:
- Inverse Kinematics: feet that step onto slopes, hands that grip, heads that look at a target, and joint limits.
- Secondary Motion: tails, antennae and ears that swing and trail on their own.
Where Procedural Animation Runs
AzureLib does the following every frame, for each animatable, on the client:
- Pose modifier changes from the previous frame are removed.
- Molang queries are updated for the animatable.
- Every animation controller applies its animation to the bones, layered in the order the controllers were added.
- Bones that no controller moved ease back toward their default pose.
setCustomAnimationsruns.- Pose modifiers run, in priority order, and their result is what gets rendered this frame.
Because step 1 removes last frame's changes, a pose modifier always starts from this frame's animated pose. That is what makes it safe to add to a bone's rotation every frame.
Warning
Both run on the client, every frame, for every animated instance on screen. Keep them cheap: avoid creating objects, avoid searching the world, and read values the entity already has. Remember that the client only knows synced data. A field that only exists on the server (such as a mob's attack target) won't have a useful value here.
Pose Modifiers
Registering Modifiers
Override registerPoseModifiers in your animator and add modifiers with a unique id. A modifier is a lambda (or class) that receives a pose context:
public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {@Overrideprotected void registerPoseModifiers(AzPoseModifierRegistry<ExampleEntity> modifiers) {modifiers.add("look", AzPoseModifiers.headLook("neck", "head"));modifiers.add("core_pulse", pose -> {float pulse = 1 + Mth.sin((float) pose.animTime() * 0.2F) * 0.05F;pose.bone("core").scale(pulse);});}// ... registerControllers, getAnimationLocation, etc.}
registerPoseModifiers is called once, before the animator's first frame. AzItemAnimator and AzBlockAnimator have it too, so everything on this page also works for items and block entities.
Addons and compat code can add or remove modifiers later through animator.getPoseModifiers(); changes apply from the next frame. Removing a modifier puts the bones it posed back to their animated pose.
Reading and Writing Bones
Inside a modifier, get bones with pose.bone("name"). It never returns null: for a bone the model doesn't have, it returns a handle whose writes do nothing. Check exists() if the bone is optional.
A bone handle can read three versions of the pose:
| Method | What it returns |
|---|---|
bindRotation(), bindPosition(), bindScale() | The model's default pose from Blockbench |
animatedRotation(), animatedPosition(), animatedScale() | What your animation controllers produced this frame, before any modifier |
currentRotation(), currentPosition(), currentScale() | The animated pose plus everything earlier modifiers have done to it |
And write in two ways:
| Kind | Methods | What it does |
|---|---|---|
| Additive | rotate, rotateDegrees, translate, scale | Adds to (or, for scale, multiplies) the current pose |
| Absolute | setRotation, setPosition, setScale, set, resetToBind, resetToAnimated | Moves the current pose to the given value |
modifiers.add("lean", pose -> {pose.bone("upper_body").rotateDegrees(15, 0, 0); // 15° on top of whatever the animation does.pose.bone("jaw").setRotation(0, 0, 0); // Hold the jaw shut, ignoring the animation.});
Units are the same as on bones: rotations in radians (rotateDegrees takes degrees), positions in Blockbench pixels, scale as a multiplier. Writes can be chained: pose.bone("tail").rotate(0, sway, 0).scale(1.1F).
Timing and Per-Animatable State
The pose context also gives you:
| Method | Use |
|---|---|
animatable() | The entity, item or block entity being posed |
partialTick() | Interpolate game values with it so motion is smooth |
animTime() | Animation time in ticks; stops while the game is paused |
deltaTicks() | Ticks since this animatable was last posed. Step anything time-based with it so the result doesn't depend on frame rate |
isDiscontinuous() | true on the first frame, after a long gap, or after the modifier was switched off. Snap time-based state instead of smoothing it |
state(Supplier) | An object owned by this modifier for this animatable, created on first use |
weight() | The modifier's current weight (already applied to every write) |
Modifiers are shared by every animatable that uses the animator, so keep per-animatable data in state, not in fields on the modifier:
private static final class Smoothed {float value;}modifiers.add("tail_speed_sway", pose -> {var entity = pose.animatable();var smoothed = pose.state(Smoothed::new);float target = entity.walkAnimation.speed(pose.partialTick());// Ease towards the walk speed over about 5 ticks, at any frame rate.smoothed.value = pose.isDiscontinuous()? target: Mth.lerp(Math.min(1F, (float) pose.deltaTicks() / 5F), smoothed.value, target);float sway = Mth.sin((float) pose.animTime() * (0.1F + smoothed.value * 0.3F)) * (0.15F + smoothed.value * 0.35F);pose.bone("tail").rotate(0, sway, 0);});
Order, Bone Masks and Weights
Each modifier returns an entry you can configure when you add it:
modifiers.add("look", AzPoseModifiers.headLook("head")).priority(10) // Lower runs first. Default 0..mask(AzBoneMask.only("neck")) // Only bones under "neck" can be written..defaultWeight(1); // 0 = off, 1 = full effect. Default 1.
- Priority: modifiers run in ascending priority, then in the order they were added. Later modifiers see what earlier ones did through
currentRotation()and friends. - Mask: the same
AzBoneMaskcontrollers use. Writes to bones outside the mask are ignored; reads still work. - Weight: scales every write. Additive writes are scaled down, absolute writes land partway, so at
0.5a modifier has half its effect.
Fading Modifiers In and Out
Weights can be changed per animatable through the animation context's pose layer, instantly or as a fade so the motion eases in and out instead of popping:
var pose = context().poseLayer();pose.setWeight("look", 0); // Instantly.pose.fadeWeight("look", 1, 5); // Over 5 ticks.pose.fadeWeight("look", 0, 10); // Back out over 10 ticks.
This leans the upper body forward while the mob is aggressive, fading in and out:
public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {private boolean wasAggressive;@Overrideprotected void registerPoseModifiers(AzPoseModifierRegistry<ExampleEntity> modifiers) {modifiers.add("aggressive_lean", pose -> pose.bone("upper_body").rotateDegrees(20, 0, 0)).defaultWeight(0);}@Overridepublic void setCustomAnimations(ExampleEntity entity, float partialTicks) {super.setCustomAnimations(entity, partialTicks);if (entity.isAggressive() != wasAggressive) {wasAggressive = entity.isAggressive();context().poseLayer().fadeWeight("aggressive_lean", wasAggressive ? 1 : 0, 5);}}}
Warning
Start a fade when the state changes, not every frame. Each call restarts the fade from the current weight, so calling fadeWeight every frame keeps it from ever moving.
resetState("id") and resetAllState() on the pose layer clear the state objects modifiers keep, for example after a teleport.
Built-in Modifiers
AzPoseModifiers has ready-made modifiers you can weight, mask, prioritize and fade like your own:
| Modifier | What it does |
|---|---|
headLook("neck", "head") | Turns the bones towards where a living entity is looking, on top of the animation, split evenly across the bones. limits(yaw, pitch) caps the angle in degrees; invertYaw() and invertPitch() fix rigs that turn the wrong way |
sway(bone, x, y, z, periodTicks, phase) | A sine-wave rotation (peaks in radians) for idle sway |
followRotation(source, target, factor) | Makes one bone copy another's rotation away from its default pose, for example a jaw or eyes following the head |
spring(bone) | Lags and overshoots the bone's own animated rotation. For chains that swing when the entity moves, use AzSpringBone |
modifiers.add("look", AzPoseModifiers.<ExampleEntity>headLook("neck", "head").limits(60, 45));modifiers.add("antenna_sway", AzPoseModifiers.sway("antenna", 0, 0, 0.2F, 40, 0));modifiers.add("jaw_follow", AzPoseModifiers.followRotation("head", "jaw", 0.3F)).priority(10);
Example: Head Tracking
The built-in headLook covers this, but here it is by hand to show the pattern. The head turns towards where the entity is looking, on top of whatever the animation does with it:
modifiers.add("look", pose -> {var entity = pose.animatable();float partialTicks = pose.partialTick();// Where the entity is looking, relative to its body, in degrees.float bodyYaw = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot);float headYaw = Mth.clamp(Mth.wrapDegrees(entity.getViewYRot(partialTicks) - bodyYaw), -60, 60);float headPitch = entity.getViewXRot(partialTicks);pose.bone("head").rotateDegrees(headPitch, headYaw, 0);});
If your rig's head turns the opposite way, negate the value for that axis.
Working with Whole Transforms
AzBoneTransform holds a bone's position, rotation and scale as one reusable value, for saving, blending or copying poses:
private final AzBoneTransform guardPose = new AzBoneTransform();// Blend the arm towards a saved pose by the modifier's weight:pose.bone("right_arm").set(guardPose);// Or add its offset from the default pose on top of the current pose:pose.bone("right_arm").add(guardPose);
It can also be filled from a bone (setFromBone, setFromBind) and written back (applyTo), and has blendTowards and addOffset for combining two transforms with the same rules controllers use.
Writing Bones Directly
Override setCustomAnimations in your animator to set bone values yourself. It runs after the controllers and before the pose modifiers:
@Overridepublic void setCustomAnimations(ExampleEntity entity, float partialTicks) {super.setCustomAnimations(entity, partialTicks);var head = context().boneCache().getBakedModel().getBoneOrNull("head");if (head != null) {// Change 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.
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(...).
| Value | Methods | Units |
|---|---|---|
| Rotation | setRotX, setRotY, setRotZ | Radians. Convert from degrees with Mth.DEG_TO_RAD |
| Position | setPosX, setPosY, setPosZ | Blockbench pixels (16 to a block), on the same axes as Blockbench |
| Scale | setScaleX, setScaleY, setScaleZ | A multiplier, where 1 is normal size |
Setting Values Safely
Values set here become part of the animated pose, so they behave differently from pose modifiers:
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 only when an animation moved the bone 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);
Warning
Don't write bone.setRotY(bone.getRotY() + angle) on its own here. If no animation moved the bone this frame, it still holds the value your code set last frame, so the angle is added again every frame and the bone spins away. Pose modifiers don't have this problem.
wasPositionAnimatedThisFrame and wasScaleAnimatedThisFrame do the same check for position and scale.
Hiding and Resizing Bones
Visibility isn't part of the pose, so hiding bones belongs in setCustomAnimations. Resizing can be done either here or in a pose modifier with scale:
var model = context().boneCache().getBakedModel();var backpack = model.getBoneOrNull("backpack");if (backpack != null) {backpack.setHidden(!entity.hasBackpack());}
Reading Bone Positions
Sometimes you need to know where a bone is in the world, for example to aim at something or spawn particles from a hand. Use a bone socket, which gives a world position and direction for any bone or Blockbench locator, and also works on the server.
For quick lookups, each bone can also report its position from the last rendered frame:
bone.getLocalPosition(): relative to its parent bone.bone.getModelPosition(): relative to the model.bone.getWorldPosition(): in the world.
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 Blockbench | A normal animation controller |
| Drive a keyframe from a value AzureLib already exposes | A Molang query in the keyframe |
| Drive a keyframe from your own value | A custom Molang query |
| Mix authored animations (breathing over walking, upper body only) | Layered controllers with weights, ADDITIVE and bone masks |
| Blend walk/run or strafe animations by speed or direction | A blend space |
| React to live data with logic, limits or smoothing | A pose modifier |
| Reach for something, plant feet, look at a target | Inverse kinematics |
| Make tails and antennae swing on their own | Secondary motion |
| Hide or show bones | setCustomAnimations |
Troubleshooting
-
Nothing moves.
- Check the bone name matches Blockbench exactly.
pose.bone(...)quietly ignores bones that don't exist; checkexists()while testing. - Make sure the modifier's weight isn't
0and that its mask includes the bone. - Rotations are in radians. A value like
20is over three full turns; userotateDegreesor20 * Mth.DEG_TO_RAD.
- Check the bone name matches Blockbench exactly.
-
A bone spins or drifts further every frame.
- This only happens in
setCustomAnimations. Move the code into a pose modifier, or set it fromgetInitialAzSnapshot()as shown in Setting Values Safely.
- This only happens in
-
Motion is choppy.
- Interpolate game values with
partialTick(), and step smoothing withdeltaTicks().
- Interpolate game values with
-
Motion pops when it switches on or off.
- Fade the modifier's weight instead of adding or removing it; see Fading Modifiers In and Out.
-
A fade never starts.
- You're calling
fadeWeightevery frame. Call it once when the state changes.
- You're calling
-
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.
-
Two modifiers fight over a bone.
- The later one (higher priority) builds on the earlier one. Set priorities so the order is what you want, or mask one of them away from the bone.