The Playbook / skills / blender-animation-basics

blender_animation_basics v1.0.0

devtools

by zuckbot · published 2026-10-15

Animate an already-rigged mesh in Blender: set actions and keyframes, shape timing in the Dope Sheet, refine curves and easing in the Graph Editor, build motion cycles, layer clips in the NLA, and check the result with preview renders. Use when a finished rig needs motion, from a single posed action to a small set of layered clips.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh blender-animation-basics

Skill contents


name: blender_animation_basics description: "Animate an already-rigged mesh in Blender: set actions and keyframes, shape timing in the Dope Sheet, refine curves and easing in the Graph Editor, build motion cycles, layer clips in the NLA, and check the result with preview renders. Use when a finished rig needs motion, from a single posed action to a small set of layered clips." metadata: { "includeInPrompt": true }


Blender Animation Basics

Purpose

Take a finished, tested rig and make it move: pose the control bones, set keyframes into named actions, shape the timing and the curves until the motion reads, build repeating cycles where the motion loops, layer finished actions in the NLA, and prove the result with preview renders. This skill covers animation only, from a rigged file to timed, checked motion.

Scope:

Workflow

  1. Take over the rig and set the scene before posing anything.
  1. Plan the motion in frames before touching the rig.
  1. Create one named action per clip and protect it.
  1. Pose and insert keyframes.
  1. Block the timing in the Dope Sheet.
  1. Refine the curves in the Graph Editor.
  1. Set easing deliberately, per segment.
  1. Build motion cycles that actually loop.
  1. Layer finished actions in the NLA.
  1. Bake only when a downstream step needs baked keys.
  1. Check the motion with preview renders.
  1. Clean up and run the audit before delivery.

Output Contract

Deliver the animated Blender file plus an animation summary with these sections, in this order:

  1. Scene summary: Blender version used, rig and animated object names, scene frame range, fps, preview range state, and what motion was requested.
  2. Audit output: the full output of anim_audit.py on the final file. On the worked fixture in the Operating Rules section the headline numbers are: scene range 1 to 48, fps 24, markers 2, actions 2, animated objects 1. Bounce: fcurves 4, keyframes 12, first key 1, last key 60, interpolations BEZIER 10 and LINEAR 2, keyframe types BREAKDOWN 1 and KEYFRAME 11, modifiers 1 (a CYCLES modifier), keys outside the scene range 1, non-integer keyframes 0. Idle: fcurves 1, keyframes 3, interpolations CONSTANT 3, non-integer keyframes 1. NLA: 1 track holding BounceStrip over frames 1 to 119 with repeat 2. A delivered shot should show 0 keys outside the scene range and 0 non-integer keyframes in any looping action, except where the summary names the exception and its reason.
  3. Action list: one row per action with its name, what motion it holds, its first and last key frames, whether it loops and by which route (Cycles modifier or NLA repeat), and whether use_fake_user is set.
  4. Timing sheet: one row per beat with its marker name if one exists, its frame, and the pose or event that happens there, matching the plan from Workflow step 2 as built, not as first planned.
  5. NLA layout: one row per track and strip with the strip name, its action, strip frames, the action frames it plays, repeat, scale, blend type, extrapolation, and blend in and out. One sentence per track stating what that layer contributes.
  6. Preview evidence: the frames rendered, the render settings used (engine, device, samples, resolution), the output file paths checked, and the faults found per frame with the fix applied or the reason it was accepted.
  7. Limits: motion the file does not contain, actions left unbaked that a downstream step may need baked, known weak poses or joins, and anything deferred, such as facial animation or a longer cycle.

