@rig-c/pose 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/cli.ts +16 -1
- package/package.json +1 -1
- package/src/legs.ts +104 -0
- package/src/retarget.ts +34 -3
- package/src/run.ts +19 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Part of the rig-c toolchain: [core](https://github.com/rig-c/core) compiles and
|
|
|
10
10
|
|
|
11
11
|
## Try it
|
|
12
12
|
|
|
13
|
-
[`examples/wave`](examples/wave) runs with nothing else: a bones-only stick rig (no art), a 3-second wave made from joint angles (`make-pose.ts` writes it), and the map that joins them.
|
|
13
|
+
[`examples/wave`](https://github.com/rig-c/pose/tree/main/examples/wave) is in the repository, not the npm package: clone the repository, or copy its four files. It runs with nothing else: a bones-only stick rig (no art), a 3-second wave made from joint angles (`make-pose.ts` writes it), and the map that joins them.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
rig-pose retarget --rig examples/wave/rig --map examples/wave/map.json --pose examples/wave/pose.json --out wave.motion.json --build wave-build
|
|
@@ -18,6 +18,10 @@ rig-pose retarget --rig examples/wave/rig --map examples/wave/map.json --pose ex
|
|
|
18
18
|
|
|
19
19
|
It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json` and the gated build. `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
|
|
20
20
|
|
|
21
|
+
## What the rig needs
|
|
22
|
+
|
|
23
|
+
The map names **chains**: each limb as bones that descend from one another, parent to child. That means a thigh with its shin below it, and an upper arm with its forearm below it, and likewise for the trunk and the head. A rig whose arm is one bone, or that has no leg bones, cannot take a pose: the map is refused by name (`MAP_*`). [@rig-c/parts](https://github.com/rig-c/parts) builds such a rig from a painting's keypoints with `propose --keypoints`. A rig drawn by hand needs the same chains.
|
|
24
|
+
|
|
21
25
|
## What it does
|
|
22
26
|
|
|
23
27
|
```sh
|
|
@@ -53,12 +57,23 @@ and what the run cannot decide for you it lists:
|
|
|
53
57
|
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, and the frames clamped at each limit.
|
|
54
58
|
- `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
|
|
55
59
|
- `FEET` — when the map has both legs: the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
|
|
60
|
+
- `STANCE` — with `--stance` ([below](#the-legs-foreshortening-and-planted-feet)): one line per run of frames planted both (with the stretch keyed, its largest k) or lifted (the pose's height kept), with the free side, the frames and the tolerance; and one per run whose stretch fell past its bounds (`past the bounds`), with the k the floor needed and how far the foot was left from it.
|
|
56
61
|
- `SHEAR` — with `--leg-scale`: every scaled bone and every bone under one, how far its art is sheared and how long its world axes are against setup. A reading.
|
|
57
62
|
- `IDLE`, `IDLE-DROPPED`, `IDLE-KEPT-OUT`, `MERGE` — the rig's idle against the new animation, and what `--carry-idle` and `--merge` put back ([below](#the-idle-what-the-retarget-drops)).
|
|
58
63
|
- `REDUCE` — with `--reduce` ([below](#few-keys---reduce)): per bone, its keys before and after and its largest error, with the frame; then the total.
|
|
59
64
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
60
65
|
|
|
61
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale` and `
|
|
66
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet` and `stance` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, `legScale`, `pivot`, `feet` (`planted`, `legLength`, `floor`, `supports`, `jumps`, `changes`, and the posed `reading`), `stance` (`tolerance`, `bounds`, `runs`: `{ state, side, frames, maxHeight, k }`, `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
67
|
+
|
|
68
|
+
| field | each entry |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `holds` | `{ point, from, to, source, why }`: a joint held over frames `from`–`to` at frame `source`'s value |
|
|
71
|
+
| `bends` | `{ chain, joint, frame, t, angle, side, rest }`: a hinge found bent the wrong way (`angle` in degrees, Spine's sense; `side` ±1, the side it may bend) |
|
|
72
|
+
| `endOn` | `{ chain, bone, joints, frames, seconds, minFraction }`: a limb pointing at the camera for ≥ 0.15 s |
|
|
73
|
+
| `landings` | `{ bone, worst, frame, kind, tolerance }`: where rig-c's poser put each bone, against the target. `kind` is `angle` (degrees), `origin` or `foot` (world units), or `scale` (a fraction of the setup length). A run writes only when every `worst` ≤ `tolerance` |
|
|
74
|
+
| `folds` | `{ slot, attachment, times }`: every mesh `rigc render --folds` saw fold, at those seconds; `null` when it did not read |
|
|
75
|
+
|
|
76
|
+
The demand lists:
|
|
62
77
|
|
|
63
78
|
- `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } }, moved: { on, off } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for. `moved.on` is the boundary the span starts on (null from the first frame), `moved.off` the one the base art or the next set comes back on (null to the end), each `{ frame: [selected, keyed], frames, seconds, window: [first, last], speed: { old, new } }`: signed frames and seconds, the frames it could move to, and the speed in degrees per frame on the old and the new frame. A boundary between two sets is the same entry in both.
|
|
64
79
|
- `uncovered: [{ slot, attachment, chains, times }]` — every entry of `folds` on the times its slot still shows that mesh, with the map's chains that move it (the deepest: the arm, not the trunk it hangs from). `rigc render --folds` reads only what the pose shows, so a mesh swapped out or hidden by a switch is not read on those frames at all; `uncovered` is the fold times less the switched ones.
|
|
@@ -107,6 +122,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
107
122
|
- A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames** — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence HQ's dancer standing still (frames 0–29 and 277–312, mid_hip moving 10 and 14 px) read as jumping, 152–155 of 313 frames in all.
|
|
108
123
|
- A **step** releases the plant too: while the supporting ankle moves more than **0.03** leg lengths a frame, the other foot takes support if it is planted (not itself released or moving), and otherwise the hip follows the pose's travel. The released ankle may support again once it has moved less than **0.01** leg lengths a frame for **2 frames**. On the dancer the plant held the supporting ankle through a real step (frames 176–181, 579 → 718 px) and threw the hip sideways.
|
|
109
124
|
- `FEET` lists the supports, the jumps and the steps (each with the side, its frames, who took support and where it re-planted); the report carries them under `feet` (`supports`, `jumps`, `steps`, `changes`).
|
|
125
|
+
- **`--stance <leg lengths>`** (only with `--plant-feet`, which it is refused without; off by default) plants **both** feet when the free one is near the floor ([#60](https://github.com/rig-c/pose/issues/60)). On a frame with a supporting side (so outside every jump), the free ankle's height above the supporting one is read in the picture, in the free side's leg length — the leg length `FEET` uses. Within the tolerance the frame is **planted both**: every bone of the free leg's chain has its `scalex` key multiplied by k = (root y − floor y) / (root y − ankle y) on the posed rig, the root being the chain's first bone's origin and the floor the supporting ankle, so the foot reaches the floor. A free ankle below the supporting one is planted too: its k is under 1, since a foot under the floor is the same fault as one floating above it. k is held within **[0.92, 1.08]**, and a frame past those bounds is listed `past the bounds`, held at the bound and not forced. Above the tolerance the foot is **lifted** and keeps the pose's height. The stretched scales are the scale targets, so the landing check holds them. Measured with `--stance 0.07` and HQ's flags (`--smooth 7 --flip 90 --short 0.35 --bends clamp --leg-scale 0.8,1.05 --plant-feet --carry-idle`): on HQ's dancer, frames 94–175 outside the jumps, the free ankle's height above the supporting one on the posed build went from median 18.3 and p90 25.4 units to 0.0 and 0.0. On a second sequence (Toca, 492 frames at 30 fps) the 29 frames labelled by eye as both feet flat, outside the jumps, read 0.0 and 0.0. Of the 6 labelled one foot lifted, 5 stay above the tolerance; the sixth (frame 360) was 0.042 leg lengths up in the picture, within the tolerance, and so planted; without `--stance` it stood 6.5 units up, also under the tolerance (22.6 units there). That is 83 %, under the 90 % the card asked.
|
|
110
126
|
- The map needs one chain ending at each ankle whose last bone is aimed `"x"` (the ankle is that bone's tip), and a `translate` the legs hang from (`LEGS_MAP`). A `range`, or a set's `turn` or `offsets`, on a bone between the hip and an ankle is refused (`LEGS_RANGE`): the plant is derived before the range clamp, and a clamp after it would move the planted foot.
|
|
111
127
|
|
|
112
128
|
Both are checked on the gated build: every scale lands, each ankle lands where the keys put it, and the hip's origin where the plant put it. The figures were fixed in #47 before the dancer was measured; the defaults stay off until they pass.
|
|
@@ -194,7 +210,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
194
210
|
- A **point** of the rig is a bone's origin (`name`), the end of its x axis (`name:tip`), or the midpoint of two of those (`"a+b"`: `"sleeve_r0+sleeve_l0"` is the shoulders' midpoint). An aim's points must be the bone itself or descend from it.
|
|
195
211
|
- `reach: true`, on a one-bone chain from the translated joint (a trunk from `mid_hip`): the bone is aimed from where its origin is posed — the hip moved and turned — at where the pose puts the segment's end relative to mid_hip, scaled. Without it, a tilted pelvis carries the chest's origin sideways and the trunk, parallel to the pose's, misses the shoulders. Its aim is one point, from the bone's own origin: `{ "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "mid_shoulder"], "aim": ["sleeve_r0+sleeve_l0"], "reach": true }`. The chain must descend from the translated bone.
|
|
196
212
|
- `bend`: every joint inside a chain is a hinge. `"rest"` (default) takes the side it may bend from the rig's rest pose and refuses a rest that is straight (under 0.5°); `"ccw"`/`"cw"` state it; `"free"` skips the check.
|
|
197
|
-
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material
|
|
213
|
+
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On a second dance with knee bends (Toca Toca, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
|
|
198
214
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
199
215
|
- `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
|
|
200
216
|
|
|
@@ -329,7 +345,7 @@ Exit codes: `0` compared, whether it found anything or not, since a finding is n
|
|
|
329
345
|
|
|
330
346
|
[Bun](https://bun.sh) ≥ 1.2 on `PATH` — rig-pose, like rig-c, runs its TypeScript sources directly. `rig-c` comes as a dependency. With `@esotericsoftware/spine-core` installed beside it, rig-c's gate runs the spine-core round trip; without it, rig-c's own gate, and the gate's report says which.
|
|
331
347
|
|
|
332
|
-
##
|
|
348
|
+
## Why alternate drawings (0.2)
|
|
333
349
|
|
|
334
350
|
A flat painting turned by its bones matches each limb's direction, not its depth. On a real 13 s front-facing dance, retargeted onto a @rig-c/parts rig, five moments were found where it cannot follow. They were found by measuring the pose against the rig and checked against the video:
|
|
335
351
|
|
|
@@ -341,13 +357,15 @@ A flat painting turned by its bones matches each limb's direction, not its depth
|
|
|
341
357
|
| S4 | 7.7–10.9 | hands crossed in front | which forearm is in front |
|
|
342
358
|
| S5 | 11.1–13.0 | an arm thrust at the camera, then a V sign | foreshortening (0.63) and the shape of the hand; the arm mesh folds |
|
|
343
359
|
|
|
344
|
-
The answer is a second drawing of the arm for each of those poses, shown in place of the bent mesh while the pose lasts. rig-pose chooses when to show it, keys the switch in the motion, and lists every fold no drawing covers ([#6](https://github.com/rig-c/pose/issues/6), [above](#alternate-drawings-alternates)).
|
|
360
|
+
The answer, since 0.2, is a second drawing of the arm for each of those poses, shown in place of the bent mesh while the pose lasts. rig-pose chooses when to show it, keys the switch in the motion, and lists every fold no drawing covers ([#6](https://github.com/rig-c/pose/issues/6), [above](#alternate-drawings-alternates)).
|
|
345
361
|
|
|
346
|
-
## What
|
|
362
|
+
## What it does not do
|
|
347
363
|
|
|
348
|
-
- **Extract poses from video.** The input is joint positions
|
|
349
|
-
- **Turn the body.** 2D only
|
|
350
|
-
- **
|
|
364
|
+
- **Extract poses from video.** The input is joint positions. How you get them is up to you: a pose model, a dataset, or by hand.
|
|
365
|
+
- **Turn the body.** It works in 2D only. A segment pointing at the camera foreshortens, and the rig cannot: the direction is matched, the length is not. With `--leg-scale`, leg segments are the exception.
|
|
366
|
+
- **Read a moving camera.** A reference whose camera zooms or pans moves the planted floor with it under `--plant-feet` ([#52](https://github.com/rig-c/pose/issues/52)).
|
|
367
|
+
- **Front and behind, blinks, mouth shapes.** The detector's scores and face points do not carry them ([#33](https://github.com/rig-c/pose/issues/33), [#46](https://github.com/rig-c/pose/issues/46)).
|
|
368
|
+
- **Hands and fingers.** There are no bones for them yet (rig-c/parts#171).
|
|
351
369
|
|
|
352
370
|
## Licence
|
|
353
371
|
|
package/cli.ts
CHANGED
|
@@ -64,6 +64,12 @@ retarget
|
|
|
64
64
|
travel and re-plants on landing; a supporting ankle moving > 0.03 leg lengths a frame
|
|
65
65
|
releases its plant to the other foot, or to the pose's travel, until it is still
|
|
66
66
|
(< 0.01 for 2 frames) (FEET lines)
|
|
67
|
+
--stance <leg lengths> with --plant-feet: on a frame outside the jumps, a free ankle within this many
|
|
68
|
+
leg lengths (FEET's) of the supporting one in the picture is planted too — its leg's
|
|
69
|
+
scalex keys stretched by k = (root y − floor y) / (root y − ankle y) until the foot
|
|
70
|
+
reaches the floor, raised or lowered, k within [0.92, 1.08] (past it, reported, not forced); above
|
|
71
|
+
it the foot is lifted and keeps the pose's height (STANCE lines; default: off; 0.07 is
|
|
72
|
+
the card's, rig-c/pose#60)
|
|
67
73
|
--idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
|
|
68
74
|
animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
|
|
69
75
|
breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
|
|
@@ -200,7 +206,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
|
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
203
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip'], ['coco', 'plant-feet', 'carry-idle']);
|
|
209
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance'], ['coco', 'plant-feet', 'carry-idle']);
|
|
204
210
|
const bends = f.get('bends') ?? 'refuse';
|
|
205
211
|
if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
|
|
206
212
|
const switchAt = f.get('switch-at') ?? 'first';
|
|
@@ -225,6 +231,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
225
231
|
short: shortOf(f),
|
|
226
232
|
legScale: legScaleOf(f),
|
|
227
233
|
plantFeet: f.has('plant-feet'),
|
|
234
|
+
stance: stanceOf(f),
|
|
228
235
|
bends,
|
|
229
236
|
bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
230
237
|
switchTolerance: num(f, 'switch-tolerance', 25, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
@@ -241,6 +248,14 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
241
248
|
};
|
|
242
249
|
}
|
|
243
250
|
|
|
251
|
+
/** --stance (rig-c/pose#60): off unless given, and only with --plant-feet. */
|
|
252
|
+
function stanceOf(f: Map<string, string>): number | 'off' {
|
|
253
|
+
if (!f.has('stance') || f.get('stance') === 'off') return 'off';
|
|
254
|
+
const v = num(f, 'stance', 0, (n) => n > 0 && n < 1, 'off, or leg lengths in (0, 1)');
|
|
255
|
+
if (!f.has('plant-feet')) throw new Usage(`--stance is "${f.get('stance')}" without --plant-feet; it plants the free foot beside the supporting one that --plant-feet holds`);
|
|
256
|
+
return v;
|
|
257
|
+
}
|
|
258
|
+
|
|
244
259
|
/** --reduce and --reduce-hip (rig-c/pose#32): off unless given; the hip only with the bones. */
|
|
245
260
|
function reduceOf(f: Map<string, string>): { reduce?: number; reduceHip?: number } {
|
|
246
261
|
if (!f.has('reduce')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/legs.ts
CHANGED
|
@@ -486,3 +486,107 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
486
486
|
}
|
|
487
487
|
return hip;
|
|
488
488
|
}
|
|
489
|
+
|
|
490
|
+
// --- stance (rig-c/pose#60) --------------------------------------------------
|
|
491
|
+
|
|
492
|
+
/** A free leg is scaled within these factors to reach the floor (raised from below or lowered from above); past them the frame is reported, not forced. */
|
|
493
|
+
export const STANCE_BOUNDS: readonly [number, number] = [0.92, 1.08];
|
|
494
|
+
|
|
495
|
+
/** A run of frames with one free side in one state. */
|
|
496
|
+
export interface StanceRun {
|
|
497
|
+
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: above it, the pose's height kept. */
|
|
498
|
+
state: 'both' | 'lifted';
|
|
499
|
+
/** The side that is not supporting. */
|
|
500
|
+
side: Side;
|
|
501
|
+
frames: [number, number];
|
|
502
|
+
/** The free ankle's largest height above the supporting one in the picture, in that side's leg lengths. */
|
|
503
|
+
maxHeight: number;
|
|
504
|
+
/** `both`: the smallest and largest stretch keyed (bounded); `lifted`: null. */
|
|
505
|
+
k: { min: number; max: number } | null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** A run of planted-both frames whose stretch needed a factor past the bounds (or could not reach: `needed` null). */
|
|
509
|
+
export interface StanceOver {
|
|
510
|
+
side: Side;
|
|
511
|
+
frames: [number, number];
|
|
512
|
+
/** The stretch the floor needed furthest from 1, or null where the free ankle is not below its leg's root. */
|
|
513
|
+
needed: number | null;
|
|
514
|
+
/** The free ankle's largest distance left from the floor after the bound, world units. */
|
|
515
|
+
left: number;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export interface Stance {
|
|
519
|
+
/** The tolerance, in leg lengths (FEET's: the median picture hip → ankle of the free side). */
|
|
520
|
+
tolerance: number;
|
|
521
|
+
bounds: readonly [number, number];
|
|
522
|
+
runs: StanceRun[];
|
|
523
|
+
over: StanceOver[];
|
|
524
|
+
/** Per frame: the free side and the stretch keyed on it, or null (lifted, a jump, no support). */
|
|
525
|
+
stretch: Array<{ side: Side; k: number } | null>;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** World positions (y up) on one frame: each leg chain's first bone's origin and each ankle. */
|
|
529
|
+
export interface StancePosed {
|
|
530
|
+
root: Record<Side, Pt>;
|
|
531
|
+
ankle: Record<Side, Pt>;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Both feet planted when the free ankle is near the floor (rig-c/pose#60). On a
|
|
536
|
+
* frame with a supporting side (so outside every jump), the free ankle's height
|
|
537
|
+
* above the supporting one is read in the picture (`ankle`, px, y down) in the
|
|
538
|
+
* free side's leg length — the unit of every FEET threshold. Within `tolerance`,
|
|
539
|
+
* the frame is planted both: the free leg is to be stretched along its chain by
|
|
540
|
+
* k = (root y − floor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
541
|
+
* y up), the floor being the supporting ankle's posed y; k is held within
|
|
542
|
+
* `STANCE_BOUNDS` (a free ankle below the floor shortens its leg, one above
|
|
543
|
+
* lengthens it), and a frame past them is listed in `over`. Above the tolerance
|
|
544
|
+
* the foot is lifted and keeps the pose's height.
|
|
545
|
+
*/
|
|
546
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed): Stance {
|
|
547
|
+
const n = feet.support.length;
|
|
548
|
+
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
549
|
+
type Row = { state: 'both' | 'lifted'; side: Side; height: number; k: number | null; needed: number | null; over: boolean; left: number };
|
|
550
|
+
const rows: Array<Row | null> = new Array<Row | null>(n).fill(null);
|
|
551
|
+
for (let i = 0; i < n; i++) {
|
|
552
|
+
const s = feet.support[i] ?? null;
|
|
553
|
+
if (s === null) continue;
|
|
554
|
+
const f: Side = s === 'r' ? 'l' : 'r';
|
|
555
|
+
const height = (ankle[s][i]![1] - ankle[f][i]![1]) / feet.legLength[f];
|
|
556
|
+
if (height > tolerance) {
|
|
557
|
+
rows[i] = { state: 'lifted', side: f, height, k: null, needed: null, over: false, left: 0 };
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const p = posed(i);
|
|
561
|
+
const floor = p.ankle[s][1];
|
|
562
|
+
const root = p.root[f][1];
|
|
563
|
+
const a = p.ankle[f][1];
|
|
564
|
+
const ok = root - a > 1e-9 && root - floor > 1e-9;
|
|
565
|
+
const needed = ok ? (root - floor) / (root - a) : null;
|
|
566
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, STANCE_BOUNDS[0]), STANCE_BOUNDS[1]);
|
|
567
|
+
const over = needed === null || needed < STANCE_BOUNDS[0] || needed > STANCE_BOUNDS[1];
|
|
568
|
+
const left = Math.abs(root - k * (root - a) - floor);
|
|
569
|
+
rows[i] = { state: 'both', side: f, height, k, needed, over, left };
|
|
570
|
+
if (k !== 1) stretch[i] = { side: f, k };
|
|
571
|
+
}
|
|
572
|
+
const runs: StanceRun[] = [];
|
|
573
|
+
const over: StanceOver[] = [];
|
|
574
|
+
for (let i = 0; i < n; i++) {
|
|
575
|
+
const r = rows[i];
|
|
576
|
+
if (r === null) continue;
|
|
577
|
+
const last = runs[runs.length - 1];
|
|
578
|
+
if (last !== undefined && last.frames[1] === i - 1 && last.state === r.state && last.side === r.side) {
|
|
579
|
+
last.frames[1] = i;
|
|
580
|
+
last.maxHeight = Math.max(last.maxHeight, r.height);
|
|
581
|
+
if (last.k !== null && r.k !== null) last.k = { min: Math.min(last.k.min, r.k), max: Math.max(last.k.max, r.k) };
|
|
582
|
+
} else runs.push({ state: r.state, side: r.side, frames: [i, i], maxHeight: r.height, k: r.k === null ? null : { min: r.k, max: r.k } });
|
|
583
|
+
if (!r.over) continue;
|
|
584
|
+
const o = over[over.length - 1];
|
|
585
|
+
if (o !== undefined && o.frames[1] === i - 1 && o.side === r.side) {
|
|
586
|
+
o.frames[1] = i;
|
|
587
|
+
o.needed = o.needed === null || r.needed === null ? null : Math.abs(r.needed - 1) > Math.abs(o.needed - 1) ? r.needed : o.needed;
|
|
588
|
+
o.left = Math.max(o.left, r.left);
|
|
589
|
+
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left });
|
|
590
|
+
}
|
|
591
|
+
return { tolerance, bounds: STANCE_BOUNDS, runs, over, stretch };
|
|
592
|
+
}
|
package/src/retarget.ts
CHANGED
|
@@ -69,8 +69,8 @@ import type { Joint } from './pose.ts';
|
|
|
69
69
|
import { Problems, Refusal } from './problems.ts';
|
|
70
70
|
import type { ArtRange, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
71
71
|
import { clampArt, coveredChains, measureArt, overflow, RANGE_SLACK, runsOf, selectSwitches } from './switch.ts';
|
|
72
|
-
import type { Feet, LegScale, PathPose, Posing, Side } from './legs.ts';
|
|
73
|
-
import { isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, supportsOf, tipOf } from './legs.ts';
|
|
72
|
+
import type { Feet, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
73
|
+
import { isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, supportsOf, tipOf } from './legs.ts';
|
|
74
74
|
|
|
75
75
|
const DEG = 180 / Math.PI;
|
|
76
76
|
|
|
@@ -91,6 +91,8 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
91
91
|
legScale?: [number, number] | 'off';
|
|
92
92
|
/** The hip derived from the supporting foot (`legs.ts`); false (the default) moves it by the pose's travel. */
|
|
93
93
|
plantFeet?: boolean;
|
|
94
|
+
/** With plantFeet: a free ankle within this many leg lengths of the floor plants both feet, its leg stretched to reach it (`legs.ts` stanceOf, #60); 'off' (the default) plants one. */
|
|
95
|
+
stance?: number | 'off';
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export interface Hold {
|
|
@@ -267,6 +269,8 @@ export interface Retarget {
|
|
|
267
269
|
feet: Feet | null;
|
|
268
270
|
/** Whether the hip was derived from the supporting foot. */
|
|
269
271
|
planted: boolean;
|
|
272
|
+
/** With --stance: the frames planted both and lifted, and the free leg's stretch (#60). */
|
|
273
|
+
stance: Stance | null;
|
|
270
274
|
/** Each ankle's world position on every frame, as the keys put it, when legs are scaled or feet planted. */
|
|
271
275
|
ankles: { bones: Record<Side, string>; at: Record<Side, Pt[]> } | null;
|
|
272
276
|
/** The translated bone's origin against its thigh roots' midpoint, at setup. */
|
|
@@ -638,6 +642,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
638
642
|
const plantOn = opts.plantFeet === true;
|
|
639
643
|
const legScale: LegScale[] = [];
|
|
640
644
|
const scales = new Map<string, number[]>();
|
|
645
|
+
let stance: Stance | null = null;
|
|
641
646
|
let feet: Feet | null = null;
|
|
642
647
|
let legs: ReturnType<typeof legsOf> | null = null;
|
|
643
648
|
let legPath: string[] | null = null;
|
|
@@ -692,6 +697,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
692
697
|
}
|
|
693
698
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
694
699
|
}
|
|
700
|
+
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
695
701
|
p.throwIfAny();
|
|
696
702
|
/** Every bone on the legs' path posed on frame i, by the targets as they stand. */
|
|
697
703
|
const poseLegs = (i: number): PathPose => {
|
|
@@ -714,6 +720,31 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
714
720
|
const tb = rest.get(t!.bone)!;
|
|
715
721
|
const hip = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY]);
|
|
716
722
|
for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
|
|
723
|
+
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
724
|
+
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
725
|
+
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
726
|
+
const L = (legs as { legs: Record<Side, { chain: Chain; ankleBone: string }> }).legs;
|
|
727
|
+
const world = (i: number, v: Pt): Pt => [hip[i]![0] + v[0], hip[i]![1] + v[1]];
|
|
728
|
+
const ankleSeries = { r: series.get('r_ankle')!, l: series.get('l_ankle')! };
|
|
729
|
+
stance = stanceOf(feet!, ankleSeries, opts.stance, (i) => {
|
|
730
|
+
const lp = poseLegs(i);
|
|
731
|
+
return {
|
|
732
|
+
root: { r: world(i, lp.origin.get(L.r.chain.bones[0]!)!), l: world(i, lp.origin.get(L.l.chain.bones[0]!)!) },
|
|
733
|
+
ankle: { r: world(i, tipOf(rest, lp, L.r.ankleBone)), l: world(i, tipOf(rest, lp, L.l.ankleBone)) },
|
|
734
|
+
};
|
|
735
|
+
});
|
|
736
|
+
for (const side of SIDES) {
|
|
737
|
+
if (!stance.stretch.some((x) => x?.side === side)) continue;
|
|
738
|
+
for (const b of L[side].chain.bones) {
|
|
739
|
+
const own = scales.get(b);
|
|
740
|
+
const s = own === undefined ? new Array<number>(n).fill(1) : [...own];
|
|
741
|
+
stance.stretch.forEach((x, i) => {
|
|
742
|
+
if (x?.side === side) s[i] = s[i]! * x.k;
|
|
743
|
+
});
|
|
744
|
+
scales.set(b, s);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
717
748
|
}
|
|
718
749
|
if (t !== null && scale !== null) {
|
|
719
750
|
const bone = rest.get(t.bone)!;
|
|
@@ -884,7 +915,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
884
915
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
885
916
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
886
917
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
887
|
-
legScale, scales, feet, planted: plantOn, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs),
|
|
918
|
+
legScale, scales, feet, planted: plantOn, stance, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs),
|
|
888
919
|
};
|
|
889
920
|
}
|
|
890
921
|
|
package/src/run.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { artRangeLine, chainsMoving, switchLine, uncoveredFolds } from './switch
|
|
|
17
17
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
18
18
|
import { legsOf } from './legs.ts';
|
|
19
19
|
import { reduceKeys, type ReduceBone, type Reduced } from './reduce.ts';
|
|
20
|
-
import type { LegScale } from './legs.ts';
|
|
20
|
+
import type { LegScale, Stance } from './legs.ts';
|
|
21
21
|
|
|
22
22
|
export interface RetargetArgs extends RetargetOptions {
|
|
23
23
|
rig: string;
|
|
@@ -104,7 +104,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
104
104
|
resolveMap(map, rest, a.map, slots);
|
|
105
105
|
|
|
106
106
|
const r = retarget(rest, map, frames, times, a, slots);
|
|
107
|
-
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, smooth ${a.smooth}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
107
|
+
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, smooth ${a.smooth}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
108
108
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
109
109
|
if (r.scale !== null) say(` SCALE ${r.scale.factor.toFixed(5)} rig units per picture px (rig ${r.scale.rig.toFixed(2)} / pose median ${r.scale.pose.toFixed(2)})`);
|
|
110
110
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
@@ -133,6 +133,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
133
133
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
134
134
|
say(` FEET steps (the supporting ankle > 0.03 leg lengths a frame; re-planted after 2 frames < 0.01): ${f.steps.map((x) => `${x.side} ${span(x.frames)} → ${x.to === null ? 'the pose\'s travel' : `${x.to} supports`}, ${x.replant === null ? 'never re-planted' : `re-planted at ${x.replant}`}`).join('; ') || 'none'}`);
|
|
135
135
|
}
|
|
136
|
+
if (r.stance !== null) for (const l of stanceLines(r.stance, r.times)) say(` STANCE ${l}`);
|
|
136
137
|
|
|
137
138
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
138
139
|
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip);
|
|
@@ -263,7 +264,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
263
264
|
animation: a.name,
|
|
264
265
|
frames: seq.frames.length,
|
|
265
266
|
duration: r.animation.duration,
|
|
266
|
-
options: { minScore: a.minScore, smooth: a.smooth, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off' },
|
|
267
|
+
options: { minScore: a.minScore, smooth: a.smooth, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off' },
|
|
267
268
|
scale: r.scale,
|
|
268
269
|
holds: r.holds,
|
|
269
270
|
short: r.short,
|
|
@@ -281,6 +282,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
281
282
|
artRange: r.artRange,
|
|
282
283
|
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
283
284
|
pivot: r.pivot,
|
|
285
|
+
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, bounds: r.stance.bounds, runs: r.stance.runs, over: r.stance.over },
|
|
284
286
|
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, reading: feetReading },
|
|
285
287
|
shear,
|
|
286
288
|
idleDropped: idle === null ? null : idle.dropped,
|
|
@@ -395,6 +397,20 @@ function legBonesOf(map: JointMap): { r: { root: string; ankle: string }; l: { r
|
|
|
395
397
|
return { r: { root: l.legs.r.chain.bones[0]!, ankle: l.legs.r.ankleBone }, l: { root: l.legs.l.chain.bones[0]!, ankle: l.legs.l.ankleBone } };
|
|
396
398
|
}
|
|
397
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The STANCE lines (#60): one per run of planted-both or lifted frames, with
|
|
402
|
+
* the tolerance and the stretch keyed; one per run a stretch was capped.
|
|
403
|
+
*/
|
|
404
|
+
export function stanceLines(st: Stance, times: readonly number[]): string[] {
|
|
405
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
406
|
+
const tol = `the tolerance ${st.tolerance} leg lengths`;
|
|
407
|
+
const out = st.runs.map((r) => (r.state === 'both'
|
|
408
|
+
? `planted both: free foot ${r.side} ${span(r.frames)} — its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture, within ${tol}; its leg stretched by k ${r.k!.min === r.k!.max ? r.k!.max.toFixed(4) : `${r.k!.min.toFixed(4)} … ${r.k!.max.toFixed(4)}`} (largest ${r.k!.max.toFixed(4)})`
|
|
409
|
+
: `lifted: free foot ${r.side} ${span(r.frames)} — its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture, over ${tol}; the pose's height kept`));
|
|
410
|
+
for (const o of st.over) out.push(`past the bounds: free foot ${o.side} ${span(o.frames)} — ${o.needed === null ? 'its ankle is not below its leg\'s root, so no stretch reaches the floor' : `the floor needed k ${o.needed.toFixed(4)}, past [${st.bounds[0]}, ${st.bounds[1]}]; held at the bound`}, the foot left up to ${o.left.toFixed(2)} units from the floor`);
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
398
414
|
/** A LEG-SCALE line: the segment, its straight length, its ratio's spread, and the frames clamped. */
|
|
399
415
|
function legScaleLine(l: LegScale, times: readonly number[]): string {
|
|
400
416
|
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|