Installs into .claude/skills of the current project.
Are you the author of Procedural Animation?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/summerengine-procedural-animation)
---
name: procedural-animation
description: "Runtime bone changes on top of clips: head look-at, foot IK, hand grabs a prop, lean, recoil."
license: MIT
compatibility: [Cursor, Claude Code, Windsurf, Codex]
category: animation
user-invocable: false
allowed-tools: Read Grep Edit Write summer_get_scene_tree summer_inspect_node summer_inspect_resource summer_run_script summer_add_node summer_set_prop summer_set_resource_property summer_save_scene summer_get_script_errors
paths: ["**/*.gd", "**/*.tscn", "**/*.tres"]
---
# Procedural Animation — Bones Driven by Code
Generated clips cover most of a character's motion. The rest — the eye contact when an NPC speaks, feet that plant on slopes, hands that wrap a sword grip, the lean into a sprint, the recoil from a hit on the left shoulder — is procedural. In Summer Engine this is done via the `SkeletonModifier3D` family attached to the `Skeleton3D`, plus additive blend layers on the `AnimationTree`. None of it is generative; this skill is a set of recipes.
Limit: Summer Engine's current IK is good enough for look-at, foot-snap,
and simple two-bone arm/leg chains. It is not a full-body IK system with weight
redistribution. For combat-grappling or contact-heavy interactions, hand-author
the contact frame and accept some clipping.
## When to use this skill
- Feet clip into / float over uneven terrain.
- NPC head doesn't track the player during conversation — feels dead-eyed.
- Character's hand doesn't actually hold the sword — pen-through.
- Body doesn't lean into turns / aiming.
- Hit reactions need to come from the hit *direction* (recoil left when struck left), not just one canned animation.
- Ragdoll on death.
## When NOT to use this skill
- The clip already covers it. Don't IK what mocap already nailed — adds CPU cost and rounding error.
- The character is far from the camera (LOD2+). Procedural cost outpaces visual benefit. Disable modifiers past 15m.
- You need facial animation. That's `facial-and-lipsync`.
- You haven't generated base clips yet. Skeleton modifiers compose ON TOP of clips; without a base pose they'll stack on T-pose and look wrong.
## The three patterns
Almost every procedural request maps to one of these three. Pick the right one before writing code.
### Pattern A — SkeletonModifier3D (post-clip bone overrides)
Attached as a child of `Skeleton3D`. Runs after the AnimationTree writes the pose, modifies specific bones in-place. Includes `LookAtModifier3D` and `SkeletonIK3D`. Cheap, deterministic, no animation graph dependency.
### Pattern B — Additive AnimationNodeAdd2 layer in the tree
A clip recorded as a *delta from base pose* (e.g., a "lean left" clip that's 0 at center, fully leaned at +1) added on top of locomotion. Driven by writing `parameters/Add/blend_amount` from script. Use for stylistic overlays — turning lean, aiming offset, breathing.
### Pattern C — PhysicalBoneSimulator3D (ragdoll)
The skeleton hands off to physics on death. Bones become `PhysicalBone3D` rigid bodies that fall under gravity and collide with the world. One-way: once ragdoll fires, you can't easily blend back to clip-driven without snapping.
## Pattern A recipes
### A1 — Head look-at the player
The most common NPC liveliness fix.
**ctx lane (animation-tier engines):** one `summer_run_script` call creates the owned modifier and sets the props — prefer it over the 10-call CRUD chain below:
```gdscript
func run(ctx):
var skel = ctx.find("NPC").find_children("*", "Skeleton3D", true, false)[0]
var mod = ctx.look_at_modifier(skel, ctx.find("Player"), {
"bone_name": "Head", "forward_axis": 2, "primary_rotation_axis": 1,
"use_secondary_rotation": true, "use_angle_limitation": true,
"symmetry_limitation": true, "primary_limit_angle": 1.4, "secondary_limit_angle": 1.0,
})
```
Unknown prop names land in the `prop_warnings` report entry instead of failing silently — read it. On an older engine the helper is a plain script error; fall back to plain GDScript in the same tool (confirm the bone names first with `summer_inspect_node`):
```
summer_run_script(scenePath="res://main.tscn", source="""
func run(ctx):
var skel = ctx.find("NPC").find_children("*", "Skeleton3D", true, false)[0]
var look = LookAtModifier3D.new()
look.name = "HeadLookAt"
look.bone_name = "Head"
look.forward_axis = 2 # +Z forward (Meshy default)
look.primary_rotation_axis = 1 # Y-up
look.use_secondary_rotation = true
look.use_angle_limitation = true # OFF by default: nothing clamps until you set this
look.symmetry_limitation = true # one angle drives both directions
look.primary_limit_angle = 1.4 # ~80 degrees, prevents the Exorcist
look.secondary_limit_angle = 1.0
skel.add_child(look)
ctx.set_owner_recursive(look)
ctx.save_scene()
""")
```
There is no `symmetry_limit` and no `secondary_rotation_axis` property on
`LookAtModifier3D` — the real names are the ones above, and the secondary
rotation axis is derived, not set. Confirmed against the shipped 4.6.1 binary;
`ClassDB.class_get_property_list("LookAtModifier3D")` is the check if you're
unsure of a name.
Drive the target each frame:
```gdscript
@onready var head_look: LookAtModifier3D = $Skeleton3D/HeadLookAt
func _process(_delta: float) -> void:
var player := get_tree().get_first_node_in_group("player")
if player == null:
head_look.influence = 0.0
return
var to_player := player.global_position - global_position
var dist := to_player.length()
# Disable look-at past 8m — feels uncanny at long range.
head_look.target_node = player.get_path() if dist < 8.0 else NodePath("")
head_look.influence = clamp(1.0 - (dist - 6.0) / 2.0, 0.0, 1.0) # fade out 6→8m
```
`use_angle_limitation` is the key setting, and it defaults to **false** — a stock `LookAtModifier3D` does not clamp at all, so the head can rotate 180° and the model looks possessed. Turn it on and set `primary_limit_angle` to ~80° (1.4 rad), which matches a human's neck-only range; for a "whole-body turn" use it with the spine chain (next recipe). `primary_limit_angle` defaults to 2π, i.e. unlimited.
### A2 — Spine + head chain (look-at with body turn)
Multiple `LookAtModifier3D` nodes on the same chain — one for `Spine1` (`primary_limit_angle` 0.4 rad), one for `Spine2` (0.4 rad), one for `Head` (1.0 rad), each with `use_angle_limitation = true`. Total reach: ~110°, distributed naturally. Without the spine contribution, big angles look like the head detaches.
### A3 — Foot IK (slopes & uneven terrain)
The clip has the foot at Y=0; on a slope the ground is at Y=0.15. Without IK,
the foot floats. Use `SkeletonIK3D` on each two-bone leg chain:
```gdscript
@onready var skel: Skeleton3D = $Skeleton3D
@onready var ik_left: SkeletonIK3D = $Skeleton3D/IKLeft
@onready var ik_right: SkeletonIK3D = $Skeleton3D/IKRight
@onready var ray: RayCast3D = $FootRay # cast straight down
func _physics_process(_delta: float) -> void:
_solve_foot(ik_left, "LeftFoot")
_solve_foot(ik_right, "RightFoot")
func _solve_foot(ik: SkeletonIK3D, bone_name: String) -> void:
var bone_idx := skel.find_bone(bone_name)
var foot_world := skel.get_bone_global_pose(bone_idx).origin
ray.global_position = foot_world + Vector3.UP * 0.5
ray.target_position = Vector3.DOWN * 1.0
ray.force_raycast_update()
if ray.is_colliding():
var ground := ray.get_collision_point()
if ground.y > foot_world.y - 0.05: # only lift, never drop into the ground
ik.target_node = NodePath("../IKTarget" + bone_name)
(get_node("Skeleton3D/IKTarget" + bone_name) as Node3D).global_position = ground + Vector3.UP * 0.05
ik.start()
else:
ik.stop()
else:
ik.stop()
```
The 0.5m up-offset on the ray prevents self-occlusion. The "only lift, never drop" rule matters: letting IK push feet into the ground breaks knee bends.
Limits: this works for stairs and gentle slopes. On 45°+ slopes, use a pelvis lowering pass first (drop the root by `min(left_offset, right_offset)`) so the body squats and the legs don't over-extend.
### A4 — Hand grab on prop (sword grip, rifle stock)
`SkeletonIK3D` on the arm chain (Shoulder → UpperArm → LowerArm → Hand), with `target_node` set to a Marker3D parented to the prop. The clip drives the body; IK pins the hand to the grip. For a rifle held two-handed, do the right hand from clip and IK the left hand to a foregrip Marker3D — locks them together regardless of recoil.
## Pattern B — Additive layer
In the AnimationTree, wrap Locomotion in an `AnimationNodeAdd2`:
```
[parent: AnimationNodeStateMachine.Locomotion]
AnimationNodeAdd2
in: BlendSpace1D (idle/walk/run) # the base
add: AnimationNodeAnimation # an additive clip — must be authored as delta
```
Drive `parameters/Locomotion/Add/blend_amount` (0..1) from script. Use cases: turn-lean (lean amount = clamp(turn_rate, -1, 1)), aim offset (lean while aiming up/down), breathing-while-idle. The lean clip must be authored as a delta from the base pose, or the pose is applied twice.
## Pattern C — Ragdoll on death
```gdscript
@onready var sim: PhysicalBoneSimulator3D = $Skeleton3D/PhysicalBoneSimulator
func die(impulse_world: Vector3) -> void:
$AnimationTree.active = false
sim.physical_bones_start_simulation()
var pelvis := $Skeleton3D/PhysicalBoneSimulator/PelvisPhysicalBone as PhysicalBone3D
pelvis.apply_central_impulse(impulse_world * 4.0)
```
You need PhysicalBone3D children matching every major bone (set up once via the Skeleton3D context menu → "Create Physical Skeleton"). The impulse parameter sells the death — without directional impulse the body just collapses straight down.
## Confirmation gates
- **Before adding modifiers:** state which bones, which axes. Bone-name typos are a top failure.
- **Before driving from script:** confirm influence will fade out at distance and the target node is correct.
- **Before enabling ragdoll:** confirm physical bones exist; without them, simulation is a no-op.
## Reference card
### Bone names (Meshy humanoid skeleton, exact strings)
`Hips`, `Spine`, `Spine1`, `Spine2`, `Neck`, `Head`, `LeftShoulder`, `LeftArm`, `LeftForeArm`, `LeftHand`, `RightShoulder`, `RightArm`, `RightForeArm`, `RightHand`, `LeftUpLeg`, `LeftLeg`, `LeftFoot`, `LeftToeBase`, `RightUpLeg`, `RightLeg`, `RightFoot`, `RightToeBase`. (Meshy uses Mixamo-compatible naming with no `mixamorig:` prefix.)
### Axis conventions
| Setting | Value (Meshy default) |
|---|---|
| Forward axis (head/spine) | Z+ (`forward_axis`) |
| Up axis | Y+ |
| Right axis | X+ |
| Foot down | Y- |
### Influence fade-out distances (tuned defaults)
| Modifier | Disable past |
|---|---|
| Head look-at | 8m |
| Foot IK | 15m (snap-to-ground only matters when player can see) |
| Hand-on-prop IK | always on (cheap, breaks if disabled) |
| Additive lean | always on |
| Ragdoll | one-shot, never disabled |
### Pitfalls
- **LookAt rotates wildly / no clamp at all.** `use_angle_limitation` is false by default and `primary_limit_angle` defaults to 2π. Set both. ~80° (1.4 rad) for a head-only modifier; ~25° (0.4 rad) for a single spine bone.
- **IK pops on the first frame.** Solver hasn't been initialized. Call `ik.start()` after the AnimationTree's first tick, not in `_ready()` — the bone pose is identity until the tree runs.
- **Foot IK pushes the body up on stairs but the camera doesn't follow.** Camera is parented to the root, not the head. Either parent the camera to a chest bone via `BoneAttachment3D`, or accept that the camera doesn't bob with foot IK.
- **Additive layer doubles the clip.** The "additive" clip wasn't authored as a delta from the base pose. Re-author it as a delta.
- **Ragdoll just collapses, no spread.** Missing per-bone impulse + missing collision shapes on hands/feet PhysicalBone3D children. Auto-generate via Skeleton3D inspector → "Create Physical Skeleton" with the "Add collisions" option.
- **Modifiers run before the AnimationTree, not after.** Modifier order is set by tree-position in the scene. SkeletonModifier3D nodes must be CHILDREN of Skeleton3D and appear AFTER any nodes that drive the pose. AnimationTree itself doesn't sit under Skeleton3D — it sits at the character root and writes into Skeleton3D's pose every frame.
- **Look-at influence flickers at fade boundary.** Use `lerp(current_influence, target, 5 * delta)` instead of an instant assignment. Avoids the sub-pixel oscillation when the player walks the threshold.
### Performance budget
- Each `LookAtModifier3D`: ~5 µs/frame. Cheap; have many.
- Each `SkeletonIK3D` two-bone solve: ~30 µs/frame. ~30 active solves per frame on mid-range desktop is fine; budget more conservatively on Steam Deck.
- Ragdoll with full physical skeleton: ~200 µs/frame *while simulating*. Cap concurrent ragdolls at 5–8 before culling oldest.
## Anti-patterns
- Writing `skel.set_bone_pose_position(...)` in `_process`. Bypasses the AnimationTree, fights it next frame, results in jitter. Use modifiers instead; they run after the tree. (Edit-time STILL poses — a corpse, a statue — are the exception: no tree is running, so `ctx.bone_pose(skel, bone, {position/rotation/scale})` on animation-tier engines, or `set_bone_pose_*` in a one-off `summer_run_script`, is exactly right. See `character-animation-wiring`.)
- Putting IK targets in worldspace and forgetting they don't follow the character. Parent IK targets under the character root or bone — IK target is in the modifier's local space.
- Procedural look-at without a fade-out at distance. Distant NPCs all snap to player every frame, looks like a hivemind.
- Foot IK on flying / floating characters. Disable when `is_on_floor() == false`.
## Edge cases
- **Character is mounted (riding a horse).** Disable foot IK on the rider; ride animation already sells the contact. Enable upper-body additive sway driven by horse acceleration.
- **First-person hands.** Same modifier set, but only the arm chain is visible — disable head/spine modifiers, keep hand-on-prop IK.
- **Character is a quadruped.** All four legs need foot IK. The bone names are different (`FrontLeftLeg`, `BackRightLeg`, etc.); inspect first.
- **Network-synced ragdoll.** Don't simulate it on every client and don't sync bones with `MultiplayerSynchronizer`. In a Summer multiplayer game, send the few values that drive the ragdoll (the impulse, the moment it starts) as State or an Event and let each client simulate locally; see `multiplayer-state`.
## Fallback (no MCP)
Add modifiers in Summer Engine under Skeleton3D and set bone names from the
Inspector dropdown, which autocompletes from the rig. Hand-write the GDScript
driving them. Foot raycasts can be set up visually with a RayCast3D child.
## Handoff
- For the end-to-end rigged-character wiring these layers sit on top of (locomotion, method-track events, root motion), `character-animation-wiring`.
- For the AnimationTree these procedural layers compose with, `animation-tree`.
- For the source clips that procedural overlays modify, `generate-motion`.
- For face / lipsync (a different modifier family — BlendShapes, not bones), `facial-and-lipsync`.
- For NPC behavior triggering ragdoll on death, `design-npc`.
- For first-person aim sway / ADS, `fps-controller`.
## See also
- Upstream API reference matching the current Summer technical base:
`SkeletonModifier3D`, `LookAtModifier3D`, `SkeletonIK3D`,
`PhysicalBoneSimulator3D`.
<!-- shell-commands: generated from the operation catalog by npm run generate:registry; do not edit by hand -->
## From a shell
No Summer MCP tools in this session? Run each one as a command; when `summer --version` is missing or below 4.0, use `npx -y summer-engine@latest` in place of `summer`. Fields are flags in kebab-case (`--scene-path res://main.tscn`) or one `--input '<json>'`, and `summer ops describe <op>` prints the exact input. The `summer-cli` skill has the rest.
| In this skill | Command |
|---|---|
| `summer_add_node` | `summer engine add-node` |
| `summer_get_scene_tree` | `summer engine get-scene-tree` |
| `summer_get_script_errors` | `summer engine get-script-errors` |
| `summer_inspect_node` | `summer engine inspect-node` |
| `summer_inspect_resource` | `summer engine inspect-resource` |
| `summer_run_script` | `summer engine run-script` |
| `summer_save_scene` | `summer engine save-scene` |
| `summer_set_prop` | `summer engine set-prop` |
| `summer_set_resource_property` | `summer engine set-resource-property` |
<!-- /shell-commands -->