@rig-c/pose 0.17.0 → 0.18.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 +29 -10
- package/cli.ts +18 -12
- package/package.json +1 -1
- package/src/clean.ts +7 -3
- package/src/legs.ts +246 -60
- package/src/map.ts +49 -3
- package/src/pose.ts +15 -3
- package/src/reduce.ts +167 -7
- package/src/retarget.ts +118 -37
- package/src/run.ts +202 -27
- package/src/switch.ts +31 -8
package/src/legs.ts
CHANGED
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
* less than `STILL_SPEED` for `STILL_FRAMES` frames (rig-c/pose#47, round 2).
|
|
40
40
|
* The floor is rolling because an ankle's picture y drifts with depth: under
|
|
41
41
|
* one floor for the sequence, HQ's dancer standing still read as jumping.
|
|
42
|
+
*
|
|
43
|
+
* **In place** (rig-c/pose#92). A sequence declared `travel: "in-place"` is a
|
|
44
|
+
* gait that does not travel: its supporting foot moves back in the picture
|
|
45
|
+
* while the body stays, so holding it turns that slide into travel. Under the
|
|
46
|
+
* declaration each plant's anchor follows its own ankle's track (`follow`, the
|
|
47
|
+
* picture x scaled) from the frame it planted, its y still on the one floor; no
|
|
48
|
+
* step is read, since a foot that follows its track has no plant to drag; and
|
|
49
|
+
* the hip's x drift from the first frame to the last is spread back over the
|
|
50
|
+
* frames (`closeLoop`), so the loop carries no net travel.
|
|
42
51
|
*/
|
|
43
52
|
import type { Chain, JointMap, Rest } from './map.ts';
|
|
44
53
|
|
|
@@ -325,6 +334,28 @@ export interface Feet {
|
|
|
325
334
|
steps: Step[];
|
|
326
335
|
/** The frames a change of support (or a landing, or a re-plant) was keyed on: the blend starts there. */
|
|
327
336
|
changes: Array<{ frame: number; from: Side | null; to: Side }>;
|
|
337
|
+
/**
|
|
338
|
+
* rig-c/pose#113, read from the lower ankle: each run of frames whose
|
|
339
|
+
* supporting side differs from the reading before #113 (`was`, `now`; null:
|
|
340
|
+
* no foot supports). Absent where the rule does not apply (in place, or
|
|
341
|
+
* without `lowerAnkle`).
|
|
342
|
+
*/
|
|
343
|
+
lowered?: Lowered[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
|
347
|
+
export interface Lowered {
|
|
348
|
+
frames: [number, number];
|
|
349
|
+
was: Side | null;
|
|
350
|
+
now: Side | null;
|
|
351
|
+
/**
|
|
352
|
+
* Why no foot supports, where `now` is null: `released` — the supporting foot
|
|
353
|
+
* was released on the run's first frame because the other ankle had been lower
|
|
354
|
+
* past the margin for `PLANT_SWITCH_SECONDS` and could not take support
|
|
355
|
+
* (released by a step, or moving); `higher` — the only foot free to take
|
|
356
|
+
* support was the higher one.
|
|
357
|
+
*/
|
|
358
|
+
cause?: 'released' | 'higher';
|
|
328
359
|
}
|
|
329
360
|
|
|
330
361
|
/** Each frame's rolling floor of a picture-y series: its `FLOOR_PERCENTILE` over the frames within ± `FLOOR_SECONDS`. */
|
|
@@ -339,9 +370,36 @@ export function rollingFloor(ys: readonly number[], times: readonly number[]): n
|
|
|
339
370
|
/**
|
|
340
371
|
* Which foot supports on each frame, from the reference ankles and hips (picture
|
|
341
372
|
* px, y down) — the header's rules. `times` are the key times; `secondsOf`
|
|
342
|
-
* times a run of frames as the rest of the retarget does.
|
|
373
|
+
* times a run of frames as the rest of the retarget does. `inPlace` (#92): no
|
|
374
|
+
* ankle is moving for the step rules, so nothing is released and every foot is
|
|
375
|
+
* free to take support.
|
|
376
|
+
*
|
|
377
|
+
* `lowerAnkle` (rig-c/pose#113, `--plant-feet` on a sequence not declared in
|
|
378
|
+
* place): a foot whose ankle is higher in the picture than the other's by
|
|
379
|
+
* `PLANT_SWITCH` leg lengths or more never takes support — not when the plant
|
|
380
|
+
* starts, not when a step releases the other foot. A supporting foot whose
|
|
381
|
+
* other ankle has been that much lower for `PLANT_SWITCH_SECONDS` hands support
|
|
382
|
+
* to it when it is free (as before) and is otherwise released (the hip follows
|
|
383
|
+
* the pose until a foot may take support). Steps and jumps are read as before.
|
|
384
|
+
* The frames whose support this changes are `lowered`.
|
|
343
385
|
*/
|
|
344
|
-
export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number): Feet {
|
|
386
|
+
export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace = false, lowerAnkle = false): Feet {
|
|
387
|
+
if (inPlace || !lowerAnkle) return readSupports(ankle, hip, times, secondsOf, inPlace, false).feet;
|
|
388
|
+
const { feet, released } = readSupports(ankle, hip, times, secondsOf, false, true);
|
|
389
|
+
const before = readSupports(ankle, hip, times, secondsOf, false, false).feet.support;
|
|
390
|
+
const lowered: Lowered[] = [];
|
|
391
|
+
feet.support.forEach((now, i) => {
|
|
392
|
+
const was = before[i] ?? null;
|
|
393
|
+
if (was === now) return;
|
|
394
|
+
const last = lowered[lowered.length - 1];
|
|
395
|
+
if (last !== undefined && last.frames[1] === i - 1 && last.was === was && last.now === now) last.frames[1] = i;
|
|
396
|
+
else lowered.push({ frames: [i, i], was, now, ...(now === null ? { cause: released.includes(i) ? ('released' as const) : ('higher' as const) } : {}) });
|
|
397
|
+
});
|
|
398
|
+
return { ...feet, lowered };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The reader; `released` (#113): the frames a supporting foot was released because the lower foot could not take support. */
|
|
402
|
+
function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace: boolean, lowerAnkle: boolean): { feet: Feet; released: number[] } {
|
|
345
403
|
const n = ankle.r.length;
|
|
346
404
|
const legLength = {} as Record<Side, number>;
|
|
347
405
|
for (const s of SIDES) legLength[s] = percentile(ankle[s].map((a, i) => Math.hypot(a[0] - hip[s][i]![0], a[1] - hip[s][i]![1])), 0.5);
|
|
@@ -364,7 +422,9 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
364
422
|
// Ankle speeds, leg lengths a frame (0 on the first frame).
|
|
365
423
|
const speed = {} as Record<Side, number[]>;
|
|
366
424
|
for (const s of SIDES) speed[s] = ankle[s].map((a, i) => (i === 0 ? 0 : Math.hypot(a[0] - ankle[s][i - 1]![0], a[1] - ankle[s][i - 1]![1]) / legLength[s]));
|
|
367
|
-
|
|
425
|
+
// In place (rig-c/pose#92) the supporting foot follows its own track instead of being held, so a
|
|
426
|
+
// moving ankle has no plant to drag and nothing is released: no step, and every foot is free.
|
|
427
|
+
const moving = (s: Side, i: number) => !inPlace && speed[s][i]! > STEP_SPEED;
|
|
368
428
|
const still = (s: Side, i: number) => {
|
|
369
429
|
for (let k = i - STILL_FRAMES + 1; k <= i; k++) if (k >= 0 && !(speed[s][k]! < STILL_SPEED)) return false;
|
|
370
430
|
return true;
|
|
@@ -375,6 +435,7 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
375
435
|
const open: Partial<Record<Side, Step>> = {};
|
|
376
436
|
let cur: Side | null = null;
|
|
377
437
|
let lowerFrom = -1;
|
|
438
|
+
const released: number[] = [];
|
|
378
439
|
const lower = (cands: Side[], i: number): Side => (cands.length === 2 ? (ankle.l[i]![1] > ankle.r[i]![1] ? 'l' : 'r') : cands[0]!);
|
|
379
440
|
for (let i = 0; i < n; i++) {
|
|
380
441
|
// A released ankle re-plants once it has been still for STILL_FRAMES frames.
|
|
@@ -392,9 +453,12 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
392
453
|
continue;
|
|
393
454
|
}
|
|
394
455
|
const free = (s: Side) => open[s] === undefined && !moving(s, i);
|
|
456
|
+
// #113: a foot higher than the other by PLANT_SWITCH leg lengths or more does not take support.
|
|
457
|
+
const higher = (s: Side) => ankle[s === 'r' ? 'l' : 'r'][i]![1] - ankle[s][i]![1] >= PLANT_SWITCH * mean;
|
|
458
|
+
const may = (s: Side) => free(s) && !(lowerAnkle && higher(s));
|
|
395
459
|
if (cur !== null && moving(cur, i)) {
|
|
396
460
|
const other: Side = cur === 'r' ? 'l' : 'r';
|
|
397
|
-
const to: Side | null =
|
|
461
|
+
const to: Side | null = may(other) ? other : null;
|
|
398
462
|
const st: Step = { side: cur, frames: [i, n - 1], replant: null, to };
|
|
399
463
|
steps.push(st);
|
|
400
464
|
open[cur] = st;
|
|
@@ -402,7 +466,7 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
402
466
|
cur = to;
|
|
403
467
|
lowerFrom = -1;
|
|
404
468
|
} else if (cur === null) {
|
|
405
|
-
const cands = SIDES.filter(
|
|
469
|
+
const cands = SIDES.filter(may);
|
|
406
470
|
if (cands.length > 0) {
|
|
407
471
|
cur = lower(cands, i);
|
|
408
472
|
changes.push({ frame: i, from: null, to: cur });
|
|
@@ -410,11 +474,16 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
410
474
|
}
|
|
411
475
|
} else {
|
|
412
476
|
const other: Side = cur === 'r' ? 'l' : 'r';
|
|
413
|
-
if (free(other)
|
|
477
|
+
if ((free(other) || lowerAnkle) && higher(cur)) {
|
|
414
478
|
if (lowerFrom < 0) lowerFrom = i;
|
|
415
479
|
if (secondsOf([lowerFrom, i]) >= PLANT_SWITCH_SECONDS - 1e-9) {
|
|
416
|
-
|
|
417
|
-
|
|
480
|
+
if (free(other)) {
|
|
481
|
+
changes.push({ frame: i, from: cur, to: other });
|
|
482
|
+
cur = other;
|
|
483
|
+
} else {
|
|
484
|
+
cur = null; // #113: the lower foot may not take support (released, or moving); neither foot supports
|
|
485
|
+
released.push(i);
|
|
486
|
+
}
|
|
418
487
|
lowerFrom = -1;
|
|
419
488
|
}
|
|
420
489
|
} else lowerFrom = -1;
|
|
@@ -429,7 +498,7 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
429
498
|
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
430
499
|
else supports.push({ side: s, frames: [i, i] });
|
|
431
500
|
}
|
|
432
|
-
return { legLength, support, supports, jumps, steps, changes };
|
|
501
|
+
return { feet: { legLength, support, supports, jumps, steps, changes }, released };
|
|
433
502
|
}
|
|
434
503
|
|
|
435
504
|
/** One support span's shift onto the floor (rig-c/pose#85): its side, its frames, and floor − its planted height, world units. */
|
|
@@ -466,16 +535,41 @@ export interface Floor {
|
|
|
466
535
|
* `blends` lists those frames with the anchor's y, and `holdBlend` stretches
|
|
467
536
|
* the supporting leg to keep it there. Null `floor` in the result: there is no
|
|
468
537
|
* support span.
|
|
538
|
+
*
|
|
539
|
+
* `follow(i, side)` (#92, an in-place sequence): the supporting ankle's track,
|
|
540
|
+
* world x. A plant's anchor x then moves by follow(i) − follow(the frame it
|
|
541
|
+
* planted), and the loop is closed (`closeLoop`, `closure` in the result; null
|
|
542
|
+
* without `follow`).
|
|
469
543
|
*/
|
|
470
|
-
export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[] } {
|
|
471
|
-
const picture = placeHip(feet, offset, travel, start, null);
|
|
544
|
+
export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number, follow?: (i: number, s: Side) => number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[]; closure: Closure | null } {
|
|
545
|
+
const picture = placeHip(feet, offset, travel, start, null, follow);
|
|
472
546
|
const heights = feet.supports.map((sp) => picture.anchors.get(sp.frames[0]));
|
|
473
|
-
if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends };
|
|
547
|
+
if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends, closure: null };
|
|
474
548
|
if (heights.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
|
|
475
549
|
const y = floor ?? percentile(heights as number[], 0.5);
|
|
476
550
|
const shifts = feet.supports.map((sp, k) => ({ side: sp.side, frames: [sp.frames[0], sp.frames[1]] as [number, number], shift: y - heights[k]! }));
|
|
477
|
-
const placed = placeHip(feet, offset, travel, start, y);
|
|
478
|
-
|
|
551
|
+
const placed = placeHip(feet, offset, travel, start, y, follow);
|
|
552
|
+
const closure = follow === undefined ? null : closeLoop(placed.hip);
|
|
553
|
+
return { hip: placed.hip, floor: { y, given: floor !== undefined, shifts }, blends: placed.blends, closure };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** In place (rig-c/pose#92): the hip's x travel from the first frame to the last before the loop was closed, world units, and the frames it was spread over. */
|
|
557
|
+
export interface Closure {
|
|
558
|
+
drift: number;
|
|
559
|
+
frames: number;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Close an in-place loop (rig-c/pose#92): the hip's x on frame i moves by
|
|
564
|
+
* −drift · i / (n − 1), drift its x on the last frame less its x on the first,
|
|
565
|
+
* so the last frame's hip x is the first's and the loop carries no net travel.
|
|
566
|
+
* The y is the floor's (#85) and is not touched. In place.
|
|
567
|
+
*/
|
|
568
|
+
export function closeLoop(hip: Pt[]): Closure {
|
|
569
|
+
const n = hip.length;
|
|
570
|
+
const drift = n > 1 ? hip[n - 1]![0] - hip[0]![0] : 0;
|
|
571
|
+
if (n > 1) for (let i = 0; i < n; i++) hip[i]![0] -= (drift * i) / (n - 1);
|
|
572
|
+
return { drift, frames: n };
|
|
479
573
|
}
|
|
480
574
|
|
|
481
575
|
/** A frame inside the blend after a change of support (or a landing): the side now supporting, and the world y its plant holds the ankle at. */
|
|
@@ -490,17 +584,19 @@ export interface BlendFrame {
|
|
|
490
584
|
* picture puts it (`floorY` null) or at world y `floorY`; and each plant's
|
|
491
585
|
* picture anchor y, by the frame its support span starts on.
|
|
492
586
|
*/
|
|
493
|
-
function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floorY: number | null): { hip: Pt[]; anchors: Map<number, number>; blends: BlendFrame[] } {
|
|
587
|
+
function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floorY: number | null, follow?: (i: number, s: Side) => number): { hip: Pt[]; anchors: Map<number, number>; blends: BlendFrame[] } {
|
|
494
588
|
const n = feet.support.length;
|
|
495
589
|
const hip: Pt[] = [];
|
|
496
590
|
const anchors = new Map<number, number>();
|
|
497
591
|
const blends: BlendFrame[] = [];
|
|
498
592
|
/** How the hip is derived on a frame: held over a foot's anchor, or carried by the pose's travel from a frame's hip. */
|
|
499
|
-
type Mode = { kind: 'plant'; side: Side; anchor: Pt } | { kind: 'travel'; from: number; at: Pt };
|
|
593
|
+
type Mode = { kind: 'plant'; side: Side; anchor: Pt; from: number } | { kind: 'travel'; from: number; at: Pt };
|
|
500
594
|
const of = (mode: Mode, i: number): Pt => {
|
|
501
595
|
if (mode.kind === 'plant') {
|
|
502
596
|
const o = offset(i, mode.side);
|
|
503
|
-
|
|
597
|
+
// In place (#92) the anchor's x follows the supporting ankle's own track from the frame it planted.
|
|
598
|
+
const dx = follow === undefined ? 0 : follow(i, mode.side) - follow(mode.from, mode.side);
|
|
599
|
+
return [mode.anchor[0] + dx - o[0], mode.anchor[1] - o[1]];
|
|
504
600
|
}
|
|
505
601
|
const a = travel(i);
|
|
506
602
|
const b = travel(mode.from);
|
|
@@ -509,7 +605,7 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
509
605
|
/** A plant from frame i: its anchor's x where the foot is, its y there or on the floor. */
|
|
510
606
|
const plant = (i: number, side: Side, at: Pt): Mode => {
|
|
511
607
|
anchors.set(i, at[1]);
|
|
512
|
-
return { kind: 'plant', side, anchor: [at[0], floorY ?? at[1]] };
|
|
608
|
+
return { kind: 'plant', side, anchor: [at[0], floorY ?? at[1]], from: i };
|
|
513
609
|
};
|
|
514
610
|
let mode: Mode | null = null;
|
|
515
611
|
let from: Mode | null = null;
|
|
@@ -551,23 +647,72 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
551
647
|
|
|
552
648
|
// --- stance (rig-c/pose#60) --------------------------------------------------
|
|
553
649
|
|
|
554
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* 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.
|
|
652
|
+
* The default: a frame whose leg the picture folds further has its lower bound from the leg's own range (`stanceRangeOf`, #95).
|
|
653
|
+
*/
|
|
555
654
|
export const STANCE_BOUNDS: readonly [number, number] = [0.92, 1.08];
|
|
556
655
|
|
|
656
|
+
/** Per side, per frame: the lower bound of the leg's stretch (#95). */
|
|
657
|
+
export type StanceLower = Record<Side, readonly number[]>;
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The leg's own range (rig-c/pose#95). Per side, r(i) = the leg's picture
|
|
661
|
+
* length hip → ankle on frame i ÷ its straight length (`STRAIGHT_PERCENTILE`
|
|
662
|
+
* of that length over the sequence, as `--leg-scale` reads a segment's), and
|
|
663
|
+
* the lower bound on frame i is min(`STANCE_BOUNDS[0]`, r(i)): the rig leg may
|
|
664
|
+
* fold as far as the picture's leg is folded on that frame, never less far
|
|
665
|
+
* than the default. A leg with no straight length keeps the default.
|
|
666
|
+
*/
|
|
667
|
+
export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>): StanceLower {
|
|
668
|
+
const out = {} as Record<Side, number[]>;
|
|
669
|
+
for (const s of SIDES) {
|
|
670
|
+
const len = ankle[s].map((a, i) => Math.hypot(a[0] - hip[s][i]![0], a[1] - hip[s][i]![1]));
|
|
671
|
+
const straight = percentile(len, STRAIGHT_PERCENTILE);
|
|
672
|
+
out[s] = len.map((l) => (straight > 1e-9 && Number.isFinite(l) ? Math.min(STANCE_BOUNDS[0], l / straight) : STANCE_BOUNDS[0]));
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** The lower bound on frame i for side s: the leg's own (#95), or the default. */
|
|
678
|
+
function lowerOf(lower: StanceLower | undefined, s: Side, i: number): number {
|
|
679
|
+
return lower?.[s][i] ?? STANCE_BOUNDS[0];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** The lowest lower bound per side over `frames`, where any is below the default (#95); null where none is. */
|
|
683
|
+
function rangeUsed(lower: StanceLower | undefined, frames: ReadonlyArray<{ side: Side; frame: number }>): Partial<Record<Side, number>> | null {
|
|
684
|
+
const out: Partial<Record<Side, number>> = {};
|
|
685
|
+
for (const { side, frame } of frames) {
|
|
686
|
+
const lo = lowerOf(lower, side, frame);
|
|
687
|
+
if (lo < STANCE_BOUNDS[0]) out[side] = Math.min(out[side] ?? Infinity, lo);
|
|
688
|
+
}
|
|
689
|
+
return Object.keys(out).length === 0 ? null : out;
|
|
690
|
+
}
|
|
691
|
+
|
|
557
692
|
/** A lifted free foot is planted again only when its height comes back under this fraction of the tolerance (#87): hysteresis, a stated factor, not tuned. */
|
|
558
693
|
export const STANCE_RELEASE = 0.8;
|
|
559
694
|
/** A run of one state shorter than this many seconds, between two runs of the other state, takes their state (#87): a foot cannot take a stance for one frame. */
|
|
560
695
|
export const STANCE_MIN_SECONDS = 0.1;
|
|
561
696
|
|
|
697
|
+
/**
|
|
698
|
+
* A free ankle's state on a frame (#60, #114): `both`, within the tolerance of
|
|
699
|
+
* the supporting one in either direction, so both feet are planted; `lifted`,
|
|
700
|
+
* past it above; `below`, past it below — a support error, not a stance, never
|
|
701
|
+
* planted. Past the tolerance either way the pose's height is kept.
|
|
702
|
+
*/
|
|
703
|
+
export type StanceState = 'both' | 'lifted' | 'below';
|
|
704
|
+
|
|
562
705
|
/** A run of frames with one free side in one state. */
|
|
563
706
|
export interface StanceRun {
|
|
564
|
-
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: above it, the pose's height kept. */
|
|
565
|
-
state:
|
|
707
|
+
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: past it above, `below`: past it below (#114), the pose's height kept. */
|
|
708
|
+
state: StanceState;
|
|
566
709
|
/** The side that is not supporting. */
|
|
567
710
|
side: Side;
|
|
568
711
|
frames: [number, number];
|
|
569
|
-
/** The free ankle's largest height above the supporting one in the picture, in that side's leg lengths. */
|
|
712
|
+
/** The free ankle's largest height above the supporting one in the picture, in that side's leg lengths (negative where it is below on every frame). */
|
|
570
713
|
maxHeight: number;
|
|
714
|
+
/** `below` (#114): the free ankle's largest distance below the supporting one in the picture, in that side's leg lengths. */
|
|
715
|
+
maxDepth?: number;
|
|
571
716
|
/** `both`: the smallest and largest stretch keyed (bounded); `lifted`: null. */
|
|
572
717
|
k: { min: number; max: number } | null;
|
|
573
718
|
/** #87: a run read in `state`, shorter than `STANCE_MIN_SECONDS` between two runs of the other state, which took their state; its frames are in their run too, and `k` is null. */
|
|
@@ -582,6 +727,8 @@ export interface StanceOver {
|
|
|
582
727
|
needed: number | null;
|
|
583
728
|
/** The free ankle's largest distance left from the floor after the bound, world units. */
|
|
584
729
|
left: number;
|
|
730
|
+
/** #95: the bound on the frame of `needed`, where the leg's own range set it below the default. */
|
|
731
|
+
bound?: [number, number];
|
|
585
732
|
}
|
|
586
733
|
|
|
587
734
|
export interface Stance {
|
|
@@ -592,6 +739,8 @@ export interface Stance {
|
|
|
592
739
|
/** The shortest run kept as its own state, seconds (#87). */
|
|
593
740
|
minSeconds: number;
|
|
594
741
|
bounds: readonly [number, number];
|
|
742
|
+
/** #95: per side, the lowest lower bound the leg's own range set on a planted-both frame, where any is below `bounds[0]`. */
|
|
743
|
+
range?: Partial<Record<Side, number>>;
|
|
595
744
|
runs: StanceRun[];
|
|
596
745
|
over: StanceOver[];
|
|
597
746
|
/** Per frame: the free side and the stretch keyed on it, or null (lifted, a jump, no support). */
|
|
@@ -608,26 +757,33 @@ export interface StancePosed {
|
|
|
608
757
|
* Both feet planted when the free ankle is near the floor (rig-c/pose#60). On a
|
|
609
758
|
* frame with a supporting side (so outside every jump), the free ankle's height
|
|
610
759
|
* above the supporting one is read in the picture (`ankle`, px, y down) in the
|
|
611
|
-
* free side's leg length — the unit of every FEET threshold.
|
|
612
|
-
*
|
|
760
|
+
* free side's leg length — the unit of every FEET threshold. The tolerance is a
|
|
761
|
+
* distance (#114): within `tolerance` of the supporting ankle, above it or below
|
|
762
|
+
* it (|h| ≤ tolerance), the frame is planted both: the free leg is to be stretched along its chain by
|
|
613
763
|
* k = (root y − floor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
614
764
|
* y up), the floor being the supporting ankle's posed y; k is held within
|
|
615
765
|
* `STANCE_BOUNDS` (a free ankle below the floor shortens its leg, one above
|
|
616
|
-
* lengthens it),
|
|
617
|
-
* the
|
|
766
|
+
* lengthens it), its lower bound on the frame the leg's own (`lower`, #95)
|
|
767
|
+
* where the picture folds that leg further, and a frame past them is listed in
|
|
768
|
+
* `over`. Past the tolerance above, the foot is lifted and keeps the pose's
|
|
769
|
+
* height. Past it below (h < −tolerance), the free ankle is far below the
|
|
770
|
+
* supporting one: a support error, not a stance. That frame is never planted
|
|
771
|
+
* both, keeps the pose's height, and is read `below` (#114); before #114 the
|
|
772
|
+
* gap was signed, and a lifted foot read as supporting had the grounded leg
|
|
773
|
+
* shrunk up to it.
|
|
618
774
|
*
|
|
619
|
-
* The state is read with hysteresis (#87)
|
|
620
|
-
*
|
|
621
|
-
* `STANCE_RELEASE × tolerance`, so noise at the threshold does not flip it. A
|
|
775
|
+
* The state is read with hysteresis (#87) on |h|: a planted foot leaves the
|
|
776
|
+
* stance when |h| goes past `tolerance`, and one past it is planted again only
|
|
777
|
+
* under `STANCE_RELEASE × tolerance`, so noise at the threshold does not flip it. A
|
|
622
778
|
* run of one state shorter than `STANCE_MIN_SECONDS` (`secondsOf`, the
|
|
623
779
|
* retarget's timing of a run) between two runs of the other state, on the same
|
|
624
780
|
* free side and with no frame between, takes their state, and is listed with
|
|
625
781
|
* `absorbed`.
|
|
626
782
|
*/
|
|
627
|
-
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number): Stance {
|
|
783
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number, lower?: StanceLower): Stance {
|
|
628
784
|
const n = feet.support.length;
|
|
629
785
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
630
|
-
type Read = { state:
|
|
786
|
+
type Read = { state: StanceState; side: Side; height: number };
|
|
631
787
|
const read: Array<Read | null> = new Array<Read | null>(n).fill(null);
|
|
632
788
|
for (let i = 0; i < n; i++) {
|
|
633
789
|
const s = feet.support[i] ?? null;
|
|
@@ -636,9 +792,12 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
636
792
|
read[i] = { state: 'both', side: f, height: (ankle[s][i]![1] - ankle[f][i]![1]) / feet.legLength[f] };
|
|
637
793
|
}
|
|
638
794
|
// Blocks of consecutive frames with one free side. Inside a block the state changes only when the
|
|
639
|
-
// other state's condition (
|
|
640
|
-
// holds for STANCE_MIN_SECONDS; a shorter crossing with the current state on
|
|
641
|
-
// absorbed. A crossing that runs to the block's end is taken: nothing of the
|
|
795
|
+
// other state's condition (out of the stance: |h| past the tolerance; planted again: |h| under
|
|
796
|
+
// STANCE_RELEASE of it) holds for STANCE_MIN_SECONDS; a shorter crossing with the current state on
|
|
797
|
+
// both sides of it is absorbed. A crossing that runs to the block's end is taken: nothing of the
|
|
798
|
+
// current state follows it. Out of the stance, a frame is `lifted` or `below` by the sign of h; a
|
|
799
|
+
// frame absorbed into it takes the state of the frame before it.
|
|
800
|
+
const away = (h: number): 'lifted' | 'below' => (h > 0 ? 'lifted' : 'below');
|
|
642
801
|
const absorbed: StanceRun[] = [];
|
|
643
802
|
for (let i = 0; i < n; ) {
|
|
644
803
|
const r0 = read[i];
|
|
@@ -648,34 +807,36 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
648
807
|
}
|
|
649
808
|
let end = i;
|
|
650
809
|
while (end + 1 < n && read[end + 1] !== null && read[end + 1]!.side === r0.side) end++;
|
|
651
|
-
let
|
|
810
|
+
let planted = Math.abs(r0.height) <= tolerance;
|
|
652
811
|
let j = i;
|
|
653
812
|
while (j <= end) {
|
|
654
|
-
const crosses = (k: number) => (
|
|
813
|
+
const crosses = (k: number) => (planted ? Math.abs(read[k]!.height) > tolerance : Math.abs(read[k]!.height) <= STANCE_RELEASE * tolerance);
|
|
655
814
|
if (!crosses(j)) {
|
|
656
|
-
read[j]!.state =
|
|
815
|
+
read[j]!.state = planted ? 'both' : away(read[j]!.height);
|
|
657
816
|
j++;
|
|
658
817
|
continue;
|
|
659
818
|
}
|
|
660
819
|
let k = j;
|
|
661
820
|
while (k + 1 <= end && crosses(k + 1)) k++;
|
|
662
|
-
const other = state === 'both' ? 'lifted' : 'both';
|
|
663
821
|
if (k < end && secondsOf([j, k]) < STANCE_MIN_SECONDS) {
|
|
664
822
|
let maxHeight = -Infinity;
|
|
823
|
+
let far = 0;
|
|
665
824
|
for (let x = j; x <= k; x++) {
|
|
666
|
-
read[x]!.state = state;
|
|
825
|
+
read[x]!.state = planted ? 'both' : read[x - 1]!.state;
|
|
667
826
|
maxHeight = Math.max(maxHeight, read[x]!.height);
|
|
827
|
+
if (Math.abs(read[x]!.height) > Math.abs(far)) far = read[x]!.height;
|
|
668
828
|
}
|
|
669
|
-
|
|
829
|
+
const other: StanceState = planted ? away(far) : 'both';
|
|
830
|
+
absorbed.push({ state: other, side: r0.side, frames: [j, k], maxHeight, ...(other === 'below' ? { maxDepth: -far } : {}), k: null, absorbed: true });
|
|
670
831
|
} else {
|
|
671
|
-
|
|
672
|
-
for (let x = j; x <= k; x++) read[x]!.state =
|
|
832
|
+
planted = !planted;
|
|
833
|
+
for (let x = j; x <= k; x++) read[x]!.state = planted ? 'both' : away(read[x]!.height);
|
|
673
834
|
}
|
|
674
835
|
j = k + 1;
|
|
675
836
|
}
|
|
676
837
|
i = end + 1;
|
|
677
838
|
}
|
|
678
|
-
type Row = { state:
|
|
839
|
+
type Row = { state: StanceState; side: Side; height: number; k: number | null; needed: number | null; over: boolean; left: number; lo: number };
|
|
679
840
|
const rows: Array<Row | null> = new Array<Row | null>(n).fill(null);
|
|
680
841
|
for (let i = 0; i < n; i++) {
|
|
681
842
|
const r = read[i];
|
|
@@ -683,8 +844,9 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
683
844
|
const s: Side = r.side === 'r' ? 'l' : 'r';
|
|
684
845
|
const f = r.side;
|
|
685
846
|
const height = r.height;
|
|
686
|
-
|
|
687
|
-
|
|
847
|
+
const lo = lowerOf(lower, f, i);
|
|
848
|
+
if (r.state !== 'both') {
|
|
849
|
+
rows[i] = { state: r.state, side: f, height, k: null, needed: null, over: false, left: 0, lo };
|
|
688
850
|
continue;
|
|
689
851
|
}
|
|
690
852
|
const p = posed(i);
|
|
@@ -693,12 +855,13 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
693
855
|
const a = p.ankle[f][1];
|
|
694
856
|
const ok = root - a > 1e-9 && root - floor > 1e-9;
|
|
695
857
|
const needed = ok ? (root - floor) / (root - a) : null;
|
|
696
|
-
const k = needed === null ? 1 : Math.min(Math.max(needed,
|
|
697
|
-
const over = needed === null || needed <
|
|
858
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, lo), STANCE_BOUNDS[1]);
|
|
859
|
+
const over = needed === null || needed < lo || needed > STANCE_BOUNDS[1];
|
|
698
860
|
const left = Math.abs(root - k * (root - a) - floor);
|
|
699
|
-
rows[i] = { state: 'both', side: f, height, k, needed, over, left };
|
|
861
|
+
rows[i] = { state: 'both', side: f, height, k, needed, over, left, lo };
|
|
700
862
|
if (k !== 1) stretch[i] = { side: f, k };
|
|
701
863
|
}
|
|
864
|
+
const boundOf = (lo: number): { bound?: [number, number] } => (lo < STANCE_BOUNDS[0] ? { bound: [lo, STANCE_BOUNDS[1]] } : {});
|
|
702
865
|
const runs: StanceRun[] = [];
|
|
703
866
|
const over: StanceOver[] = [];
|
|
704
867
|
for (let i = 0; i < n; i++) {
|
|
@@ -708,19 +871,26 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
708
871
|
if (last !== undefined && last.frames[1] === i - 1 && last.state === r.state && last.side === r.side) {
|
|
709
872
|
last.frames[1] = i;
|
|
710
873
|
last.maxHeight = Math.max(last.maxHeight, r.height);
|
|
874
|
+
if (last.maxDepth !== undefined) last.maxDepth = Math.max(last.maxDepth, -r.height);
|
|
711
875
|
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) };
|
|
712
|
-
} 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 } });
|
|
876
|
+
} else runs.push({ state: r.state, side: r.side, frames: [i, i], maxHeight: r.height, ...(r.state === 'below' ? { maxDepth: -r.height } : {}), k: r.k === null ? null : { min: r.k, max: r.k } });
|
|
713
877
|
if (!r.over) continue;
|
|
714
878
|
const o = over[over.length - 1];
|
|
715
879
|
if (o !== undefined && o.frames[1] === i - 1 && o.side === r.side) {
|
|
716
880
|
o.frames[1] = i;
|
|
717
|
-
|
|
881
|
+
const takes = o.needed !== null && (r.needed === null || Math.abs(r.needed - 1) > Math.abs(o.needed - 1));
|
|
882
|
+
o.needed = o.needed === null || r.needed === null ? null : takes ? r.needed : o.needed;
|
|
883
|
+
if (takes) {
|
|
884
|
+
delete o.bound;
|
|
885
|
+
Object.assign(o, boundOf(r.lo));
|
|
886
|
+
}
|
|
718
887
|
o.left = Math.max(o.left, r.left);
|
|
719
|
-
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left });
|
|
888
|
+
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left, ...boundOf(r.lo) });
|
|
720
889
|
}
|
|
721
890
|
for (const x of absorbed) runs.push(x);
|
|
722
891
|
runs.sort((a, b) => a.frames[0] - b.frames[0] || (a.absorbed === true ? 1 : 0) - (b.absorbed === true ? 1 : 0));
|
|
723
|
-
|
|
892
|
+
const range = rangeUsed(lower, rows.flatMap((r, i) => (r !== null && r.state === 'both' ? [{ side: r.side, frame: i }] : [])));
|
|
893
|
+
return { tolerance, release: STANCE_RELEASE, minSeconds: STANCE_MIN_SECONDS, bounds: STANCE_BOUNDS, ...(range === null ? {} : { range }), runs, over, stretch };
|
|
724
894
|
}
|
|
725
895
|
|
|
726
896
|
// --- the blend held on the floor (rig-c/pose#85) ------------------------------
|
|
@@ -734,10 +904,14 @@ export interface BlendRun {
|
|
|
734
904
|
frames: [number, number];
|
|
735
905
|
/** The smallest and largest stretch keyed (bounded). */
|
|
736
906
|
k: { min: number; max: number };
|
|
907
|
+
/** #95: the lowest lower bound on the run's frames, where the leg's own range set it below the default. */
|
|
908
|
+
lower?: number;
|
|
737
909
|
}
|
|
738
910
|
|
|
739
911
|
export interface BlendHold {
|
|
740
912
|
bounds: readonly [number, number];
|
|
913
|
+
/** #95: per side, the lowest lower bound the leg's own range set on a blend frame, where any is below `bounds[0]`. */
|
|
914
|
+
range?: Partial<Record<Side, number>>;
|
|
741
915
|
runs: BlendRun[];
|
|
742
916
|
/** Runs whose stretch needed a factor past the bounds: held at the bound, the remainder left to the blend. */
|
|
743
917
|
over: StanceOver[];
|
|
@@ -751,38 +925,50 @@ export interface BlendHold {
|
|
|
751
925
|
* supporting ankle is off the y its plant holds. Its leg is stretched along its
|
|
752
926
|
* chain about the chain's root, as `--stance` stretches a free leg (`stanceOf`):
|
|
753
927
|
* k = (root y − anchor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
754
|
-
* y up), held within `STANCE_BOUNDS
|
|
928
|
+
* y up), held within `STANCE_BOUNDS` — its lower bound on the frame the leg's own
|
|
929
|
+
* (`lower`, #95) as `stanceOf`'s; a frame past them is listed in `over`, and
|
|
755
930
|
* the remainder stays with the blend. An ankle within `BLEND_HOLD_WITHIN` of the
|
|
756
931
|
* anchor is not stretched.
|
|
757
932
|
*/
|
|
758
|
-
export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: number) => StancePosed): BlendHold {
|
|
933
|
+
export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: number) => StancePosed, lower?: StanceLower): BlendHold {
|
|
759
934
|
const stretch: BlendHold['stretch'] = new Array(n).fill(null);
|
|
760
935
|
const runs: BlendRun[] = [];
|
|
761
936
|
const over: StanceOver[] = [];
|
|
937
|
+
const held: Array<{ side: Side; frame: number }> = [];
|
|
762
938
|
for (const b of blends) {
|
|
763
939
|
const p = posed(b.frame);
|
|
764
940
|
const root = p.root[b.side][1];
|
|
765
941
|
const a = p.ankle[b.side][1];
|
|
766
942
|
if (Math.abs(a - b.y) <= BLEND_HOLD_WITHIN) continue;
|
|
943
|
+
held.push({ side: b.side, frame: b.frame });
|
|
767
944
|
const ok = root - a > 1e-9 && root - b.y > 1e-9;
|
|
768
945
|
const needed = ok ? (root - b.y) / (root - a) : null;
|
|
769
|
-
const
|
|
946
|
+
const lo = lowerOf(lower, b.side, b.frame);
|
|
947
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, lo), STANCE_BOUNDS[1]);
|
|
770
948
|
const left = Math.abs(root - k * (root - a) - b.y);
|
|
949
|
+
const bound = lo < STANCE_BOUNDS[0] ? { bound: [lo, STANCE_BOUNDS[1]] as [number, number] } : {};
|
|
771
950
|
if (k !== 1) {
|
|
772
951
|
stretch[b.frame] = { side: b.side, k };
|
|
773
952
|
const last = runs[runs.length - 1];
|
|
774
953
|
if (last !== undefined && last.side === b.side && last.frames[1] === b.frame - 1) {
|
|
775
954
|
last.frames[1] = b.frame;
|
|
776
955
|
last.k = { min: Math.min(last.k.min, k), max: Math.max(last.k.max, k) };
|
|
777
|
-
|
|
956
|
+
if (lo < STANCE_BOUNDS[0]) last.lower = Math.min(last.lower ?? Infinity, lo);
|
|
957
|
+
} else runs.push({ side: b.side, frames: [b.frame, b.frame], k: { min: k, max: k }, ...(lo < STANCE_BOUNDS[0] ? { lower: lo } : {}) });
|
|
778
958
|
}
|
|
779
|
-
if (needed !== null && needed >=
|
|
959
|
+
if (needed !== null && needed >= lo && needed <= STANCE_BOUNDS[1]) continue;
|
|
780
960
|
const o = over[over.length - 1];
|
|
781
961
|
if (o !== undefined && o.side === b.side && o.frames[1] === b.frame - 1) {
|
|
782
962
|
o.frames[1] = b.frame;
|
|
783
|
-
|
|
963
|
+
const takes = o.needed !== null && (needed === null || Math.abs(needed - 1) > Math.abs(o.needed - 1));
|
|
964
|
+
o.needed = o.needed === null || needed === null ? null : takes ? needed : o.needed;
|
|
965
|
+
if (takes) {
|
|
966
|
+
delete o.bound;
|
|
967
|
+
Object.assign(o, bound);
|
|
968
|
+
}
|
|
784
969
|
o.left = Math.max(o.left, left);
|
|
785
|
-
} else over.push({ side: b.side, frames: [b.frame, b.frame], needed, left });
|
|
970
|
+
} else over.push({ side: b.side, frames: [b.frame, b.frame], needed, left, ...bound });
|
|
786
971
|
}
|
|
787
|
-
|
|
972
|
+
const range = rangeUsed(lower, held);
|
|
973
|
+
return { bounds: STANCE_BOUNDS, ...(range === null ? {} : { range }), runs, over, stretch };
|
|
788
974
|
}
|