init: animation pipeline hub — Mac↔PC bridge for converting and implementing animations
Skills (.claude/skills/): animation-creation, iclone-video-mocap, pose-estimation (MediaPipe models via LFS), retarget-animations (deprecated). Tools: cc/mixamo/kevin/mocap retargeters + composite baker. Plans: animation-gen-pipeline + skills-adoption. exchange/: incoming-fbx, converted-glb, reference-video (LFS for fbx/glb/mp4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# blender-mcp recipes
|
||||
|
||||
Practical `bpy` snippets the agent pastes through blender-mcp's `execute-code` tool to
|
||||
author animations on the shared Quaternius skeleton and export them as game-ready GLB
|
||||
packs. These mirror the proven conventions in `tools/mixamo_retarget.py` — keep them in
|
||||
lockstep.
|
||||
|
||||
## Setup assumptions
|
||||
|
||||
- Blender **5.1.2** at `/Applications/Blender.app` (call headless as
|
||||
`/Applications/Blender.app/Contents/MacOS/Blender`).
|
||||
- The BlenderMCP addon is installed and **connected** (3D View → sidebar → BlenderMCP →
|
||||
Connect). If the `blender` MCP tool isn't available, the addon isn't connected.
|
||||
- **One game GLB imported** so the scene holds the real Quaternius armature. The game
|
||||
ships these as animations-only GLBs; import one to reuse its exact rig:
|
||||
|
||||
```python
|
||||
bpy.ops.import_scene.gltf(filepath="/Users/jeremykashkett/Tinqs/local.repo/ariki-game/assets/quaternius/kevin/kevin_male_social.glb")
|
||||
arm = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||||
arm.name = "Armature" # match the convention mixamo_retarget.py enforces
|
||||
bpy.context.scene.render.fps = 30
|
||||
```
|
||||
|
||||
## Skeleton orientation
|
||||
|
||||
65 bones, UE-style names: `pelvis`, `spine_01`, `spine_02`, `spine_03`, `neck_01`,
|
||||
`Head` (**capital H**), `clavicle_l`, `upperarm_l`, `lowerarm_l`, `hand_l`, `thigh_l`,
|
||||
`calf_l`, `foot_l`, `ball_l`, and the `_r` twins, plus 3-segment fingers
|
||||
(`index_0{l,r}` … `pinky_03`, `thumb_0{l,r}` … `thumb_03`). The armature object is named
|
||||
`Armature`; work in pose mode via `bpy.context.object.pose.bones["<name>"]`.
|
||||
|
||||
## Keyframing recipe
|
||||
|
||||
Build an action from an explicit list of poses (name, frame, per-bone quaternions).
|
||||
Quaternius bones are authored in **quaternion** rotation mode, so set every pose bone's
|
||||
`rotation_mode = "QUATERNION"` first (this is what `mixamo_retarget.py` does):
|
||||
|
||||
```python
|
||||
import bpy
|
||||
from mathutils import Quaternion
|
||||
|
||||
arm = bpy.data.objects["Armature"]
|
||||
for pb in arm.pose.bones:
|
||||
pb.rotation_mode = "QUATERNION"
|
||||
|
||||
act = bpy.data.actions.new("Wave01")
|
||||
arm.animation_data_create()
|
||||
arm.animation_data.action = act
|
||||
bpy.context.scene.frame_start, bpy.context.scene.frame_end = 1, 30
|
||||
|
||||
# poses: (frame, {bone: (w,x,y,z)})
|
||||
poses = [
|
||||
(1, {"hand_l": (1,0,0,0), "lowerarm_l": (1,0,0,0)}),
|
||||
(15, {"hand_l": (0.7,0,0,0.7), "lowerarm_l": (0.9,0.3,0,0)}), # raise hand
|
||||
(30, {"hand_l": (1,0,0,0), "lowerarm_l": (1,0,0,0)}),
|
||||
]
|
||||
for frame, bones in poses:
|
||||
bpy.context.scene.frame_set(frame)
|
||||
for name, q in bones.items():
|
||||
pb = arm.pose.bones[name]
|
||||
pb.rotation_quaternion = Quaternion(q)
|
||||
pb.keyframe_insert(data_path="rotation_quaternion", frame=frame)
|
||||
```
|
||||
|
||||
## IK setup recipe
|
||||
|
||||
Drive `calf_l` (or `lowerarm_l`) toward a target empty with a 2-bone chain, then bake
|
||||
the solved motion to real keyframes and drop the constraint — the game never needs IK:
|
||||
|
||||
```python
|
||||
import bpy
|
||||
|
||||
arm = bpy.data.objects["Armature"]
|
||||
target = bpy.data.objects.new("IK_target_l", None) # empty
|
||||
bpy.context.collection.objects.link(target)
|
||||
target.location = arm.matrix_world @ arm.pose.bones["foot_l"].head + (0.0, 0.0, 0.2)
|
||||
|
||||
pb = arm.pose.bones["calf_l"]
|
||||
ik = pb.constraints.new("IK")
|
||||
ik.target = target
|
||||
ik.chain_count = 2 # thigh_l + calf_l
|
||||
ik.use_tail = True
|
||||
|
||||
# Bake the visual (constraint-solved) result to plain keyframes, then remove the constraint.
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
arm.select_set(True)
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.nla.bake(frame_start=1, frame_end=30, visual_keying=True,
|
||||
bake_types={"POSE"}, clean_curves=True)
|
||||
for c in list(pb.constraints):
|
||||
pb.constraints.remove(c)
|
||||
```
|
||||
|
||||
## NLA layering recipe
|
||||
|
||||
The glTF exporter emits each **NLA track** as a separate animation clip. To ship multiple
|
||||
clips in one GLB (or layer retargeted clips), push each action onto its own NLA track —
|
||||
this is the exact pattern `tools/mixamo_retarget.py` uses to export a pack:
|
||||
|
||||
```python
|
||||
import bpy
|
||||
arm = bpy.data.objects["Armature"]
|
||||
if arm.animation_data is None:
|
||||
arm.animation_data_create()
|
||||
for act in bpy.data.actions: # every action you want to ship
|
||||
tr = arm.animation_data.nla_tracks.new()
|
||||
tr.name = act.name # clip name = track name
|
||||
tr.strips.new(act.name, 1, act)
|
||||
arm.animation_data.action = None # avoid a stray duplicate clip
|
||||
```
|
||||
|
||||
To **blend** two clips on the stack (e.g. a base loop under a gesture), set the upper
|
||||
strip's `blend_type` (`"ADD"`, `"REPLACE"`) and `blend_in`/`blend_out` frames, then bake
|
||||
the combined result down to a single action (visual keying) and re-export that.
|
||||
|
||||
## Export recipe
|
||||
|
||||
The canonical export block lives in `tools/mixamo_retarget.py` and **must be mirrored
|
||||
exactly** — the game relies on these flags (`NLA_TRACKS` mode = one clip per track;
|
||||
`export_force_sampling` for clean per-bone channels; size optimization off to preserve
|
||||
retargeted curves). Select only the armature and drop meshes first so the GLB is
|
||||
animations-only (the game binds clips to its own body):
|
||||
|
||||
```python
|
||||
import bpy
|
||||
out = "/Users/jeremykashkett/Tinqs/local.repo/ariki-game/assets/quaternius/mixamo/wave_pack.glb"
|
||||
|
||||
# Drop meshes — animations-only GLB (mirrors mixamo_retarget.py).
|
||||
for o in [o for o in bpy.data.objects if o.type == "MESH"]:
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
arm = bpy.data.objects["Armature"]
|
||||
arm.select_set(True)
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=out, use_selection=True,
|
||||
export_animations=True, export_animation_mode="NLA_TRACKS",
|
||||
export_force_sampling=True, export_optimize_animation_size=False)
|
||||
print("[blender-mcp] EXPORTED →", out)
|
||||
```
|
||||
|
||||
Output GLBs in `assets/quaternius/mixamo/` are auto-discovered by ClipCatalog /
|
||||
AnimationShowcase — referenceable immediately as `<pack_name>/<ClipName>` where
|
||||
`<ClipName>` is the NLA track (Blender action) name.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Never export `*RM` root-motion variants** for dances/composites — they translate the
|
||||
rig and break compositing. Author clips in place.
|
||||
- **Skip the `RESET`/rest action** — don't push it to an NLA track or it ships as a clip.
|
||||
- **Clip name = Blender action/NLA-track name.** The game refers to clips as
|
||||
`<pack>/<ClipName>` (e.g. `wave_pack/Wave01`).
|
||||
- **One armature per exported GLB.** If you imported a GLB that brought its own body
|
||||
meshes, delete them before export (see the export block).
|
||||
- **Match `fps=30`** so frame counts line up with shipped packs and the dance beat clock.
|
||||
- **Quaternion mode** for all pose bones before keyframing (matches the retargeter).
|
||||
- `.uid` files auto-generate in Godot — never hand-author them.
|
||||
@@ -0,0 +1,101 @@
|
||||
# gltf-transform recipes
|
||||
|
||||
Headless GLB surgery with gltf-transform (donmccurdy, gltf-transform.dev) — inspect,
|
||||
merge, transplant, and shrink animation packs with no Blender required. Same Quaternius
|
||||
skeleton across all game GLBs, so node names line up 1:1 and clip moves are mechanical.
|
||||
|
||||
## Install / run
|
||||
|
||||
CLI (no global install — `npx` pulls it on demand):
|
||||
|
||||
```bash
|
||||
npx @gltf-transform/cli --help
|
||||
npx @gltf-transform/cli inspect assets/quaternius/kevin/kevin_male_social.glb
|
||||
```
|
||||
|
||||
Or as a JS SDK from a node script (mirror the layout of the existing
|
||||
`tools/bake_run_punch.mjs`):
|
||||
|
||||
```js
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { prune, dedup, resample } from "@gltf-transform/functions";
|
||||
const io = new NodeIO();
|
||||
const doc = io.read("path/to/file.glb");
|
||||
```
|
||||
|
||||
Install once into the repo's node tooling: `npm i -D @gltf-transform/core @gltf-transform/functions`
|
||||
(the orchestrator installs — you author the script only).
|
||||
|
||||
## Inspect clips
|
||||
|
||||
```bash
|
||||
# Full report: meshes, animations, channels, duration.
|
||||
npx @gltf-transform/cli inspect assets/quaternius/kevin/kevin_male_social.glb
|
||||
```
|
||||
|
||||
For a quick clip-name list without the SDK, the pure-node JSON-chunk one-liner already in
|
||||
`SKILL.md` (Path 1) reads the GLB's JSON chunk and prints `animations[].name` — use it to
|
||||
confirm a clip exists before importing/transplanting.
|
||||
|
||||
## Clip transplant between same-skeleton GLBs
|
||||
|
||||
Both files share the Quaternius skeleton, so every channel's target node matches by name.
|
||||
Sketch: read source + target, copy the source `Animation` into the target document, then
|
||||
re-point each channel's `targetNode` to the target-doc node with the same name.
|
||||
|
||||
```js
|
||||
// verify against gltf-transform v4 API before first use
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { copyToDocument } from "@gltf-transform/functions";
|
||||
|
||||
const io = new NodeIO();
|
||||
const target = io.read("assets/quaternius/mixamo/northern_soul.glb"); // pack to extend
|
||||
const source = io.read("downloads/clip_to_add.glb"); // same skeleton
|
||||
|
||||
const srcAnim = source.getRoot().listAnimations()
|
||||
.find(a => a.getName() === "NewClip");
|
||||
const targetNodes = new Map(target.getRoot().listNodes().map(n => [n.getName(), n]));
|
||||
|
||||
copyToDocument(target, source, [srcAnim]); // copy the Animation property
|
||||
const dstAnim = target.getRoot().listAnimations().at(-1);
|
||||
for (const ch of dstAnim.listChannels()) {
|
||||
const srcNode = ch.getTargetNode();
|
||||
if (srcNode) ch.setTargetNode(targetNodes.get(srcNode.getName())); // re-point by name
|
||||
}
|
||||
io.write("assets/quaternius/mixamo/northern_soul.glb", target); // overwrites — back up first
|
||||
```
|
||||
|
||||
Common variations: copy **all** source animations (iterate `listAnimations()`), rename on
|
||||
the way in (`dstAnim.setName("Wave01")`), or strip a clip (remove its channels then
|
||||
`prune`). If `copyToDocument` isn't the right entry point in your installed version, fall
|
||||
back to walking `srcAnim.listChannels()` and rebuilding each (sampler interpolation +
|
||||
input/output accessors + targetNode/path) on a fresh `target.createAnimation()`.
|
||||
|
||||
## Shrink / clean
|
||||
|
||||
Standard transforms, applied in this order for pack size:
|
||||
|
||||
```bash
|
||||
npx @gltf-transform/cli optimize <in.glb> <out.glb> # resample + prune + dedup combined
|
||||
# or individually:
|
||||
npx @gltf-transform/cli <in.glb> <out.glb> resample # drop redundant keyframes
|
||||
npx @gltf-transform/cli <in.glb> <out.glb> prune # remove unreferenced data
|
||||
npx @gltf-transform/cli <in.glb> <out.glb> dedup # merge identical accessors/meshes
|
||||
```
|
||||
|
||||
In the SDK: `await doc.transform(prune(), dedup(), resample())`. `resample` takes an
|
||||
optional `tolerance`/`ratio` to trade fidelity for size — retargeted clips have clean
|
||||
curves and tolerate aggressive resampling; mocap-sourced clips do not.
|
||||
|
||||
## When to prefer this over Blender
|
||||
|
||||
Prefer gltf-transform when there's **no new motion** to author:
|
||||
|
||||
- Same-skeleton clip moves (one clip from a downloaded GLB into a shipped pack).
|
||||
- Batch renames (fix a pack of `*_Loop` suffixes, normalize clip names).
|
||||
- Pack merges / splits (combine two `assets/quaternius/mixamo/` packs).
|
||||
- Headless size passes before shipping (resample/prune/dedup).
|
||||
|
||||
Reach for **Blender (Path 2 retarget / Path 5 authoring)** when the source uses a
|
||||
*different* skeleton (needs the world-rotation-delta retarget), or when you must author,
|
||||
IK-solve, or layer new motion that exists nowhere.
|
||||
Reference in New Issue
Block a user