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:
- 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. Anything you set here is what gets rendered this frame.
public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {@Overridepublic 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.
Figyelmeztetés
setCustomAnimations runs on the client, every frame, for every animated instance on screen. Keep it 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.
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(...).
| 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 |
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);
Figyelmeztetés
Only add to the current value (bone.getRotY() + angle) when an animation moved that bone this frame. If no animation moved it, the bone still holds the value your code set last frame, so the angle gets added again every frame and the bone spins away. This happens because AzureLib doesn't ease a bone back to its default pose while your code keeps setting it.
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.
@Overridepublic 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;@Overridepublic 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.
Figyelmeztetés
Fade your motion out instead of just stopping it. AzureLib's own easing back to the default pose doesn't know about values your code set, so if you suddenly stop setting a bone, it can jump before it settles.
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 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 |
| React to live data with logic, limits or smoothing | Procedural animation in setCustomAnimations |
Troubleshooting
-
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 checkwasRotationAnimatedThisFrameas shown in Setting Values Safely.
- You're adding to the current value on a bone no animation is moving. Set it from
-
Nothing moves.
- Check the bone name matches Blockbench exactly;
getBoneOrNullreturnsnullfor a name that doesn't exist. - Make sure you call
super.setCustomAnimations(...)and that your animator is the one set on the renderer withsetAnimatorProvider. - Rotations are in radians. A value like
20is over three full turns; use20 * Mth.DEG_TO_RAD.
- Check the bone name matches Blockbench exactly;
-
Motion is choppy.
- Interpolate game values with
partialTicks.
- Interpolate game values with
-
A bone pops when the motion stops.
- Fade the motion out with a weight, as in Fading Motion In and Out.
-
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.