Skip to content

Commit be105f0

Browse files
committed
docs: L3.1 foot-flat correction design spec
1 parent 6576d90 commit be105f0

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# L3.1 — Foot-Flat (Plantigrade) Correction (Design)
2+
3+
**Date:** 2026-07-11
4+
**Status:** Approved (design)
5+
**Sub-project:** Layer 3, slice 1 of the 5-layer animation-naturalness program
6+
**Branch:** `feat/l3-post-ik` (stacked on `feat/l2-spline-interpolation`)
7+
8+
---
9+
10+
## 1. Context and motivation
11+
12+
Posecode builds a base pose from procedural FK (soon also mocap clips), then a stack of
13+
contact solvers in the viewer frame loop makes it touch the world
14+
(`packages/posecode-render/src/index.ts`, `frame()`): `depenetrate → applyGroundLock →
15+
applyPins → applyReaches → alignFloorPalms → floor clamp`.
16+
17+
Ground-lock (`groundlock.ts`) plants feet by resting the **lowest mesh point** on `y=0`.
18+
When knee/hip flex tilts the rigidly-attached foot toe-down (a squat, lunge, sit-to-stand,
19+
deadlift), the ball of the foot becomes the lowest point, so the figure **balances on its
20+
toes** — the reported squat-on-toes bug. Nothing keeps the sole plane parallel to the floor.
21+
22+
Real feet stay plantigrade (flat) while the shin travels over them; in mocap this is baked in.
23+
Posecode needs a procedural equivalent.
24+
25+
**Goal:** keep planted soles level with the floor regardless of shin angle, so grounded
26+
lower-body movements rest flat — without disturbing authored leg angles, tiptoe moves, or
27+
swing feet.
28+
29+
### Non-goals (deferred)
30+
31+
- Bar grip / two-point anchors / finger wrap — that is **L3.2**.
32+
- Look-at — folds into **L4**.
33+
- Full leg re-IK (ground-lock deliberately never CCD-solves legs; we keep that).
34+
35+
---
36+
37+
## 2. Approach
38+
39+
### 2.1 Mechanism — an ankle-orientation correction (analog of `alignFloorPalms`)
40+
41+
`contacts.ts` already has `alignFloorPalms`, which rotates a floor-contacting wrist so the
42+
palm normal points into the floor (`DOWN`). Foot-flat is the direct analog for feet: a new
43+
`levelPlantedFeet` that rotates each planted ankle so the **sole normal points world-down**,
44+
which lays the whole sole flat. It preserves the foot's yaw (toe direction) and the leg's
45+
authored hip/knee flex — it only removes the pitch/roll the leg chain induced in the foot.
46+
47+
The sole sits at ankle local `-Y` (see `addShoe` in `mannequin.ts`: shoe box at local
48+
`(0,-0.036,0.05)`), so the sole-down direction is the ankle's local `-Y`. Leveling aligns
49+
that local `-Y` to world `DOWN` with the minimal rotation (`setFromUnitVectors`), exactly as
50+
`alignFloorPalms` aligns the palm normal — the minimal rotation leaves yaw intact.
51+
52+
### 2.2 Planted-ness soft blend (the natural, better-rendering variant)
53+
54+
A hard snap would force-level a foot that is legitimately lifting (swing foot in a lunge,
55+
marching knee raise, the airborne leg of a kick). Instead the correction is **weighted by how
56+
planted the foot is**, mirroring the research's distance-based IK blending:
57+
58+
- Compute each foot's mesh-bottom height `y` (bbox min, as ground-lock does).
59+
- `weight = clamp01((PLANT_FADE - y) / PLANT_FADE)``1` when the sole is on the floor,
60+
fading to `0` as it rises past `PLANT_FADE` (a swing foot is left alone).
61+
- Apply the leveling rotation `slerp`ed by `weight`, so a lifting foot smoothly relaxes back
62+
to its authored orientation.
63+
64+
### 2.3 Tiptoe opt-out
65+
66+
Some moves are deliberately on the toes: relevé, calf-raise, demi-plié, plantarflex dance
67+
phases. Foot-flat must not flatten those. Rule: **skip leveling when the ankle carries a
68+
meaningful authored plantarflex angle.** Plantarflexion rotates the ankle about local X in the
69+
toe-down direction; at frame time we read the ankle bone's local Euler X and, if it exceeds
70+
`PLANTARFLEX_SKIP` (toe-down beyond a small threshold), leave the foot as authored. So
71+
`ankles: plantarflex 30` opts out naturally, a squat that never plantarflexes gets leveled —
72+
**no new DSL keyword, no library rewrite required.**
73+
74+
### 2.4 Frame-loop placement
75+
76+
`levelPlantedFeet` runs **after** `applyGroundLock`/`applyPins`/`applyReaches` (so the foot is
77+
in its final planted spot and the legs hold their solved pose) and **before** the final
78+
vertical floor clamp (so the now-level sole is what gets rested on `y=0`). It sits next to the
79+
existing `alignFloorPalms` call in `frame()`, and is also invoked once in `load()` so the
80+
initial captured pose is already flat.
81+
82+
---
83+
84+
## 3. Components and boundaries
85+
86+
### 3.1 `packages/posecode-render/src/contacts.ts` (modified)
87+
88+
New exported function, no new file (it is the same concern as `alignFloorPalms`, ~40 lines):
89+
90+
```ts
91+
export function levelPlantedFeet(
92+
m: Mannequin,
93+
activeGroundLock: readonly string[],
94+
): void
95+
```
96+
97+
- **Purpose:** for each ground-locked foot, rotate the ankle so the sole is horizontal,
98+
weighted by planted-ness, skipped on authored plantarflex.
99+
- **Depends on:** `three`, `Mannequin`. Reuses module constants.
100+
- **Constants (named, exported for tests):** `PLANT_FADE = 0.06` (m), `PLANTARFLEX_SKIP`
101+
(radians, ~`15°`), sole-normal local axis `(0,-1,0)`.
102+
103+
### 3.2 `packages/posecode-render/src/index.ts` (modified)
104+
105+
- Import `levelPlantedFeet`; call it in `frame()` after `alignFloorPalms(...)` and before the
106+
final bbox floor clamp, passing `info.groundLock`.
107+
- Call it once in `load()` after `groundFigureOf(mannequin)` so the captured base is flat.
108+
109+
### 3.3 Editor discoverability (DSL side, no new syntax)
110+
111+
- `packages/posecode-language/src/vocab.ts`: extend the `ground-lock` `KEYWORD_DOCS` entry to
112+
note that planted feet auto-level to the floor unless the ankle is plantarflexed (so the
113+
behavior is discoverable on hover/completion).
114+
115+
### 3.4 Documents
116+
117+
- Fix `spec/examples/squat.posecode`: the authored `ankles: plantarflex 50` forces tiptoe and
118+
is biomechanically wrong for a squat (the shin dorsiflexes over a flat foot). Remove it /
119+
set to a small dorsiflexion so foot-flat lands the sole. Keep it as the demo.
120+
- Verify relevé / calf-raise still tiptoe (their authored plantarflex opts out).
121+
122+
---
123+
124+
## 4. Data flow
125+
126+
```
127+
frame():
128+
base pose (squad FK) → depenetrate → applyGroundLock (plant feet)
129+
→ applyPins → applyReaches → alignFloorPalms
130+
→ levelPlantedFeet(m, info.groundLock) ← NEW: level each planted sole (weighted, opt-out)
131+
→ floor clamp (rest the flat sole on y=0)
132+
```
133+
134+
## 5. Error handling
135+
136+
- Missing ankle bone / no ground-locked feet → no-op.
137+
- Degenerate rotation (sole already vertical, cross ~0) → `setFromUnitVectors` handles
138+
antiparallel; guard NaN and fall back to identity (no correction) as `alignFloorPalms` does.
139+
- Swing foot (weight ~0) → correction ~identity, foot keeps authored orientation.
140+
141+
## 6. Testing (TDD)
142+
143+
Write first, watch fail, implement:
144+
145+
1. **Levels a tilted planted foot:** author a squat-like pose (knee/hip flex, foot tilted);
146+
after `levelPlantedFeet`, the sole normal is within tolerance of world-up (`0,1,0`).
147+
2. **Plantarflex opt-out:** a foot with authored `ankles: plantarflex 30` is left unchanged.
148+
3. **Swing foot unaffected:** a foot lifted above `PLANT_FADE` keeps its authored orientation.
149+
4. **Squat rests flat end-to-end:** load the squat IR, sample the descend keyframe, run the
150+
frame solve; the foot mesh bbox min.y ≈ 0 and the sole is level (not ball-only contact).
151+
5. **Relevé still on toes:** the relevé example keeps a plantarflexed, non-level foot.
152+
6. **Existing suites stay green** (render, eval invariants, parser, language).
153+
154+
Manual: browser-verify squat rests flat, relevé stays on toes, no console errors.
155+
156+
## 7. Risks
157+
158+
- **Ankle over-rotation past ROM:** leveling could push the ankle beyond healthy ROM on an
159+
extreme knee bend. Mitigation: clamp the corrected ankle Euler to the ankle ROM
160+
(`eulerRomFor("ankle_*")`) after leveling, widened to admit the authored angle (same pattern
161+
as reach-IK's `jointLimitsFor`).
162+
- **Interaction with `alignFloorPalms` ordering:** feet and palms are independent bones; no
163+
conflict. Both run before the clamp.
164+
- **Plantarflex threshold tuning:** `PLANTARFLEX_SKIP` chosen so relevé/calf-raise opt out but
165+
a near-zero incidental ankle angle in a squat still levels; verified against the library.
166+
167+
## 8. Definition of done
168+
169+
- `levelPlantedFeet` implemented + wired into `frame()` and `load()`.
170+
- Tests 1–5 pass; all existing suites green; typecheck clean.
171+
- Squat demo rests flat; relevé/calf-raise still tiptoe; browser-verified, no console errors.

0 commit comments

Comments
 (0)