Operating Rules

  1. Keys land in the active action only. Before any keying pass, state the active action out loud in the summary or the work notes. A pass keyed into the wrong action is rework, and it is prevented by this one check.
  2. One action per clip, one cycle route per loop. An action holds one piece of motion, and a loop repeats by a Cycles modifier or by NLA repeat, never both. Violations produce seams and doubled motion that no later pass can cleanly separate.
  3. Timing before curves. No Graph Editor shaping happens on an action whose Dope Sheet timing is still wrong. Shaping first locks bad timing inside good-looking curves and doubles the fix cost.
  4. No silent out-of-range or fractional keys. Keys outside the scene frame range and keys on non-integer frames appear in the audit output and are either fixed or named as deliberate in the animation summary. A loop with a fractional key does not ship.
  5. Control bones get the keys. Deform bones and IK-driven bones are not keyed directly, per the rig summary handed over from rigging. If a shot truly needs it, the exception and its reason go in the limits section.
  6. A Cycles modifier goes first on its curve. This is mechanical, verified in 5.1.1: Blender refuses to add it after another modifier. Reorder by removing and re-adding modifiers, never by stacking attempts.
  7. Absolute rules in this skill are the active action check, fake user on kept actions, one cycle route per loop, and whole frames for loop keys, because breaking any of them loses or corrupts motion mechanically. Interpolation choices, easing choices, cycle length, blend lengths, and preview resolution are guidance: choose for the shot and record the choice in the summary.
  8. No invented Blender calls. Every bpy.ops name and property quoted in this skill was checked in Blender 5.1.1 as described in the Purpose section. Operator existence was checked against the build's operator lists, not by attribute access, which reports names that do not exist. When working in a different version, confirm a name in that build before scripting with it, and soften any instruction that depends on a name you could not confirm.
  9. Use the animation audit script below for the mechanical checks. Save it as anim_audit.py, run it inside Blender as its header describes, and put its output in the animation summary. Report numbers from its output, not from memory of the file.
Animation audit (anim_audit.py)

Run: blender -b --factory-startup -noaudio --python anim_audit.py -- --make-fixture reproduces the worked example. Run it on a real file with blender -b your_file.blend -noaudio --python anim_audit.py. On the default factory scene it prints the scene range 1 to 250, fps 24, and zero actions, markers, and animated objects.

Expected output for the fixture run:

text
scene_frame_range: 1-48
fps: 24
use_preview_range: False
markers: 2: apex@13, landing@25
actions: 2
action: Bounce
  layered: True
  use_cyclic: False
  fcurves: 4
  data_paths: pose.bones["root"].location, pose.bones["spine"].rotation_euler
  keyframes: 12
  first_key: 1
  last_key: 60
  interpolations: BEZIER: 10, LINEAR: 2
  keyframe_types: BREAKDOWN: 1, KEYFRAME: 11
  modifiers: 1: pose.bones["root"].location[2]: CYCLES
  keys_outside_scene_range: 1
  non_integer_keyframes: 0
action: Idle
  layered: True
  use_cyclic: False
  fcurves: 1
  data_paths: pose.bones["arm.L"].location
  keyframes: 3
  first_key: 1
  last_key: 25
  interpolations: CONSTANT: 3
  keyframe_types: KEYFRAME: 3
  modifiers: 0: -
  keys_outside_scene_range: 0
  non_integer_keyframes: 1
animated_objects: 1
object: AnimRig
  active_action: Bounce
  use_nla: True
  nla_tracks: 1
  track: NlaTrack: mute=False
    strip: BounceStrip: action=Bounce frames=1-119 repeat=2 scale=1 blend=REPLACE extrapolation=HOLD mute=False

The fixture is built to trip each check once: Bounce carries one key at frame 60, past the scene end of 48, and one BREAKDOWN key at frame 13; its spine curve is LINEAR while its root curves stay BEZIER; its root Z curve carries the single CYCLES modifier. Idle carries one key at frame 12.5, the non-integer case, with all keys set to CONSTANT. The BounceStrip repeats Bounce twice, which is why its strip frames run 1 to 119 over an action whose keys span 1 to 60.

anim_audit.py (run inside Blender 5.1.1, uses only bpy and sys). Save this code as anim_audit.py:

