|
| 1 | +# L4 — Additive Secondary Motion (Design) |
| 2 | + |
| 3 | +**Date:** 2026-07-11 |
| 4 | +**Status:** Approved (design) |
| 5 | +**Sub-project:** Layer 4 of the animation-naturalness program |
| 6 | +**Branch:** `feat/l3-post-ik` (isolated worktree `/Users/aaaa/Developer/posecode-l3`) |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1. Context and motivation |
| 11 | + |
| 12 | +L2 (spline flow) and L3 (foot-flat, bar grip) fixed the base pose and its contacts. What still |
| 13 | +reads as "not alive" is the absence of **secondary motion** — the reactive/idle detail real |
| 14 | +bodies always have. The most glaring, universal instance: the procedural hands are **flat open |
| 15 | +palms** everywhere except when gripping a bar. A relaxed human hand always carries a slight |
| 16 | +finger curl. This is the biggest cheap win and it's the "hands acting weird" the user flagged. |
| 17 | + |
| 18 | +L4 is a family of additive layers applied on top of the base pose: |
| 19 | + |
| 20 | +- **L4.1 — Relaxed resting hand pose** (this slice): a natural finger curl on any hand that |
| 21 | + isn't gripping and whose fingers aren't explicitly authored. |
| 22 | +- **L4.2 — Locomotion arm swing** (later): arms counter-swing to leg motion during travel. |
| 23 | +- **L4.3 — Follow-through & weight shift** (later): spine lag / overshoot-settle, idle weight |
| 24 | + shift, plus head look-at (folded in from L3). |
| 25 | + |
| 26 | +Slices are independent and shippable; build L4.1 first. |
| 27 | + |
| 28 | +### Non-goals for L4.1 |
| 29 | + |
| 30 | +- Arm swing, spine follow-through, weight shift, look-at (L4.2 / L4.3). |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## 2. Approach — L4.1 relaxed hand pose |
| 35 | + |
| 36 | +A new `relaxHands(m, gripSides, authoredFingers)` in `contacts.ts` applies a gentle rest curl to |
| 37 | +finger bones, so idle hands read as relaxed rather than splayed flat. |
| 38 | + |
| 39 | +Rules (so it never fights intent): |
| 40 | +- **Skip gripping hands** — those are wrapped by `wrapGrip` (a full grip curl). |
| 41 | +- **Skip authored fingers** — a move that explicitly poses fingers (make-a-fist, finger-spell, |
| 42 | + hand-wave) is respected; `relaxHands` only touches finger bones NOT in the timeline's |
| 43 | + `bonesUsed` (i.e., left at rest). |
| 44 | +- For each remaining hand, curl the four fingers to `REST_CURL` (~18°) at the knuckle and give |
| 45 | + the thumb a light inward rest, turning the flat palm into a natural relaxed hand. |
| 46 | + |
| 47 | +Applied each frame after `wrapGrip` (grip wins) and once on `load()`. Because it only writes |
| 48 | +finger-bone local rotations that nothing else drives, it can't disturb the solved body pose or |
| 49 | +contacts (same safety property as the breathing mesh layer). |
| 50 | + |
| 51 | +### Wiring |
| 52 | + |
| 53 | +- `index.ts frame()`: after `wrapGrip` (inside `applyGrips`) has run, call `relaxHands`, passing |
| 54 | + the grip sides for this phase and the authored finger set (`timeline.bonesUsed ∩ fingers`). |
| 55 | +- `index.ts load()`: call once after the base solve so the initial frame shows relaxed hands. |
| 56 | +- The authored finger set is derived once per load from `timeline.bonesUsed`. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 3. Components and boundaries |
| 61 | + |
| 62 | +- **`packages/posecode-render/src/contacts.ts`:** new `relaxHands(m, gripSides, authoredFingers)` |
| 63 | + + `REST_CURL` constant. Reuses the `FINGERS` list already there for `wrapGrip`. |
| 64 | +- **`packages/posecode-render/src/index.ts`:** compute `authoredFingers` at load; call |
| 65 | + `relaxHands` in `frame()` and `load()`; derive `gripSides` from `info.grips`. |
| 66 | +- No parser/DSL change (this is automatic aliveness, not an authored feature). No editor change. |
| 67 | +- **Tests:** `relaxHands` curls a rest hand's fingers; leaves a gripping side to `wrapGrip`; |
| 68 | + never overrides an authored finger; existing suites stay green. |
| 69 | + |
| 70 | +### Data flow |
| 71 | + |
| 72 | +``` |
| 73 | +frame(): base pose → contacts → applyGrips (wrapGrip on gripping hands) |
| 74 | + → relaxHands(m, gripSides, authoredFingers) ← NEW: rest curl on idle, un-authored hands |
| 75 | + → floor clamp |
| 76 | +``` |
| 77 | + |
| 78 | +## 4. Error handling |
| 79 | + |
| 80 | +- Missing finger bones → no-op per bone. |
| 81 | +- A hand both gripping and (somehow) authored → grip/authored win; `relaxHands` skips it. |
| 82 | +- `REST_CURL` is small and within finger ROM (no clamp needed; fingers are cosmetic 1-DOF). |
| 83 | + |
| 84 | +## 5. Testing (TDD) |
| 85 | + |
| 86 | +1. `relaxHands` curls `index_left` etc. from flat toward a rest curl for a non-gripping hand. |
| 87 | +2. A gripping side (passed in `gripSides`) is left untouched by `relaxHands` (wrapGrip owns it). |
| 88 | +3. An authored finger (in `authoredFingers`) is not overridden. |
| 89 | +4. Existing render/eval/parser/language suites stay green. |
| 90 | + |
| 91 | +Manual: browser-verify a plain move (e.g. biceps curl / squat) shows relaxed hands, not flat |
| 92 | +splayed palms; a gripping move still shows the full bar wrap; make-a-fist still makes a fist. |
| 93 | + |
| 94 | +## 6. Risks |
| 95 | + |
| 96 | +- **Double-curl with grip:** avoided by skipping grip sides. |
| 97 | +- **Overriding expressive hands:** avoided by skipping authored fingers. |
| 98 | +- **Reset each frame:** `relaxHands` sets absolute finger rotations, so it must run every frame |
| 99 | + after sampling (sampling leaves un-authored fingers at identity); idempotent. |
| 100 | + |
| 101 | +## 7. Definition of done |
| 102 | + |
| 103 | +- `relaxHands` implemented + wired; tests 1-4 pass; suites green; typecheck clean. |
| 104 | +- Browser: idle hands relaxed, grips still wrap, authored hands respected. |
0 commit comments