@rig-c/pose 0.38.0 → 0.40.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 +64 -9
- package/cli.ts +64 -10
- package/package.json +1 -1
- package/src/clean.ts +24 -2
- package/src/depth.ts +190 -0
- package/src/draworder.ts +290 -0
- package/src/legs.ts +189 -0
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +100 -15
- package/src/pose.ts +150 -18
- package/src/reduce.ts +21 -12
- package/src/retarget.ts +82 -3
- package/src/run.ts +162 -35
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +64 -2
- package/src/verify.ts +90 -9
package/src/pose.ts
CHANGED
|
@@ -26,14 +26,57 @@
|
|
|
26
26
|
* the version the file records of what wrote it: a file of a minor newer than
|
|
27
27
|
* this reader's is read, and each key it does not know is refused naming the
|
|
28
28
|
* minor the file says it was written as.
|
|
29
|
+
*
|
|
30
|
+
* `rig-pose-sequence/1.2` (rig-c/pose#281) adds derived depth: a per-frame
|
|
31
|
+
* `depth`, one `{ ratio, sign, source }` per limb segment, written by `clean
|
|
32
|
+
* --depth walk|run` (`source: "derived"`) or by a 3D detector (`"measured"`,
|
|
33
|
+
* read and never written here). A minor includes the keys of the minors before
|
|
34
|
+
* it, so a sequence with depth is "/1.2" whether or not it also carries the
|
|
35
|
+
* pelvis track; one with the track and no depth stays "/1.1".
|
|
29
36
|
*/
|
|
30
37
|
import { BODY18, COCO17, COCO17_TO_BODY18, isBody18, type Body18 } from './joints.ts';
|
|
31
38
|
import { isObject, parseJson, Problems, refuseUnknownKeys, refuseUnknownKeys as refuseKeys, typeName } from './problems.ts';
|
|
32
39
|
|
|
33
40
|
export const POSE_SPEC = 'rig-pose-sequence/1';
|
|
34
|
-
/** The minor with the pelvis track (rig-c/pose#288)
|
|
41
|
+
/** The minor with the pelvis track (rig-c/pose#288). */
|
|
35
42
|
export const POSE_SPEC_PELVIS = 'rig-pose-sequence/1.1';
|
|
36
|
-
|
|
43
|
+
/** The minor with derived depth (rig-c/pose#281), and the newest minor this reader reads. */
|
|
44
|
+
export const POSE_SPEC_DEPTH = 'rig-pose-sequence/1.2';
|
|
45
|
+
const POSE_MINOR = 2;
|
|
46
|
+
/**
|
|
47
|
+
* The limb segments a frame's `depth` names (rig-c/pose#281), each from its
|
|
48
|
+
* proximal joint to its distal one: the sign is + when the distal joint is
|
|
49
|
+
* nearer the camera than the proximal one.
|
|
50
|
+
*/
|
|
51
|
+
export const DEPTH_SEGMENTS = {
|
|
52
|
+
r_upper_arm: ['r_shoulder', 'r_elbow'],
|
|
53
|
+
r_forearm: ['r_elbow', 'r_wrist'],
|
|
54
|
+
r_thigh: ['r_hip', 'r_knee'],
|
|
55
|
+
r_shin: ['r_knee', 'r_ankle'],
|
|
56
|
+
l_upper_arm: ['l_shoulder', 'l_elbow'],
|
|
57
|
+
l_forearm: ['l_elbow', 'l_wrist'],
|
|
58
|
+
l_thigh: ['l_hip', 'l_knee'],
|
|
59
|
+
l_shin: ['l_knee', 'l_ankle'],
|
|
60
|
+
} as const satisfies Record<string, readonly [Body18, Body18]>;
|
|
61
|
+
export type DepthSegment = keyof typeof DEPTH_SEGMENTS;
|
|
62
|
+
export const DEPTH_SEGMENT_NAMES = Object.keys(DEPTH_SEGMENTS) as DepthSegment[];
|
|
63
|
+
/** Where a segment's depth came from: read off the picture's 2D lengths (`clean --depth`), or a 3D detector's. */
|
|
64
|
+
export const DEPTH_SOURCES = ['derived', 'measured'] as const;
|
|
65
|
+
export type DepthSource = (typeof DEPTH_SOURCES)[number];
|
|
66
|
+
/**
|
|
67
|
+
* One segment's depth on one frame (rig-c/pose#281): `ratio` its drawn length ÷
|
|
68
|
+
* its rest length; `sign` +1 when its distal joint is nearer the camera than its
|
|
69
|
+
* proximal one, −1 farther, 0 flat (or not decided); `source` as above.
|
|
70
|
+
*/
|
|
71
|
+
export interface SegmentDepth {
|
|
72
|
+
ratio: number;
|
|
73
|
+
sign: -1 | 0 | 1;
|
|
74
|
+
source: DepthSource;
|
|
75
|
+
}
|
|
76
|
+
export type FrameDepth = Partial<Record<DepthSegment, SegmentDepth>>;
|
|
77
|
+
/** The gait `clean --depth` was told the clip is: it decides the sign of a foreshortened shin. */
|
|
78
|
+
export const DEPTH_GAITS = ['walk', 'run'] as const;
|
|
79
|
+
export type DepthGait = (typeof DEPTH_GAITS)[number];
|
|
37
80
|
/**
|
|
38
81
|
* Where a sequence's `pelvisTilt` came from (rig-c/pose#288): `silhouette` — the
|
|
39
82
|
* straight edge of the drawn figure across the hips; `waistband` — the top edge
|
|
@@ -72,6 +115,12 @@ export interface PoseFrame {
|
|
|
72
115
|
* a sequence with a `pelvisSource`, on none of one without.
|
|
73
116
|
*/
|
|
74
117
|
pelvisTilt?: number | null;
|
|
118
|
+
/**
|
|
119
|
+
* rig-c/pose#281, minor 1.2: each limb segment's depth on this frame (a segment
|
|
120
|
+
* whose joints are not both placed on the frame is not named). Present on every
|
|
121
|
+
* frame of a sequence with depth, on none of one without.
|
|
122
|
+
*/
|
|
123
|
+
depth?: FrameDepth;
|
|
75
124
|
}
|
|
76
125
|
/**
|
|
77
126
|
* The picture's camera on one frame against frame 0's (rig-c/pose#63), as
|
|
@@ -207,6 +256,10 @@ export interface CleanUnheld {
|
|
|
207
256
|
export interface PelvisParams {
|
|
208
257
|
window: number;
|
|
209
258
|
}
|
|
259
|
+
/** rig-c/pose#281: clean derived each frame's `depth` (`source: "derived"`) for this gait. */
|
|
260
|
+
export interface DepthParams {
|
|
261
|
+
gait: DepthGait;
|
|
262
|
+
}
|
|
210
263
|
export interface CleanRecord {
|
|
211
264
|
spec: typeof CLEAN_SPEC;
|
|
212
265
|
/**
|
|
@@ -214,7 +267,7 @@ export interface CleanRecord {
|
|
|
214
267
|
* `hold` (#91): the stillness hold's figures, or "off"; absent in a record
|
|
215
268
|
* written before #91.
|
|
216
269
|
*/
|
|
217
|
-
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
270
|
+
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
218
271
|
/**
|
|
219
272
|
* `found`: every joint the spike step replaced, by frame (index into `frames`)
|
|
220
273
|
* and joint. `untested`: the frames the spike step did not test, because a
|
|
@@ -282,15 +335,20 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
282
335
|
const frames0 = doc.spec === POSE_SPEC_FRAMES0;
|
|
283
336
|
// rig-c/pose#288: the minor the file says it was written as; a newer one is read, and what it adds is refused naming it.
|
|
284
337
|
const minor = frames0 ? 0 : minorOf(doc.spec);
|
|
285
|
-
const note = minor !== null && minor > POSE_MINOR ? `; the file says it was written as ${JSON.stringify(doc.spec)}, a newer minor than this reader's "${
|
|
338
|
+
const note = minor !== null && minor > POSE_MINOR ? `; the file says it was written as ${JSON.stringify(doc.spec)}, a newer minor than this reader's "${POSE_SPEC_DEPTH}" — read it with a rig-pose that reads that minor` : '';
|
|
286
339
|
const pelvisKnown = minor !== null && minor >= 1;
|
|
287
|
-
|
|
288
|
-
const
|
|
340
|
+
// rig-c/pose#281: depth is minor 2's.
|
|
341
|
+
const depthKnown = minor !== null && minor >= 2;
|
|
342
|
+
/** A key of a later minor in a file that says an earlier one: one line naming the minor, not an unknown key. */
|
|
343
|
+
const minorKey = (key: string, on: string) => (key === 'depth'
|
|
344
|
+
? p.add('POSE_SPEC', `${on} carries "depth", a key of "${POSE_SPEC_DEPTH}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying depth is written as "${POSE_SPEC_DEPTH}" (rig-pose clean --depth)`)
|
|
345
|
+
: p.add('POSE_SPEC', `${on} carries ${JSON.stringify(key)}, a key of "${POSE_SPEC_PELVIS}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying the pelvis track is written as "${POSE_SPEC_PELVIS}" (rig-pose pelvis)`));
|
|
289
346
|
const without = (o: Record<string, unknown>, key: string) => (pelvisKnown || !(key in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== key)));
|
|
347
|
+
const withoutDepth = (o: Record<string, unknown>) => (depthKnown || !('depth' in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== 'depth')));
|
|
290
348
|
if (!pelvisKnown && 'pelvisSource' in doc) minorKey('pelvisSource', where);
|
|
291
349
|
refuseUnknownKeys(p, 'POSE_KEYS', where, without(doc, 'pelvisSource'), frames0 ? [...TOP_KEYS, ...OPTIONAL_KEYS, 'fps'] : [...TOP_KEYS, ...OPTIONAL_KEYS, ...(pelvisKnown ? ['pelvisSource'] : [])], note);
|
|
292
350
|
for (const k of TOP_KEYS) if (!(k in doc)) p.add('POSE_KEYS', `${where} has no "${k}"; a ${POSE_SPEC} document carries ${TOP_KEYS.join(', ')}`);
|
|
293
|
-
if ('spec' in doc && minor === null) p.add('POSE_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${POSE_SPEC}" and its minors ("${POSE_SPEC_PELVIS}"; a newer minor is read, and a key it adds is refused naming it), and the older name "${POSE_SPEC_FRAMES0}"`);
|
|
351
|
+
if ('spec' in doc && minor === null) p.add('POSE_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${POSE_SPEC}" and its minors ("${POSE_SPEC_PELVIS}", "${POSE_SPEC_DEPTH}"; a newer minor is read, and a key it adds is refused naming it), and the older name "${POSE_SPEC_FRAMES0}"`);
|
|
294
352
|
const space = frames0 ? FRAMES0_SPACE : POSE_SPACE;
|
|
295
353
|
if ('space' in doc && JSON.stringify(doc.space) !== JSON.stringify(space)) {
|
|
296
354
|
p.add('POSE_SPACE', `${where} "space" is ${JSON.stringify(doc.space)}; for "${frames0 ? POSE_SPEC_FRAMES0 : POSE_SPEC}" it must be exactly ${JSON.stringify(space)}`);
|
|
@@ -307,6 +365,7 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
307
365
|
} else {
|
|
308
366
|
let lastT = -Infinity;
|
|
309
367
|
const minorFrames: number[] = [];
|
|
368
|
+
const depthFrames: number[] = [];
|
|
310
369
|
doc.frames.forEach((f: unknown, i: number) => {
|
|
311
370
|
const fw = `${where} frames[${i}]`;
|
|
312
371
|
if (!isObject(f)) {
|
|
@@ -314,7 +373,9 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
314
373
|
return;
|
|
315
374
|
}
|
|
316
375
|
if (!pelvisKnown && 'pelvisTilt' in f) minorFrames.push(i);
|
|
317
|
-
|
|
376
|
+
if (!depthKnown && 'depth' in f) depthFrames.push(i);
|
|
377
|
+
refuseUnknownKeys(p, 'POSE_KEYS', fw, withoutDepth(without(f, 'pelvisTilt')), [...(pelvisKnown ? ['t', 'people', 'pelvisTilt'] : ['t', 'people']), ...(depthKnown ? ['depth'] : [])], note);
|
|
378
|
+
const depth = depthKnown && 'depth' in f ? readDepth(p, fw, f.depth) : undefined;
|
|
318
379
|
let pelvisTilt: number | null | undefined;
|
|
319
380
|
if (pelvisKnown && 'pelvisTilt' in f) {
|
|
320
381
|
const v = f.pelvisTilt;
|
|
@@ -349,9 +410,11 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
349
410
|
people.push({ id: person.id, joints: readJoints(p, pw, person.joints, note) });
|
|
350
411
|
});
|
|
351
412
|
}
|
|
352
|
-
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}) });
|
|
413
|
+
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}), ...(depth !== undefined ? { depth } : {}) });
|
|
353
414
|
});
|
|
354
|
-
|
|
415
|
+
const framesText = (fs: number[]) => `${where} ${fs.length === 1 ? `frames[${fs[0]}]` : `${fs.length} frames (${fs[0]}–${fs[fs.length - 1]})`}`;
|
|
416
|
+
if (minorFrames.length > 0) minorKey('pelvisTilt', framesText(minorFrames));
|
|
417
|
+
if (depthFrames.length > 0) minorKey('depth', framesText(depthFrames));
|
|
355
418
|
}
|
|
356
419
|
}
|
|
357
420
|
let pelvisSource: PelvisSource | undefined;
|
|
@@ -369,13 +432,30 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
369
432
|
if (ids.length > 1) p.add('POSE_PELVIS', `${where} carries a pelvis track and ${ids.length} people (${ids.join(', ')}); the track is one figure's, so the sequence holds one person (rig-pose pelvis --person writes only that one)`);
|
|
370
433
|
}
|
|
371
434
|
}
|
|
435
|
+
if (depthKnown && Array.isArray(doc.frames)) {
|
|
436
|
+
const carry = doc.frames.map((f: unknown) => isObject(f) && 'depth' in f);
|
|
437
|
+
const lacking = carry.flatMap((c: boolean, i: number) => (c ? [] : [i]));
|
|
438
|
+
if (lacking.length > 0 && lacking.length < carry.length) p.add('POSE_DEPTH', `${where} has "depth" on ${carry.length - lacking.length} frame(s) and none on frame(s) ${spansText(lacking)}; a sequence with depth carries it on every frame (an empty object where no segment is placed)`);
|
|
439
|
+
if (lacking.length < carry.length) {
|
|
440
|
+
const ids = [...new Set(frames.flatMap((f) => f.people.map((q) => q.id)))].sort();
|
|
441
|
+
if (ids.length > 1) p.add('POSE_DEPTH', `${where} carries depth and ${ids.length} people (${ids.join(', ')}); the depth is one figure's, so the sequence holds one person (rig-pose clean writes only the one it cleaned)`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
372
444
|
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null, note) : undefined;
|
|
373
445
|
if ('cameraSource' in doc) {
|
|
374
446
|
if (typeof doc.cameraSource !== 'string' || doc.cameraSource.trim() === '') p.add('POSE_CAMERA_SOURCE', `${where} "cameraSource" is ${typeName(doc.cameraSource)}; it must be a non-empty string saying where the camera came from ("tracked", "hand-labelled, N keys")`);
|
|
375
447
|
else if (!('camera' in doc)) p.add('POSE_CAMERA_SOURCE', `${where} has "cameraSource" ${JSON.stringify(doc.cameraSource)} and no "camera"; it states where a camera came from, so it needs one`);
|
|
376
448
|
}
|
|
377
449
|
if ('travel' in doc && doc.travel !== TRAVEL_IN_PLACE) p.add('POSE_TRAVEL', `${where} "travel" is ${typeName(doc.travel)}; the one value is "${TRAVEL_IN_PLACE}" (a gait filmed without the figure travelling), and without the key the picture's travel is the figure's`);
|
|
378
|
-
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note) : undefined;
|
|
450
|
+
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note, depthKnown) : undefined;
|
|
451
|
+
if (depthKnown) {
|
|
452
|
+
// rig-c/pose#281: "derived" depth is clean's, and clean records the gait it read it for; "measured" is a 3D detector's.
|
|
453
|
+
const sources = new Set(frames.flatMap((f) => Object.values(f.depth ?? {}).map((d) => d.source)));
|
|
454
|
+
const recorded = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'depth' in doc.cleaned.params;
|
|
455
|
+
if (sources.size > 1) p.add('POSE_DEPTH', `${where} carries "derived" and "measured" depth together; a sequence's depth comes from one source — clean --depth derives it, a 3D detector measures it`);
|
|
456
|
+
else if (sources.has('derived') && !recorded) p.add('POSE_DEPTH', `${where} carries "derived" depth and ${'cleaned' in doc ? 'its "cleaned" record has no "params" "depth"' : 'no "cleaned" record'}; derived depth is written by rig-pose clean --depth walk|run, which records the gait it was read for`);
|
|
457
|
+
if (recorded && cleaned !== undefined && frames.some((f) => f.depth === undefined || Object.values(f.depth).some((d) => d.source !== 'derived'))) p.add('POSE_DEPTH', `${where} "cleaned" "params" has "depth" and the frames do not all carry derived depth; the record says clean derived it on every frame`);
|
|
458
|
+
}
|
|
379
459
|
if (cleaned !== undefined && pelvisKnown) {
|
|
380
460
|
const hasPelvis = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'pelvis' in doc.cleaned.params;
|
|
381
461
|
if ('pelvisSource' in doc && !hasPelvis) p.add('POSE_PELVIS', `${where} carries a pelvis track and its "cleaned" record has no "params" "pelvis"; clean smooths the track and records it, so the track was added after clean — run rig-pose pelvis before rig-pose clean`);
|
|
@@ -386,6 +466,47 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
386
466
|
return { spec: POSE_SPEC, space: POSE_SPACE, width: doc.width as number, height: doc.height as number, source: doc.source as string, ...(cameraSource !== undefined ? { cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(doc.travel === TRAVEL_IN_PLACE ? { travel: TRAVEL_IN_PLACE } : {}), ...(pelvisSource !== undefined ? { pelvisSource } : {}), frames };
|
|
387
467
|
}
|
|
388
468
|
|
|
469
|
+
/** rig-c/pose#281: a frame's `depth` — `{ "<segment>": { ratio, sign, source } }`, every part refused by name. */
|
|
470
|
+
function readDepth(p: Problems, fw: string, d: unknown): FrameDepth | undefined {
|
|
471
|
+
const dw = `${fw} "depth"`;
|
|
472
|
+
if (!isObject(d)) {
|
|
473
|
+
p.add('POSE_DEPTH', `${dw} is ${typeName(d)}; it must be an object keyed by limb segment (${DEPTH_SEGMENT_NAMES.join(', ')}), each { ratio, sign, source }`);
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const out: FrameDepth = {};
|
|
477
|
+
for (const [name, v] of Object.entries(d)) {
|
|
478
|
+
const sw = `${dw} "${name}"`;
|
|
479
|
+
if (!(DEPTH_SEGMENT_NAMES as readonly string[]).includes(name)) {
|
|
480
|
+
p.add('POSE_DEPTH', `${dw} names ${JSON.stringify(name)}; the segments are ${DEPTH_SEGMENT_NAMES.join(', ')}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!isObject(v)) {
|
|
484
|
+
p.add('POSE_DEPTH', `${sw} is ${typeName(v)}; it must be { ratio, sign, source }`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
refuseUnknownKeys(p, 'POSE_DEPTH', sw, v, ['ratio', 'sign', 'source']);
|
|
488
|
+
let ok = true;
|
|
489
|
+
for (const k of ['ratio', 'sign', 'source']) if (!(k in v)) {
|
|
490
|
+
p.add('POSE_DEPTH', `${sw} has no "${k}"; a segment's depth is { ratio, sign, source }`);
|
|
491
|
+
ok = false;
|
|
492
|
+
}
|
|
493
|
+
if ('ratio' in v && !(typeof v.ratio === 'number' && Number.isFinite(v.ratio) && v.ratio >= 0)) {
|
|
494
|
+
p.add('POSE_DEPTH', `${sw} "ratio" is ${typeName(v.ratio)}; it must be a finite number ≥ 0 (the segment's drawn length ÷ its rest length)`);
|
|
495
|
+
ok = false;
|
|
496
|
+
}
|
|
497
|
+
if ('sign' in v && v.sign !== -1 && v.sign !== 0 && v.sign !== 1) {
|
|
498
|
+
p.add('POSE_DEPTH', `${sw} "sign" is ${typeName(v.sign)}; it must be 1 (the distal joint nearer the camera), -1 (farther) or 0 (flat, or not decided)`);
|
|
499
|
+
ok = false;
|
|
500
|
+
}
|
|
501
|
+
if ('source' in v && !(typeof v.source === 'string' && (DEPTH_SOURCES as readonly string[]).includes(v.source))) {
|
|
502
|
+
p.add('POSE_DEPTH', `${sw} "source" is ${typeName(v.source)}; it must be "derived" (clean --depth, from the picture's lengths) or "measured" (a 3D detector)`);
|
|
503
|
+
ok = false;
|
|
504
|
+
}
|
|
505
|
+
if (ok) out[name as DepthSegment] = { ratio: v.ratio as number, sign: v.sign as -1 | 0 | 1, source: v.source as DepthSource };
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
389
510
|
/** The minor a sequence's spec names: 0 for "rig-pose-sequence/1", N for "rig-pose-sequence/1.N"; null for anything else. */
|
|
390
511
|
function minorOf(spec: unknown): number | null {
|
|
391
512
|
if (spec === POSE_SPEC) return 0;
|
|
@@ -443,7 +564,7 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
443
564
|
}
|
|
444
565
|
|
|
445
566
|
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }. */
|
|
446
|
-
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string): CleanRecord | undefined {
|
|
567
|
+
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string, depthKnown: boolean): CleanRecord | undefined {
|
|
447
568
|
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
448
569
|
const cw = `${where} "cleaned"`;
|
|
449
570
|
const shape = '{ spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }';
|
|
@@ -460,7 +581,16 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
460
581
|
const q = c.params;
|
|
461
582
|
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred?, hold? }`);
|
|
462
583
|
else {
|
|
463
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : [])]);
|
|
584
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : []), ...(depthKnown ? ['depth'] : [])]);
|
|
585
|
+
if (depthKnown && 'depth' in q) {
|
|
586
|
+
const v = q.depth;
|
|
587
|
+
const vw = `${cw} "params" "depth"`;
|
|
588
|
+
if (!isObject(v)) p.add('POSE_CLEANED', `${vw} is ${typeName(v)}; it must be { gait } (rig-c/pose#281: the gait clean --depth read the depth for)`);
|
|
589
|
+
else {
|
|
590
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', vw, v, ['gait']);
|
|
591
|
+
if (!(typeof v.gait === 'string' && (DEPTH_GAITS as readonly string[]).includes(v.gait))) p.add('POSE_CLEANED', `${vw} "gait" is ${typeName(v.gait)}; it must be ${DEPTH_GAITS.map((g) => `"${g}"`).join(' or ')}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
464
594
|
if (pelvisKnown && 'pelvis' in q) {
|
|
465
595
|
const v = q.pelvis;
|
|
466
596
|
const vw = `${cw} "params" "pelvis"`;
|
|
@@ -589,11 +719,11 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
589
719
|
});
|
|
590
720
|
}
|
|
591
721
|
if (p.list.length > before) return undefined;
|
|
592
|
-
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
722
|
+
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
593
723
|
const j = c.jitter as { before: number | null; after: number | null };
|
|
594
724
|
return {
|
|
595
725
|
spec: CLEAN_SPEC,
|
|
596
|
-
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}), ...(pelvisKnown && q.pelvis !== undefined ? { pelvis: { window: q.pelvis.window } } : {}) },
|
|
726
|
+
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}), ...(pelvisKnown && q.pelvis !== undefined ? { pelvis: { window: q.pelvis.window } } : {}), ...(depthKnown && q.depth !== undefined ? { depth: { gait: q.depth.gait } } : {}) },
|
|
597
727
|
spikes: {
|
|
598
728
|
found: ((c.spikes as { found: Array<{ frame: number; joint: Body18 }> }).found).map((s) => ({ frame: s.frame, joint: s.joint })),
|
|
599
729
|
untested: [...(c.spikes as { untested: number[] }).untested],
|
|
@@ -922,6 +1052,8 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
922
1052
|
const frames = seq.frames.map((f) => ({
|
|
923
1053
|
t: f.t,
|
|
924
1054
|
...(f.pelvisTilt !== undefined ? { pelvisTilt: f.pelvisTilt === null ? null : round(f.pelvisTilt, 3) } : {}),
|
|
1055
|
+
// rig-c/pose#281: the segments in DEPTH_SEGMENTS' order, each { ratio, sign, source }.
|
|
1056
|
+
...(f.depth !== undefined ? { depth: Object.fromEntries(DEPTH_SEGMENT_NAMES.filter((k) => f.depth![k] !== undefined).map((k) => [k, { ratio: round(f.depth![k]!.ratio, 3), sign: f.depth![k]!.sign, source: f.depth![k]!.source }])) } : {}),
|
|
925
1057
|
people: f.people.map((q) => ({
|
|
926
1058
|
id: q.id,
|
|
927
1059
|
joints: Object.fromEntries(
|
|
@@ -941,14 +1073,14 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
941
1073
|
const jit = (v: number | null) => (v === null ? null : round(v, 4));
|
|
942
1074
|
const cleaned = c === undefined ? undefined : {
|
|
943
1075
|
spec: c.spec,
|
|
944
|
-
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}), ...(c.params.pelvis !== undefined ? { pelvis: { window: c.params.pelvis.window } } : {}) },
|
|
1076
|
+
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}), ...(c.params.pelvis !== undefined ? { pelvis: { window: c.params.pelvis.window } } : {}), ...(c.params.depth !== undefined ? { depth: { gait: c.params.depth.gait } } : {}) },
|
|
945
1077
|
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
946
1078
|
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
947
1079
|
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] } : {}), jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
948
1080
|
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
949
1081
|
...(c.unheld !== undefined ? { unheld: c.unheld.map((u) => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value === null ? null : round(u.value, 4) })) } : {}),
|
|
950
1082
|
};
|
|
951
|
-
// rig-c/pose#288: the minor the content needs — /1.1 with a pelvis track, /1 otherwise.
|
|
952
|
-
const spec = seq.pelvisSource !== undefined ? POSE_SPEC_PELVIS : seq.spec;
|
|
1083
|
+
// rig-c/pose#288, #281: the minor the content needs — /1.2 with depth (it includes 1.1's keys), /1.1 with a pelvis track, /1 otherwise.
|
|
1084
|
+
const spec = seq.frames.some((f) => f.depth !== undefined) ? POSE_SPEC_DEPTH : seq.pelvisSource !== undefined ? POSE_SPEC_PELVIS : seq.spec;
|
|
953
1085
|
return `${JSON.stringify({ spec, space: seq.space, width: seq.width, height: seq.height, source: seq.source, ...(seq.cameraSource !== undefined ? { cameraSource: seq.cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.pelvisSource !== undefined ? { pelvisSource: seq.pelvisSource } : {}), frames }, null, 2)}\n`;
|
|
954
1086
|
}
|
package/src/reduce.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface ReduceBone {
|
|
|
48
48
|
dense: number[];
|
|
49
49
|
/** Frame indices that must keep a key regardless (the first and last always do). */
|
|
50
50
|
keep: ReadonlySet<number>;
|
|
51
|
+
/** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283: its chain's). */
|
|
52
|
+
tolerance?: number;
|
|
51
53
|
}
|
|
52
54
|
export interface ReduceHip {
|
|
53
55
|
bone: string;
|
|
@@ -328,6 +330,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
328
330
|
const byName = new Map<string, ReduceBone>();
|
|
329
331
|
for (const b of bones) {
|
|
330
332
|
if (byName.has(b.bone)) throw new Error(`reduce: bone "${b.bone}" is given twice`);
|
|
333
|
+
if (b.tolerance !== undefined && !(Number.isFinite(b.tolerance) && b.tolerance > 0)) throw new Error(`reduce: bone "${b.bone}"'s tolerance is ${b.tolerance}°, not a positive number`);
|
|
331
334
|
if (b.dense.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${b.dense.length} dense key(s) for ${n} frame(s)`);
|
|
332
335
|
byName.set(b.bone, b);
|
|
333
336
|
}
|
|
@@ -349,7 +352,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
349
352
|
const keys = fitTrack(times, samples, {
|
|
350
353
|
targets,
|
|
351
354
|
keep: keptFrames(b.keep, n, `bone "${b.bone}"`),
|
|
352
|
-
tolerance,
|
|
355
|
+
tolerance: b.tolerance ?? tolerance,
|
|
353
356
|
error: (i, v) => Math.abs(v[0]! - targets[i]![0]!),
|
|
354
357
|
});
|
|
355
358
|
const e = trackAt(keys, samples, 1).map((v, i) => v[0]! - targets[i]![0]!);
|
|
@@ -617,11 +620,12 @@ export function smoothTrunk(
|
|
|
617
620
|
if (n < 4 || trunkBones.size === 0) return;
|
|
618
621
|
const samples = samplesOf(times);
|
|
619
622
|
const byName = new Map(bones.map((b) => [b.bone, b]));
|
|
623
|
+
/** rig-c/pose#283: a bone's own tolerance (its chain's under a per-chain --reduce), else the call's. */
|
|
624
|
+
const tolOf = (b: string): number => byName.get(b)?.tolerance ?? tolerance;
|
|
620
625
|
const roots = [...trunkBones].filter((b) => {
|
|
621
626
|
const info = byName.get(b);
|
|
622
627
|
return info !== undefined && (info.above === null || !trunkBones.has(info.above));
|
|
623
628
|
});
|
|
624
|
-
const tolInner = 0.985 * tolerance;
|
|
625
629
|
|
|
626
630
|
for (const root of roots) {
|
|
627
631
|
const rootKeys = out.rotate.get(root);
|
|
@@ -646,8 +650,10 @@ export function smoothTrunk(
|
|
|
646
650
|
const targetRoot = rootInfo.dense.map((d, i) => d - eAboveRoot[i]!);
|
|
647
651
|
const yRootOld = yOldOf.get(root)!;
|
|
648
652
|
|
|
649
|
-
|
|
650
|
-
const
|
|
653
|
+
// #283: each bone's own tolerance (its chain's under a per-chain --reduce; the call's otherwise).
|
|
654
|
+
const rootInner = 0.985 * tolOf(root);
|
|
655
|
+
const rootLb = targetRoot.map((t) => t - rootInner);
|
|
656
|
+
const rootUb = targetRoot.map((t) => t + rootInner);
|
|
651
657
|
let smRoot = optimizeBasis(basisOf.get(root)!, rootLb, rootUb);
|
|
652
658
|
let eRoot = smRoot.y.map((v, i) => v - targetRoot[i]!);
|
|
653
659
|
|
|
@@ -667,14 +673,15 @@ export function smoothTrunk(
|
|
|
667
673
|
for (const h of headBones) {
|
|
668
674
|
const dH = byName.get(h)!.dense;
|
|
669
675
|
const targetH = dH.map((d, i) => d - eRoot[i]!);
|
|
670
|
-
const
|
|
676
|
+
const hInner = 0.985 * tolOf(h);
|
|
677
|
+
const smH = optimizeBasis(basisOf.get(h)!, targetH.map((t) => t - hInner), targetH.map((t) => t + hInner));
|
|
671
678
|
smSub.set(h, smH);
|
|
672
679
|
eSub.set(h, smH.y.map((v, i) => v - targetH[i]!));
|
|
673
680
|
}
|
|
674
681
|
for (const b0 of arm0Bones) {
|
|
675
682
|
const yOld = yOldOf.get(b0)!;
|
|
676
683
|
const wOld = Math.max(...out.errors.get(b0)!.map(Math.abs));
|
|
677
|
-
const cap = Math.min(0.98 *
|
|
684
|
+
const cap = Math.min(0.98 * tolOf(b0), wOld + 0.035 * tolOf(b0));
|
|
678
685
|
const targetB = byName.get(b0)!.dense.map((d, i) => d - eRoot[i]!);
|
|
679
686
|
const smB = solveLimbPreservingJitter(basisOf.get(b0)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
680
687
|
smSub.set(b0, smB);
|
|
@@ -685,7 +692,7 @@ export function smoothTrunk(
|
|
|
685
692
|
const eUp = eSub.get(up)!;
|
|
686
693
|
const yOld = yOldOf.get(b1)!;
|
|
687
694
|
const wOld = Math.max(...out.errors.get(b1)!.map(Math.abs));
|
|
688
|
-
const cap = Math.min(0.98 *
|
|
695
|
+
const cap = Math.min(0.98 * tolOf(b1), wOld + 0.035 * tolOf(b1));
|
|
689
696
|
const targetB = byName.get(b1)!.dense.map((d, i) => d - eUp[i]!);
|
|
690
697
|
const smB = solveLimbPreservingJitter(basisOf.get(b1)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
691
698
|
smSub.set(b1, smB);
|
|
@@ -699,17 +706,17 @@ export function smoothTrunk(
|
|
|
699
706
|
for (let i = 0; i < n; i++) {
|
|
700
707
|
for (const h of headBones) {
|
|
701
708
|
const eh = eSub.get(h)![i]!;
|
|
702
|
-
if (Math.abs(eh) > 0.99 *
|
|
703
|
-
tightenAround(i, Math.abs(eh) - 0.975 *
|
|
709
|
+
if (Math.abs(eh) > 0.99 * tolOf(h)) {
|
|
710
|
+
tightenAround(i, Math.abs(eh) - 0.975 * tolOf(h));
|
|
704
711
|
anyTightened = true;
|
|
705
712
|
}
|
|
706
713
|
}
|
|
707
714
|
for (const b of [...arm0Bones, ...arm1Bones]) {
|
|
708
715
|
const wOld = Math.max(...out.errors.get(b)!.map(Math.abs));
|
|
709
|
-
const cap = Math.min(0.985 *
|
|
716
|
+
const cap = Math.min(0.985 * tolOf(b), wOld + 0.04 * tolOf(b));
|
|
710
717
|
const eb = eSub.get(b)![i]!;
|
|
711
718
|
if (Math.abs(eb) > cap) {
|
|
712
|
-
tightenAround(i, Math.abs(eb) - (cap - 0.01 *
|
|
719
|
+
tightenAround(i, Math.abs(eb) - (cap - 0.01 * tolOf(b)));
|
|
713
720
|
anyTightened = true;
|
|
714
721
|
}
|
|
715
722
|
}
|
|
@@ -827,6 +834,8 @@ export interface UnderBone {
|
|
|
827
834
|
/** The dense rotate keys (unwrapped, as written). */
|
|
828
835
|
dense: readonly number[];
|
|
829
836
|
keep: ReadonlySet<number>;
|
|
837
|
+
/** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283). */
|
|
838
|
+
tolerance?: number;
|
|
830
839
|
}
|
|
831
840
|
|
|
832
841
|
/**
|
|
@@ -879,7 +888,7 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
|
|
|
879
888
|
return Math.abs(wrap180(Math.atan2(w[1], w[0]) * DEG - b.target[i]!));
|
|
880
889
|
};
|
|
881
890
|
const keep = keptFrames(b.keep, n, `bone "${b.bone}"`);
|
|
882
|
-
const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance, error: (i, v) => aimError(i, v[0]!) });
|
|
891
|
+
const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance: b.tolerance ?? tolerance, error: (i, v) => aimError(i, v[0]!) });
|
|
883
892
|
const kAt = trackAt(rk, samples, 1).map((v) => v[0]!);
|
|
884
893
|
const ae = worstOf(kAt.map((_, i) => i), (i) => aimError(i, kAt[i]!))!;
|
|
885
894
|
rows.push({ bone: b.bone, kind: 'angle', before: n, after: rk.length, worst: ae.worst, frame: ae.frame });
|
package/src/retarget.ts
CHANGED
|
@@ -79,7 +79,7 @@ import { aimAngle, aimEnds, bodyYawSets, descends, faceYawSets, knownSetsOf, par
|
|
|
79
79
|
import type { Joint, Travel } from './pose.ts';
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
|
-
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
82
|
+
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SetRuns, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
83
|
import { clampArt, coveredChains, isRaised, 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
85
|
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';
|
|
@@ -369,6 +369,34 @@ export interface ReachReading {
|
|
|
369
369
|
ratio: number | null;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* rig-c/pose#282: where a `reach` chain's bone starts against the source joint
|
|
374
|
+
* it re-aims from. The pose's pair starts at the translated joint, which the
|
|
375
|
+
* rig puts at the translated bone's posed origin; the reach aims from its own
|
|
376
|
+
* bone's posed origin. The distance between the two, at setup and on the
|
|
377
|
+
* frames, is the lever #236 names — a reading, with a WARN past
|
|
378
|
+
* `REACH_ORIGIN_WARN` of the segment. One per reach chain, in map order.
|
|
379
|
+
*/
|
|
380
|
+
export interface ReachOrigin {
|
|
381
|
+
chain: string;
|
|
382
|
+
bone: string;
|
|
383
|
+
/** The pair: the chain's first joint (the translated joint) and its second. */
|
|
384
|
+
joints: [string, string];
|
|
385
|
+
/** The translated bone, whose posed origin is where the rig puts the source joint. */
|
|
386
|
+
anchor: string;
|
|
387
|
+
/** The distance at setup, world units. */
|
|
388
|
+
setup: number;
|
|
389
|
+
/** The largest distance as posed on the frames, world units, and its frame. */
|
|
390
|
+
posed: { max: number; frame: number };
|
|
391
|
+
/** The reached length: the pair's median picture length, scaled (world units). */
|
|
392
|
+
segment: number;
|
|
393
|
+
/** max(setup, posed.max) ÷ segment. */
|
|
394
|
+
ofSegment: number;
|
|
395
|
+
/** ofSegment > threshold. */
|
|
396
|
+
warn: boolean;
|
|
397
|
+
threshold: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
/**
|
|
373
401
|
* Mean |second difference| of a series of angles, degrees: |wrap(v[i+1] − v[i])
|
|
374
402
|
* − wrap(v[i] − v[i−1])| over every i whose three frames `ok` admits (every
|
|
@@ -419,6 +447,8 @@ export interface Retarget {
|
|
|
419
447
|
scaleFit: ScaleFit | null;
|
|
420
448
|
/** The spans each alternate set shows, with their art turn. */
|
|
421
449
|
switches: Switch[];
|
|
450
|
+
/** rig-c/pose#279: per set without a `phase`, the runs it was nearest on and the runs the minimum span suppressed. */
|
|
451
|
+
switchRuns: SetRuns[];
|
|
422
452
|
/** Per switched or hidden slot, what it shows on each frame (null: nothing). */
|
|
423
453
|
shown: Map<string, Array<string | null>>;
|
|
424
454
|
/** Per set, its distance to the pose on each frame, degrees. */
|
|
@@ -475,6 +505,8 @@ export interface Retarget {
|
|
|
475
505
|
nodSkipped: NodSkipped | null;
|
|
476
506
|
/** Each `reach` chain hanging below a bone the animation turns or translates (#236): its aim's departure from the pose pair's angle. */
|
|
477
507
|
reach: ReachReading[];
|
|
508
|
+
/** rig-c/pose#282: each `reach` chain's origin against the source joint it re-aims from; empty when the map has none. */
|
|
509
|
+
reachOrigin: ReachOrigin[];
|
|
478
510
|
/** rig-c/pose#288: the pelvis bone's source; null when the map has no pelvis chain and the sequence no track. */
|
|
479
511
|
pelvis: PelvisReading | null;
|
|
480
512
|
}
|
|
@@ -1486,6 +1518,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1486
1518
|
const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
|
|
1487
1519
|
let aimReach: ((c: Chain) => void) | null = null;
|
|
1488
1520
|
const reachAims = new Map<string, { aim: number[]; pair: number[]; length: number }>();
|
|
1521
|
+
/** #282: per reach chain, the distance from its bone's posed origin to the anchor's on each frame (the last aim's). */
|
|
1522
|
+
const reachGaps = new Map<string, number[]>();
|
|
1489
1523
|
if (reaches.length > 0 && t !== null && scale !== null) {
|
|
1490
1524
|
const s = scale.factor;
|
|
1491
1525
|
const tb = rest.get(t.bone)!;
|
|
@@ -1494,8 +1528,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1494
1528
|
const anchor = series.get(c.joints[0]!)!;
|
|
1495
1529
|
const end = series.get(c.joints[1]!)!;
|
|
1496
1530
|
const angles: number[] = [];
|
|
1531
|
+
const gaps: number[] = [];
|
|
1497
1532
|
for (let i = 0; i < n; i++) {
|
|
1498
1533
|
const o = posedOrigin(b, i);
|
|
1534
|
+
gaps.push(Math.hypot(o[0] - (tb.worldX + travel[i]![0]), o[1] - (tb.worldY + travel[i]![1])));
|
|
1499
1535
|
const tx = tb.worldX + travel[i]![0] + (end[i]![0] - anchor[i]![0]) * s;
|
|
1500
1536
|
const ty = tb.worldY + travel[i]![1] - (end[i]![1] - anchor[i]![1]) * s;
|
|
1501
1537
|
const d = Math.hypot(tx - o[0], ty - o[1]);
|
|
@@ -1503,6 +1539,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1503
1539
|
}
|
|
1504
1540
|
fillLeading(angles, () => p.add('POSE_SEGMENT_ZERO', `chain "${c.name}" — bone "${b}" reaches for ${c.joints[1]}, which the pose puts on the bone's posed origin on every frame; it has no direction`));
|
|
1505
1541
|
targets.set(b, angles);
|
|
1542
|
+
reachGaps.set(c.name, gaps);
|
|
1506
1543
|
// #236: the aim as reached, before its own range clamp, beside the pair's own angle (a reading).
|
|
1507
1544
|
reachAims.set(c.name, { aim: [...angles], pair: Array.from({ length: n }, (_, i) => pictureAngle(anchor[i]!, end[i]!)), length: median(Array.from({ length: n }, (_, i) => Math.hypot(end[i]![0] - anchor[i]![0], end[i]![1] - anchor[i]![1]) * s)) });
|
|
1508
1545
|
};
|
|
@@ -3087,6 +3124,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3087
3124
|
if (headNodTrack !== null) tracks.push(headNodTrack);
|
|
3088
3125
|
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
|
3089
3126
|
const reach = reachReadings(rest, map, tracks, reachAims, t?.bone ?? null);
|
|
3127
|
+
const reachOrigin = reachOrigins(rest, map, reachAims, reachGaps, t?.bone ?? null);
|
|
3090
3128
|
|
|
3091
3129
|
const pivot = pivotOf(rest, t?.bone ?? null, legs);
|
|
3092
3130
|
const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
|
|
@@ -3099,8 +3137,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3099
3137
|
return {
|
|
3100
3138
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
3101
3139
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale, scaleFit,
|
|
3102
|
-
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3103
|
-
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, pelvis,
|
|
3140
|
+
switches: sw.switches, switchRuns: sw.runs, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3141
|
+
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, reachOrigin, pelvis,
|
|
3104
3142
|
};
|
|
3105
3143
|
}
|
|
3106
3144
|
|
|
@@ -3209,6 +3247,47 @@ function reachReadings(rest: Rest, map: JointMap, tracks: ReadonlyArray<MotionTr
|
|
|
3209
3247
|
return out;
|
|
3210
3248
|
}
|
|
3211
3249
|
|
|
3250
|
+
/**
|
|
3251
|
+
* #282: a reach bone whose origin sits more than this fraction of the segment
|
|
3252
|
+
* from the source joint is warned. The aim from an origin a fraction f up the
|
|
3253
|
+
* pair turns about 1 ÷ (1 − f) times the pair's turn (#236's lever: the example
|
|
3254
|
+
* wave's trunk, f = 0.5, measured 2.00×); 0.2 is 1.25×, a quarter more turn
|
|
3255
|
+
* than the pose has.
|
|
3256
|
+
*/
|
|
3257
|
+
export const REACH_ORIGIN_WARN = 0.2;
|
|
3258
|
+
|
|
3259
|
+
/** #282: each reach chain's origin against the anchor's, at setup and as posed, in map order. */
|
|
3260
|
+
function reachOrigins(rest: Rest, map: JointMap, aims: ReadonlyMap<string, { length: number }>, gaps: ReadonlyMap<string, number[]>, anchor: string | null): ReachOrigin[] {
|
|
3261
|
+
const out: ReachOrigin[] = [];
|
|
3262
|
+
if (anchor === null) return out;
|
|
3263
|
+
const a0 = rest.get(anchor)!;
|
|
3264
|
+
for (const c of map.chains) {
|
|
3265
|
+
const got = aims.get(c.name);
|
|
3266
|
+
const g = gaps.get(c.name);
|
|
3267
|
+
if (!c.reach || got === undefined || g === undefined) continue;
|
|
3268
|
+
const b = c.bones[0]!;
|
|
3269
|
+
const bo = rest.get(b)!;
|
|
3270
|
+
const setup = r3(Math.hypot(bo.worldX - a0.worldX, bo.worldY - a0.worldY));
|
|
3271
|
+
let frame = 0;
|
|
3272
|
+
for (let i = 1; i < g.length; i++) if (g[i]! > g[frame]!) frame = i;
|
|
3273
|
+
const max = r3(g[frame] ?? 0);
|
|
3274
|
+
const segment = r3(got.length);
|
|
3275
|
+
const ofSegment = got.length > 0 ? r3(Math.max(setup, max) / got.length) : 0;
|
|
3276
|
+
out.push({ chain: c.name, bone: b, joints: [c.joints[0]!, c.joints[1]!], anchor, setup, posed: { max, frame }, segment, ofSegment, warn: ofSegment > REACH_ORIGIN_WARN, threshold: REACH_ORIGIN_WARN });
|
|
3277
|
+
}
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
/** #282: the `REACH-ORIGIN` line, one per reach chain. */
|
|
3282
|
+
export function reachOriginLine(e: ReachOrigin, times: readonly number[]): string {
|
|
3283
|
+
return `chain "${e.chain}" bone "${e.bone}" (${e.joints[0]} → ${e.joints[1]}): its origin ${e.setup.toFixed(2)} units from the source joint ${e.joints[0]} (bone "${e.anchor}"'s origin) at setup, up to ${e.posed.max.toFixed(2)} on the frames (frame ${e.posed.frame}, t=${times[e.posed.frame]!.toFixed(3)}s): ${e.ofSegment.toFixed(3)} of the ${e.segment.toFixed(2)}-unit segment`;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
/** #282: the WARN of a reach chain whose origin sits past REACH_ORIGIN_WARN of the segment. */
|
|
3287
|
+
export function reachOriginWarn(e: ReachOrigin): string {
|
|
3288
|
+
return `REACH_ORIGIN: chain "${e.chain}" bone "${e.bone}" starts ${e.ofSegment.toFixed(3)} of the segment from ${e.joints[0]}, past ${e.threshold.toFixed(2)}: re-aimed from there, it turns more than the pair (JITTER bone "${e.bone}"); "reach": false follows the measured angle`;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3212
3291
|
/** The `REACH` line of a reach chain whose aim departs from its pair (#236). */
|
|
3213
3292
|
export function reachCostLine(e: ReachReading, times: readonly number[]): string {
|
|
3214
3293
|
const f = (v: number | null) => (v === null ? 'not measured' : `${v.toFixed(4)}°/f²`);
|