roble3/cc-blender-skill3 files

Blender Animation

Animate objects, cameras, lights, and properties in Blender — keyframes, F-curves, easing (Bezier, Linear, Sine, Bounce, Elastic), shape keys (morph targets / blendshapes / visemes), drivers (Python expressions on properties), NLA actions for reuse and layering. Use whenever the user asks to "animate this", "make it move / rotate / scale over time", "add keyframes", "loop / oscillate", "shape key / morph / blendshape", "visemes for lip sync", or any time-based property change. Make sure to use this skill even if the user does not say "animate" — also covers "spin it slowly", "make it wave", "fade in / out", "pulse", "facial expression".

Specification
Skill ID
roble3/cc-blender-skill/blender-animation
Publisher
roble3
Repository
cc-blender-skill
Installs
0
Files
3
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

roble3/cc-blender-skill/blender-animationInstalls these files
  • SKILL.md
  • evals/evals.json
  • references/overview.md

What this skill tells the agent

Blender Animation

Animate properties over time. Most animation is just keyframes — the trick is choosing the right interpolation and easing for the motion's character.

Decision tree

What kind of motion?
├── Object movement (translate / rotate / scale)
│   → Keyframe `location` / `rotation_euler` / `scale`
│   → Recipe 1, 2
│
├── Mechanical / constant speed (gears, conveyor belts, scrolling)
│   → Linear interpolation
│   → Recipe 3
│
├── Natural / organic (most things)
│   → Bezier interpolation with auto handles
│   → Recipe 1
│
├── Cartoon / stylized (overshoot, bounce, anticipation)
│   → Bounce / Elastic / Back easing
│   → Recipe 4
│
├── Facial / morph / blendshape
│   → Shape Keys, animate `value` property
│   → Recipe 5
│
├── Mechanical relations (one property = function of another)
│   → Drivers (Python expression)
│   → Recipe 6
│
└── Reusable / layered animations
    → NLA actions
    → Recipe 7

Recipes

Recipe 1 — Animate object position (Bezier, natural)

import bpy

obj = bpy.data.objects['GEO-target']

scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 60
scene.render.fps = 24

# Keyframe 1: at frame 1, at origin
scene.frame_set(1)
obj.location = (0, 0, 0)
obj.keyframe_insert('location', frame=1)

# Keyframe 2: at frame 60, moved to (5, 0, 0)
scene.frame_set(60)
obj.location = (5, 0, 0)
obj.keyframe_insert('location', frame=60)

print(f"animated:{obj.name} 1->60")

Default interpolation = Bezier (smooth in/out). To make it linear, see Recipe 3.

Recipe 2 — Animate rotation (a 360° spin)

import bpy, math

obj = bpy.data.objects['GEO-target']
scene = bpy.context.scene

# Use rotation_euler with a single axis.
# WARNING: animating past 180° on Euler can flip; use multiple keyframes or quaternions for full rotations.

scene.frame_set(1)
obj.rotation_euler = (0, 0, 0)
obj.keyframe_insert('rotation_euler', frame=1)

scene.frame_set(120)
obj.rotation_euler = (0, 0, math.radians(180))   # half-turn
obj.keyframe_insert('rotation_euler', frame=120)

scene.frame_set(240)
obj.rotation_euler = (0, 0, math.radians(360))   # full turn
obj.keyframe_insert('rotation_euler', frame=240)

print(f"rotated:{obj.name} 360 over 240 frames")

For perfectly constant spin, set keyframes to Linear interpolation (Recipe 3).

Recipe 3 — Set keyframes to Linear interpolation

⚠ Blender 5.x changed the Action API. Legacy action.fcurves was removed in favour of layered Actions: action.layers[].strips[].channelbags[].fcurves. Use this compat helper.

import bpy

def get_fcurves_compat(action):
    """Return all fcurves on an Action — works on both legacy (≤4.x) and layered (5.x+) actions."""
    if hasattr(action, 'fcurves'):
        return list(action.fcurves)
    fcurves = []
    for layer in action.layers:
        for strip in layer.strips:
            if hasattr(strip, 'channelbags'):
                for cb in strip.channelbags:
                    fcurves.extend(cb.fcurves)
    return fcurves

obj = bpy.data.objects['GEO-target']
if obj.animation_data and obj.animation_data.action:
    for fc in get_fcurves_compat(obj.animation_data.action):
        for kp in fc.keyframe_points:
            kp.interpolation = 'LINEAR'
print(f"interp:linear {obj.name}")

Other options: 'BEZIER' (default), 'CONSTANT' (step), 'SINE', 'QUAD', 'CUBIC', 'QUART', 'QUINT', 'BOUNCE', 'ELASTIC', 'BACK'.

Recipe 4 — Bouncy / cartoon easing on a specific keyframe

import bpy

