@rig-c/pose 0.15.0 → 0.17.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 +66 -9
- package/cli.ts +70 -7
- package/package.json +1 -1
- package/src/clean.ts +339 -7
- package/src/legs.ts +211 -15
- package/src/pose.ts +307 -21
- package/src/retarget.ts +351 -41
- package/src/run.ts +63 -10
- package/src/switch.ts +164 -0
- package/src/verify.ts +29 -4
package/src/legs.ts
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* every frame one foot supports: the one whose reference ankle is lower in the
|
|
26
26
|
* picture, changing only when the other ankle has been ≥ `PLANT_SWITCH` leg
|
|
27
27
|
* lengths lower for ≥ `PLANT_SWITCH_SECONDS`. The supporting foot is held where
|
|
28
|
-
* it was on the frame it took support,
|
|
28
|
+
* it was on the frame it took support, moved in y onto the one floor every span
|
|
29
|
+
* shares (rig-c/pose#85, `plantHip`), and the hip is placed so it stays there,
|
|
29
30
|
* through the posed (scaled) legs. A change of support blends the two
|
|
30
31
|
* derivations over `PLANT_BLEND` frames. A jump — both ankles and mid_hip more
|
|
31
32
|
* than `JUMP_RISE` leg lengths above their rolling floor (the 95th-percentile
|
|
@@ -431,15 +432,69 @@ export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side,
|
|
|
431
432
|
return { legLength, support, supports, jumps, steps, changes };
|
|
432
433
|
}
|
|
433
434
|
|
|
435
|
+
/** One support span's shift onto the floor (rig-c/pose#85): its side, its frames, and floor − its planted height, world units. */
|
|
436
|
+
export interface FloorShift {
|
|
437
|
+
side: Side;
|
|
438
|
+
frames: [number, number];
|
|
439
|
+
shift: number;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Where the planted feet were put (rig-c/pose#85): the floor's world y, whether `--floor` gave it, and each support span's shift. */
|
|
443
|
+
export interface Floor {
|
|
444
|
+
y: number;
|
|
445
|
+
given: boolean;
|
|
446
|
+
shifts: FloorShift[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The hip's world origin on every frame, from the supporting foot (header),
|
|
451
|
+
* with every support span on one floor (rig-c/pose#85). `offset(i, side)` is
|
|
452
|
+
* the ankle's posed position relative to the hip's origin on frame i;
|
|
453
|
+
* `travel(i)` the pose's own travel of the hip from frame 0, world. `start` is
|
|
454
|
+
* the hip's origin on frame 0 (its setup origin).
|
|
455
|
+
*
|
|
456
|
+
* **One floor.** A span's planted height is the world y its supporting ankle is
|
|
457
|
+
* held at when it is held where the picture puts it on the frame it takes
|
|
458
|
+
* support. A 2D rig has one floor, so every span is planted on it instead: the
|
|
459
|
+
* floor is `floor` when given (`--floor`, world units), and otherwise the median
|
|
460
|
+
* of the spans' planted heights; each span's anchor moves in y by floor − its
|
|
461
|
+
* planted height, and the hip with it, so everything above follows. A change of
|
|
462
|
+
* support still blends the two derivations over `PLANT_BLEND` frames, now
|
|
463
|
+
* between two spans on the floor; a jump follows the pose's travel from the hip
|
|
464
|
+
* it left (on the floor) and lands on the floor. On a blend frame the hip is
|
|
465
|
+
* between the two derivations, so the supporting ankle is off its anchor:
|
|
466
|
+
* `blends` lists those frames with the anchor's y, and `holdBlend` stretches
|
|
467
|
+
* the supporting leg to keep it there. Null `floor` in the result: there is no
|
|
468
|
+
* support span.
|
|
469
|
+
*/
|
|
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);
|
|
472
|
+
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 };
|
|
474
|
+
if (heights.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
|
|
475
|
+
const y = floor ?? percentile(heights as number[], 0.5);
|
|
476
|
+
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
|
+
return { hip: placed.hip, floor: { y, given: floor !== undefined, shifts }, blends: placed.blends };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 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. */
|
|
482
|
+
export interface BlendFrame {
|
|
483
|
+
frame: number;
|
|
484
|
+
side: Side;
|
|
485
|
+
y: number;
|
|
486
|
+
}
|
|
487
|
+
|
|
434
488
|
/**
|
|
435
|
-
* The hip
|
|
436
|
-
* `
|
|
437
|
-
*
|
|
438
|
-
* `start` is the hip's origin on frame 0 (its setup origin).
|
|
489
|
+
* The hip placed from the supporting foot, each plant's anchor held where the
|
|
490
|
+
* picture puts it (`floorY` null) or at world y `floorY`; and each plant's
|
|
491
|
+
* picture anchor y, by the frame its support span starts on.
|
|
439
492
|
*/
|
|
440
|
-
|
|
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[] } {
|
|
441
494
|
const n = feet.support.length;
|
|
442
495
|
const hip: Pt[] = [];
|
|
496
|
+
const anchors = new Map<number, number>();
|
|
497
|
+
const blends: BlendFrame[] = [];
|
|
443
498
|
/** 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. */
|
|
444
499
|
type Mode = { kind: 'plant'; side: Side; anchor: Pt } | { kind: 'travel'; from: number; at: Pt };
|
|
445
500
|
const of = (mode: Mode, i: number): Pt => {
|
|
@@ -451,6 +506,11 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
451
506
|
const b = travel(mode.from);
|
|
452
507
|
return [mode.at[0] + a[0] - b[0], mode.at[1] + a[1] - b[1]];
|
|
453
508
|
};
|
|
509
|
+
/** A plant from frame i: its anchor's x where the foot is, its y there or on the floor. */
|
|
510
|
+
const plant = (i: number, side: Side, at: Pt): Mode => {
|
|
511
|
+
anchors.set(i, at[1]);
|
|
512
|
+
return { kind: 'plant', side, anchor: [at[0], floorY ?? at[1]] };
|
|
513
|
+
};
|
|
454
514
|
let mode: Mode | null = null;
|
|
455
515
|
let from: Mode | null = null;
|
|
456
516
|
let blendAt = -1;
|
|
@@ -460,7 +520,7 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
460
520
|
if (s === null) mode = { kind: 'travel', from: 0, at: start };
|
|
461
521
|
else {
|
|
462
522
|
const o = offset(0, s);
|
|
463
|
-
mode =
|
|
523
|
+
mode = plant(0, s, [start[0] + o[0], start[1] + o[1]]);
|
|
464
524
|
}
|
|
465
525
|
} else if (s === null && mode!.kind === 'plant') {
|
|
466
526
|
mode = { kind: 'travel', from: i - 1, at: hip[i - 1]! };
|
|
@@ -471,7 +531,7 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
471
531
|
const here = of(mode!, i);
|
|
472
532
|
const o = offset(i, s);
|
|
473
533
|
from = mode;
|
|
474
|
-
mode =
|
|
534
|
+
mode = plant(i, s, [here[0] + o[0], here[1] + o[1]]);
|
|
475
535
|
blendAt = i;
|
|
476
536
|
}
|
|
477
537
|
const to = of(mode!, i);
|
|
@@ -479,12 +539,14 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
479
539
|
const w = (i - blendAt + 1) / (PLANT_BLEND + 1);
|
|
480
540
|
const f = of(from, i);
|
|
481
541
|
hip.push([f[0] + (to[0] - f[0]) * w, f[1] + (to[1] - f[1]) * w]);
|
|
542
|
+
const m = mode!;
|
|
543
|
+
if (m.kind === 'plant') blends.push({ frame: i, side: m.side, y: m.anchor[1] });
|
|
482
544
|
} else {
|
|
483
545
|
from = null;
|
|
484
546
|
hip.push(to);
|
|
485
547
|
}
|
|
486
548
|
}
|
|
487
|
-
return hip;
|
|
549
|
+
return { hip, anchors, blends };
|
|
488
550
|
}
|
|
489
551
|
|
|
490
552
|
// --- stance (rig-c/pose#60) --------------------------------------------------
|
|
@@ -492,6 +554,11 @@ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel:
|
|
|
492
554
|
/** 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
555
|
export const STANCE_BOUNDS: readonly [number, number] = [0.92, 1.08];
|
|
494
556
|
|
|
557
|
+
/** 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
|
+
export const STANCE_RELEASE = 0.8;
|
|
559
|
+
/** 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
|
+
export const STANCE_MIN_SECONDS = 0.1;
|
|
561
|
+
|
|
495
562
|
/** A run of frames with one free side in one state. */
|
|
496
563
|
export interface StanceRun {
|
|
497
564
|
/** `both`: the free ankle within the tolerance of the floor, so both feet are planted; `lifted`: above it, the pose's height kept. */
|
|
@@ -503,6 +570,8 @@ export interface StanceRun {
|
|
|
503
570
|
maxHeight: number;
|
|
504
571
|
/** `both`: the smallest and largest stretch keyed (bounded); `lifted`: null. */
|
|
505
572
|
k: { min: number; max: number } | null;
|
|
573
|
+
/** #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. */
|
|
574
|
+
absorbed?: true;
|
|
506
575
|
}
|
|
507
576
|
|
|
508
577
|
/** A run of planted-both frames whose stretch needed a factor past the bounds (or could not reach: `needed` null). */
|
|
@@ -518,6 +587,10 @@ export interface StanceOver {
|
|
|
518
587
|
export interface Stance {
|
|
519
588
|
/** The tolerance, in leg lengths (FEET's: the median picture hip → ankle of the free side). */
|
|
520
589
|
tolerance: number;
|
|
590
|
+
/** A lifted foot is planted again under `release × tolerance` (#87). */
|
|
591
|
+
release: number;
|
|
592
|
+
/** The shortest run kept as its own state, seconds (#87). */
|
|
593
|
+
minSeconds: number;
|
|
521
594
|
bounds: readonly [number, number];
|
|
522
595
|
runs: StanceRun[];
|
|
523
596
|
over: StanceOver[];
|
|
@@ -542,18 +615,75 @@ export interface StancePosed {
|
|
|
542
615
|
* `STANCE_BOUNDS` (a free ankle below the floor shortens its leg, one above
|
|
543
616
|
* lengthens it), and a frame past them is listed in `over`. Above the tolerance
|
|
544
617
|
* the foot is lifted and keeps the pose's height.
|
|
618
|
+
*
|
|
619
|
+
* The state is read with hysteresis (#87): a planted foot is lifted when its
|
|
620
|
+
* height goes past `tolerance`, and a lifted one is planted again only under
|
|
621
|
+
* `STANCE_RELEASE × tolerance`, so noise at the threshold does not flip it. A
|
|
622
|
+
* run of one state shorter than `STANCE_MIN_SECONDS` (`secondsOf`, the
|
|
623
|
+
* retarget's timing of a run) between two runs of the other state, on the same
|
|
624
|
+
* free side and with no frame between, takes their state, and is listed with
|
|
625
|
+
* `absorbed`.
|
|
545
626
|
*/
|
|
546
|
-
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed): Stance {
|
|
627
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number) => StancePosed, secondsOf: (run: [number, number]) => number): Stance {
|
|
547
628
|
const n = feet.support.length;
|
|
548
629
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
549
|
-
type
|
|
550
|
-
const
|
|
630
|
+
type Read = { state: 'both' | 'lifted'; side: Side; height: number };
|
|
631
|
+
const read: Array<Read | null> = new Array<Read | null>(n).fill(null);
|
|
551
632
|
for (let i = 0; i < n; i++) {
|
|
552
633
|
const s = feet.support[i] ?? null;
|
|
553
634
|
if (s === null) continue;
|
|
554
635
|
const f: Side = s === 'r' ? 'l' : 'r';
|
|
555
|
-
|
|
556
|
-
|
|
636
|
+
read[i] = { state: 'both', side: f, height: (ankle[s][i]![1] - ankle[f][i]![1]) / feet.legLength[f] };
|
|
637
|
+
}
|
|
638
|
+
// Blocks of consecutive frames with one free side. Inside a block the state changes only when the
|
|
639
|
+
// other state's condition (lifted: past the tolerance; planted again: under STANCE_RELEASE of it)
|
|
640
|
+
// holds for STANCE_MIN_SECONDS; a shorter crossing with the current state on both sides of it is
|
|
641
|
+
// absorbed. A crossing that runs to the block's end is taken: nothing of the current state follows it.
|
|
642
|
+
const absorbed: StanceRun[] = [];
|
|
643
|
+
for (let i = 0; i < n; ) {
|
|
644
|
+
const r0 = read[i];
|
|
645
|
+
if (r0 === null || r0 === undefined) {
|
|
646
|
+
i++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
let end = i;
|
|
650
|
+
while (end + 1 < n && read[end + 1] !== null && read[end + 1]!.side === r0.side) end++;
|
|
651
|
+
let state: 'both' | 'lifted' = r0.height > tolerance ? 'lifted' : 'both';
|
|
652
|
+
let j = i;
|
|
653
|
+
while (j <= end) {
|
|
654
|
+
const crosses = (k: number) => (state === 'both' ? read[k]!.height > tolerance : read[k]!.height <= STANCE_RELEASE * tolerance);
|
|
655
|
+
if (!crosses(j)) {
|
|
656
|
+
read[j]!.state = state;
|
|
657
|
+
j++;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
let k = j;
|
|
661
|
+
while (k + 1 <= end && crosses(k + 1)) k++;
|
|
662
|
+
const other = state === 'both' ? 'lifted' : 'both';
|
|
663
|
+
if (k < end && secondsOf([j, k]) < STANCE_MIN_SECONDS) {
|
|
664
|
+
let maxHeight = -Infinity;
|
|
665
|
+
for (let x = j; x <= k; x++) {
|
|
666
|
+
read[x]!.state = state;
|
|
667
|
+
maxHeight = Math.max(maxHeight, read[x]!.height);
|
|
668
|
+
}
|
|
669
|
+
absorbed.push({ state: other, side: r0.side, frames: [j, k], maxHeight, k: null, absorbed: true });
|
|
670
|
+
} else {
|
|
671
|
+
state = other;
|
|
672
|
+
for (let x = j; x <= k; x++) read[x]!.state = state;
|
|
673
|
+
}
|
|
674
|
+
j = k + 1;
|
|
675
|
+
}
|
|
676
|
+
i = end + 1;
|
|
677
|
+
}
|
|
678
|
+
type Row = { state: 'both' | 'lifted'; side: Side; height: number; k: number | null; needed: number | null; over: boolean; left: number };
|
|
679
|
+
const rows: Array<Row | null> = new Array<Row | null>(n).fill(null);
|
|
680
|
+
for (let i = 0; i < n; i++) {
|
|
681
|
+
const r = read[i];
|
|
682
|
+
if (r === null || r === undefined) continue;
|
|
683
|
+
const s: Side = r.side === 'r' ? 'l' : 'r';
|
|
684
|
+
const f = r.side;
|
|
685
|
+
const height = r.height;
|
|
686
|
+
if (r.state === 'lifted') {
|
|
557
687
|
rows[i] = { state: 'lifted', side: f, height, k: null, needed: null, over: false, left: 0 };
|
|
558
688
|
continue;
|
|
559
689
|
}
|
|
@@ -588,5 +718,71 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
588
718
|
o.left = Math.max(o.left, r.left);
|
|
589
719
|
} else over.push({ side: r.side, frames: [i, i], needed: r.needed, left: r.left });
|
|
590
720
|
}
|
|
591
|
-
|
|
721
|
+
for (const x of absorbed) runs.push(x);
|
|
722
|
+
runs.sort((a, b) => a.frames[0] - b.frames[0] || (a.absorbed === true ? 1 : 0) - (b.absorbed === true ? 1 : 0));
|
|
723
|
+
return { tolerance, release: STANCE_RELEASE, minSeconds: STANCE_MIN_SECONDS, bounds: STANCE_BOUNDS, runs, over, stretch };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// --- the blend held on the floor (rig-c/pose#85) ------------------------------
|
|
727
|
+
|
|
728
|
+
/** A supporting ankle already within this many world units of its anchor on a blend frame is left as it is: the landing check's origin tolerance (`verify.ts` ORIGIN_TOLERANCE). */
|
|
729
|
+
export const BLEND_HOLD_WITHIN = 0.01;
|
|
730
|
+
|
|
731
|
+
/** A run of blend frames on one supporting side whose leg was stretched. */
|
|
732
|
+
export interface BlendRun {
|
|
733
|
+
side: Side;
|
|
734
|
+
frames: [number, number];
|
|
735
|
+
/** The smallest and largest stretch keyed (bounded). */
|
|
736
|
+
k: { min: number; max: number };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export interface BlendHold {
|
|
740
|
+
bounds: readonly [number, number];
|
|
741
|
+
runs: BlendRun[];
|
|
742
|
+
/** Runs whose stretch needed a factor past the bounds: held at the bound, the remainder left to the blend. */
|
|
743
|
+
over: StanceOver[];
|
|
744
|
+
/** Per frame: the supporting side and the stretch keyed on it, or null. */
|
|
745
|
+
stretch: Array<{ side: Side; k: number } | null>;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The supporting foot does not leave the floor during a blend (rig-c/pose#85).
|
|
750
|
+
* On a blend frame the hip is between the old derivation and the new, so the
|
|
751
|
+
* supporting ankle is off the y its plant holds. Its leg is stretched along its
|
|
752
|
+
* chain about the chain's root, as `--stance` stretches a free leg (`stanceOf`):
|
|
753
|
+
* k = (root y − anchor y) / (root y − ankle y) on the posed rig (`posed`, world,
|
|
754
|
+
* y up), held within `STANCE_BOUNDS`; a frame past them is listed in `over`, and
|
|
755
|
+
* the remainder stays with the blend. An ankle within `BLEND_HOLD_WITHIN` of the
|
|
756
|
+
* anchor is not stretched.
|
|
757
|
+
*/
|
|
758
|
+
export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: number) => StancePosed): BlendHold {
|
|
759
|
+
const stretch: BlendHold['stretch'] = new Array(n).fill(null);
|
|
760
|
+
const runs: BlendRun[] = [];
|
|
761
|
+
const over: StanceOver[] = [];
|
|
762
|
+
for (const b of blends) {
|
|
763
|
+
const p = posed(b.frame);
|
|
764
|
+
const root = p.root[b.side][1];
|
|
765
|
+
const a = p.ankle[b.side][1];
|
|
766
|
+
if (Math.abs(a - b.y) <= BLEND_HOLD_WITHIN) continue;
|
|
767
|
+
const ok = root - a > 1e-9 && root - b.y > 1e-9;
|
|
768
|
+
const needed = ok ? (root - b.y) / (root - a) : null;
|
|
769
|
+
const k = needed === null ? 1 : Math.min(Math.max(needed, STANCE_BOUNDS[0]), STANCE_BOUNDS[1]);
|
|
770
|
+
const left = Math.abs(root - k * (root - a) - b.y);
|
|
771
|
+
if (k !== 1) {
|
|
772
|
+
stretch[b.frame] = { side: b.side, k };
|
|
773
|
+
const last = runs[runs.length - 1];
|
|
774
|
+
if (last !== undefined && last.side === b.side && last.frames[1] === b.frame - 1) {
|
|
775
|
+
last.frames[1] = b.frame;
|
|
776
|
+
last.k = { min: Math.min(last.k.min, k), max: Math.max(last.k.max, k) };
|
|
777
|
+
} else runs.push({ side: b.side, frames: [b.frame, b.frame], k: { min: k, max: k } });
|
|
778
|
+
}
|
|
779
|
+
if (needed !== null && needed >= STANCE_BOUNDS[0] && needed <= STANCE_BOUNDS[1]) continue;
|
|
780
|
+
const o = over[over.length - 1];
|
|
781
|
+
if (o !== undefined && o.side === b.side && o.frames[1] === b.frame - 1) {
|
|
782
|
+
o.frames[1] = b.frame;
|
|
783
|
+
o.needed = o.needed === null || needed === null ? null : Math.abs(needed - 1) > Math.abs(o.needed - 1) ? needed : o.needed;
|
|
784
|
+
o.left = Math.max(o.left, left);
|
|
785
|
+
} else over.push({ side: b.side, frames: [b.frame, b.frame], needed, left });
|
|
786
|
+
}
|
|
787
|
+
return { bounds: STANCE_BOUNDS, runs, over, stretch };
|
|
592
788
|
}
|