Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Animate 3D objects and characters in Blender with Python. Use when the user wants to keyframe properties, create armatures and rigs, set up IK/FK chains, animate shape keys for facial animation, edit F-Curves, use the NLA editor to blend actions, add drivers for expression-based animation, or script any animation workflow in Blender.
.claude/skills/terminalskills-blender-animation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-06 | ✓→✗ | ▼ Worse | 120% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 135% | 0% |
| case-12 | ✓→✓ | = Same ✓ | 90% | 0% |
Create and control animations in Blender using Python. Keyframe object and bone transforms, build armatures with IK/FK rigs, animate shape keys for facial motion, edit F-Curves for timing control, layer actions in the NLA editor, and wire up drivers for expression-based automation — all scriptable from the terminal.
pythonimport bpy obj = bpy.data.objects["Cube"] # Keyframe location at multiple frames for loc, frame in [((0,0,0), 1), ((5,0,3), 30), ((5,4,0), 60)]: obj.location = loc obj.keyframe_insert(data_path="location", frame=frame) # Also works for rotation_euler, scale, single-axis (index=2 for Z), custom props obj.rotation_euler = (0, 0, 3.14159) obj.keyframe_insert(data_path="rotation_euler", frame=60) obj.scale = (2, 2, 2) obj.keyframe_insert(data_path="scale", frame=60) obj["intensity"] = 1.0 # custom property obj.keyframe_insert(data_path='["intensity"]', frame=30) bpy.context.scene.frame_start, bpy.context.scene.frame_end = 1, 60
pythonimport bpy action = bpy.data.objects["Cube"].animation_data.action for fcurve in action.fcurves: for kp in fcurve.keyframe_points: kp.interpolation = 'BEZIER' # CONSTANT, LINEAR, BEZIER, SINE, EXPO, BOUNCE, ELASTIC kp.easing = 'EASE_IN_OUT' # AUTO, EASE_IN, EASE_OUT, EASE_IN_OUT # Cycles modifier to loop the animation mod = fcurve.modifiers.new(type='CYCLES') mod.mode_before = 'REPEAT' # NONE, REPEAT, REPEAT_OFFSET, MIRROR mod.mode_after = 'REPEAT' # Add noise to a specific channel z_curve = action.fcurves.find("location", index=2) # Z location if z_curve: noise = z_curve.modifiers.new(type='NOISE') noise.strength, noise.scale = 0.3, 5.0
pythonimport bpy arm_data = bpy.data.armatures.new("Rig") arm_obj = bpy.data.objects.new("Rig", arm_data) bpy.context.collection.objects.link(arm_obj) bpy.context.view_layer.objects.active = arm_obj bpy.ops.object.mode_set(mode='EDIT') # (name, head, tail, parent_name, connected) bone_defs = [ ("Spine", (0,0,1.0), (0,0,1.4), None, False), ("Chest", (0,0,1.4), (0,0,1.8), "Spine", False), ("UpperArm.L", (0.2,0,1.7), (0.5,0,1.4), "Chest", False), ("Forearm.L", (0.5,0,1.4), (0.8,0,1.1), "UpperArm.L", True), ("Thigh.L", (0.1,0,1.0), (0.1,0,0.5), "Spine", False), ("Shin.L", (0.1,0,0.5), (0.1,0,0.05),"Thigh.L", True), ] for name, head, tail, parent, connected in bone_defs: b = arm_data.edit_bones.new(name) b.head, b.tail = head, tail if parent: b.parent = arm_data.edit_bones[parent] b.use_connect = connected bpy.ops.object.mode_set(mode='OBJECT')
pythonimport bpy arm_obj = bpy.data.objects["Rig"] bpy.context.view_layer.objects.active = arm_obj bpy.ops.object.mode_set(mode='POSE') # IK — use an empty as target, chain_count limits affected bones ik = arm_obj.pose.bones["Forearm.L"].constraints.new('IK') ik.target = bpy.data.objects["IK_Target"] ik.pole_target = bpy.data.objects["IK_Pole"] ik.chain_count = 2 # Copy Rotation — mirrors another object's rotation cr = arm_obj.pose.bones["Chest"].constraints.new('COPY_ROTATION') cr.target = bpy.data.objects["ChestCtrl"] # Other types: LIMIT_ROTATION, DAMPED_TRACK, STRETCH_TO, COPY_LOCATION, TRACK_TO bpy.ops.object.mode_set(mode='OBJECT')
pythonimport bpy, math arm_obj = bpy.data.objects["Rig"] bpy.context.view_layer.objects.active = arm_obj bpy.ops.object.mode_set(mode='POSE') if not arm_obj.animation_data: arm_obj.animation_data_create() action = bpy.data.actions.new("WalkCycle") arm_obj.animation_data.action = action # Keyframe thigh swing: back → neutral → forward thigh = arm_obj.pose.bones["Thigh.L"] for angle, frame in [(-30, 1), (0, 13), (30, 25)]: thigh.rotation_euler = (math.radians(angle), 0, 0) thigh.keyframe_insert(data_path="rotation_euler", frame=frame) bpy.ops.object.mode_set(mode='OBJECT')
pythonimport bpy obj = bpy.data.objects["Head"] mesh = obj.data if not mesh.shape_keys: # Basis key required first obj.shape_key_add(name="Basis", from_mix=False) smile = obj.shape_key_add(name="Smile", from_mix=False) for i, vert in enumerate(smile.data): if i in [42, 43, 56, 57]: # mouth corner vertices vert.co.z += 0.02 # Animate shape key value (0.0 → 1.0 → 0.0) sk = mesh.shape_keys.key_blocks["Smile"] for val, frame in [(0.0, 1), (1.0, 15), (0.0, 30)]: sk.value = val sk.keyframe_insert(data_path="value", frame=frame)
pythonimport bpy arm_obj = bpy.data.objects["Rig"] if not arm_obj.animation_data: arm_obj.animation_data_create() # Push actions as NLA strips on separate tracks track1 = arm_obj.animation_data.nla_tracks.new() track1.name = "Walk" strip1 = track1.strips.new("Walk", start=1, action=bpy.data.actions["WalkCycle"]) strip1.repeat = 4 # loop 4 times strip1.blend_type = 'REPLACE' # REPLACE, COMBINE, ADD, SUBTRACT, MULTIPLY track2 = arm_obj.animation_data.nla_tracks.new() track2.name = "Wave" strip2 = track2.strips.new("Wave", start=25, action=bpy.data.actions["WaveHand"]) strip2.blend_type = 'COMBINE' # blend on top of walk strip2.influence = 0.8 # partial blend arm_obj.animation_data.action = None # clear active action so NLA takes over
pythonimport bpy driver = bpy.data.objects["Cube"].driver_add("location", 2).driver # drive Z location driver.type = 'SCRIPTED' driver.expression = "sin(frame * 0.1) * 2" # Add a variable that reads another object's transform var = driver.variables.new() var.name = "ctrl" var.type = 'TRANSFORMS' var.targets[0].id = bpy.data.objects["Controller"] var.targets[0].transform_type = 'LOC_X' var.targets[0].transform_space = 'WORLD_SPACE' driver.expression = "ctrl * 3" # also works on shape keys via sk.driver_add("value")
User request: "Animate a ball bouncing with squash and stretch"
pythonimport bpy bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, location=(0, 0, 3)) ball = bpy.context.active_object ball.name = "BouncingBall" keyframes = [ # (frame, z_pos, scale_x, scale_y, scale_z) (1, 3.0, 1.0, 1.0, 1.0), (12, 0.5, 1.0, 1.0, 1.0), # fall (15, 0.3, 1.3, 1.3, 0.6), (18, 0.5, 0.85, 0.85, 1.2), # squash + stretch (30, 2.2, 1.0, 1.0, 1.0), (42, 0.5, 1.0, 1.0, 1.0), # apex + second fall (45, 0.3, 1.2, 1.2, 0.7), (55, 1.5, 1.0, 1.0, 1.0), # smaller bounce (62, 0.5, 1.0, 1.0, 1.0), # settle ] for frame, z, sx, sy, sz in keyframes: ball.location = (0, 0, z) ball.keyframe_insert(data_path="location", frame=frame) ball.scale = (sx, sy, sz) ball.keyframe_insert(data_path="scale", frame=frame) for fc in ball.animation_data.action.fcurves: for kp in fc.keyframe_points: kp.interpolation = 'BEZIER' bpy.context.scene.frame_end = 62 bpy.ops.wm.save_as_mainfile(filepath="/tmp/bouncing_ball.blend")
User request: "Create a simple arm rig with IK and animate it reaching for a cube"
pythonimport bpy bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() arm_data = bpy.data.armatures.new("ArmRig") arm_obj = bpy.data.objects.new("ArmRig", arm_data) bpy.context.collection.objects.link(arm_obj) bpy.context.view_layer.objects.active = arm_obj bpy.ops.object.mode_set(mode='EDIT') prev = None for name, head, tail in [("UpperArm",(0,0,1.5),(0.6,0,1.5)), ("Forearm",(0.6,0,1.5),(1.2,0,1.5)), ("Hand",(1.2,0,1.5),(1.4,0,1.5))]: b = arm_data.edit_bones.new(name) b.head, b.tail = head, tail if prev: b.parent, b.use_connect = prev, True prev = b bpy.ops.object.mode_set(mode='OBJECT') # IK target + constraint bpy.ops.object.empty_add(type='PLAIN_AXES', location=(1.2, 0, 1.5)) ik_target = bpy.context.active_object ik_target.name = "IK_Hand" bpy.context.view_layer.objects.active = arm_obj bpy.ops.object.mode_set(mode='POSE') ik = arm_obj.pose.bones["Hand"].constraints.new('IK') ik.target, ik.chain_count = ik_target, 3 bpy.ops.object.mode_set(mode='OBJECT') # Animate IK target reaching toward a cube bpy.ops.mesh.primitive_cube_add(size=0.3, location=(1.5, 0.5, 1.0)) for loc, frame in [((1.2,0,1.5),1), ((1.5,0.5,1.0),30), ((1.5,0.5,1.3),50)]: ik_target.location = loc ik_target.keyframe_insert(data_path="location", frame=frame) for fc in ik_target.animation_data.action.fcurves: for kp in fc.keyframe_points: kp.interpolation, kp.easing = 'BEZIER', 'EASE_IN_OUT' bpy.context.scene.frame_end = 50 bpy.ops.wm.save_as_mainfile(filepath="/tmp/arm_grab.blend")
data_path must match the RNA path exactly: "location", "rotation_euler", "scale", '["custom_prop"]'.edit_bones), Pose mode for constraints and keyframes (pose.bones).chain_count=0 solves the entire chain to root.key_blocks["Name"].value (0.0 to 1.0).CONSTANT for hold/step, LINEAR for mechanical, BEZIER with easing for organic motion.animation_data.action = None to let NLA strips drive — an active action overrides NLA.bpy.ops.nla.bake() before export — other software cannot read them.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,885 | 14,766 | -26% | 1 | 1 | 0% | 3,247 | 6,917 | +113% | 0 | 0 | — |
case-02 | pass→pass | 12,780 | 13,399 | +5% | 1 | 1 | 0% | 2,796 | 6,581 | +135% | 0 | 0 | — |
case-12 | pass→pass | 11,442 | 5,654 | -51% | 1 | 1 | 0% | 2,497 | 4,754 | +90% | 0 | 0 | — |
case-03 | pass→pass | 9,426 | 8,515 | -10% | 1 | 1 | 0% | 2,067 | 5,532 | +168% | 0 | 0 | — |
case-04 | pass→pass | 12,010 | 9,408 | -22% | 1 | 1 | 0% | 2,467 | 6,093 | +147% | 0 | 0 | — |
case-05 | pass→pass | 11,845 | 8,879 | -25% | 1 | 1 | 0% | 2,304 | 5,034 | +118% | 0 | 0 | — |
case-06 | pass→fail | 10,183 | 3,288 | -68% | 1 | 1 | 0% | 1,950 | 4,290 | +120% | 0 | 0 | — |
case-07 | pass→pass | 6,913 | 3,559 | -49% | 1 | 1 | 0% | 1,375 | 4,291 | +212% | 0 | 0 | — |
case-08 | pass→pass | 8,453 | 4,721 | -44% | 1 | 1 | 0% | 1,699 | 4,527 | +166% | 0 | 0 | — |
case-09 | pass→pass | 11,451 | 6,081 | -47% | 1 | 1 | 0% | 2,424 | 5,059 | +109% | 0 | 0 | — |
case-10 | pass→pass | 9,341 | 4,045 | -57% | 1 | 1 | 0% | 2,027 | 4,497 | +122% | 0 | 0 | — |
case-11 | pass→pass | 4,566 | 2,957 | -35% | 1 | 1 | 0% | 955 | 4,139 | +333% | 0 | 0 | — |
case-13 | pass→pass | 8,056 | 4,558 | -43% | 1 | 1 | 0% | 1,678 | 4,532 | +170% | 0 | 0 | — |
case-14 | fail→pass | 10,385 | 5,342 | -49% | 1 | 1 | 0% | 1,971 | 4,556 | +131% | 0 | 0 | — |
case-15 | pass→pass | 5,846 | 4,163 | -29% | 1 | 1 | 0% | 1,248 | 4,522 | +262% | 0 | 0 | — |
case-16 | pass→pass | 6,335 | 5,495 | -13% | 1 | 1 | 0% | 1,299 | 4,844 | +273% | 0 | 0 | — |
case-17 | pass→pass | 16,098 | 15,155 | -6% | 1 | 1 | 0% | 3,136 | 6,671 | +113% | 0 | 0 | — |
case-18 | pass→pass | 14,422 | 10,837 | -25% | 1 | 1 | 0% | 3,240 | 6,058 | +87% | 0 | 0 | — |
case-19 | pass→pass | 9,958 | 3,970 | -60% | 1 | 1 | 0% | 1,971 | 4,372 | +122% | 0 | 0 | — |
case-20 | pass→pass | 9,271 | 5,947 | -36% | 1 | 1 | 0% | 1,876 | 4,881 | +160% | 0 | 0 | — |
case-21 | pass→pass | 4,518 | 2,151 | -52% | 1 | 1 | 0% | 951 | 3,979 | +318% | 0 | 0 | — |
case-22 | pass→pass | 3,203 | 3,172 | -1% | 1 | 1 | 0% | 619 | 4,308 | +596% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.