@rig-c/pose 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -14
- package/cli.ts +73 -17
- package/package.json +1 -1
- package/src/clean.ts +346 -10
- package/src/legs.ts +423 -41
- package/src/map.ts +49 -3
- package/src/pose.ts +322 -24
- package/src/reduce.ts +167 -7
- package/src/retarget.ts +159 -48
- package/src/run.ts +251 -27
- package/src/switch.ts +31 -8
- package/src/verify.ts +29 -4
package/src/reduce.ts
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
* target is its world error: errors do not pile up down a chain. A knot's value
|
|
19
19
|
* is the target's, not the dense key's.
|
|
20
20
|
*
|
|
21
|
+
* Scaled bones (rig-c/pose#94). A `scalex` track is thinned in value space
|
|
22
|
+
* within `SCALE_REDUCE` (`reduceScale`). A bone under a scaled bone is not
|
|
23
|
+
* keyed by the formula above but through its parent's posed matrix (legs.ts
|
|
24
|
+
* `posePath`), so `reduceUnderScale` solves its keys again through its parent
|
|
25
|
+
* as reduced and reads each error as the world aim, and the world length, it
|
|
26
|
+
* lands on.
|
|
27
|
+
*
|
|
21
28
|
* Every error is read after the numbers are rounded as written and evaluated as
|
|
22
29
|
* rig-c's poser evaluates them (`curveAt`): the curve is the poser's ten-piece
|
|
23
30
|
* polyline, not the cubic. The poser is the judge — run.ts measures the landing
|
|
@@ -52,12 +59,13 @@ export interface ReduceHip {
|
|
|
52
59
|
}
|
|
53
60
|
export interface ReduceRow {
|
|
54
61
|
bone: string;
|
|
55
|
-
|
|
62
|
+
/** `scale`: a `scalex` track (#94), its error the bone's world x-axis length against its setup length times the scale asked. */
|
|
63
|
+
kind: 'angle' | 'origin' | 'scale';
|
|
56
64
|
/** Dense key count. */
|
|
57
65
|
before: number;
|
|
58
66
|
/** Reduced key count. */
|
|
59
67
|
after: number;
|
|
60
|
-
/** The largest error over all frames: degrees of world aim for a bone, world units for the hip. */
|
|
68
|
+
/** The largest error over all frames: degrees of world aim for a bone, world units for the hip, a fraction of the setup length for a scale. */
|
|
61
69
|
worst: number;
|
|
62
70
|
/** The first frame where it is. */
|
|
63
71
|
frame: number;
|
|
@@ -67,6 +75,8 @@ export interface Reduced {
|
|
|
67
75
|
hip: ReducedKey[] | null;
|
|
68
76
|
/** The bones in the order given, then the hip. */
|
|
69
77
|
rows: ReduceRow[];
|
|
78
|
+
/** Each bone's world-aim error on every frame (reduced less dense, degrees), as the poser evaluates the reduced keys. */
|
|
79
|
+
errors: Map<string, number[]>;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +196,11 @@ const r3 = (v: number): number => {
|
|
|
186
196
|
const r = Math.round(v * 1000) / 1000;
|
|
187
197
|
return Object.is(r, -0) ? 0 : r;
|
|
188
198
|
};
|
|
199
|
+
/** A scale as the retarget writes one (retarget.ts `r6`). */
|
|
200
|
+
const r6 = (v: number): number => {
|
|
201
|
+
const r = Math.round(v * 1e6) / 1e6;
|
|
202
|
+
return Object.is(r, -0) ? 0 : r;
|
|
203
|
+
};
|
|
189
204
|
|
|
190
205
|
/** One track to reduce: its targets per frame (one array per frame, a value per channel) and how a frame's error is read. */
|
|
191
206
|
interface Fit {
|
|
@@ -194,6 +209,8 @@ interface Fit {
|
|
|
194
209
|
tolerance: number;
|
|
195
210
|
/** The error at frame `i` of `values` against the target. */
|
|
196
211
|
error: (i: number, values: readonly number[]) => number;
|
|
212
|
+
/** How a written value is rounded: 0.001 (an angle, a translate) unless given. */
|
|
213
|
+
round?: (v: number) => number;
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
/** The frame with the largest error over `frames` (the first, on a tie), or null when there is none. */
|
|
@@ -242,7 +259,8 @@ function solveHandles(a: ReducedKey, b: ReducedKey, channel: number, hx1: number
|
|
|
242
259
|
function fitTrack(times: readonly number[], samples: readonly number[], fit: Fit): ReducedKey[] {
|
|
243
260
|
const n = times.length;
|
|
244
261
|
const channels = fit.targets[0]!.length;
|
|
245
|
-
const
|
|
262
|
+
const round = fit.round ?? r3;
|
|
263
|
+
const keyOn = (i: number): ReducedKey => ({ t: times[i]!, v: fit.targets[i]!.map(round) });
|
|
246
264
|
const knots = [...new Set([0, n - 1, ...fit.keep])].sort((x, y) => x - y);
|
|
247
265
|
const curves = new Map<number, number[]>();
|
|
248
266
|
const done: number[] = [];
|
|
@@ -266,7 +284,7 @@ function fitTrack(times: readonly number[], samples: readonly number[], fit: Fit
|
|
|
266
284
|
const hx2 = times[k0]! + (2 * (times[k1]! - times[k0]!)) / 3;
|
|
267
285
|
const handles = Array.from({ length: channels }, (_, c) => solveHandles(a, b, c, hx1, hx2, interior, samples, (i) => fit.targets[i]![c]!));
|
|
268
286
|
if (handles.every((h) => h !== null)) {
|
|
269
|
-
const curve = handles.flatMap(([cy1, cy2]) => [hx1,
|
|
287
|
+
const curve = handles.flatMap(([cy1, cy2]) => [hx1, round(cy1), hx2, round(cy2)]);
|
|
270
288
|
const bent = errorWith({ ...a, curve });
|
|
271
289
|
if (bent.worst <= fit.tolerance) {
|
|
272
290
|
curves.set(k0, curve);
|
|
@@ -304,8 +322,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
304
322
|
if (!(Number.isFinite(tolerance) && tolerance > 0)) throw new Error(`reduce: the tolerance is ${tolerance}°, not a positive number`);
|
|
305
323
|
if (hip !== null && !(Number.isFinite(hip.tolerance) && hip.tolerance > 0)) throw new Error(`reduce: the hip's tolerance is ${hip.tolerance} units, not a positive number`);
|
|
306
324
|
// The poser samples at the running sum of the frame steps (verify.ts `poseFrames`).
|
|
307
|
-
const samples
|
|
308
|
-
for (let i = 1; i < n; i++) samples.push(samples[i - 1]! + (times[i]! - times[i - 1]!));
|
|
325
|
+
const samples = samplesOf(times);
|
|
309
326
|
|
|
310
327
|
const byName = new Map<string, ReduceBone>();
|
|
311
328
|
for (const b of bones) {
|
|
@@ -341,6 +358,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
341
358
|
rowOf.set(b.bone, { bone: b.bone, kind: 'angle', before: n, after: keys.length, worst: worst.worst, frame: worst.frame });
|
|
342
359
|
}
|
|
343
360
|
const rows = bones.map((b) => rowOf.get(b.bone)!);
|
|
361
|
+
const errorsOrdered = new Map(bones.map((b) => [b.bone, errors.get(b.bone)!]));
|
|
344
362
|
const ordered = new Map(bones.map((b) => [b.bone, rotate.get(b.bone)!]));
|
|
345
363
|
|
|
346
364
|
let hipKeys: ReducedKey[] | null = null;
|
|
@@ -357,5 +375,147 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
357
375
|
const worst = worstOf(at.map((_, i) => i), (i) => world(i, at[i]!))!;
|
|
358
376
|
rows.push({ bone: hip.bone, kind: 'origin', before: n, after: hipKeys.length, worst: worst.worst, frame: worst.frame });
|
|
359
377
|
}
|
|
360
|
-
return { rotate: ordered, hip: hipKeys, rows };
|
|
378
|
+
return { rotate: ordered, hip: hipKeys, rows, errors: errorsOrdered };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The times the poser samples a track at: the running sum of the frame steps (verify.ts `poseFrames`). */
|
|
382
|
+
export function samplesOf(times: readonly number[]): number[] {
|
|
383
|
+
const samples: number[] = [times[0]!];
|
|
384
|
+
for (let i = 1; i < times.length; i++) samples.push(samples[i - 1]! + (times[i]! - times[i - 1]!));
|
|
385
|
+
return samples;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// scales, and bones keyed through a scaled parent (rig-c/pose#94)
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The tolerance a reduced `scalex` track is held to: half of the landing
|
|
394
|
+
* check's scale tolerance (verify.ts `SCALE_TOLERANCE`, 1e-4 of the setup
|
|
395
|
+
* length), so a reduced curve cannot fail the check it is re-run against.
|
|
396
|
+
*/
|
|
397
|
+
export const SCALE_REDUCE = 5e-5;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* One `scalex` track of a bone with no scaled bone above it, reduced: its world
|
|
401
|
+
* x-axis length over setup is its own scale, so the error is the value's
|
|
402
|
+
* distance from the scale asked.
|
|
403
|
+
*/
|
|
404
|
+
export function reduceScale(times: readonly number[], bone: string, asked: readonly number[], keep: ReadonlySet<number>, tolerance: number): { keys: ReducedKey[]; at: number[]; row: ReduceRow } {
|
|
405
|
+
const n = times.length;
|
|
406
|
+
if (asked.length !== n) throw new Error(`reduce: bone "${bone}" asks ${asked.length} scale(s) for ${n} frame(s)`);
|
|
407
|
+
const samples = samplesOf(times);
|
|
408
|
+
const keys = fitTrack(times, samples, { targets: asked.map((v) => [v]), keep: keptFrames(keep, n, `bone "${bone}" scalex`), tolerance, error: (i, v) => Math.abs(v[0]! - asked[i]!), round: r6 });
|
|
409
|
+
const at = trackAt(keys, samples, 1).map((v) => v[0]!);
|
|
410
|
+
const worst = worstOf(at.map((_, i) => i), (i) => Math.abs(at[i]! - asked[i]!))!;
|
|
411
|
+
return { keys, at, row: { bone, kind: 'scale', before: n, after: keys.length, worst: worst.worst, frame: worst.frame } };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** A world 2×2: columns (a, c) — the x axis — and (b, d) — the y axis (legs.ts `M2`). */
|
|
415
|
+
export type M2 = [number, number, number, number];
|
|
416
|
+
const DEG = 180 / Math.PI;
|
|
417
|
+
const mul = (p: M2, q: M2): M2 => [p[0] * q[0] + p[1] * q[2], p[0] * q[1] + p[1] * q[3], p[2] * q[0] + p[3] * q[2], p[2] * q[1] + p[3] * q[3]];
|
|
418
|
+
const inv = (m: M2): M2 => {
|
|
419
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
420
|
+
return [m[3] / det, -m[1] / det, -m[2] / det, m[0] / det];
|
|
421
|
+
};
|
|
422
|
+
const apply = (m: M2, v: [number, number]): [number, number] => [m[0] * v[0] + m[1] * v[1], m[2] * v[0] + m[3] * v[1]];
|
|
423
|
+
const rot = (deg: number): M2 => {
|
|
424
|
+
const r = deg / DEG;
|
|
425
|
+
return [Math.cos(r), -Math.sin(r), Math.sin(r), Math.cos(r)];
|
|
426
|
+
};
|
|
427
|
+
const scaleX = (s: number): M2 => [s, 0, 0, 1];
|
|
428
|
+
/** A bone's setup local 2×2 in its parent's axes: inv(parent's setup world) · its setup world (legs.ts `posePath`'s L0). */
|
|
429
|
+
export const setupLocal = (parentWorld0: M2, world0: M2): M2 => mul(inv(parentWorld0), world0);
|
|
430
|
+
/** `p · q`. */
|
|
431
|
+
export const mulM2 = mul;
|
|
432
|
+
const IDENTITY: M2 = [1, 0, 0, 1];
|
|
433
|
+
export { IDENTITY as M2_IDENTITY };
|
|
434
|
+
const wrap180 = (d: number): number => {
|
|
435
|
+
const x = ((d + 180) % 360 + 360) % 360 - 180;
|
|
436
|
+
return x === -180 ? 180 : x;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A mapped bone under a scaled bone (legs.ts `posePath`: keyed through its
|
|
441
|
+
* parent's posed matrix), reduced once the bones above it are. Its parent's
|
|
442
|
+
* world 2×2 is posed from keys already reduced — a bone the key formula holds
|
|
443
|
+
* for (`rigid`: R(turn) · its setup world · diag(scale, 1), its turn and scale
|
|
444
|
+
* the reduced ones), or another bone of the same call given before it — times
|
|
445
|
+
* the setup locals of the unmapped bones between (`via`).
|
|
446
|
+
*/
|
|
447
|
+
export interface UnderBone {
|
|
448
|
+
bone: string;
|
|
449
|
+
parent: { rigid: { turn: readonly number[]; world0: M2; scale: readonly number[] }; via: M2 } | { under: string; via: M2 };
|
|
450
|
+
/** Its setup local 2×2 in its parent's axes: inv(parent's setup world) · its setup world, by rig-c's setup. */
|
|
451
|
+
local: M2;
|
|
452
|
+
/** Its setup world x-axis length. */
|
|
453
|
+
len0: number;
|
|
454
|
+
/** The world angle its x axis is asked on every frame (degrees), and the scale of it asked (1: none). */
|
|
455
|
+
target: readonly number[];
|
|
456
|
+
asked: readonly number[];
|
|
457
|
+
/** The dense rotate keys (unwrapped, as written). */
|
|
458
|
+
dense: readonly number[];
|
|
459
|
+
keep: ReadonlySet<number>;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Each bone under a scaled bone, in the order given (parents first): its
|
|
464
|
+
* rotate and scalex keys re-solved through its parent's reduced posed matrix
|
|
465
|
+
* (the key that puts its x axis on the target, and the scale that gives it the
|
|
466
|
+
* length asked), then reduced — the rotate with its error read as the world aim
|
|
467
|
+
* it lands on through that matrix, the scale with its error read as the world
|
|
468
|
+
* length over setup against the scale asked, on the rotate as reduced. Its
|
|
469
|
+
* world error is its own fit error, as down a chain the key formula holds for.
|
|
470
|
+
*/
|
|
471
|
+
export function reduceUnderScale(times: readonly number[], bones: readonly UnderBone[], tolerance: number, scaleTolerance: number): { rotate: Map<string, ReducedKey[]>; scale: Map<string, ReducedKey[]>; rows: ReduceRow[] } {
|
|
472
|
+
const n = times.length;
|
|
473
|
+
const samples = samplesOf(times);
|
|
474
|
+
const world = new Map<string, M2[]>();
|
|
475
|
+
const rotate = new Map<string, ReducedKey[]>();
|
|
476
|
+
const scale = new Map<string, ReducedKey[]>();
|
|
477
|
+
const rows: ReduceRow[] = [];
|
|
478
|
+
for (const b of bones) {
|
|
479
|
+
for (const [what, xs] of [['target', b.target], ['asked', b.asked], ['dense', b.dense]] as const) {
|
|
480
|
+
if (xs.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${xs.length} ${what} value(s) for ${n} frame(s)`);
|
|
481
|
+
}
|
|
482
|
+
const p = b.parent;
|
|
483
|
+
let parent: M2[];
|
|
484
|
+
if ('under' in p) {
|
|
485
|
+
const above = world.get(p.under);
|
|
486
|
+
if (above === undefined) throw new Error(`reduce: bone "${b.bone}" hangs from bone "${p.under}", which is not given before it`);
|
|
487
|
+
parent = above.map((m) => mul(m, p.via));
|
|
488
|
+
} else {
|
|
489
|
+
parent = Array.from({ length: n }, (_, i) => mul(mul(mul(rot(p.rigid.turn[i]!), p.rigid.world0), scaleX(p.rigid.scale[i]!)), p.via));
|
|
490
|
+
}
|
|
491
|
+
const l0: [number, number] = [b.local[0], b.local[2]];
|
|
492
|
+
const a0 = Math.atan2(l0[1], l0[0]) * DEG;
|
|
493
|
+
/** The x axis's world vector through the parent on frame i, at rotate key k, before its own scale. */
|
|
494
|
+
const axis = (i: number, k: number) => apply(parent[i]!, apply(rot(k), l0));
|
|
495
|
+
// The dense keys re-solved through the reduced parent (posePath's solve).
|
|
496
|
+
const solved = b.target.map((t, i) => {
|
|
497
|
+
const v = apply(inv(parent[i]!), [b.asked[i]! * b.len0 * Math.cos(t / DEG), b.asked[i]! * b.len0 * Math.sin(t / DEG)]);
|
|
498
|
+
const k = Math.atan2(v[1], v[0]) * DEG - a0;
|
|
499
|
+
return b.dense[i]! + wrap180(k - b.dense[i]!);
|
|
500
|
+
});
|
|
501
|
+
const aimError = (i: number, k: number) => {
|
|
502
|
+
const w = axis(i, k);
|
|
503
|
+
return Math.abs(wrap180(Math.atan2(w[1], w[0]) * DEG - b.target[i]!));
|
|
504
|
+
};
|
|
505
|
+
const keep = keptFrames(b.keep, n, `bone "${b.bone}"`);
|
|
506
|
+
const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance, error: (i, v) => aimError(i, v[0]!) });
|
|
507
|
+
const kAt = trackAt(rk, samples, 1).map((v) => v[0]!);
|
|
508
|
+
const ae = worstOf(kAt.map((_, i) => i), (i) => aimError(i, kAt[i]!))!;
|
|
509
|
+
rows.push({ bone: b.bone, kind: 'angle', before: n, after: rk.length, worst: ae.worst, frame: ae.frame });
|
|
510
|
+
// The scale that gives it the length asked on the rotate as reduced.
|
|
511
|
+
const reach = kAt.map((k, i) => Math.hypot(...axis(i, k)) / b.len0);
|
|
512
|
+
const sk = fitTrack(times, samples, { targets: reach.map((r, i) => [b.asked[i]! / r]), keep, tolerance: scaleTolerance, error: (i, v) => Math.abs(v[0]! * reach[i]! - b.asked[i]!), round: r6 });
|
|
513
|
+
const sAt = trackAt(sk, samples, 1).map((v) => v[0]!);
|
|
514
|
+
const se = worstOf(sAt.map((_, i) => i), (i) => Math.abs(sAt[i]! * reach[i]! - b.asked[i]!))!;
|
|
515
|
+
rows.push({ bone: b.bone, kind: 'scale', before: n, after: sk.length, worst: se.worst, frame: se.frame });
|
|
516
|
+
world.set(b.bone, parent.map((m, i) => mul(mul(mul(m, rot(kAt[i]!)), b.local), scaleX(sAt[i]!))));
|
|
517
|
+
rotate.set(b.bone, rk);
|
|
518
|
+
scale.set(b.bone, sk);
|
|
519
|
+
}
|
|
520
|
+
return { rotate, scale, rows };
|
|
361
521
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
* are posed from the final targets of the bones above. A target out of reach
|
|
58
58
|
* goes to the reach limit along the head-relative direction (REACH); the set's
|
|
59
59
|
* `turn` is part of the solve, and only a forearm it still holds is ART-RANGE.
|
|
60
|
+
* A set with `place: "by-hip"` (rig-c/pose#93) gets the same solve against the
|
|
61
|
+
* hip, in the same pass: each of its chains whose key skeleton puts the wrist
|
|
62
|
+
* within one hip width of the mid-hip lands where the reference's wrist is from
|
|
63
|
+
* its mid-hip, in hip widths.
|
|
60
64
|
*
|
|
61
65
|
* The lines across the body — hips, shoulders, eyes — are read from the pose
|
|
62
66
|
* after holds, whether the map uses them or not: a line under
|
|
@@ -72,12 +76,13 @@ import type { Body18 } from './joints.ts';
|
|
|
72
76
|
import { isBody18, VIRTUAL } from './joints.ts';
|
|
73
77
|
import type { Chain, JointMap, Rest, RigSlots } from './map.ts';
|
|
74
78
|
import { aimAngle, aimEnds, descends, partBone, pointParts, rigPoint } from './map.ts';
|
|
75
|
-
import type { Joint } from './pose.ts';
|
|
79
|
+
import type { Joint, Travel } from './pose.ts';
|
|
80
|
+
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
76
81
|
import { Problems, Refusal } from './problems.ts';
|
|
77
|
-
import type { ArtRange, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
82
|
+
import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
78
83
|
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
79
|
-
import type { Feet, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
80
|
-
import { isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, supportsOf, tipOf } from './legs.ts';
|
|
84
|
+
import type { BlendHold, Closure, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
+
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf } from './legs.ts';
|
|
81
86
|
|
|
82
87
|
const DEG = 180 / Math.PI;
|
|
83
88
|
|
|
@@ -108,8 +113,12 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
108
113
|
plantFeet?: boolean;
|
|
109
114
|
/** With plantFeet: a free ankle within this many leg lengths of the floor plants both feet, its leg stretched to reach it (`legs.ts` stanceOf, #60); 'off' (the default) plants one. */
|
|
110
115
|
stance?: number | 'off';
|
|
116
|
+
/** With plantFeet: the floor's world y every support span is planted on (#85); absent, the median of the spans' planted heights. */
|
|
117
|
+
floor?: number;
|
|
111
118
|
/** While a set whose arm is drawn raised shows, its wrist is placed against the rig's head in eye distances (rig-c/pose#62); false (the default) keeps the pose's angles. */
|
|
112
119
|
raisedByHead?: boolean;
|
|
120
|
+
/** The sequence's `travel` (rig-c/pose#92): "in-place" makes --plant-feet let the supporting foot follow its own track and close the loop; absent, nothing changes. */
|
|
121
|
+
travel?: Travel;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
export interface Hold {
|
|
@@ -288,12 +297,20 @@ export interface Retarget {
|
|
|
288
297
|
planted: boolean;
|
|
289
298
|
/** With --stance: the frames planted both and lifted, and the free leg's stretch (#60). */
|
|
290
299
|
stance: Stance | null;
|
|
300
|
+
/** With --plant-feet: the one floor every support span was planted on, and each span's shift onto it (#85). */
|
|
301
|
+
floor: Floor | null;
|
|
302
|
+
/** With --plant-feet: the supporting leg stretched on the blend frames so its ankle stays on the floor (#85). */
|
|
303
|
+
blendHold: BlendHold | null;
|
|
304
|
+
/** With --plant-feet on an in-place sequence: the hip's x drift closed over the loop (#92). */
|
|
305
|
+
closure: Closure | null;
|
|
291
306
|
/** Each ankle's world position on every frame, as the keys put it, when legs are scaled or feet planted. */
|
|
292
307
|
ankles: { bones: Record<Side, string>; at: Record<Side, Pt[]> } | null;
|
|
293
308
|
/** The translated bone's origin against its thigh roots' midpoint, at setup. */
|
|
294
309
|
pivot: Pivot | null;
|
|
295
310
|
/** With --raised-by-head: each raised set's span on each raised chain, its wrist placed against the head. */
|
|
296
311
|
raised: Raised[];
|
|
312
|
+
/** Each span of a `place: "by-hip"` set on each chain it places, its wrist placed against the hip (#93). */
|
|
313
|
+
byHip: ByHip[];
|
|
297
314
|
}
|
|
298
315
|
|
|
299
316
|
/** The hip's pivot against the thigh roots, at setup (rig-c/pose#47). */
|
|
@@ -394,6 +411,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
394
411
|
// eye line with an "a>b" aim, whose two ends are the rig's eyes.
|
|
395
412
|
const head = opts.raisedByHead === true ? headOf(map) : null;
|
|
396
413
|
if (head !== null && 'problem' in head) p.add('RAISED_HEAD', head.problem);
|
|
414
|
+
// `place: "by-hip"` (#93) measures against the hip in hip widths: the map must name the rig's two hip points.
|
|
415
|
+
const hips = map.alternates.some((a) => a.place !== undefined && a.place !== null) ? hipOf(map) : null;
|
|
416
|
+
if (hips !== null && 'problem' in hips) p.add('PLACE_HIP', hips.problem);
|
|
397
417
|
|
|
398
418
|
// Every point the map reads, held where it is not good.
|
|
399
419
|
const needed = new Set<string>();
|
|
@@ -606,9 +626,14 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
606
626
|
// which are aimed from where the hip is.
|
|
607
627
|
const legScaleOn = opts.legScale !== undefined && opts.legScale !== 'off';
|
|
608
628
|
const plantOn = opts.plantFeet === true;
|
|
629
|
+
/** #92: an in-place sequence under --plant-feet — the supporting foot follows its own track, no step releases, the loop closed. */
|
|
630
|
+
const inPlace = plantOn && opts.travel === TRAVEL_IN_PLACE;
|
|
631
|
+
let closure: Closure | null = null;
|
|
609
632
|
const legScale: LegScale[] = [];
|
|
610
633
|
const scales = new Map<string, number[]>();
|
|
611
634
|
let stance: Stance | null = null;
|
|
635
|
+
let floor: Floor | null = null;
|
|
636
|
+
let blendHold: BlendHold | null = null;
|
|
612
637
|
let feet: Feet | null = null;
|
|
613
638
|
let legs: ReturnType<typeof legsOf> | null = null;
|
|
614
639
|
let legPath: string[] | null = null;
|
|
@@ -635,7 +660,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
635
660
|
const hipOf = (s: Side) => series.get(`${s}_hip`);
|
|
636
661
|
const ankleOf = (s: Side) => series.get(`${s}_ankle`);
|
|
637
662
|
const hr = hipOf('r'), hl = hipOf('l'), ar = ankleOf('r'), al = ankleOf('l');
|
|
638
|
-
if (hr !== undefined && hl !== undefined && ar !== undefined && al !== undefined) feet = supportsOf({ r: ar, l: al }, { r: hr, l: hl }, keyTimes, secondsOf);
|
|
663
|
+
if (hr !== undefined && hl !== undefined && ar !== undefined && al !== undefined) feet = supportsOf({ r: ar, l: al }, { r: hr, l: hl }, keyTimes, secondsOf, inPlace, plantOn); // #113: planted, the support is read from the lower ankle
|
|
639
664
|
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`);
|
|
640
665
|
}
|
|
641
666
|
}
|
|
@@ -664,6 +689,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
664
689
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
665
690
|
}
|
|
666
691
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
692
|
+
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
693
|
+
if (opts.floor !== undefined && !Number.isFinite(opts.floor)) p.add('FLOOR', `--floor is ${opts.floor}; it must be a finite world y`);
|
|
667
694
|
p.throwIfAny();
|
|
668
695
|
/** Every bone on the legs' path posed on frame i, by the targets as they stand. */
|
|
669
696
|
const poseLegs = (i: number): PathPose => {
|
|
@@ -684,32 +711,56 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
684
711
|
return { r: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.r.ankleBone : ''), l: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.l.ankleBone : '') };
|
|
685
712
|
});
|
|
686
713
|
const tb = rest.get(t!.bone)!;
|
|
687
|
-
|
|
714
|
+
// #85: every support span on one floor — the median of their planted heights, or --floor.
|
|
715
|
+
// #92: in place, the supporting ankle's own picture x, scaled, is the track its anchor follows.
|
|
716
|
+
const follow = inPlace ? (i: number, s: Side) => series.get(`${s}_ankle`)![i]![0] * scale!.factor : undefined;
|
|
717
|
+
const planted = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY], opts.floor, follow);
|
|
718
|
+
const hip = planted.hip;
|
|
719
|
+
floor = planted.floor;
|
|
720
|
+
closure = planted.closure;
|
|
688
721
|
for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
});
|
|
722
|
+
const L = (legs as { legs: Record<Side, { chain: Chain; ankleBone: string }> }).legs;
|
|
723
|
+
const world = (i: number, v: Pt): Pt => [hip[i]![0] + v[0], hip[i]![1] + v[1]];
|
|
724
|
+
/** Both legs posed on frame i by the scales as they stand, world (y up): each chain's root and ankle. */
|
|
725
|
+
const posedFeet = (i: number) => {
|
|
726
|
+
const lp = poseLegs(i);
|
|
727
|
+
return {
|
|
728
|
+
root: { r: world(i, lp.origin.get(L.r.chain.bones[0]!)!), l: world(i, lp.origin.get(L.l.chain.bones[0]!)!) },
|
|
729
|
+
ankle: { r: world(i, tipOf(rest, lp, L.r.ankleBone)), l: world(i, tipOf(rest, lp, L.l.ankleBone)) },
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
/** Every bone of a side's chain has its scalex multiplied by the stretch keyed on that side. */
|
|
733
|
+
const stretchChain = (stretch: ReadonlyArray<{ side: Side; k: number } | null>) => {
|
|
702
734
|
for (const side of SIDES) {
|
|
703
|
-
if (!
|
|
735
|
+
if (!stretch.some((x) => x?.side === side)) continue;
|
|
704
736
|
for (const b of L[side].chain.bones) {
|
|
705
737
|
const own = scales.get(b);
|
|
706
738
|
const s = own === undefined ? new Array<number>(n).fill(1) : [...own];
|
|
707
|
-
|
|
739
|
+
stretch.forEach((x, i) => {
|
|
708
740
|
if (x?.side === side) s[i] = s[i]! * x.k;
|
|
709
741
|
});
|
|
710
742
|
scales.set(b, s);
|
|
711
743
|
}
|
|
712
744
|
}
|
|
745
|
+
};
|
|
746
|
+
// #85: on a blend frame the hip is between two derivations; the supporting leg is stretched
|
|
747
|
+
// (stanceOf's mechanism, its bounds) so its ankle stays on the floor. Before --stance, whose
|
|
748
|
+
// floor is that supporting ankle. #95: the lower bound on a frame is the leg's own range there.
|
|
749
|
+
const ankleSeries = { r: series.get('r_ankle')!, l: series.get('l_ankle')! };
|
|
750
|
+
const lower = stanceRangeOf(ankleSeries, { r: series.get('r_hip')!, l: series.get('l_hip')! });
|
|
751
|
+
blendHold = holdBlend(planted.blends, n, posedFeet, lower);
|
|
752
|
+
stretchChain(blendHold.stretch);
|
|
753
|
+
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
754
|
+
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
755
|
+
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
756
|
+
stance = stanceOf(feet!, ankleSeries, opts.stance, posedFeet, secondsOf, lower);
|
|
757
|
+
// The supporting leg (the blend's) and the free leg (the stance's) are different chains on a frame.
|
|
758
|
+
for (let i = 0; i < n; i++) {
|
|
759
|
+
const b = blendHold.stretch[i];
|
|
760
|
+
if (b !== null && b !== undefined && stance.stretch[i]?.side === b.side) p.add('LEGS_STRETCH', `frame ${i}: chain "${L[b.side].chain.name}" is stretched both as the supporting leg of a blend (k ${b.k}) and as the free leg of --stance (k ${stance.stretch[i]!.k}); one chain takes one stretch`);
|
|
761
|
+
}
|
|
762
|
+
p.throwIfAny();
|
|
763
|
+
stretchChain(stance.stretch);
|
|
713
764
|
}
|
|
714
765
|
}
|
|
715
766
|
if (t !== null && scale !== null) {
|
|
@@ -851,44 +902,60 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
851
902
|
// neither. A target out of reach is placed at the reach limit along the head-relative direction
|
|
852
903
|
// (`reachLimit`, a REACH line). The set's `turn` is part of the solve: a held upper arm has its forearm
|
|
853
904
|
// re-aimed at the placed wrist, and only a forearm the turn still holds is ART-RANGE.
|
|
905
|
+
// `place: "by-hip"` (#93) is the same solve against the hip: while a set that asks for it shows, each chain
|
|
906
|
+
// its key skeleton puts the wrist within one hip width of the mid-hip (`byHipChains`) has its wrist placed
|
|
907
|
+
// at the rig's mid-hip + the reference's offset from its mid-hip, in hip widths (each side's median over the
|
|
908
|
+
// span). The rig's hips are the two points `hipOf` reads off the map, posed as the eyes are.
|
|
854
909
|
const raised: Raised[] = [];
|
|
910
|
+
const byHip: ByHip[] = [];
|
|
855
911
|
const raisedArt: Array<{ sw: Switch; entries: ArtRange[] }> = [];
|
|
856
|
-
const raisedJobs: Array<{ sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; l0: number; l1: number; farthest: number
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
912
|
+
const raisedJobs: Array<{ anchor: Anchor; sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; l0: number; l1: number; farthest: number }> = [];
|
|
913
|
+
const headOk: Anchor | null = head !== null && !('problem' in head) ? { kind: 'head', joints: head.joints, points: head.points } : null;
|
|
914
|
+
const hipsOk: Anchor | null = hips !== null && !('problem' in hips) ? { kind: 'hip', joints: hips.joints, points: hips.points } : null;
|
|
915
|
+
if (headOk !== null || hipsOk !== null) {
|
|
916
|
+
/** The anchor's two rig points posed on frame i: their midpoint and their distance apart. */
|
|
917
|
+
const pairOn = (an: Anchor, i: number): { centre: Pt; dist: number } => {
|
|
918
|
+
const a = posedPoint(an.points[0], i);
|
|
919
|
+
const b = posedPoint(an.points[1], i);
|
|
861
920
|
return { centre: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], dist: Math.hypot(b[0] - a[0], b[1] - a[1]) };
|
|
862
921
|
};
|
|
863
|
-
const headBones = new Set([...head.points.flatMap((x) => pointParts(x).map(partBone))]);
|
|
864
|
-
const ra = series.get(head.joints[0])!;
|
|
865
|
-
const rb = series.get(head.joints[1])!;
|
|
866
|
-
const refCentre = (i: number): Pt => [(ra[i]![0] + rb[i]![0]) / 2, (ra[i]![1] + rb[i]![1]) / 2];
|
|
867
922
|
for (const s of sw.switches) {
|
|
868
923
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
869
924
|
const art: ArtRange[] = [];
|
|
870
925
|
for (const cn of s.chains) {
|
|
871
926
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
872
927
|
const drawn = set.drawn[c.bones[0]!];
|
|
873
|
-
|
|
874
|
-
const
|
|
928
|
+
const up = headOk !== null && drawn !== undefined && isRaised(drawn);
|
|
929
|
+
const low = hipsOk !== null && set.place?.chains.includes(c.name) === true;
|
|
930
|
+
if (up && low) {
|
|
931
|
+
p.add('PLACE_BOTH', `set "${set.name}" chain "${c.name}": --raised-by-head places its wrist against the head (bone "${c.bones[0]}" drawn at ${drawn}°, raised), and "place": "by-hip" against the hip (its key skeleton's wrist within one hip width of the mid-hip); a wrist is placed against one — drop "place" from the set or the flag`);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const anchor = up ? headOk : low ? hipsOk : null;
|
|
935
|
+
if (anchor === null) continue;
|
|
936
|
+
const ra = series.get(anchor.joints[0])!;
|
|
937
|
+
const rb = series.get(anchor.joints[1])!;
|
|
938
|
+
const refCentre = (i: number): Pt => [(ra[i]![0] + rb[i]![0]) / 2, (ra[i]![1] + rb[i]![1]) / 2];
|
|
939
|
+
const anchorBones = new Set([...anchor.points.flatMap((x) => pointParts(x).map(partBone))]);
|
|
940
|
+
const label = up ? `--raised-by-head: set "${set.name}" draws chain "${c.name}" raised (bone "${c.bones[0]}" at ${drawn}°)` : `"place": "by-hip": set "${set.name}" places chain "${c.name}"'s wrist against the hip`;
|
|
941
|
+
const CHAIN = up ? 'RAISED_CHAIN' : 'PLACE_CHAIN';
|
|
875
942
|
if (c.bones.length !== 2) {
|
|
876
|
-
p.add(
|
|
943
|
+
p.add(CHAIN, `${label}, and the chain has ${c.bones.length} bone(s); the wrist is placed by a two-bone solve, so it must have two (upper arm, forearm)`);
|
|
877
944
|
continue;
|
|
878
945
|
}
|
|
879
946
|
if (c.reach || c.aim.some((x) => x.includes('>'))) {
|
|
880
|
-
p.add(
|
|
947
|
+
p.add(CHAIN, `${label}, and it aims ${JSON.stringify(c.aim)}${c.reach ? ' with "reach"' : ''}; the solve turns each bone about its own origin toward the next point, so each must aim "x" or at one point`);
|
|
881
948
|
continue;
|
|
882
949
|
}
|
|
883
950
|
const [b0, b1] = c.bones as [string, string];
|
|
884
|
-
const moved = [...
|
|
951
|
+
const moved = [...anchorBones].filter((h) => h === b0 || h === b1 || descends(rest, h, b0));
|
|
885
952
|
if (moved.length > 0) {
|
|
886
|
-
p.add(
|
|
953
|
+
p.add(CHAIN, up ? `${label}, and the head's eye point ${moved.map((h) => `"${h}"`).join(', ')} turns with it; the wrist cannot be placed against a head the arm moves` : `${label}, and the hip point ${moved.map((h) => `"${h}"`).join(', ')} turns with it; the wrist cannot be placed against a hip the arm moves`);
|
|
887
954
|
continue;
|
|
888
955
|
}
|
|
889
956
|
const wristSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
890
957
|
if (c.aim[1] === 'x' && !(rest.get(b1)!.length > 1e-9)) {
|
|
891
|
-
p.add(
|
|
958
|
+
p.add(CHAIN, `${label}, and its last bone "${b1}" aims "x" with no length; its wrist (the tip) is its origin, so there is nothing to place`);
|
|
892
959
|
continue;
|
|
893
960
|
}
|
|
894
961
|
const wristOf = (i: number): Pt => posedPoint(wristSpec, i);
|
|
@@ -897,9 +964,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
897
964
|
const span: number[] = [];
|
|
898
965
|
for (let i = s.frames[0]; i <= s.frames[1]; i++) span.push(i);
|
|
899
966
|
const refEye = median(span.map((i) => Math.hypot(rb[i]![0] - ra[i]![0], rb[i]![1] - ra[i]![1])));
|
|
900
|
-
const rigEye = median(span.map((i) =>
|
|
967
|
+
const rigEye = median(span.map((i) => pairOn(anchor, i).dist));
|
|
901
968
|
if (!(refEye > 1e-9) || !(rigEye > 1e-9)) {
|
|
902
|
-
p.add('RAISED_EYES', `${label}: over frames ${s.frames[0]}–${s.frames[1]} the median eye distance is ${refEye > 1e-9 ? `${rigEye} units on the rig (points "${
|
|
969
|
+
if (up) p.add('RAISED_EYES', `${label}: over frames ${s.frames[0]}–${s.frames[1]} the median eye distance is ${refEye > 1e-9 ? `${rigEye} units on the rig (points "${anchor.points[0]}", "${anchor.points[1]}")` : `${refEye}px in the reference (${anchor.joints[0]}, ${anchor.joints[1]})`}; an eye distance of 0 is no unit`);
|
|
970
|
+
else p.add('PLACE_HIPS', `${label}: over frames ${s.frames[0]}–${s.frames[1]} the median hip width is ${refEye > 1e-9 ? `${rigEye} units on the rig (points "${anchor.points[0]}", "${anchor.points[1]}")` : `${refEye}px in the reference (${anchor.joints[0]}, ${anchor.joints[1]})`}; a hip width of 0 is no unit`);
|
|
903
971
|
continue;
|
|
904
972
|
}
|
|
905
973
|
// The rig's own lengths, and each bone's aim against the vector it carries, at setup.
|
|
@@ -914,7 +982,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
914
982
|
const t1 = targets.get(b1)!;
|
|
915
983
|
const turn0 = set.turn[b0];
|
|
916
984
|
const turn1 = set.turn[b1];
|
|
917
|
-
const job: (typeof raisedJobs)[number] = { sw: s, chain: c, wrist: wristOf, before: [], reference: [], placed: [], refEye, rigEye, short: [], upperHeld: [], l0, l1, farthest: 0
|
|
985
|
+
const job: (typeof raisedJobs)[number] = { anchor, sw: s, chain: c, wrist: wristOf, before: [], reference: [], placed: [], refEye, rigEye, short: [], upperHeld: [], l0, l1, farthest: 0 };
|
|
918
986
|
/** Per frame of the span: the forearm's art turn the re-aim asked, and how far past its turn. */
|
|
919
987
|
const foreAsked: number[] = [];
|
|
920
988
|
const foreOver: number[] = [];
|
|
@@ -923,7 +991,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
923
991
|
const upperOver: number[] = [];
|
|
924
992
|
const heldMiss: number[] = [];
|
|
925
993
|
for (const i of span) {
|
|
926
|
-
const h =
|
|
994
|
+
const h = pairOn(anchor, i);
|
|
927
995
|
const rc = refCentre(i);
|
|
928
996
|
// The reference's wrist from its head centre, in its eye distances, y turned up.
|
|
929
997
|
const ref: Pt = [(refWrist[i]![0] - rc[0]) / refEye, -(refWrist[i]![1] - rc[1]) / refEye];
|
|
@@ -988,7 +1056,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
988
1056
|
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
989
1057
|
const first = s.frames[0] + a;
|
|
990
1058
|
const last = s.frames[0] + b;
|
|
991
|
-
art.push({ set: set.name, chain: c.name, bone: b0, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(upperOver[worst]!), turn: r3(upperAsked[worst]!), limits: turn0, clamped: true, shortfall: r3(heldMiss[worst]!) });
|
|
1059
|
+
art.push({ set: set.name, chain: c.name, bone: b0, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(upperOver[worst]!), turn: r3(upperAsked[worst]!), limits: turn0, clamped: true, shortfall: r3(heldMiss[worst]!), ...(up ? {} : { anchor: 'hip' as const }) });
|
|
992
1060
|
}
|
|
993
1061
|
}
|
|
994
1062
|
if (turn1 !== undefined) {
|
|
@@ -1020,8 +1088,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1020
1088
|
for (const job of raisedJobs) {
|
|
1021
1089
|
const after: Pt[] = [];
|
|
1022
1090
|
for (let i = job.sw.frames[0]; i <= job.sw.frames[1]; i++) {
|
|
1023
|
-
const a = posedPoint(job.
|
|
1024
|
-
const b = posedPoint(job.
|
|
1091
|
+
const a = posedPoint(job.anchor.points[0], i);
|
|
1092
|
+
const b = posedPoint(job.anchor.points[1], i);
|
|
1025
1093
|
const w = job.wrist(i);
|
|
1026
1094
|
after.push([(w[0] - (a[0] + b[0]) / 2) / job.rigEye, (w[1] - (a[1] + b[1]) / 2) / job.rigEye]);
|
|
1027
1095
|
}
|
|
@@ -1035,12 +1103,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1035
1103
|
short.push({ frames: [job.short[k]!.i, job.short[m]!.i], how: job.short[k]!.how, shortfall: { max: r6(Math.max(...fs)), median: r6(median(fs)) } });
|
|
1036
1104
|
k = m + 1;
|
|
1037
1105
|
}
|
|
1038
|
-
|
|
1106
|
+
const unit = { rig: r6(job.rigEye), reference: r6(job.refEye) };
|
|
1107
|
+
const entry = {
|
|
1039
1108
|
set: job.sw.set,
|
|
1040
1109
|
chain: job.chain.name,
|
|
1041
1110
|
frames: job.sw.frames,
|
|
1042
1111
|
seconds: secondsOf(job.sw.frames),
|
|
1043
|
-
eyes: { rig: r6(job.rigEye), reference: r6(job.refEye) },
|
|
1044
1112
|
reference: round(job.reference),
|
|
1045
1113
|
placed: round(job.placed),
|
|
1046
1114
|
before: round(job.before),
|
|
@@ -1049,7 +1117,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1049
1117
|
short,
|
|
1050
1118
|
upperHeld: job.upperHeld,
|
|
1051
1119
|
reach: { min: r6(Math.abs(job.l0 - job.l1)), max: r6(job.l0 + job.l1), farthest: r6(job.farthest) },
|
|
1052
|
-
}
|
|
1120
|
+
};
|
|
1121
|
+
// The unit sits after `seconds`, as the report has always written it.
|
|
1122
|
+
const { set, chain, frames, seconds, ...body } = entry;
|
|
1123
|
+
if (job.anchor.kind === 'head') raised.push({ set, chain, frames, seconds, eyes: unit, ...body });
|
|
1124
|
+
else byHip.push({ set, chain, frames, seconds, hips: unit, ...body });
|
|
1053
1125
|
}
|
|
1054
1126
|
|
|
1055
1127
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
@@ -1110,10 +1182,49 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1110
1182
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
1111
1183
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
1112
1184
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
1113
|
-
legScale, scales, feet, planted: plantOn, stance, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised,
|
|
1185
|
+
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip,
|
|
1114
1186
|
};
|
|
1115
1187
|
}
|
|
1116
1188
|
|
|
1189
|
+
/** What a placed wrist is measured against: the head (#62, the eyes) or the hip (#93, the hips) — two pose joints and the rig's two points. */
|
|
1190
|
+
interface Anchor {
|
|
1191
|
+
kind: 'head' | 'hip';
|
|
1192
|
+
joints: [string, string];
|
|
1193
|
+
points: [string, string];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** The hips as `place: "by-hip"` reads them: two pose joints (r_hip, l_hip) and the rig's two hip points. */
|
|
1197
|
+
interface Hips {
|
|
1198
|
+
joints: [string, string];
|
|
1199
|
+
points: [string, string];
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* The rig's hip points for `place: "by-hip"` (#93): the two points a chain
|
|
1204
|
+
* carrying the hip line (r_hip → l_hip, either way round) names with an "a>b"
|
|
1205
|
+
* aim — a pelvis turned by its hip line; else the origins of the first bones of
|
|
1206
|
+
* the two chains that start at r_hip and at l_hip — the legs, whose roots are
|
|
1207
|
+
* the hip joints; or why the map names neither.
|
|
1208
|
+
*/
|
|
1209
|
+
function hipOf(map: JointMap): Hips | { problem: string } {
|
|
1210
|
+
for (const c of map.chains) {
|
|
1211
|
+
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
1212
|
+
const a = c.joints[k]!;
|
|
1213
|
+
const b = c.joints[k + 1]!;
|
|
1214
|
+
if (!((a === 'r_hip' && b === 'l_hip') || (a === 'l_hip' && b === 'r_hip'))) continue;
|
|
1215
|
+
const aim = c.aim[k]!;
|
|
1216
|
+
if (!aim.includes('>')) continue;
|
|
1217
|
+
const [pa, pb] = aimEnds(c.bones[k]!, aim);
|
|
1218
|
+
return { joints: [a, b], points: [pa, pb] };
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const from = (j: string, other: string) => map.chains.find((c) => c.joints[0] === j && c.joints[1] !== other);
|
|
1222
|
+
const r = from('r_hip', 'l_hip');
|
|
1223
|
+
const l = from('l_hip', 'r_hip');
|
|
1224
|
+
if (r !== undefined && l !== undefined) return { joints: ['r_hip', 'l_hip'], points: [r.bones[0]!, l.bones[0]!] };
|
|
1225
|
+
return { problem: `"place": "by-hip" places a wrist against the hip in hip widths, read on the rig between two hip points: the two a chain carrying the hip line (r_hip → l_hip) names with an "a>b" aim, or else the first bones' origins of the two chains starting at r_hip and l_hip (the legs); the map has neither (${r === undefined && l === undefined ? 'no chain starts at r_hip or l_hip' : `no chain starts at ${r === undefined ? 'r_hip' : 'l_hip'}`})` };
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1117
1228
|
/** The head as --raised-by-head reads it: the chain carrying the eye line, its two pose joints and the rig's two eye points. */
|
|
1118
1229
|
interface Head {
|
|
1119
1230
|
chain: string;
|