Skip to content

Commit ce3f90e

Browse files
committed
docs: L4 secondary-motion design spec (L4.1 relaxed hands)
1 parent ffec912 commit ce3f90e

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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

Comments
 (0)