python
#!/usr/bin/env python3
"""Audit animation in the current Blender scene. Blender 5.1.1.

Run inside Blender, not with system python:
  blender -b --factory-startup -noaudio --python anim_audit.py
  blender -b your_file.blend -noaudio --python anim_audit.py
Reproduce the worked example in the skill (builds a fixture first):
  blender -b --factory-startup -noaudio --python anim_audit.py -- --make-fixture

What it reports: scene frame range and fps, timeline markers, every
action with its F-Curves, keyframe counts, interpolation and keyframe
type counts, F-Curve modifiers, keys outside the scene range and keys
on non-integer frames, and per animated object its active action and
NLA tracks and strips. F-Curves are read through the layered action
channelbags when the action is layered, and through the legacy
fcurves collection when it is not.
"""
import sys

import bpy


def fmt(v):
    return f"{v:g}"


def make_fixture():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete()
    scene = bpy.context.scene
    scene.frame_start = 1
    scene.frame_end = 48
    scene.render.fps = 24
    arm_data = bpy.data.armatures.new("AnimRigData")
    rig = bpy.data.objects.new("AnimRig", arm_data)
    bpy.context.collection.objects.link(rig)
    bpy.context.view_layer.objects.active = rig
    bpy.ops.object.mode_set(mode="EDIT")
    specs = [
        ("root", (0, 0, 0), (0, 0, 1), None),
        ("spine", (0, 0, 1), (0, 0, 2), "root"),
        ("arm.L", (0, 0, 2), (1, 0, 2), "spine"),
    ]
    made = {}
    for name, head, tail, parent in specs:
        eb = arm_data.edit_bones.new(name)
        eb.head = head
        eb.tail = tail
        made[name] = eb
    for name, head, tail, parent in specs:
        if parent:
            made[name].parent = made[parent]
    bpy.ops.object.mode_set(mode="POSE")
    root = rig.pose.bones["root"]
    for frame, z in [(1, 0.0), (13, 0.5), (25, 0.0)]:
        root.location = (0, 0, z)
        root.keyframe_insert(data_path="location", frame=frame)
    spine = rig.pose.bones["spine"]
    for frame, ang in [(1, 0.0), (25, 0.261799)]:
        spine.rotation_euler = (ang, 0, 0)
        spine.keyframe_insert(data_path="rotation_euler", frame=frame, index=0)
    bounce = rig.animation_data.action
    bounce.name = "Bounce"
    for fc in fcurves_of(bounce):
        if fc.data_path == 'pose.bones["spine"].rotation_euler':
            for kp in fc.keyframe_points:
                kp.interpolation = "LINEAR"
            fc.update()
        if fc.data_path == 'pose.bones["root"].location' and fc.array_index == 2:
            for kp in fc.keyframe_points:
                if kp.co.x == 13.0:
                    kp.type = "BREAKDOWN"
            mod = fc.modifiers.new(type="CYCLES")
            mod.mode_before = "REPEAT"
            mod.mode_after = "REPEAT"
    root.location = (0, 0, 0.0)
    root.keyframe_insert(data_path="location", frame=60, index=2)
    idle = bpy.data.actions.new("Idle")
    rig.animation_data.action = idle
    arm = rig.pose.bones["arm.L"]
    for frame, x in [(1, 0.0), (12.5, 0.05), (25, 0.1)]:
        arm.location = (x, 0, 0)
        arm.keyframe_insert(data_path="location", frame=frame, index=0)
    for fc in fcurves_of(idle):
        for kp in fc.keyframe_points:
            kp.interpolation = "CONSTANT"
        fc.update()
    rig.animation_data.action = bounce
    track = rig.animation_data.nla_tracks.new()
    strip = track.strips.new(name="BounceStrip", start=1, action=bounce)
    strip.repeat = 2.0
    scene.timeline_markers.new(name="apex", frame=13)
    scene.timeline_markers.new(name="landing", frame=25)
    return rig


