@rig-c/pose 0.39.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 +26 -5
- package/cli.ts +31 -5
- package/package.json +1 -1
- package/src/draworder.ts +290 -0
- package/src/reduce.ts +21 -12
- package/src/retarget.ts +82 -3
- package/src/run.ts +88 -17
- package/src/switch.ts +64 -2
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²`);
|
package/src/run.ts
CHANGED
|
@@ -11,13 +11,14 @@ import { dirname, join, relative, resolve } from 'node:path';
|
|
|
11
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
12
12
|
import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
|
|
13
13
|
import { depthLine, layersOf, type FrameLayers } from './depth.ts';
|
|
14
|
+
import { drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
|
|
14
15
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
15
16
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
16
17
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
17
|
-
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, pelvisLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
18
|
+
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, pelvisLine, rangeLine, reachCostLine, reachOriginLine, reachOriginWarn, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
18
19
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, seamRender, slotsOf } from './rigc.ts';
|
|
19
20
|
import { partsPath, projectSeams, readSeamDoc, readSeamPairs, seamLines, type SeamPair } from './seams.ts';
|
|
20
|
-
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
21
|
+
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, switchMinLines, uncoveredFolds } from './switch.ts';
|
|
21
22
|
import { jitterLine, landed, type FeetReading, type JitterRow, type Landing, measureFeet, measureJitter, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
22
23
|
import { FRONTAL_DISAGREE, legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
23
24
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
@@ -64,6 +65,8 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
64
65
|
merge?: string;
|
|
65
66
|
/** Thin the mapped bones' rotate keys within this many degrees (#32). */
|
|
66
67
|
reduce?: number;
|
|
68
|
+
/** With reduce: a map chain's own tolerance in degrees, by chain name, in place of `reduce` on its bones (#283). */
|
|
69
|
+
reduceChains?: Record<string, number>;
|
|
67
70
|
/** With reduce: thin the hip's translate keys within this many rig units. */
|
|
68
71
|
reduceHip?: number;
|
|
69
72
|
/** With reduce and plantFeet: keep each support span's frames on the supporting leg at its dense keys (#251). */
|
|
@@ -76,6 +79,8 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
76
79
|
* same in all three and the gate is the same gate.
|
|
77
80
|
*/
|
|
78
81
|
output?: 'default' | 'verbose' | 'summary';
|
|
82
|
+
/** `--draw-order depth` (rig-c/pose#304): key the animation's draw order from the sequence's depth. Absent: no draw-order key, the output as before. */
|
|
83
|
+
drawOrder?: 'depth';
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export interface RunResult {
|
|
@@ -128,9 +133,13 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
128
133
|
const fragment = a.merge === undefined ? [] : readFragment(parseJson(readFileSync(a.merge, 'utf8'), a.merge, 'MERGE_JSON'), a.merge);
|
|
129
134
|
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
130
135
|
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
136
|
+
// #283: each chain --reduce names must be the map's; a bone takes its chain's tolerance (the tighter, if two chains name it).
|
|
137
|
+
const reduceOf = a.reduce === undefined ? null : reduceTolerances(map, a.reduce, a.reduceChains, a.map);
|
|
131
138
|
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
132
139
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
133
140
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
141
|
+
// #304: the draw order is read off the sequence's depth; asked for on a sequence without it, refused by name.
|
|
142
|
+
if (a.drawOrder === 'depth' && !seq.frames.some((x) => x.depth !== undefined)) refuse([{ code: 'DRAWORDER_NO_DEPTH', text: `--draw-order depth keys the draw order from the sequence's depth, and ${a.pose} carries none (rig-pose-sequence/${seq.cleaned!.params.depth === undefined ? '1 or 1.1, no "depth" on any frame' : '1.2 with no frame naming a segment'}); clean it with --depth walk|run, or retarget without --draw-order` }]);
|
|
134
143
|
const frames = personTrack(seq, a.person, a.pose);
|
|
135
144
|
const times = seq.frames.map((f) => f.t);
|
|
136
145
|
|
|
@@ -164,7 +173,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
164
173
|
}
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
|
-
tell(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}${a.reduceKeepPlanted === true ? ', planted legs kept dense' : ''}` : ''}`);
|
|
176
|
+
tell(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${chainsText(a.reduceChains)}${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}${a.reduceKeepPlanted === true ? ', planted legs kept dense' : ''}` : ''}`);
|
|
168
177
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
169
178
|
// #250: the gate was green without it, so nothing else would say the rig's own animations are not in --out.
|
|
170
179
|
const unread = unreadMotionLine(a.rig, input.motion);
|
|
@@ -191,6 +200,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
191
200
|
if (map.alternates.length > 0) {
|
|
192
201
|
if (r.switches.length === 0) say(` SWITCH none — no set within ${a.switchTolerance}° of the pose for ${a.switchMin}s`);
|
|
193
202
|
for (const s of r.switches) say(` SWITCH ${switchLine(s, r.times)}`);
|
|
203
|
+
// #279: only for a set the minimum span held back, so a run where nothing was suppressed prints what it always has.
|
|
204
|
+
for (const l of switchMinLines(r.switchRuns, a.switchMin)) say(` SWITCH-MIN ${l}`);
|
|
194
205
|
for (const l of windowLines(map, r.switches, r.distances, r.times, a.switchTolerance)) say(` FRAMES ${l}`);
|
|
195
206
|
for (const l of phaseLines(map, r.switches)) say(` PHASE ${l}`);
|
|
196
207
|
}
|
|
@@ -202,6 +213,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
202
213
|
for (const e of r.byHip) say(` BY-HIP ${raisedLine(e, r.times)}`);
|
|
203
214
|
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
204
215
|
for (const e of r.reach) say(` REACH ${reachCostLine(e, r.times)}`);
|
|
216
|
+
// rig-c/pose#282: each reach chain's origin against its source joint; a map without one prints none.
|
|
217
|
+
for (const e of r.reachOrigin) {
|
|
218
|
+
say(` REACH-ORIGIN ${reachOriginLine(e, r.times)}`);
|
|
219
|
+
if (e.warn) say(` WARN ${reachOriginWarn(e)}`);
|
|
220
|
+
}
|
|
205
221
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
206
222
|
// #287: a range that clamps no frame says so once, on the last segment's line (the line count stays one per segment).
|
|
207
223
|
const noClamp = legScaleNoClamp(r.legScale);
|
|
@@ -211,6 +227,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
211
227
|
// rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
|
|
212
228
|
const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
|
|
213
229
|
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
230
|
+
// rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from those layers.
|
|
231
|
+
const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop) : null;
|
|
232
|
+
if (drawOrder !== null) for (const l of drawOrderLines(drawOrder)) say(` ${l}`);
|
|
214
233
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
215
234
|
if (r.feet !== null) {
|
|
216
235
|
const f = r.feet;
|
|
@@ -238,15 +257,16 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
238
257
|
if (r.body !== null) say(` BODY ${bodyLine(r.body)}`);
|
|
239
258
|
|
|
240
259
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
241
|
-
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true);
|
|
260
|
+
const reduced = a.reduce === undefined ? null : a.reduceChains === undefined ? reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true) : reduceTracks(r, map, rest, a.reduce, a.reduceHip, a.reduceKeepPlanted === true, reduceOf!);
|
|
242
261
|
if (reduced !== null) {
|
|
243
262
|
for (const row of reduced.rows) {
|
|
244
263
|
const [who, track, what, unit] = row.kind === 'angle' ? ['bone', 'rotate', 'aim', '°'] : row.kind === 'origin' ? ['hip', 'translate', 'origin', ' units'] : ['bone', 'scalex', 'scale', ' of its setup length'];
|
|
245
|
-
|
|
264
|
+
const own = row.kind === 'angle' && reduceOf!(row.bone) !== a.reduce ? ` — within its chain's ${reduceOf!(row.bone)}°` : '';
|
|
265
|
+
say(` REDUCE ${who} "${row.bone}" ${track}: ${row.before} → ${row.after} key(s); worst ${what} error ${row.worst.toPrecision(3)}${unit} at frame ${row.frame} (t=${r.times[row.frame]!.toFixed(3)}s)${own}`);
|
|
246
266
|
}
|
|
247
267
|
const sum = (kind: ReduceRow['kind'], k: 'before' | 'after') => reduced.rows.filter((x) => x.kind === kind).reduce((t, x) => t + x[k], 0);
|
|
248
268
|
const scales = reduced.rows.some((x) => x.kind === 'scale') ? `; scalex keys ${sum('scale', 'before')} → ${sum('scale', 'after')} within ${SCALE_REDUCE} of the setup length` : '';
|
|
249
|
-
say(` REDUCE rotate keys ${sum('angle', 'before')} → ${sum('angle', 'after')} (${(sum('angle', 'before') / sum('angle', 'after')).toFixed(2)}×) within ${a.reduce}°${scales}${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
269
|
+
say(` REDUCE rotate keys ${sum('angle', 'before')} → ${sum('angle', 'after')} (${(sum('angle', 'before') / sum('angle', 'after')).toFixed(2)}×) within ${a.reduce}°${chainsText(a.reduceChains)}${scales}${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
// The idle against the new animation (#43 item 2): what it drops, and with --carry-idle, looped back in.
|
|
@@ -295,7 +315,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
295
315
|
for (const tr of fragment) say(` MERGE ${a.merge}: ${trackLabel(tr, groups)}, ${tr.keys.length} key(s) at t=${tr.keys[0]!.t}${tr.keys.length > 1 ? `–${tr.keys[tr.keys.length - 1]!.t}` : ''}s`);
|
|
296
316
|
}
|
|
297
317
|
const added = [...carried, ...fragment];
|
|
298
|
-
const
|
|
318
|
+
const tracked = added.length === 0 ? r.animation : { ...r.animation, tracks: [...own, ...added] };
|
|
319
|
+
// #304: rig-c's one draw-order timeline sits on the animation beside its tracks; written only when keyed.
|
|
320
|
+
const animation = drawOrder === null || drawOrder.keys.length === 0 ? tracked : { ...tracked, drawOrder: drawOrderKeys(drawOrder) };
|
|
299
321
|
const motion = { ...base, animations: { ...animations, [a.name]: animation } };
|
|
300
322
|
const g1 = gate(input.rig, motion, join(scratch, 'with'), a.profile, a.images);
|
|
301
323
|
if (!g1.green) {
|
|
@@ -314,11 +336,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
314
336
|
const support = a.reduce !== undefined && a.reduceKeepPlanted === true && r.feet !== null ? supportFrames(r.feet.supports) : undefined;
|
|
315
337
|
const landings = measureLanding(doc, a.name, map, r, posed, support).map((l) => {
|
|
316
338
|
if (a.reduce === undefined) return l;
|
|
317
|
-
|
|
339
|
+
// #283: a chain's own tolerance is added as the run's is; the sum is rounded only under a per-chain --reduce.
|
|
340
|
+
if (l.kind === 'angle') return { ...l, tolerance: a.reduceChains === undefined ? l.tolerance + a.reduce : Number((l.tolerance + reduceOf!(l.bone)).toPrecision(12)) };
|
|
318
341
|
if (l.kind === 'origin' && a.reduceHip !== undefined) return { ...l, tolerance: l.tolerance + a.reduceHip };
|
|
319
342
|
if (l.kind !== 'foot') return l;
|
|
320
343
|
const ankle = l.bone.replace(/:tip$/, '');
|
|
321
|
-
const by = footReduce(rest, r, ankle, a.reduce, a.reduceHip);
|
|
344
|
+
const by = footReduce(rest, r, ankle, a.reduce, a.reduceHip, a.reduceChains === undefined ? undefined : reduceOf!);
|
|
322
345
|
// #251: on its support frames every mapped bone from the hip down was kept at its target, so only the hip's tolerance moves it.
|
|
323
346
|
const hip = a.reduceHip ?? 0;
|
|
324
347
|
if (!drift.some((d) => d.bone === l.bone)) {
|
|
@@ -326,7 +349,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
326
349
|
const side = lb?.r.ankle === ankle ? 'r' : lb?.l.ankle === ankle ? 'l' : null;
|
|
327
350
|
const legs = (u: number) => (side === null || r.rigLeg === null ? null : u / r.rigLeg[side]);
|
|
328
351
|
const held = support === undefined ? {} : { support: side === null || support[side].size === 0 ? null : { reduce: hip, units: l.tolerance + hip, legLengths: legs(l.tolerance + hip) } };
|
|
329
|
-
|
|
352
|
+
// #283: under a per-chain --reduce, the tolerance its leg was reduced within.
|
|
353
|
+
const leg = a.reduceChains === undefined ? {} : { legTolerance: legTolerance(rest, r, ankle, reduceOf!) };
|
|
354
|
+
drift.push({ bone: l.bone, side, plant: l.tolerance, reduce: by, units: l.tolerance + by, legLengths: legs(l.tolerance + by), ...held, ...leg });
|
|
330
355
|
}
|
|
331
356
|
return { ...l, tolerance: l.tolerance + (l.on === 'support' ? hip : by) };
|
|
332
357
|
});
|
|
@@ -389,7 +414,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
389
414
|
|
|
390
415
|
assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
391
416
|
// Every retarget option as resolved: the report's `options`, and with the name the build record's `flags` (#270).
|
|
392
|
-
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}) };
|
|
417
|
+
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}) };
|
|
393
418
|
const reportText = writeReport({
|
|
394
419
|
spec: 'rig-pose-report/1',
|
|
395
420
|
rigc,
|
|
@@ -414,9 +439,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
414
439
|
seams: seamRead.seams,
|
|
415
440
|
seamPairs: seamRead.seamPairs,
|
|
416
441
|
switches: r.switches,
|
|
442
|
+
switchRuns: r.switchRuns,
|
|
417
443
|
uncovered,
|
|
418
444
|
turns: r.turns,
|
|
419
445
|
reach: r.reach,
|
|
446
|
+
// Only when the map has a reach chain (#282): a map without one writes the report it always has.
|
|
447
|
+
...(r.reachOrigin.length > 0 ? { reachOrigin: r.reachOrigin } : {}),
|
|
420
448
|
range: r.range,
|
|
421
449
|
artRange: r.artRange,
|
|
422
450
|
raised: r.raised,
|
|
@@ -448,13 +476,15 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
448
476
|
pelvis: r.pelvis,
|
|
449
477
|
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
450
478
|
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
479
|
+
// Only with --draw-order depth (#304).
|
|
480
|
+
...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys, spans: drawOrder.spans } } : {}),
|
|
451
481
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
452
482
|
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, straight: r.legStraight, rigLeg: r.rigLeg, segments: r.segments, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
453
483
|
shear,
|
|
454
484
|
idleDropped: idle === null ? null : idle.dropped,
|
|
455
485
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
456
486
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
457
|
-
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
487
|
+
reduce: reduced === null ? null : { tolerance: a.reduce, ...(a.reduceChains === undefined ? {} : { chains: a.reduceChains }), hipTolerance: a.reduceHip ?? null, rows: reduced.rows, ankleDrift: drift },
|
|
458
488
|
}, map, { frameTimes: r.times }, f.meshes !== null && doc.slots.length > 0 ? reportRenderDomain(JSON.parse(readFileSync(join(f.frames, '..', 'frames.json'), 'utf8')), a.name) : seamRead.frames === null ? null : reportRenderDomain(seamRead.frames, a.name), {rig:rigSnapshot.identity,map:mapSnapshot.identity,pose:poseSnapshot.identity});
|
|
459
489
|
if (a.build !== undefined) {
|
|
460
490
|
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
@@ -626,7 +656,7 @@ const spanText = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}
|
|
|
626
656
|
* aim and length it lands on (`reduceUnderScale`). With none, this is #32's
|
|
627
657
|
* reduction, unchanged.
|
|
628
658
|
*/
|
|
629
|
-
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined, keepPlanted: boolean): Reduced {
|
|
659
|
+
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined, keepPlanted: boolean, tolOf: (bone: string) => number = () => tolerance): Reduced {
|
|
630
660
|
const n = r.times.length;
|
|
631
661
|
const keep = new Map<string, Set<number>>();
|
|
632
662
|
const add = (bone: string, ...frames: number[]) => {
|
|
@@ -665,7 +695,9 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
665
695
|
return false;
|
|
666
696
|
};
|
|
667
697
|
const dense = new Map([...rotate].map(([b, tr]) => [b, tr.keys.map((k) => k.v[0]!)]));
|
|
668
|
-
|
|
698
|
+
// #283: a bone whose chain --reduce names carries its own tolerance; every other takes the run's.
|
|
699
|
+
const own = (b: string): { tolerance?: number } => (tolOf(b) === tolerance ? {} : { tolerance: tolOf(b) });
|
|
700
|
+
const bones: ReduceBone[] = [...rotate.values()].filter((tr) => !under(tr.bone)).map((tr) => ({ bone: tr.bone, above: above(tr.bone), dense: dense.get(tr.bone)!, keep: keep.get(tr.bone) ?? new Set<number>(), ...own(tr.bone) }));
|
|
669
701
|
const hipTrack = r.origin === null || hipTolerance === undefined ? undefined : r.animation.tracks.find((tr): tr is MotionTrack => 'bone' in tr && tr.property === 'translate' && tr.bone === r.origin!.bone && tr.keys.every((k) => k.ease === undefined));
|
|
670
702
|
const parent = hipTrack === undefined ? null : rest.get(hipTrack.bone)!.parent;
|
|
671
703
|
const pm = parent === null ? null : rest.get(parent)!;
|
|
@@ -758,6 +790,7 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
758
790
|
asked: r.scales.get(b) ?? new Array<number>(n).fill(1),
|
|
759
791
|
dense: dense.get(b)!,
|
|
760
792
|
keep: keepOf(b),
|
|
793
|
+
...own(b),
|
|
761
794
|
};
|
|
762
795
|
});
|
|
763
796
|
const red = reduceUnderScale(r.times, given, tolerance, SCALE_REDUCE);
|
|
@@ -782,7 +815,7 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
782
815
|
* world aim is within the angle tolerance of its dense one, top-down, so the
|
|
783
816
|
* segments' errors are each bounded and the sum bounds the ankle's.
|
|
784
817
|
*/
|
|
785
|
-
export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolerance: number, hipTolerance: number | undefined): number {
|
|
818
|
+
export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolerance: number, hipTolerance: number | undefined, tolOf?: (bone: string) => number): number {
|
|
786
819
|
const top = r.origin?.bone ?? null;
|
|
787
820
|
let sMax = 1;
|
|
788
821
|
for (const s of r.scales.values()) for (const v of s) sMax = Math.max(sMax, v);
|
|
@@ -794,7 +827,8 @@ export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: st
|
|
|
794
827
|
if (p === null) break;
|
|
795
828
|
lever += Math.hypot(x.worldX - p.worldX, x.worldY - p.worldY);
|
|
796
829
|
}
|
|
797
|
-
|
|
830
|
+
const deg = tolOf === undefined ? tolerance : legTolerance(rest, r, ankle, tolOf);
|
|
831
|
+
return (hipTolerance ?? 0) + lever * sMax * ((deg * Math.PI) / 180 + SCALE_REDUCE);
|
|
798
832
|
}
|
|
799
833
|
|
|
800
834
|
function spans(ts: number[], step: number): string {
|
|
@@ -1290,6 +1324,8 @@ export interface AnkleDrift {
|
|
|
1290
1324
|
* Absent without the flag.
|
|
1291
1325
|
*/
|
|
1292
1326
|
support?: { reduce: number; units: number; legLengths: number | null } | null;
|
|
1327
|
+
/** rig-c/pose#283: under a per-chain --reduce, the largest tolerance a bone from the hip down to it was reduced within (degrees). */
|
|
1328
|
+
legTolerance?: number;
|
|
1293
1329
|
}
|
|
1294
1330
|
|
|
1295
1331
|
/** #251: each side's support frames, from the spans --plant-feet read. */
|
|
@@ -1301,11 +1337,46 @@ function supportFrames(supports: ReadonlyArray<{ side: 'r' | 'l'; frames: [numbe
|
|
|
1301
1337
|
|
|
1302
1338
|
/** The drift line: the allowance on one frame, and what it lets a planted ankle slide between two. */
|
|
1303
1339
|
export function ankleDriftLine(d: AnkleDrift): string {
|
|
1340
|
+
const chainDeg = d.legTolerance;
|
|
1304
1341
|
const legs = (v: number) => (d.legLengths === null ? '' : ` = ${(d.legLengths * v / d.units).toPrecision(3)} leg lengths`);
|
|
1305
1342
|
const line = `ankle drift allowed: bone "${d.bone}" ${d.units.toPrecision(3)} units${legs(d.units)} on any one frame (plant ${d.plant.toPrecision(3)} + reduce ${d.reduce.toPrecision(3)}); between two frames up to ${(2 * d.units).toPrecision(3)} units${legs(2 * d.units)}`;
|
|
1306
|
-
if (d.support === undefined) return `${line} — a tighter slide bar needs a smaller --reduce`;
|
|
1343
|
+
if (d.support === undefined) return `${line} — ${chainDeg === undefined ? 'a tighter slide bar needs a smaller --reduce' : `its leg reduced within ${chainDeg}° (--reduce <deg>,<chain>=<deg>); a tighter slide bar needs a smaller one on its chain`}`;
|
|
1307
1344
|
if (d.support === null) return `${line}; its side never supports, so --reduce-keep-planted keeps no frame of it`;
|
|
1308
1345
|
const s = d.support;
|
|
1309
1346
|
const sLegs = (v: number) => (s.legLengths === null ? '' : ` = ${(s.legLengths * v / s.units).toPrecision(3)} leg lengths`);
|
|
1310
1347
|
return `${line} off its support frames; on them, kept dense (--reduce-keep-planted), ${s.units.toPrecision(3)} units${sLegs(s.units)} (plant ${d.plant.toPrecision(3)} + hip ${s.reduce.toPrecision(3)}), between two frames of a support span up to ${(2 * s.units).toPrecision(3)} units${sLegs(2 * s.units)}`;
|
|
1311
1348
|
}
|
|
1349
|
+
|
|
1350
|
+
/** rig-c/pose#283: the largest tolerance any bone from `ankle` up to the legs' top was reduced within. */
|
|
1351
|
+
export function legTolerance(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolOf: (bone: string) => number): number {
|
|
1352
|
+
const top = r.origin?.bone ?? null;
|
|
1353
|
+
let deg = tolOf(ankle);
|
|
1354
|
+
for (let b: string | null = rest.get(ankle)!.parent; b !== null && b !== top; b = rest.get(b)!.parent) deg = Math.max(deg, tolOf(b));
|
|
1355
|
+
return deg;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* rig-c/pose#283: the reduction's tolerance per bone. Every bone takes `tolerance`
|
|
1360
|
+
* but a bone of a chain `chains` names, which takes that chain's (the smallest,
|
|
1361
|
+
* when two named chains list it). A name the map has no chain for is refused by
|
|
1362
|
+
* name, beside the chains it has.
|
|
1363
|
+
*/
|
|
1364
|
+
export function reduceTolerances(map: JointMap, tolerance: number, chains: Record<string, number> | undefined, mapPath: string): (bone: string) => number {
|
|
1365
|
+
if (chains === undefined) return () => tolerance;
|
|
1366
|
+
const names = map.chains.map((c) => c.name);
|
|
1367
|
+
const missing = Object.keys(chains).filter((c) => !names.includes(c));
|
|
1368
|
+
if (missing.length > 0) throw new Refusal(missing.map((c) => ({ code: 'POSE_REDUCE', text: `--reduce names chain "${c}", which ${mapPath} does not have; its chains are ${names.map((x) => `"${x}"`).join(', ')}` })));
|
|
1369
|
+
const by = new Map<string, number>();
|
|
1370
|
+
for (const c of map.chains) {
|
|
1371
|
+
const t = Object.hasOwn(chains, c.name) ? chains[c.name] : undefined;
|
|
1372
|
+
if (t === undefined) continue;
|
|
1373
|
+
for (const b of c.bones) by.set(b, Math.min(by.get(b) ?? t, t));
|
|
1374
|
+
}
|
|
1375
|
+
return (bone) => by.get(bone) ?? tolerance;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** rig-c/pose#283: the per-chain tolerances as the run and REDUCE lines print them; empty without. */
|
|
1379
|
+
export function chainsText(chains: Record<string, number> | undefined): string {
|
|
1380
|
+
if (chains === undefined) return '';
|
|
1381
|
+
return ` (${Object.entries(chains).map(([c, t]) => `chain "${c}" ${t}°`).join(', ')})`;
|
|
1382
|
+
}
|