# (Re-use get_fcurves_compat from Recipe 3.)
def get_fcurves_compat(action):
    if hasattr(action, 'fcurves'):
        return list(action.fcurves)
    fcurves = []
    for layer in action.layers:
        for strip in layer.strips:
            if hasattr(strip, 'channelbags'):
                for cb in strip.channelbags:
                    fcurves.extend(cb.fcurves)
    return fcurves

obj = bpy.data.objects['GEO-target']
target_fc = None
for fc in get_fcurves_compat(obj.animation_data.action):
    if fc.data_path == 'location' and fc.array_index == 2:   # Z axis
        target_fc = fc
        break

if target_fc and len(target_fc.keyframe_points) >= 2:
    last_kp = target_fc.keyframe_points[-1]
    last_kp.interpolation = 'BOUNCE'
    last_kp.easing = 'EASE_OUT'    # 'AUTO', 'EASE_IN', 'EASE_OUT', 'EASE_IN_OUT'
print('animated:bouncy_landing')

Recipe 5 — Shape keys (morph / blendshape / viseme)

import bpy

mesh_obj = bpy.data.objects['GEO-character_face']

# Add basis (the rest pose)
if mesh_obj.data.shape_keys is None:
    basis = mesh_obj.shape_key_add(name='Basis')

# Add a morph target
smile = mesh_obj.shape_key_add(name='Smile')
smile.value = 0.0
# ⚠ At this point, switch to Edit Mode interactively and modify the mesh while 'Smile' is selected.
#   Or set vertex coordinates programmatically (advanced).

# Animate
scene = bpy.context.scene
scene.frame_set(1)
smile.value = 0.0
smile.keyframe_insert('value', frame=1)

scene.frame_set(24)
smile.value = 1.0
smile.keyframe_insert('value', frame=24)

print('animated:shape_key_smile')

For lip sync: standard 15-viseme set (Oculus / ARKit) — name shape keys viseme_aa, viseme_E, viseme_O, etc. Animate each viseme's value across the audio timeline.

Recipe 6 — Driver (one property as expression of another)

import bpy

# Example: child object's X = parent's X × 2
target = bpy.data.objects['GEO-follower']
source = bpy.data.objects['GEO-leader']

fc = target.driver_add('location', 0)   # X axis
driver = fc.driver
driver.type = 'SCRIPTED'

# Add variable referencing source's X position
var = driver.variables.new()
var.name = 'src_x'
var.type = 'TRANSFORMS'
var.targets[0].id = source
var.targets[0].transform_type = 'LOC_X'
var.targets[0].transform_space = 'WORLD_SPACE'

driver.expression = 'src_x * 2'
print(f"driver:{target.name}.x = {source.name}.x * 2")

Recipe 7 — Push current animation to NLA strip (for reuse)

import bpy

obj = bpy.data.objects['GEO-character']

if obj.animation_data and obj.animation_data.action:
    track = obj.animation_data.nla_tracks.new()
    track.name = 'NLA-Walk'
    strip = track.strips.new('Walk', start=1, action=obj.animation_data.action)
    obj.animation_data.action = None    # clear timeline; NLA owns the animation
    print(f"nla:pushed_walk_strip")

After this, the animation is reusable — duplicate the strip, scale time, blend with other tracks.

Recipe 8 — Subtle idle animation (loopable rotation)

import bpy, math

obj = bpy.data.objects['GEO-target']

# Tiny sway around Y, 4-second loop
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 96    # 4s at 24fps

scene.frame_set(1)
obj.rotation_euler = (0, math.radians(-2), 0)
obj.keyframe_insert('rotation_euler', frame=1)

scene.frame_set(48)
obj.rotation_euler = (0, math.radians(2), 0)
obj.keyframe_insert('rotation_euler', frame=48)

scene.frame_set(96)
obj.rotation_euler = (0, math.radians(-2), 0)
obj.keyframe_insert('rotation_euler', frame=96)

# Set extrapolation mode to cycle (loop)
def get_fcurves_compat(action):
    if hasattr(action, 'fcurves'):
        return list(action.fcurves)
    fcurves = []
    for layer in action.layers:
        for strip in layer.strips:
            if hasattr(strip, 'channelbags'):
                for cb in strip.channelbags:
                    fcurves.extend(cb.fcurves)
    return fcurves

if obj.animation_data and obj.animation_data.action:
    for fc in get_fcurves_compat(obj.animation_data.action):
        fc.modifiers.new('CYCLES')
print('animated:idle_loop')

Pro animation principles

  • Slow in, slow out — Bezier handles do this automatically; matches real-world physics
  • Anticipation — small reverse motion before the main action (jump prep)
  • Follow-through — secondary parts continue after main motion stops (cape, hair)
  • Squash & stretch — exaggeration with shape keys or scale animation

More from roble3

Use this skill in a project

Open any RiverX project, search the Skills panel for roble3/cc-blender-skill/blender-animation, and install it in one click.

Use this skill