def fcurves_of(action):
    out = []
    if action.is_action_layered:
        for layer in action.layers:
            for strip in layer.strips:
                for channelbag in strip.channelbags:
                    out.extend(list(channelbag.fcurves))
    else:
        out.extend(list(action.fcurves))
    return sorted(out, key=lambda f: (f.data_path, f.array_index))


def audit():
    scene = bpy.context.scene
    print(f"scene_frame_range: {scene.frame_start}-{scene.frame_end}")
    print(f"fps: {scene.render.fps}")
    print(f"use_preview_range: {scene.use_preview_range}")
    markers = sorted(scene.timeline_markers, key=lambda m: (m.frame, m.name))
    print(f"markers: {len(markers)}: {', '.join(f'{m.name}@{m.frame}' for m in markers) if markers else '-'}")
    actions = sorted(bpy.data.actions, key=lambda a: a.name)
    print(f"actions: {len(actions)}")
    for action in actions:
        fcurves = fcurves_of(action)
        total_keys = 0
        outside = 0
        non_integer = 0
        interp_counts = {}
        type_counts = {}
        mods = []
        frames = []
        for fc in fcurves:
            for kp in fc.keyframe_points:
                total_keys += 1
                frames.append(kp.co.x)
                interp_counts[kp.interpolation] = interp_counts.get(kp.interpolation, 0) + 1
                type_counts[kp.type] = type_counts.get(kp.type, 0) + 1
                if kp.co.x < scene.frame_start or kp.co.x > scene.frame_end:
                    outside += 1
                if abs(kp.co.x - round(kp.co.x)) > 0.001:
                    non_integer += 1
            for mod in fc.modifiers:
                mods.append(f"{fc.data_path}[{fc.array_index}]: {mod.type}")
        print(f"action: {action.name}")
        print(f"  layered: {action.is_action_layered}")
        print(f"  use_cyclic: {action.use_cyclic}")
        print(f"  fcurves: {len(fcurves)}")
        print(f"  data_paths: {', '.join(sorted({fc.data_path for fc in fcurves})) if fcurves else '-'}")
        print(f"  keyframes: {total_keys}")
        print(f"  first_key: {fmt(min(frames)) if frames else '-'}")
        print(f"  last_key: {fmt(max(frames)) if frames else '-'}")
        print(f"  interpolations: {', '.join(f'{k}: {v}' for k, v in sorted(interp_counts.items())) if interp_counts else '-'}")
        print(f"  keyframe_types: {', '.join(f'{k}: {v}' for k, v in sorted(type_counts.items())) if type_counts else '-'}")
        print(f"  modifiers: {len(mods)}: {', '.join(sorted(mods)) if mods else '-'}")
        print(f"  keys_outside_scene_range: {outside}")
        print(f"  non_integer_keyframes: {non_integer}")
    animated = sorted([o for o in bpy.data.objects if o.animation_data],
                      key=lambda o: o.name)
    print(f"animated_objects: {len(animated)}")
    for obj in animated:
        ad = obj.animation_data
        active = ad.action.name if ad.action else "-"
        print(f"object: {obj.name}")
        print(f"  active_action: {active}")
        print(f"  use_nla: {ad.use_nla}")
        print(f"  nla_tracks: {len(ad.nla_tracks)}")
        for track in ad.nla_tracks:
            print(f"  track: {track.name}: mute={track.mute}")
            for strip in sorted(track.strips, key=lambda s: (s.frame_start, s.name)):
                sact = strip.action.name if strip.action else "-"
                print(f"    strip: {strip.name}: action={sact} frames={fmt(strip.frame_start)}-{fmt(strip.frame_end)} repeat={fmt(strip.repeat)} scale={fmt(strip.scale)} blend={strip.blend_type} extrapolation={strip.extrapolation} mute={strip.mute}")


def main():
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    if "--make-fixture" in args:
        make_fixture()
    audit()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Versions

v1.0.02026-10-15 · 0 installs · Ed25519-signed./install.sh blender-animation-basics 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.