@rig-c/pose 0.11.1 → 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 +3 -1
- 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
|
@@ -57,12 +57,13 @@ and what the run cannot decide for you it lists:
|
|
|
57
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.
|
|
58
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.
|
|
59
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.
|
|
60
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.
|
|
61
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)).
|
|
62
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.
|
|
63
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.
|
|
64
65
|
|
|
65
|
-
`--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:
|
|
66
67
|
|
|
67
68
|
| field | each entry |
|
|
68
69
|
| --- | --- |
|
|
@@ -121,6 +122,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
121
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.
|
|
122
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.
|
|
123
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.
|
|
124
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.
|
|
125
127
|
|
|
126
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.
|
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]}`);
|