@rig-c/pose 0.41.0 → 0.43.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 +21 -11
- package/cli.ts +25 -8
- package/package.json +1 -1
- package/src/clean.ts +5 -4
- package/src/contacts.ts +250 -0
- package/src/depth.ts +53 -13
- package/src/draworder.ts +74 -23
- package/src/legs.ts +7 -0
- package/src/pelvis.ts +263 -4
- package/src/pose.ts +4 -3
- package/src/retarget.ts +158 -14
- package/src/run.ts +26 -9
- package/src/switch.ts +11 -4
package/src/pelvis.ts
CHANGED
|
@@ -59,7 +59,7 @@ type Pt = [number, number];
|
|
|
59
59
|
type RGB = [number, number, number];
|
|
60
60
|
|
|
61
61
|
/** The two readers. */
|
|
62
|
-
export const PELVIS_MODES = ['silhouette', 'waistband'] as const;
|
|
62
|
+
export const PELVIS_MODES = ['silhouette', 'waistband', 'below-cover'] as const;
|
|
63
63
|
export type PelvisMode = (typeof PELVIS_MODES)[number];
|
|
64
64
|
export const PELVIS_KEYS_SPEC = 'rig-pose-pelvis-keys/1';
|
|
65
65
|
|
|
@@ -85,13 +85,41 @@ export const MIN_SPAN = 6;
|
|
|
85
85
|
export const CLIMB_SPREAD = 2;
|
|
86
86
|
/** The band's top is climbed against at most this many garment colours (rig-c/pose#318): a paletted garment's own, or a painted one's fullest cells. */
|
|
87
87
|
export const MAX_CLOTH = 16;
|
|
88
|
+
/** rig-c/pose#342: the below-cover cues read the same line when their tilts lie within this many degrees (max − min). */
|
|
89
|
+
export const CUE_SPREAD = 8;
|
|
90
|
+
/** ... a leg's colour is sampled this far along its thigh, hip → knee. */
|
|
91
|
+
export const LEG_AT = 0.7;
|
|
92
|
+
/** ... the hem and a thigh root are where the garment stops for this many px (rows, or along the thigh) in a row. */
|
|
93
|
+
export const LEAVE = 3;
|
|
88
94
|
|
|
89
95
|
/**
|
|
90
96
|
* One frame's reading: the tilt (Spine degrees, r_hip → l_hip), or why it was declined. A waistband
|
|
91
97
|
* read through the window mean says so (`mean`), and one whose colour was found below the hips says
|
|
92
98
|
* how far (`below`, px); one whose band top lay above the colour's edge in some columns says how many (`raised`, #318).
|
|
93
99
|
*/
|
|
94
|
-
export type Reading = { tilt: number; cover: number; rms: number; mean?: true; below?: number; raised?: number } | { kind: string; why: string };
|
|
100
|
+
export type Reading = { tilt: number; cover: number; rms: number; mean?: true; below?: number; raised?: number; cues?: CueTilts } | { kind: string; why: string };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* rig-c/pose#342: the below-cover cues of one frame, Spine degrees r_hip → l_hip; null where the cue was not read. `band` is
|
|
104
|
+
* "covered" when something other than the ground lies over the band's top. `hips` is the keypoints' line: reported, not fitted.
|
|
105
|
+
*/
|
|
106
|
+
export interface CueTilts {
|
|
107
|
+
hem: number | null;
|
|
108
|
+
roots: number | null;
|
|
109
|
+
band: number | 'covered' | null;
|
|
110
|
+
hips: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** rig-c/pose#342: a below-cover reading's figures and points (the picture's), and why each cue not read was not. */
|
|
114
|
+
export interface CueDetail {
|
|
115
|
+
tilts: CueTilts;
|
|
116
|
+
why: { hem?: string; roots?: string; band?: string };
|
|
117
|
+
garment: RGB;
|
|
118
|
+
legs: { r: RGB | null; l: RGB | null };
|
|
119
|
+
hem: { pts: Pt[]; fit: { m: number; q: number; rms: number } | null };
|
|
120
|
+
roots: { r: Pt | null; l: Pt | null; paths: Array<[Pt, Pt]> };
|
|
121
|
+
band: { m: number; q: number } | null;
|
|
122
|
+
}
|
|
95
123
|
|
|
96
124
|
/** The frame as RGB over white, its background colour, and which pixels are masked (forearms, hands). */
|
|
97
125
|
interface Frame {
|
|
@@ -194,7 +222,38 @@ export function readPelvis(plate: Plate, joints: Partial<Record<Body18, Joint>>,
|
|
|
194
222
|
const f = frameOf(plate, arms, Math.max(3, ARM_RADIUS * span));
|
|
195
223
|
if (x0 < 0 || x1 >= f.w || c[1] < 0 || c[1] >= f.h) return { kind: 'hips outside the frame', why: `the hips (${c[0].toFixed(1)}, ${c[1].toFixed(1)}) lie outside the ${f.w}×${f.h} frame` };
|
|
196
224
|
const cols = x1 - x0 + 1;
|
|
197
|
-
|
|
225
|
+
if (mode === 'silhouette') return silhouette(f, c, span, x0, x1, cols, rHip, lHip);
|
|
226
|
+
if (mode === 'waistband') return waistband(f, c, span, x0, x1, cols, rHip, lHip);
|
|
227
|
+
return belowCover(f, c, span, x0, x1, cols, rHip, lHip, pt).reading;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* rig-c/pose#342: the below-cover reading of one frame with every cue's figures and points, for a picture of it. Null when
|
|
232
|
+
* the frame is declined before any cue is read (no hips, side-on, outside the frame); the reading is `readPelvis`'s.
|
|
233
|
+
*/
|
|
234
|
+
export function readBelowCover(plate: Plate, joints: Partial<Record<Body18, Joint>>): { reading: Reading; detail: CueDetail | null } {
|
|
235
|
+
const reading = readPelvis(plate, joints, 'below-cover');
|
|
236
|
+
const pt = (n: Body18): Pt | null => {
|
|
237
|
+
const j = joints[n];
|
|
238
|
+
return j === undefined || j.state === 'missing' || j.at === undefined ? null : [j.at[0], j.at[1]];
|
|
239
|
+
};
|
|
240
|
+
const rHip = pt('r_hip');
|
|
241
|
+
const lHip = pt('l_hip');
|
|
242
|
+
if (rHip === null || lHip === null) return { reading, detail: null };
|
|
243
|
+
const span = Math.hypot(lHip[0] - rHip[0], lHip[1] - rHip[1]);
|
|
244
|
+
const x0 = Math.round(Math.min(rHip[0], lHip[0]));
|
|
245
|
+
const x1 = Math.round(Math.max(rHip[0], lHip[0]));
|
|
246
|
+
if (span < MIN_SPAN || x1 - x0 + 1 < MIN_SPAN) return { reading, detail: null };
|
|
247
|
+
const c: Pt = [(rHip[0] + lHip[0]) / 2, (rHip[1] + lHip[1]) / 2];
|
|
248
|
+
const arms: Array<[Pt, Pt]> = [];
|
|
249
|
+
for (const [e, w] of [['r_elbow', 'r_wrist'], ['l_elbow', 'l_wrist']] as const) {
|
|
250
|
+
const a = pt(e);
|
|
251
|
+
const b = pt(w);
|
|
252
|
+
if (a !== null && b !== null) arms.push([a, b]);
|
|
253
|
+
}
|
|
254
|
+
const f = frameOf(plate, arms, Math.max(3, ARM_RADIUS * span));
|
|
255
|
+
if (x0 < 0 || x1 >= f.w || c[1] < 0 || c[1] >= f.h) return { reading, detail: null };
|
|
256
|
+
return { reading, detail: belowCover(f, c, span, x0, x1, x1 - x0 + 1, rHip, lHip, pt).detail };
|
|
198
257
|
}
|
|
199
258
|
|
|
200
259
|
function silhouette(f: Frame, c: Pt, span: number, x0: number, x1: number, cols: number, rHip: Pt, lHip: Pt): Reading {
|
|
@@ -272,7 +331,8 @@ function meanOf(f: Frame): (x: number, y: number) => RGB | null {
|
|
|
272
331
|
};
|
|
273
332
|
}
|
|
274
333
|
|
|
275
|
-
|
|
334
|
+
/** `out.fit`, when given, receives the edge read (rig-c/pose#342 looks at what lies above it). */
|
|
335
|
+
function waistband(f: Frame, c: Pt, span: number, x0: number, x1: number, cols: number, rHip: Pt, lHip: Pt, out?: { fit?: { m: number; q: number } }): Reading {
|
|
276
336
|
// The garment's colour: the median over the unmasked pixels of a patch at the hips' midpoint (or below it), as they are or through the window mean.
|
|
277
337
|
const r = Math.max(2, 0.3 * span);
|
|
278
338
|
const raw = (x: number, y: number): RGB | null => f.rgb(x, y);
|
|
@@ -341,6 +401,7 @@ function waistband(f: Frame, c: Pt, span: number, x0: number, x1: number, cols:
|
|
|
341
401
|
if (Math.abs(Math.atan(fit.m)) * (180 / Math.PI) > 45) return { kind: 'waistband too steep', why: `the garment's top edge runs ${(Math.atan(fit.m) * (180 / Math.PI)).toFixed(1)}° from level; a waistband is within 45°` };
|
|
342
402
|
const off = notTheBand(f, fit, tops, top, colourAt);
|
|
343
403
|
if (off !== null) return off;
|
|
404
|
+
if (out !== undefined) out.fit = { m: fit.m, q: fit.q };
|
|
344
405
|
return { tilt: tiltOf(fit.m, rHip, lHip), cover: share, rms: fit.rms, ...(isMean ? { mean: true as const } : {}), ...(below > 0 ? { below } : {}), ...(raised > 0 ? { raised } : {}) };
|
|
345
406
|
}
|
|
346
407
|
|
|
@@ -532,6 +593,193 @@ function notTheBand(f: Frame, fit: { m: number; q: number }, tops: Pt[], top: nu
|
|
|
532
593
|
};
|
|
533
594
|
}
|
|
534
595
|
|
|
596
|
+
/**
|
|
597
|
+
* rig-c/pose#342: the pelvis read below a cover. A garment hanging over the waistband (a sweater's hem) hides the band's top
|
|
598
|
+
* and hangs level whatever the pelvis does, so the band-top reader reads the cover. What the cover cannot hide is read:
|
|
599
|
+
*
|
|
600
|
+
* - `hem`: the bottomwear's lower hem across the hip span — in each column, from the hips' row down, the last garment row
|
|
601
|
+
* before `LEAVE` rows that are not garment; a line fitted through them (≥ `MIN_COVER` of the columns);
|
|
602
|
+
* - `roots`: where each thigh leaves the garment — along hip → knee (keypoints), the last garment point before `LEAVE` px
|
|
603
|
+
* that are not; the line from the right root to the left;
|
|
604
|
+
* - `band`: the waistband reader's band top, used only when the ground lies above it (nothing covers it);
|
|
605
|
+
* - `hips`: the keypoints' line, reported but not fitted — a detector's hips keep a fraction of a drawn tilt (#288).
|
|
606
|
+
*
|
|
607
|
+
* Every colour is the window mean (`DITHER_RADIUS`); a pixel is garment when it is nearest the garment's colour among the
|
|
608
|
+
* garment's, each leg's (sampled `LEG_AT` along its thigh) and the ground's, so both shades of a cloth and the ink on it
|
|
609
|
+
* are garment and skin or ground under the hem are not. The tilt is the mean of the cues fitted (hem, roots, an
|
|
610
|
+
* uncovered band); fewer than two, or two that differ by more than `CUE_SPREAD`, decline the frame by name.
|
|
611
|
+
*/
|
|
612
|
+
function belowCover(f: Frame, c: Pt, span: number, x0: number, x1: number, cols: number, rHip: Pt, lHip: Pt, pt: (n: Body18) => Pt | null): { reading: Reading; detail: CueDetail | null } {
|
|
613
|
+
const r = Math.max(2, 0.3 * span);
|
|
614
|
+
const mean = meanOf(f);
|
|
615
|
+
const disc = (at: Pt): RGB | null => {
|
|
616
|
+
const ps: RGB[] = [];
|
|
617
|
+
for (let y = Math.floor(at[1] - r); y <= Math.ceil(at[1] + r); y++) {
|
|
618
|
+
for (let x = Math.floor(at[0] - r); x <= Math.ceil(at[0] + r); x++) {
|
|
619
|
+
if (x < 0 || y < 0 || x >= f.w || y >= f.h || Math.hypot(x - at[0], y - at[1]) > r || f.masked(x, y)) continue;
|
|
620
|
+
const p = mean(x, y);
|
|
621
|
+
if (p !== null) ps.push(p);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return ps.length === 0 ? null : ([0, 1, 2].map((k) => median(ps.map((p) => p[k]!))) as RGB);
|
|
625
|
+
};
|
|
626
|
+
const holds = (at: Pt, g: RGB) => {
|
|
627
|
+
let n = 0;
|
|
628
|
+
let k = 0;
|
|
629
|
+
for (let y = Math.floor(at[1] - r); y <= Math.ceil(at[1] + r); y++) {
|
|
630
|
+
for (let x = Math.floor(at[0] - r); x <= Math.ceil(at[0] + r); x++) {
|
|
631
|
+
if (x < 0 || y < 0 || x >= f.w || y >= f.h || Math.hypot(x - at[0], y - at[1]) > r || f.masked(x, y)) continue;
|
|
632
|
+
const p = mean(x, y);
|
|
633
|
+
if (p === null) continue;
|
|
634
|
+
n++;
|
|
635
|
+
if (diff(p, g) <= GARMENT_DIFF) k++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return n > 0 && k >= HOLD * n;
|
|
639
|
+
};
|
|
640
|
+
const mid = disc(c);
|
|
641
|
+
if (mid === null) return { reading: { kind: 'hips behind a forearm', why: 'the hips\' midpoint is covered by a forearm; no garment colour to read' }, detail: null };
|
|
642
|
+
let g: RGB = mid;
|
|
643
|
+
for (const drop of [0, r, 2 * r]) {
|
|
644
|
+
const at: Pt = [c[0], c[1] + drop];
|
|
645
|
+
const p = drop === 0 ? mid : disc(at);
|
|
646
|
+
if (p !== null && diff(p, f.bg) > FIGURE_DIFF && holds(at, p)) {
|
|
647
|
+
g = p;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (diff(g, f.bg) <= FIGURE_DIFF) return { reading: { kind: 'no garment over the hips', why: `the colour at the hips' midpoint (${g.map((v) => v.toFixed(0)).join(', ')}) is the background's (${f.bg.map((v) => v.toFixed(0)).join(', ')})` }, detail: null };
|
|
652
|
+
const knee = { r: pt('r_knee'), l: pt('l_knee') };
|
|
653
|
+
const hip = { r: rHip, l: lHip };
|
|
654
|
+
const legs = { r: null as RGB | null, l: null as RGB | null };
|
|
655
|
+
for (const s of ['r', 'l'] as const) {
|
|
656
|
+
const k = knee[s];
|
|
657
|
+
if (k === null) continue;
|
|
658
|
+
const L = disc([hip[s][0] + LEG_AT * (k[0] - hip[s][0]), hip[s][1] + LEG_AT * (k[1] - hip[s][1])]);
|
|
659
|
+
if (L !== null && diff(L, g) > GARMENT_DIFF) legs[s] = L;
|
|
660
|
+
}
|
|
661
|
+
const others = [f.bg, ...(legs.r !== null ? [legs.r] : []), ...(legs.l !== null ? [legs.l] : [])];
|
|
662
|
+
if (others.length === 1) return { reading: { kind: 'no leg below the garment', why: `neither thigh carries a colour of its own ${LEG_AT} of the way to its knee (${knee.r === null || knee.l === null ? 'a knee has no position' : 'both within ' + GARMENT_DIFF + ' of the garment\'s'}); nothing below the garment to tell its hem by` }, detail: null };
|
|
663
|
+
// Garment: nearest the garment's colour among it, the legs' and the ground's (a tie is garment). Null where every pixel of the window is masked.
|
|
664
|
+
const isGarment = (x: number, y: number): boolean | null => {
|
|
665
|
+
if (x < 0 || y < 0 || x >= f.w || y >= f.h || f.masked(x, y)) return null;
|
|
666
|
+
const p = mean(x, y);
|
|
667
|
+
if (p === null) return null;
|
|
668
|
+
return diff(p, g) <= Math.min(...others.map((o) => diff(p, o)));
|
|
669
|
+
};
|
|
670
|
+
const why: CueDetail['why'] = {};
|
|
671
|
+
// The hem.
|
|
672
|
+
const hemPts: Pt[] = [];
|
|
673
|
+
const yEnd = Math.min(f.h - 1, Math.floor(c[1] + 2.5 * span));
|
|
674
|
+
for (let x = x0; x <= x1; x++) {
|
|
675
|
+
let inside = false;
|
|
676
|
+
let last = -1;
|
|
677
|
+
let out = 0;
|
|
678
|
+
for (let y = Math.max(0, Math.round(c[1])); y <= yEnd; y++) {
|
|
679
|
+
const gm = isGarment(x, y);
|
|
680
|
+
if (gm === null) {
|
|
681
|
+
if (inside) break;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (gm) {
|
|
685
|
+
inside = true;
|
|
686
|
+
last = y;
|
|
687
|
+
out = 0;
|
|
688
|
+
} else if (inside && ++out >= LEAVE) {
|
|
689
|
+
hemPts.push([x, last]);
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const hemFit = hemPts.length >= MIN_COVER * cols ? fitLine(hemPts) : null;
|
|
695
|
+
let hem: number | null = null;
|
|
696
|
+
if (hemFit === null) why.hem = hemPts.length < MIN_COVER * cols ? `the hem is found in ${hemPts.length} of the ${cols} columns between the hips (< ${100 * MIN_COVER}%)` : 'the hem rows do not fit a line';
|
|
697
|
+
else hem = tiltOf(hemFit.m, rHip, lHip);
|
|
698
|
+
// The thigh roots.
|
|
699
|
+
const root = { r: null as Pt | null, l: null as Pt | null };
|
|
700
|
+
const rootWhy: string[] = [];
|
|
701
|
+
const paths: Array<[Pt, Pt]> = [];
|
|
702
|
+
for (const s of ['r', 'l'] as const) {
|
|
703
|
+
const k = knee[s];
|
|
704
|
+
if (k === null) {
|
|
705
|
+
rootWhy.push(`${s}_knee has no position`);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const h = hip[s];
|
|
709
|
+
paths.push([h, k]);
|
|
710
|
+
const len = Math.hypot(k[0] - h[0], k[1] - h[1]);
|
|
711
|
+
const n = Math.ceil(len / 0.5);
|
|
712
|
+
let inside = false;
|
|
713
|
+
let last: Pt | null = null;
|
|
714
|
+
let out = 0;
|
|
715
|
+
let hidden = false;
|
|
716
|
+
for (let i = 0; i <= n; i++) {
|
|
717
|
+
const p: Pt = [h[0] + (i / n) * (k[0] - h[0]), h[1] + (i / n) * (k[1] - h[1])];
|
|
718
|
+
const gm = isGarment(Math.round(p[0]), Math.round(p[1]));
|
|
719
|
+
if (gm === null) {
|
|
720
|
+
if (inside) {
|
|
721
|
+
hidden = true;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (gm) {
|
|
727
|
+
inside = true;
|
|
728
|
+
last = p;
|
|
729
|
+
out = 0;
|
|
730
|
+
} else if (inside && ++out * (len / n) >= LEAVE) {
|
|
731
|
+
root[s] = last;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (root[s] === null) rootWhy.push(hidden ? `the ${s} thigh's root is behind a forearm` : inside ? `the ${s} thigh does not leave the garment before its knee` : `the ${s} thigh's path carries no garment`);
|
|
736
|
+
}
|
|
737
|
+
let roots: number | null = null;
|
|
738
|
+
if (root.r !== null && root.l !== null) {
|
|
739
|
+
if (Math.abs(root.l[0] - root.r[0]) < MIN_SPAN) rootWhy.push(`the roots are ${Math.abs(root.l[0] - root.r[0]).toFixed(1)} px apart across (< ${MIN_SPAN})`);
|
|
740
|
+
else roots = pictureAngle(root.r, root.l);
|
|
741
|
+
}
|
|
742
|
+
if (roots === null) why.roots = rootWhy.join('; ');
|
|
743
|
+
// The band's top, when nothing covers it.
|
|
744
|
+
const edge: { fit?: { m: number; q: number } } = {};
|
|
745
|
+
const wb = waistband(f, c, span, x0, x1, cols, rHip, lHip, edge);
|
|
746
|
+
let band: number | 'covered' | null = null;
|
|
747
|
+
if (!('tilt' in wb) || edge.fit === undefined) why.band = 'kind' in wb ? `${wb.kind}` : 'no edge';
|
|
748
|
+
else {
|
|
749
|
+
const above: RGB[] = [];
|
|
750
|
+
for (let x = x0; x <= x1; x++) {
|
|
751
|
+
const y = Math.round(edge.fit.m * x + edge.fit.q) - 3 * (DITHER_RADIUS + 1);
|
|
752
|
+
if (y < 0 || y >= f.h || f.masked(x, y)) continue;
|
|
753
|
+
const p = mean(x, y);
|
|
754
|
+
if (p !== null) above.push(p);
|
|
755
|
+
}
|
|
756
|
+
const a = above.length === 0 ? null : ([0, 1, 2].map((k) => median(above.map((p) => p[k]!))) as RGB);
|
|
757
|
+
if (a !== null && diff(a, f.bg) <= FIGURE_DIFF) band = wb.tilt;
|
|
758
|
+
else {
|
|
759
|
+
band = 'covered';
|
|
760
|
+
why.band = a === null ? 'nothing seen above the band\'s top' : `the band's top is covered (${a.map((v) => v.toFixed(0)).join(', ')} lies ${3 * (DITHER_RADIUS + 1)} px above it, not the ground)`;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const hips = pictureAngle(rHip, lHip);
|
|
764
|
+
const tilts: CueTilts = { hem, roots, band, hips };
|
|
765
|
+
const detail: CueDetail = { tilts, why, garment: g, legs, hem: { pts: hemPts, fit: hemFit === null ? null : { m: hemFit.m, q: hemFit.q, rms: hemFit.rms } }, roots: { ...root, paths }, band: typeof band === 'number' ? edge.fit! : null };
|
|
766
|
+
const fitted: Array<[string, number]> = [];
|
|
767
|
+
if (hem !== null) fitted.push(['hem', hem]);
|
|
768
|
+
if (roots !== null) fitted.push(['roots', roots]);
|
|
769
|
+
if (typeof band === 'number') fitted.push(['band', band]);
|
|
770
|
+
const fmt = (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}°`;
|
|
771
|
+
if (fitted.length < 2) {
|
|
772
|
+
const missing = (['hem', 'roots', 'band'] as const).filter((k) => why[k] !== undefined).map((k) => `${k}: ${why[k]}`);
|
|
773
|
+
return { reading: { kind: 'too few cues below the cover', why: `${fitted.length} of the cues read (${fitted.map(([n, v]) => `${n} ${fmt(v)}`).join(', ') || 'none'}); ${missing.join('; ')}` }, detail };
|
|
774
|
+
}
|
|
775
|
+
const base = fitted[0]![1];
|
|
776
|
+
const about = fitted.map(([, v]) => base + wrap(v - base));
|
|
777
|
+
const spread = Math.max(...about) - Math.min(...about);
|
|
778
|
+
if (spread > CUE_SPREAD) return { reading: { kind: 'cues disagree', why: `${fitted.map(([n, v]) => `${n} ${fmt(v)}`).join(', ')}: ${spread.toFixed(1)}° apart (> ${CUE_SPREAD}°); they do not read one line (hips ${fmt(hips)}, not fitted)` }, detail };
|
|
779
|
+
const tilt = wrap(about.reduce((s, v) => s + v, 0) / about.length);
|
|
780
|
+
return { reading: { tilt, cover: hemPts.length / cols, rms: hemFit?.rms ?? 0, mean: true, cues: tilts }, detail };
|
|
781
|
+
}
|
|
782
|
+
|
|
535
783
|
/**
|
|
536
784
|
* `rig-pose-pelvis-keys/1`: `{ spec, keys: [{ frame, deg }], note? }` — frames of the
|
|
537
785
|
* sequence, strictly increasing, within its span [0, frames − 1]; `deg` the tilt in
|
|
@@ -686,6 +934,17 @@ export function runPelvis(a: PelvisArgs, say: (l: string) => void): void {
|
|
|
686
934
|
const tilt = readings.map((r) => ('tilt' in r ? r.tilt : null));
|
|
687
935
|
const read = tilt.filter((v): v is number => v !== null);
|
|
688
936
|
say(`rig-pose pelvis: ${n} frame(s) of person "${person}" from ${a.pose}, read by ${a.mode} off ${dir} (${files.length} PNG)`);
|
|
937
|
+
if (a.mode === 'below-cover') {
|
|
938
|
+
// rig-c/pose#342: every frame's cues, read or not, so a decline or a reading can be checked cue by cue.
|
|
939
|
+
const fmt = (v: number | 'covered' | null) => (v === null ? 'not read' : v === 'covered' ? 'covered' : `${v >= 0 ? '+' : ''}${v.toFixed(2)}°`);
|
|
940
|
+
readings.forEach((r, i) => {
|
|
941
|
+
if (!('tilt' in r) || r.cues === undefined) return;
|
|
942
|
+
// The hips stay out of the vote; their sign is printed beside the fit, and an opposite sign is warned.
|
|
943
|
+
const sign = (v: number) => (Math.abs(v) < 0.5 ? '0' : v > 0 ? '+' : '−');
|
|
944
|
+
say(` CUES frame ${i}: hem ${fmt(r.cues.hem)}, roots ${fmt(r.cues.roots)}, band ${fmt(r.cues.band)}, hips ${fmt(r.cues.hips)} (not fitted) → ${fmt(r.tilt)}; hips sign ${sign(r.cues.hips)}, fit sign ${sign(r.tilt)}`);
|
|
945
|
+
if (sign(r.cues.hips) !== '0' && sign(r.tilt) !== '0' && sign(r.cues.hips) !== sign(r.tilt)) say(` HIPS-SIGN frame ${i}: the hip keypoints tilt ${fmt(r.cues.hips)}, opposite the fit's ${fmt(r.tilt)} (the hips are not fitted)`);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
689
948
|
for (const d of declinedRuns(readings)) say(` DECLINED frame${d.from === d.to ? ` ${d.from}` : `s ${d.from}–${d.to}`} (${d.to - d.from + 1}): ${d.kind} — ${d.same ? d.why : `frame ${d.from}: ${d.why}; the figures differ frame to frame`}`);
|
|
690
949
|
if (read.length > 0) {
|
|
691
950
|
const covers = readings.flatMap((r) => ('tilt' in r ? [r.cover] : []));
|
package/src/pose.ts
CHANGED
|
@@ -74,15 +74,16 @@ export interface SegmentDepth {
|
|
|
74
74
|
source: DepthSource;
|
|
75
75
|
}
|
|
76
76
|
export type FrameDepth = Partial<Record<DepthSegment, SegmentDepth>>;
|
|
77
|
-
/** The gait `clean --depth` was told the clip is: it decides the
|
|
77
|
+
/** The gait `clean --depth` was told the clip is (rig-c/pose#336): the signs are the same for both; it decides the joint `--draw-order depth` orders the legs by. */
|
|
78
78
|
export const DEPTH_GAITS = ['walk', 'run'] as const;
|
|
79
79
|
export type DepthGait = (typeof DEPTH_GAITS)[number];
|
|
80
80
|
/**
|
|
81
81
|
* Where a sequence's `pelvisTilt` came from (rig-c/pose#288): `silhouette` — the
|
|
82
82
|
* straight edge of the drawn figure across the hips; `waistband` — the top edge
|
|
83
|
-
* of a garment covering them; `
|
|
83
|
+
* of a garment covering them; `below-cover` — the cues under a garment hanging
|
|
84
|
+
* over the waistband (rig-c/pose#342); `keys` — a hand file (`--pelvis-keys`).
|
|
84
85
|
*/
|
|
85
|
-
export const PELVIS_SOURCES = ['silhouette', 'waistband', 'keys'] as const;
|
|
86
|
+
export const PELVIS_SOURCES = ['silhouette', 'waistband', 'below-cover', 'keys'] as const;
|
|
86
87
|
export type PelvisSource = (typeof PELVIS_SOURCES)[number];
|
|
87
88
|
export const POSE_SPACE = { units: 'picture-px', origin: 'top-left', y: 'down' } as const;
|
|
88
89
|
/**
|
package/src/retarget.ts
CHANGED
|
@@ -76,12 +76,13 @@ import type { Body18 } from './joints.ts';
|
|
|
76
76
|
import { isBody18, VIRTUAL } from './joints.ts';
|
|
77
77
|
import type { Chain, JointMap, Rest, RigSets, RigSlots } from './map.ts';
|
|
78
78
|
import { aimAngle, aimEnds, bodyYawSets, descends, faceYawSets, knownSetsOf, partBone, pointParts, rigPoint } from './map.ts';
|
|
79
|
-
import type { Joint, Travel } from './pose.ts';
|
|
79
|
+
import type { DepthGait, Joint, Travel } from './pose.ts';
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
82
|
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SetRuns, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
-
import { clampArt, coveredChains, isRaised, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
83
|
+
import { clampArt, coveredChains, isRaised, keyAtFrame, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, LegSegmentRatio, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
+
import { contactKeysOf, type ContactKeys } from './contacts.ts';
|
|
85
86
|
import { figureScaleOf, freeAnkleShiftOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, shiftOver, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
87
|
import type { BodyReading, FaceReading, YawSetSwitch } from './face.ts';
|
|
87
88
|
import { readBody, readFace, readFaceAngles, selectYawSets } from './face.ts';
|
|
@@ -181,6 +182,41 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
181
182
|
* the hip line on the frames it does not; absent, from the hip line.
|
|
182
183
|
*/
|
|
183
184
|
pelvis?: { source: string; tilt: ReadonlyArray<number | null> };
|
|
185
|
+
/**
|
|
186
|
+
* rig-c/pose#303: the hand contact keys (`--contact-keys`), read against the
|
|
187
|
+
* sequence. Applied last over the supports read (`contactKeysOf`): on every
|
|
188
|
+
* frame they cover they decide the support. Absent, nothing changes.
|
|
189
|
+
*/
|
|
190
|
+
contactKeys?: ContactKeys;
|
|
191
|
+
/**
|
|
192
|
+
* rig-c/pose#344: the gait the sequence was cleaned as (its `cleaned.params.depth.gait`, `clean --depth`). An
|
|
193
|
+
* in-place walk anchors the upper body (`TorsoAnchor`): the pelvis sways under it. Absent, or a run, nothing changes.
|
|
194
|
+
*/
|
|
195
|
+
gait?: DepthGait;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* rig-c/pose#344: in an in-place walk the upper body is the anchor and the pelvis sways under it. The pelvis bone
|
|
200
|
+
* carries the torso (its child), so its turn and its travel would swing the chest around it; the first spine bone
|
|
201
|
+
* (`bone`: the pelvis bone's child on the way to the trunk chain's bone) is keyed with the counter: a rotate of minus
|
|
202
|
+
* the pelvis's turn (none when it is the trunk bone itself, whose aim is already in the world), and a translate that
|
|
203
|
+
* puts its origin back where the pelvis's turn moved it from, up and down, and sideways where it carries `point`
|
|
204
|
+
* (the trunk chain's aim bone: the top of the trunk) on the drawn neck's own sideways path: about the world x `x`
|
|
205
|
+
* (the setup x plus the pelvis's mean travel), the picture's `joint` x less its mean, times `factor` — the pelvis's
|
|
206
|
+
* sideways sway in the rig against the drawing's (`joints[0]`, mid_hip), each its standard deviation over the frames,
|
|
207
|
+
* so the neck sways against the pelvis as the drawing's does; the figure's scale where the drawing's pelvis does not
|
|
208
|
+
* sway. The pelvis's own keys are as read; `neck` is the carried path's peak-to-peak and `sway` the first spine bone's
|
|
209
|
+
* sideways counter's, world units.
|
|
210
|
+
*/
|
|
211
|
+
export interface TorsoAnchor {
|
|
212
|
+
gait: DepthGait;
|
|
213
|
+
bone: string;
|
|
214
|
+
point: string;
|
|
215
|
+
joint: string;
|
|
216
|
+
x: number;
|
|
217
|
+
factor: number;
|
|
218
|
+
neck: number;
|
|
219
|
+
sway: number;
|
|
184
220
|
}
|
|
185
221
|
|
|
186
222
|
/**
|
|
@@ -201,6 +237,8 @@ export interface PelvisReading {
|
|
|
201
237
|
hipLine: number;
|
|
202
238
|
/** The frames keyed from the hip line on a sequence that carries a track: the frames its reading declined. */
|
|
203
239
|
declined: Array<[number, number]>;
|
|
240
|
+
/** rig-c/pose#344: the torso held over the swaying pelvis, in an in-place walk only. */
|
|
241
|
+
anchor?: TorsoAnchor;
|
|
204
242
|
}
|
|
205
243
|
|
|
206
244
|
export interface Hold {
|
|
@@ -897,6 +935,32 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
897
935
|
}
|
|
898
936
|
p.throwIfAny();
|
|
899
937
|
|
|
938
|
+
// rig-c/pose#344: an in-place walk holds the upper body over the swaying pelvis, on the first spine bone.
|
|
939
|
+
const anchorOf = ((): { bone: string; trunk: string; point: string; pelvis: string; joints: [string, string] } | null => {
|
|
940
|
+
if (opts.gait !== 'walk' || opts.travel !== TRAVEL_IN_PLACE || pelvisChain === null) return null;
|
|
941
|
+
const pelvisBone = pelvisChain.bones[0]!;
|
|
942
|
+
const trunk = map.chains.find((c) => !c.reach && c.joints[0] === 'mid_hip' && descends(rest, c.bones[0]!, pelvisBone));
|
|
943
|
+
if (trunk === undefined) return null;
|
|
944
|
+
const tb = trunk.bones[0]!;
|
|
945
|
+
let first = tb;
|
|
946
|
+
while (rest.get(first)!.parent !== pelvisBone) first = rest.get(first)!.parent!;
|
|
947
|
+
// The aim's upper end (aimEnds: `neck` and `chest>neck` alike end at the neck), a rig point on the trunk bone or below it.
|
|
948
|
+
const aim = trunk.aim[0]!;
|
|
949
|
+
const top = aim === 'x' ? null : aimEnds(tb, aim)[1];
|
|
950
|
+
const onTrunk = top !== null && pointParts(top).every((part) => {
|
|
951
|
+
const b = partBone(part);
|
|
952
|
+
return rest.has(b) && (b === tb || descends(rest, b, tb));
|
|
953
|
+
});
|
|
954
|
+
const point = onTrunk ? top! : tb;
|
|
955
|
+
return { bone: first, trunk: tb, point, pelvis: pelvisBone, joints: [trunk.joints[0]!, trunk.joints[1]!] };
|
|
956
|
+
})();
|
|
957
|
+
if (anchorOf !== null && anchorOf.bone !== anchorOf.trunk && !targets.has(anchorOf.bone)) {
|
|
958
|
+
// Not in a chain: its world turn is held at setup, so the bones below it are keyed against none.
|
|
959
|
+
const a0 = restAngle(rest, anchorOf.bone, 'x');
|
|
960
|
+
restOf.set(anchorOf.bone, a0);
|
|
961
|
+
targets.set(anchorOf.bone, Array.from({ length: n }, () => a0));
|
|
962
|
+
}
|
|
963
|
+
|
|
900
964
|
// The short hold, on every segment the pose aims directly (not a reach): its
|
|
901
965
|
// picture length against its own median, on the points the angle was read from.
|
|
902
966
|
// A horizontal line across the body (`isAcross`: pelvis, shoulders, head line)
|
|
@@ -1020,6 +1084,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1020
1084
|
else if (plantOn) p.add('LEGS_MAP', `--plant-feet reads both hips and both ankles from the pose; the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`);
|
|
1021
1085
|
}
|
|
1022
1086
|
}
|
|
1087
|
+
// rig-c/pose#303: the contact keys decide last, over every reading above; with no support read they have nothing to decide, so they are refused.
|
|
1088
|
+
if (opts.contactKeys !== undefined) {
|
|
1089
|
+
if (feet !== null) feet = contactKeysOf(feet, opts.contactKeys);
|
|
1090
|
+
else p.add('CONTACT_KEYS', `--contact-keys sets which foot supports, and no support is read: ${legs !== null && 'problems' in legs ? legs.problems.join('; ') : `the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`}`);
|
|
1091
|
+
}
|
|
1023
1092
|
const heldPt = (name: string): Pt[] | null => {
|
|
1024
1093
|
const known = series.get(name);
|
|
1025
1094
|
if (known !== undefined) return known;
|
|
@@ -1497,7 +1566,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1497
1566
|
const r = turnAbove(parent, i) / DEG;
|
|
1498
1567
|
const ox = own.worldX - rest.get(parent)!.worldX;
|
|
1499
1568
|
const oy = own.worldY - rest.get(parent)!.worldY;
|
|
1500
|
-
|
|
1569
|
+
const at: Pt = [po[0] + Math.cos(r) * ox - Math.sin(r) * oy + nod[0], po[1] + Math.sin(r) * ox + Math.cos(r) * oy + nod[1]];
|
|
1570
|
+
if (anchorOf === null || b !== anchorOf.bone || anchorRaw) return at;
|
|
1571
|
+
const s = anchorShift(i, at, po, oy);
|
|
1572
|
+
return [at[0] + s[0], at[1] + s[1]];
|
|
1573
|
+
};
|
|
1574
|
+
/**
|
|
1575
|
+
* rig-c/pose#344: the world shift that holds the first spine bone over the pelvis: up and down, its origin where
|
|
1576
|
+
* the pelvis's travel alone puts it (the pelvis's turn taken out); sideways, the trunk's top point on `anchorPath`:
|
|
1577
|
+
* the drawn neck's sideways path about its mean, scaled by the pelvis's rig ÷ drawn sideways sway (`TorsoAnchor`).
|
|
1578
|
+
*/
|
|
1579
|
+
let anchorRaw = false;
|
|
1580
|
+
let anchorX: number | null = null;
|
|
1581
|
+
let anchorFactor = 1;
|
|
1582
|
+
let anchorPath: number[] | null = null;
|
|
1583
|
+
const anchorShift = (i: number, at: Pt, po: Pt, oy: number): Pt => {
|
|
1584
|
+
const a = anchorOf!;
|
|
1585
|
+
if (anchorPath === null) {
|
|
1586
|
+
anchorRaw = true;
|
|
1587
|
+
const hipX = Array.from({ length: n }, (_, k) => posedOrigin(a.pelvis, k)[0] - rest.get(a.pelvis)!.worldX);
|
|
1588
|
+
anchorRaw = false;
|
|
1589
|
+
const mean = (xs: readonly number[]) => xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
1590
|
+
const sd = (xs: readonly number[]) => {
|
|
1591
|
+
const m = mean(xs);
|
|
1592
|
+
return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / xs.length);
|
|
1593
|
+
};
|
|
1594
|
+
const drawnHip = series.get(a.joints[0])!.map((p) => p[0]);
|
|
1595
|
+
const drawnNeck = series.get(a.joints[1])!.map((p) => p[0]);
|
|
1596
|
+
const sdHip = sd(drawnHip);
|
|
1597
|
+
anchorFactor = sdHip > 1e-6 ? sd(hipX) / sdHip : (scale?.factor ?? 1);
|
|
1598
|
+
anchorX = rigPoint(rest, a.point)![0] + mean(hipX);
|
|
1599
|
+
const m = mean(drawnNeck);
|
|
1600
|
+
anchorPath = drawnNeck.map((x) => anchorX! + (x - m) * anchorFactor);
|
|
1601
|
+
}
|
|
1602
|
+
anchorRaw = true;
|
|
1603
|
+
const top = posedPoint(a.point, i);
|
|
1604
|
+
anchorRaw = false;
|
|
1605
|
+
return [anchorPath[i]! - top[0], po[1] + oy - at[1]];
|
|
1501
1606
|
};
|
|
1502
1607
|
/** A rig point (a bone, `name:tip`, `a+b`) posed on frame i the same way. */
|
|
1503
1608
|
const posedPoint = (spec: string, i: number): Pt => {
|
|
@@ -2987,6 +3092,42 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2987
3092
|
}
|
|
2988
3093
|
}
|
|
2989
3094
|
}
|
|
3095
|
+
// rig-c/pose#344: the first spine bone's counter — its rotate when it is not the trunk bone, and its translate.
|
|
3096
|
+
let anchor: TorsoAnchor | null = null;
|
|
3097
|
+
if (anchorOf !== null) {
|
|
3098
|
+
const a = anchorOf;
|
|
3099
|
+
if (a.bone !== a.trunk) {
|
|
3100
|
+
const above = mappedAbove(a.bone);
|
|
3101
|
+
let prev = 0;
|
|
3102
|
+
const keys: MotionKey[] = [];
|
|
3103
|
+
for (let i = 0; i < n; i++) {
|
|
3104
|
+
const raw = turn(a.bone, i) - (above === null ? 0 : turn(above, i));
|
|
3105
|
+
prev = i === 0 ? wrap(raw) : prev + wrap(raw - prev);
|
|
3106
|
+
keys.push({ t: keyTimes[i]!, v: [r3(prev)] });
|
|
3107
|
+
}
|
|
3108
|
+
tracks.push({ bone: a.bone, property: 'rotate', keys });
|
|
3109
|
+
}
|
|
3110
|
+
const pr = rest.get(a.pelvis)!;
|
|
3111
|
+
const det = pr.a * pr.d - pr.b * pr.c;
|
|
3112
|
+
const keys: MotionKey[] = [];
|
|
3113
|
+
const xs: number[] = [];
|
|
3114
|
+
for (let i = 0; i < n; i++) {
|
|
3115
|
+
const held = posedOrigin(a.bone, i);
|
|
3116
|
+
anchorRaw = true;
|
|
3117
|
+
const free = posedOrigin(a.bone, i);
|
|
3118
|
+
anchorRaw = false;
|
|
3119
|
+
const wx = held[0] - free[0];
|
|
3120
|
+
const wy = held[1] - free[1];
|
|
3121
|
+
xs.push(wx);
|
|
3122
|
+
// Into the pelvis's turned frame, then through its setup matrix: the translate is in the parent's local axes.
|
|
3123
|
+
const r = -turnAbove(a.pelvis, i) / DEG;
|
|
3124
|
+
const ux = Math.cos(r) * wx - Math.sin(r) * wy;
|
|
3125
|
+
const uy = Math.sin(r) * wx + Math.cos(r) * wy;
|
|
3126
|
+
keys.push({ t: keyTimes[i]!, v: [r3((pr.d * ux - pr.b * uy) / det), r3((-pr.c * ux + pr.a * uy) / det)] });
|
|
3127
|
+
}
|
|
3128
|
+
tracks.push({ bone: a.bone, property: 'translate', keys });
|
|
3129
|
+
anchor = { gait: 'walk', bone: a.bone, point: a.point, joint: a.joints[1], x: r3(anchorX!), factor: r6(anchorFactor), neck: r3(Math.max(...anchorPath!) - Math.min(...anchorPath!)), sway: r3(Math.max(...xs) - Math.min(...xs)) };
|
|
3130
|
+
}
|
|
2990
3131
|
let face: FaceReading | null = null;
|
|
2991
3132
|
if (map.face !== undefined) {
|
|
2992
3133
|
const rEar = series.get('r_ear') ?? (() => {
|
|
@@ -3048,7 +3189,6 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3048
3189
|
if (!slotOrder.includes(sl)) slotOrder.push(sl);
|
|
3049
3190
|
}
|
|
3050
3191
|
}
|
|
3051
|
-
const yawExactAt = new Map<string, Set<number>>();
|
|
3052
3192
|
for (const s of activeYawSets) {
|
|
3053
3193
|
const def = known.get(s.set);
|
|
3054
3194
|
if (def === undefined) continue;
|
|
@@ -3058,10 +3198,6 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3058
3198
|
const row = sw.shown.get(sl)!;
|
|
3059
3199
|
for (const [lo, hi] of s.spans) {
|
|
3060
3200
|
for (let i = lo; i <= hi; i++) row[i] = v;
|
|
3061
|
-
const exact = yawExactAt.get(sl) ?? new Set<number>();
|
|
3062
|
-
exact.add(lo);
|
|
3063
|
-
if (hi + 1 < n) exact.add(hi + 1);
|
|
3064
|
-
yawExactAt.set(sl, exact);
|
|
3065
3201
|
}
|
|
3066
3202
|
}
|
|
3067
3203
|
}
|
|
@@ -3071,11 +3207,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3071
3207
|
if (row === undefined) continue;
|
|
3072
3208
|
let prev = slots.get(sl)?.setup ?? null;
|
|
3073
3209
|
const keys: AttachmentKey[] = [];
|
|
3074
|
-
const exact = yawExactAt.get(sl);
|
|
3075
3210
|
row.forEach((v, i) => {
|
|
3076
3211
|
if (v === prev) return;
|
|
3077
|
-
|
|
3078
|
-
|
|
3212
|
+
// Half a frame before the frame it shows on, as every discrete key pose writes (`keyAtFrame`, rig-c/pose#334):
|
|
3213
|
+
// written on their frames' exact times, 7 of the 10 yaw swaps of the 24 fps head turn in test/retarget.test.ts
|
|
3214
|
+
// showed a frame late under the render's clock.
|
|
3215
|
+
keys.push({ t: keyAtFrame(keyTimes, i), v });
|
|
3079
3216
|
prev = v;
|
|
3080
3217
|
});
|
|
3081
3218
|
if (keys.length > 0) rebuilt.push({ slot: sl, property: 'attachment', keys });
|
|
@@ -3128,6 +3265,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3128
3265
|
|
|
3129
3266
|
const pivot = pivotOf(rest, t?.bone ?? null, legs);
|
|
3130
3267
|
const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
|
|
3268
|
+
if (pelvis !== null && anchor !== null) pelvis.anchor = anchor;
|
|
3131
3269
|
const scaleFit = t === null || scale === null || origin === null ? null : scaleFitOf({
|
|
3132
3270
|
joints: t.scale.pose, joint: t.joint, bone: t.bone, lengths: scaleLengths, factor: scale.factor,
|
|
3133
3271
|
series, good, legLength: feet?.legLength ?? null, legStraight, rigLeg,
|
|
@@ -3179,10 +3317,16 @@ export function pelvisLine(r: PelvisReading, frames: number): string {
|
|
|
3179
3317
|
const where = r.bone === null ? '' : `bone "${r.bone}", chain "${r.chain}"`;
|
|
3180
3318
|
if (r.bone === null) return `PELVIS: not keyed — the sequence carries a ${r.trackSource} track (${frames - r.declined.reduce((s, [a, b]) => s + b - a + 1, 0)} of ${frames} frames read) and the map has no pelvis chain (one bone, joints r_hip → l_hip); no bone turns with the pelvis`;
|
|
3181
3319
|
const swing = r.swing === null ? '' : `, swing ${r.swing.toFixed(2)}°`;
|
|
3182
|
-
if (r.trackSource === null) return `PELVIS: from hip line (no track)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3183
|
-
if (r.track === 0) return `PELVIS: from hip line (the ${r.trackSource} track read none of the ${frames} frames)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3320
|
+
if (r.trackSource === null) return `PELVIS: from hip line (no track)${swing} (${where}; every frame from r_hip → l_hip)${anchorClause(r.anchor)}`;
|
|
3321
|
+
if (r.track === 0) return `PELVIS: from hip line (the ${r.trackSource} track read none of the ${frames} frames)${swing} (${where}; every frame from r_hip → l_hip)${anchorClause(r.anchor)}`;
|
|
3184
3322
|
const rest = r.hipLine === 0 ? `all ${frames} frames from the track` : `${r.track} of ${frames} frames from the track, the hip line on the ${r.hipLine} it declined (frames ${spans(r.declined)})`;
|
|
3185
|
-
return `PELVIS: from ${r.source}${swing} (${where}; ${rest})`;
|
|
3323
|
+
return `PELVIS: from ${r.source}${swing} (${where}; ${rest})${anchorClause(r.anchor)}`;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/** rig-c/pose#344: the PELVIS line's clause for the torso held over the pelvis. */
|
|
3327
|
+
function anchorClause(a: TorsoAnchor | undefined): string {
|
|
3328
|
+
if (a === undefined) return '';
|
|
3329
|
+
return `; torso anchored (in-place ${a.gait}): bone "${a.bone}" keyed with the pelvis's counter, "${a.point}" carried on the drawn ${a.joint}'s sideways path about x ${a.x.toFixed(2)} (× ${a.factor.toFixed(4)}, the pelvis's rig ÷ drawn sway; ${a.neck.toFixed(2)} peak to peak, the counter ${a.sway.toFixed(2)}), the pelvis swaying under it`;
|
|
3186
3330
|
}
|
|
3187
3331
|
|
|
3188
3332
|
/** A reach bone's origin within this many world units of the anchor's, with nothing above it moved, is aimed along the pair: no REACH line. */
|