This skill covers making the animation file: posing the official Decentraland avatar rig in Blender, exporting a clip as a *_emote.glb, verifying it, and handing it to the scene. The scene-side API that plays it — triggerSceneEmote, stopEmote, AvatarEmoteCommand, the AvatarMask enum, the permission — lives in player-avatar; this skill links there rather than repeating it.
Scope check before you start:
| The user wants | Skill |
|---|---|
| A new animation of the player's avatar that doesn't exist yet | this skill |
To play a built-in emote, or a _emote.glb that already exists |
player-avatar |
To play an animation clip baked into a .glb model/prop |
animations-tweens |
| To animate an NPC | npcs |
| To move/rotate an entity procedurally | animations-tweens (Tween) |
Workflow
- Get the rig —
Avatar_File.blend, never a hand-built armature. Download + bone/control/property reference:{baseDir}/references/avatar-rig.md. - Set up — set the scene to 30 fps (the shipped file is 24), capture the
Starting_Posevalues, create one new action per clip. - Author — pose
CTRL_bones only, in Pose mode, keyframing every bone you touch at every keyframe. - Verify while authoring — print world positions (clipping, floor, animation area) and render four angles per keyframe into a contact sheet.
- Export — one clip per file, armature only,
export_def_bones=True,ACTIVE_ACTIONS. - Verify the GLB — parse the JSON chunk: 1 animation, 0 meshes, 63 nodes, 62 channels per path, duration =
(frames − 1) / 30. - Hand off — rename to
*_emote.glb, drop underassets/animations/, add the permission, play it (see player-avatar).
Ready-made bpy helpers for every step — the helper module, sign detection, render/contact-sheet setup, the export call, and the GLB verifier — are in {baseDir}/references/blender-emote-patterns.md.
How to drive Blender (headless CLI vs Blender MCP, binary paths, and the rule that you must never fall back from the MCP to headless silently) is already covered in {baseDir}/../add-3d-models/references/blender-authoring.md. Emote authoring uses exactly those two paths; do not re-derive them.
RULE: One new action per clip — and reset to the idle pose, not identity
Create a fresh action for every clip. In Blender 4.4+/5 actions are slotted, and Action.fcurves no longer exists (curves live under act.layers[0].strips[0].channelbags):
act = bpy.data.actions.new(name)
slot = act.slots.new('OBJECT', 'Avatar_Animation')
arm.animation_data.action = act
arm.animation_data.action_slot = slot
Before authoring anything, scene.frame_set(0) to evaluate Starting_Pose and capture every pose bone's transform. That snapshot — not the identity transform — is the baseline you reset to between keyframes. The rest pose of the armature is an A/T-pose; the idle the runtime blends from is Starting_Pose. Resetting to identity gives you a clip that snaps to a T-pose on the first frame.
RULE: Key everything you pose, at every keyframe
scene.frame_set() re-evaluates the active action and overwrites any un-keyed pose (view_layer.update() does not). A render taken after frame_set of a pose you forgot to key silently shows the idle instead — you will debug the wrong thing. So insert keys on location, rotation and scale for every control bone you touched, plus any custom property you changed:
pb.keyframe_insert('["FK > IK Leg L"]', frame=f)
On the first and last frame, also key location/rotation/scale on every deform bone. Channels left without boundary keys stay "open" and keep whatever a previously playing emote left there — the official docs call these emote overrides, and they show up as an avatar whose legs are still doing the last emote.
RULE: Upper arms — world axes for direction, local Y for twist
The upper-arm bone's local X is tilted. Rotating about it sweeps the arm across the chest, and a pose that should have both arms forward ends up with the arms crossed — this happens in practice and is not obvious in a front render. So set the arm's direction with world axes, converting the world axis into the bone's current frame first:
M = (arm.matrix_world @ pb.matrix).to_3x3().normalized()
local_axis = M.inverted() @ Vector(world_axis)
pb.rotation_quaternion @= Quaternion(local_axis, math.radians(deg))
- Pure forward raise → world X.
- Moving a raised arm in/out → yaw about world Z.
World axes alone do not give a perfect arm pose, though. Arms move in many directions, and the rig's animator works with world and local rotations together. Two things need the bone's local axes:
- Twist — palm up/down, elbow pointing out or in, a wrist that has to face a prop — can only be done by rotating the bone about its own length, which is local Y on this rig (verified: local Y of
CTRL_FK_Avatar_Arm.*is the bone axis; rotating it leaves the elbow where it is and only rolls the forearm and hand).rl('CTRL_FK_Avatar_Arm.L', 'Y', deg). - Fine-tuning once the gross direction is right — a small local correction is often more precise than another world-axis rotation.
Elbows, knees, wrists and fingers hinge on their local X (Y/Z are locked on those controls) — rl() is correct there.
RULE: Legs mirror, the upper body does not — fix the pasted upper body by flipping X and Z
Blender's Pose ▸ Copy Pose / Paste X-Flipped only works on half of this rig (verified on Avatar_File.blend; numbers in {baseDir}/references/avatar-rig.md):
- Legs and feet mirror normally, FK or IK — Paste X-Flipped is exact for IK legs and within ~3 cm for FK legs.
- Shoulders, upper arms, forearms, hands and fingers do not — Paste X-Flipped lands the other hand 40–70 cm away. The rig animator's fix: on every pasted upper-body control, negate the X and Z values and keep Y. Example: a pasted arm reading X −0.5, Z 0.1 becomes X 0.5, Z −0.1. Apply it to the rotation (quaternion or Euler) and, on the IK hand and elbow controls, to the location too. Verified exact (0.000 m) for the whole arm chain, FK and IK, all 30 finger controls included.
mirror_side('L')in{baseDir}/references/blender-emote-patterns.mddoes both steps for every control at once.- When you pose a single joint rather than mirroring, still detect its sign programmatically — rotate +15°, measure which way the downstream bone moved, cache (
auto_sign). The same angle on.Land.Rdoes not produce a symmetric pose.
RULE: FK or IK — lose contact → FK, keep contact → IK
Legs default to IK, arms to FK (FK > IK * properties on CTRL_Avatar_UpperBody). Switch only when the pose needs it, using one rule: if a hand or foot has to keep contact with the floor, a seat, a wall or a prop, it should be IK; if it loses contact, it should be FK.
- FK legs: jumps, rolls, flying, handstands, swimming, legs dangling off a ledge or a theatre seat.
- IK hands: push-ups, climbing, leaning on a table, both hands fixed on one prop.
- Do not flip modes during walk cycles, dances or anywhere contact is lost only briefly — the defaults handle those, and every switch pops unless the two chains match exactly at that frame.
- Key the
FK > IKproperty itself when you change it, not just the bones.
Seated poses. Lower the body with CTRL_Avatar_UpperBody — it is the centre of gravity and moves the whole body. CTRL_Avatar_Hips moves only pelvis + legs and stretches the torso — wrong tool for sitting. Then decide per leg:
| Feet | Legs |
|---|---|
| Both feet on the ground (chair, bench) | Stay IK: move CTRL_IK_Foot.* to where the feet will rest, then drop the COG — the knees bend on their own, feet stay planted (verified: toes stay at Z = 0 through a 0.4 m drop). Aim the knees with CTRL_Avatar_Knee.*. |
| One leg crossed over the other | Only the lifted leg to FK; the grounded one stays IK. |
| Feet off the ground (pond edge, theatre seat, high stool) | Both legs FK. |
setprop('CTRL_Avatar_UpperBody', 'FK > IK Leg L', 0.0) # only for a leg that leaves the ground
setprop('CTRL_Avatar_UpperBody', 'FK > IK Leg R', 0.0)
# ...then pose CTRL_FK_Avatar_UpLeg / _Leg / _Foot / _ToeBase on that side
Whatever the mode, check the feet: world Z of Avatar_LeftToeBase / Avatar_RightToeBase must stay ≥ 0 or the avatar sinks through the floor.
RULE: Verify with numbers as well as renders
Renders catch composition; numbers catch clipping. Print world positions of hands, head, feet and any prop reference at every keyframe:
- Keep hands ≥ ~0.3 m from the head centre when a limb passes the head. A throw over the shoulder clipped straight through the skull until the arm was routed outward first.
- Keep hands ≥ ~0.10 m apart rather than overlapping at the centre line.
- Keep the face visible in any default/holding pose — hold objects at upper-chest height with the head tilted down, not in front of the face.
Render four angles per keyframe (front three-quarter, straight front, side, top) with Workbench and tile them into one contact sheet. Iterate in a cheap pose-only stage first (seconds per run), and only then do the full build with exports.
RULE: Stay inside the animation area
Measured on the reference objects in Avatar_File.blend (world metres, Blender Z-up):
| Reference object | Extent | Meaning |
|---|---|---|
Animation_Area_Reference |
4 × 4 × 4 m box — X, Y ∈ [−2, 2], Z ∈ [0, 4] | Everything stays inside for the whole clip: every body part, prop and particle. Nothing moves more than 4 m in any direction. |
Root_Animation_Area |
2 m diameter cylinder on the origin | The avatar root / centre of gravity (CTRL_Avatar_UpperBody, i.e. Avatar_Hips) travels at most 1 m forward, back, left or right — 2 m across. Vertically it may rise as far as the box allows (the docs say 4 m; the cylinder mesh in the file is 3 m tall). |
Ground_Reference |
plane at Z = 0 | Nothing below it. |
Legs, arms, head and props may leave the root cylinder — a kick or a cartwheel does — as long as they stay inside the box. Add the checks to the per-keyframe report: |hips.x| ≤ 1, |hips.y| ≤ 1, every deform bone head and tail inside the box, every toe Z ≥ 0 (in_area() in {baseDir}/references/blender-emote-patterns.md). The Decentraland Tools add-on's Validate Emote only warns when CTRL_Avatar_UpperBody moves more than 1 m horizontally or vertically from its first frame; it does not check the box, so the in_area() check is yours to run.
RULE: Loops must close; one-shots must land where the scene resumes
- Looping clip: make the first and last keyframes identical (e.g. frame 1 and frame 121 for a 4 s loop) or the loop pops.
- One-shot clip: start and end in the resting pose the scene returns to — for a seated game, the seated holding pose; otherwise
Starting_Pose.
RULE: Export one clip per file, armature only
Delete every other object first — meshes, WGT_* widgets, reference planes, Armature_Prop, cameras, lights — and remove the Starting_Pose action, or the exporter emits two clips. export_animation_mode='ACTIVE_ACTIONS' is what guarantees exactly one animation per file. Full export call in {baseDir}/references/blender-emote-patterns.md.
Save an editable .blend (meshes included) before stripping, so the user can tweak the actions by hand afterwards.
RULE: Verify the exported GLB before handing it over
Parse the GLB's JSON chunk and assert: 1 animation, 0 meshes, 63 nodes (Armature + 62 deform bones), 1 skin, 62 channels each for translation/rotation/scale, and duration == (last_frame − first_frame) / 30. Verifier script in {baseDir}/references/blender-emote-patterns.md. A duration mismatch means the scene FPS wasn't 30.
Typical size for a 1–4 s clip at export_frame_step=1 is 70–110 KB — far under the 3 MB cap, so there is normally no reason to raise the sampling step.
Upper-body (masked) animations
A masked emote is not a different file format — the same .glb is played with mask: AvatarMask.AM_UPPER_BODY at trigger time (API and enum details: player-avatar → "Emote masks"). What changes is what you should author.
Which bones the mask keeps — verified against the Explorer's UpperBodyAvatarMask.mask asset:
- Animated:
Avatar_Spine,_Spine1,_Spine2, both shoulders/arms/forearms/hands, all 40 finger bones,Avatar_Neck,Avatar_Head. - Ignored:
Avatar_Hipsand both legs (UpLeg/Leg/Foot/ToeBase) — these keep whatever the locomotion state or another emote is doing that frame.
Mechanically the renderer samples your clip each LateUpdate, then restores the non-masked bones' local position and rotation from the pose the Animator had already produced (MaskedLegacyEmoteBlender). So:
- Hip/root motion in a masked clip has no effect.
Avatar_Hipsis outside the mask, so the drop you authored withCTRL_Avatar_UpperBodyto sit the avatar down is discarded; the legs show the locomotion state (idle, walk, run). - A masked clip does NOT layer over another emote. Verified in the Explorer's
SceneMaskedEmoteSystem: starting a masked emote on the local player stops any full-body emote that is playing, and starting a full-body emote cancels the masked one. So a masked clip cannot run on top of a chair's sitting emote — the avatar stands up. It layers only over locomotion, which is what the mask is for: carrying, holding or cheering while the player walks, runs or idles. - For a seated player, play the clip full body. Author a seated lower body into every clip (hips lowered, FK legs bent), trigger it unmasked, and have the scene replay the sitting emote (
triggerEmote({ predefinedEmote: 'sittingChair1' })or the chair's own) when the clip ends and no looping seated clip takes over. A one-shot that returns to the same seated pose as your looping idle clip chains into it without a pop; the only visible pop is the height difference against the built-in sitting emote, which you tune with the hip drop. - Author the full body anyway, even for masked use. A plausible lower body makes the same file usable unmasked. The mask is a trigger-time decision, not a property of the file.
- Masked emotes on the local player emit no
AvatarEmoteCommandlifecycle events (documented in player-avatar). The scene must therefore time them itself — store each clip's duration alongside its path in scene config, since you know it exactly from the frame count.
Design notes for a seated holding pose that reads well: hips ≈ 0.6 m, hands at chest height, head tilted toward the hands.
The bone list and blending mechanism above were read from the Explorer source (
MaskedLegacyEmoteBlender,SceneMaskedEmoteSystem,CharacterEmoteSystemonmain, 2026-09); the "masked stops full-body" rule was confirmed in-world on a seated player. For the enum values and call signatures, trust player-avatar, which is verified against the shipped protocol.
File requirements
| Requirement | Value | Source |
|---|---|---|
| Frame rate | 30 fps (Blender defaults to 24) | docs |
| Max length | 10 s / 300 frames | docs |
| Format | .glb |
docs |
| Max file size | 3 MB | docs |
| Sampling rate | 1 by default; 2 or 3 to shrink |
docs |
| Animation clips per file | exactly 1 (Emotes 2.0 may add 1 prop clip; sequences unsupported) | docs |
| File contents | deforming skeleton + animation only — no mesh, no control bones, no cameras/lights | docs |
| Start / end pose | both from the idle (Starting_Pose) |
docs |
| Deform-bone keys | location + rotation + scale on the first and last frame, every bone | docs |
| Animation area | root ≤ 1 m in each horizontal direction (2 m across); every mesh part and prop inside the 4 × 4 × 4 m Animation_Area_Reference box (see RULE above) |
docs + measured |
| Filename | must end _emote.glb (case-insensitive) |
player-avatar |
| Scene permission | ALLOW_TO_TRIGGER_AVATAR_EMOTE in scene.json requiredPermissions |
player-avatar |
Conventional location in the scene: assets/animations/. The glTF animation's name is irrelevant to the runtime — the file path is the identifier.
Blender's glTF exporter UI equivalents, if the user is exporting by hand: Include > Limit to > Visible Objects, Data > Armature > Export Deformation Bones Only, Animation > Sampling Animations.
The Decentraland Tools add-on's Validate Emote operator checks most of this (30 fps, ≤ 300 frames, exactly one armature action, first/last-frame keys on deform channels) before it will export, and warns — without blocking — when the COG moves more than 1 m horizontally or vertically. It does not check the 4 m bounding box.
Testing in the scene
The fastest harness is a row of clickable primitive cubes — MeshRenderer + MeshCollider + pointerEventsSystem.onPointerDown with a generous maxDistance so they stay clickable from a chair — one per clip, plus a Stop cube and a mask toggle cube:
pointerEventsSystem.onPointerDown(
{ entity: cube, opts: { button: InputAction.IA_POINTER, hoverText: label, maxDistance } },
() => void triggerSceneEmote({ src, loop: false, mask: upperBody ? AvatarMask.AM_UPPER_BODY : undefined })
)
Gate the whole harness behind a config flag and remove it before publishing.
The Explorer MCP's trigger_emote only plays URN/base emotes, not scene files — it cannot test a _emote.glb. Use in-scene triggers (see unity-explorer-mcp for driving the client around them).
Cross-references
- player-avatar —
triggerSceneEmote/stopEmote/AvatarEmoteCommand/AvatarMask, the_emote.glbrule, the permission, and freezing the player withInputModifierduring a full-body emote. - add-3d-models →
references/blender-authoring.md— how to drive Blender at all (headless CLI vs MCP), plus general model export rules. - animations-tweens — animations baked into props and models, and procedural
Tweenmotion. - npcs — animating
AvatarShapeNPCs and GLB NPCs. - Official docs: creating emotes · rig features · avatar rig