@rig-c/pose 0.1.1 → 0.2.1
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 +17 -0
- package/cli.ts +11 -2
- package/package.json +1 -1
- package/src/retarget.ts +127 -1
- package/src/rigc.ts +15 -6
- package/src/run.ts +16 -10
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@ Part of the rig-c toolchain: [core](https://github.com/rig-c/core) compiles and
|
|
|
8
8
|
|
|
9
9
|
<!-- demo -->
|
|
10
10
|
|
|
11
|
+
## Try it
|
|
12
|
+
|
|
13
|
+
[`examples/wave`](examples/wave) runs with nothing else: a bones-only stick rig (no art), a 3-second wave made from joint angles (`make-pose.ts` writes it), and the map that joins them.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
rig-pose retarget --rig examples/wave/rig --map examples/wave/map.json --pose examples/wave/pose.json --out wave.motion.json --build wave-build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json` and the gated build. `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
|
|
20
|
+
|
|
11
21
|
## What it does
|
|
12
22
|
|
|
13
23
|
```sh
|
|
@@ -29,6 +39,7 @@ Nothing is written until every check passes:
|
|
|
29
39
|
and what the run cannot decide for you it lists:
|
|
30
40
|
|
|
31
41
|
- `HOLD` — a joint missing, or scored under `--min-score`, holds its last good value; one line per span, with the frames and the reason.
|
|
42
|
+
- `FLIP` — a segment that turns more than `--flip` degrees in one frame (with `--flip <deg>`; off by default) is spread over the frames around the jump; one line per spread, with the chain, the segment, the jump, the frames and h. Below.
|
|
32
43
|
- `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
|
|
33
44
|
- `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
|
|
34
45
|
- `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate.
|
|
@@ -37,6 +48,12 @@ and what the run cannot decide for you it lists:
|
|
|
37
48
|
|
|
38
49
|
**Use `--smooth 7` at 24 fps.** It was measured on a 13 s dance on a @rig-c/parts rig, read back from the gated build: the motion's timing did not move (speed cross-correlation lag 0 on the wrists, ankles and hip), the frame-to-frame second difference dropped 2.5× on the wrists, 1.7–2.1× on the ankles and 1.5× on the hip, and the hip's 95th-percentile speed fell to 0.85×. The criterion set beforehand, a 2× drop on every point, was not met on the hip. That criterion was flawed: the hip's second difference is mostly real motion, while the filter cuts white noise 4.6×. A measure that isolates noise is a later card, and the default stays off until it passes.
|
|
39
50
|
|
|
51
|
+
**The flip guard.** A pose model can put a fast sweep of a limb into one frame — under motion blur it takes the elbow or the wrist for another point — and the rig then whips the limb round between two keys. Position smoothing cannot remove a step like that. So after holds and smoothing, and before the hinge check, every segment the map aims (not a `reach`, which is aimed later from posed positions) is read frame to frame: a turn greater than `--flip <deg>` from frame k−1 to frame k is spread. For the smallest h ≥ 1 that keeps every one-frame turn at or under the threshold, frames k−h … k+h−1 are replaced by the straight line from frame k−h−1 to frame k+h, the turn's own way round. It is centred, so the step happens when the pose put it, over 2h frames, and both ends keep the pose's angles. A window that would cross the first or last frame stops there and the line says so; two windows that overlap become one. `--flip off` keeps every turn. The report lists each spread under `flips`.
|
|
52
|
+
|
|
53
|
+
The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/issues/15)). On the 13 s dance (313 frames, RTMPose), the one-frame turns of the picture angle of 10 segments — both upper arms, forearms, thighs and shins, the eye line and the hip line, 3,120 turns — run p50 1.0°, p90 5.0°, p99 23.1°, p99.5 33.4°, p99.9 78.2°, max 148.6°. 90° is the p99.9 rounded up past a margin, and marks the 3 turns above it (0.1%): both forearms between frames 181 and 182 (148.6° and 114.4°, a motion-blurred stretch) and the left forearm between 53 and 54 (99.2°, the forearm pointing at the camera). None of them is a spike that comes back: each keeps the direction the limb was already turning, and the new angle holds — a real sweep put into one frame. Holding the last angle would be wrong a frame later, so the guard spreads the step instead. With it, on the @rig-c/parts rig #11 measured, the largest one-frame turn of any guarded bone in the gated build, posed by rig-c, is 78.2° (148.6° without it), and every bone still lands.
|
|
54
|
+
|
|
55
|
+
Re-measured with the guard on, against the guarded unsmoothed run, on #11's criteria (power above 6 Hz down ≥ 2× on every point, timing lag 0, p95 speed ≥ 0.85×): no window passes. `--smooth 7` comes closest — the left wrist's power drops 1.99× and the hip's p95 speed is 0.847× — so `--smooth` stays off by default, and so does the guard: the rule posted before the run made a default for either one depend on a window passing. **Use `--flip 90`**: with it, no bone of the gated build turns more than 90° in one frame (without it, 148.6°), and every bone still lands.
|
|
56
|
+
|
|
40
57
|
Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2` bad invocation. `rig-pose --help` has every flag.
|
|
41
58
|
|
|
42
59
|
## The pose: `rig-pose-sequence/1`
|
package/cli.ts
CHANGED
|
@@ -35,13 +35,16 @@ retarget
|
|
|
35
35
|
--smooth <n> Savitzky–Golay over n frames of joint positions: a centred least-squares cubic,
|
|
36
36
|
read at each frame (default: 1, none); n is odd and ≥ 5 — a cubic through 3
|
|
37
37
|
points passes through all three, so 3 would change nothing
|
|
38
|
+
--flip <deg>|off a segment that turns more than <deg> degrees in one frame of the pose is
|
|
39
|
+
spread over the frames around the jump, centred on it, and listed as FLIP
|
|
40
|
+
(default: off, every turn kept as the pose gives it; 90 is the measured setting)
|
|
38
41
|
--bends refuse|clamp a hinge (elbow, knee: any joint inside a chain) bent the wrong way refuses
|
|
39
42
|
the run (default), or is straightened on that frame and listed
|
|
40
43
|
--bend-tolerance <d> degrees past straight a hinge may bend the wrong way before it counts (default: 0)
|
|
41
44
|
--profile spine|spine-html rigc's gate profile (default: spine, rigc's own default)
|
|
42
45
|
--build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
|
|
43
46
|
--render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
|
|
44
|
-
--report <file> write the run's measurements (holds, bends, landings, folds) as JSON
|
|
47
|
+
--report <file> write the run's measurements (holds, flips, bends, landings, folds) as JSON
|
|
45
48
|
|
|
46
49
|
It writes nothing until rigc's gate is green with the animation in, every mapped bone lands on
|
|
47
50
|
the angle the pose asks for (posed by rig-c's own poser), and no hinge bends the wrong way.
|
|
@@ -96,8 +99,13 @@ function smoothOf(f: Map<string, string>): number {
|
|
|
96
99
|
return n;
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
function flipOf(f: Map<string, string>): number | 'off' {
|
|
103
|
+
if (!f.has('flip') || f.get('flip') === 'off') return 'off';
|
|
104
|
+
return num(f, 'flip', 90, (v) => v > 0 && v < 180, 'off, or degrees in (0, 180) — a one-frame turn is never more than 180°');
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
100
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'bends', 'bend-tolerance', 'profile', 'build', 'render', 'report'], ['coco']);
|
|
108
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'flip', 'bends', 'bend-tolerance', 'profile', 'build', 'render', 'report'], ['coco']);
|
|
101
109
|
const bends = f.get('bends') ?? 'refuse';
|
|
102
110
|
if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
|
|
103
111
|
const profile = f.get('profile') ?? 'spine';
|
|
@@ -115,6 +123,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
115
123
|
baseMotion: f.get('base-motion'),
|
|
116
124
|
minScore: num(f, 'min-score', 0.3, (n) => n >= 0, 'a number ≥ 0'),
|
|
117
125
|
smooth: smoothOf(f),
|
|
126
|
+
flip: flipOf(f),
|
|
118
127
|
bends,
|
|
119
128
|
bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
120
129
|
profile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/retarget.ts
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
* evaluated at the frame, on each joint's positions after its holds. Centred,
|
|
24
24
|
* it moves no motion in time; a cubic keeps a peak a moving average flattens.
|
|
25
25
|
*
|
|
26
|
+
* The flip guard (rig-c/pose#15) then spreads a segment's turn greater than
|
|
27
|
+
* `flip` degrees in one frame over the frames around it, linearly and centred on
|
|
28
|
+
* the jump (`spreadFlips`). It reads the mapped segments' target angles after
|
|
29
|
+
* holds and smoothing and before the hinge check. A reach's target is aimed
|
|
30
|
+
* later, from posed positions, and is not guarded.
|
|
31
|
+
*
|
|
26
32
|
* What this cannot know is held to account afterwards: `verify.ts` poses the
|
|
27
33
|
* gated build with rig-c's poser and measures where each bone landed.
|
|
28
34
|
*/
|
|
@@ -45,6 +51,8 @@ export interface RetargetOptions {
|
|
|
45
51
|
bends: 'refuse' | 'clamp';
|
|
46
52
|
/** Degrees past straight a hinge may bend the wrong way before it counts. */
|
|
47
53
|
bendTolerance: number;
|
|
54
|
+
/** A segment turning more than this many degrees in one frame is spread (`spreadFlips`); 'off' keeps every turn. */
|
|
55
|
+
flip: number | 'off';
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export interface Hold {
|
|
@@ -69,6 +77,26 @@ export interface BendEvent {
|
|
|
69
77
|
rest: number;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/** One spread: a segment's turn over the threshold, and the frames whose angle was replaced. */
|
|
81
|
+
export interface Flip {
|
|
82
|
+
chain: string;
|
|
83
|
+
bone: string;
|
|
84
|
+
/** The segment the bone carries: from joint, to joint. */
|
|
85
|
+
joints: [string, string];
|
|
86
|
+
/** The largest input turn in the spread, degrees, signed (Spine's sense, counter-clockwise +). */
|
|
87
|
+
jump: number;
|
|
88
|
+
/** Every input turn over the threshold the spread covers: `frame` is k, the turn is from frame k−1 to k. */
|
|
89
|
+
turns: Array<{ frame: number; jump: number }>;
|
|
90
|
+
/** First and last frame whose angle was replaced; the frames either side keep the input's. */
|
|
91
|
+
frames: [number, number];
|
|
92
|
+
/** The half-width the spread needed (for spreads merged from neighbouring turns, the widest). */
|
|
93
|
+
h: number;
|
|
94
|
+
/** The one-frame turn across the spread, degrees, signed. */
|
|
95
|
+
step: number;
|
|
96
|
+
/** Which end of the series cut the window short, if one did. */
|
|
97
|
+
clipped: 'start' | 'end' | 'both' | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
export interface MotionKey {
|
|
73
101
|
t: number;
|
|
74
102
|
v: number[];
|
|
@@ -93,6 +121,7 @@ export interface Retarget {
|
|
|
93
121
|
origin: { bone: string; at: Array<[number, number]> } | null;
|
|
94
122
|
holds: Hold[];
|
|
95
123
|
bends: BendEvent[];
|
|
124
|
+
flips: Flip[];
|
|
96
125
|
/** The figure scale, rig units per picture pixel, and the lengths it came from. */
|
|
97
126
|
scale: { factor: number; pose: number; rig: number } | null;
|
|
98
127
|
}
|
|
@@ -329,6 +358,20 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
329
358
|
}
|
|
330
359
|
p.throwIfAny();
|
|
331
360
|
|
|
361
|
+
// The flip guard, on every segment the pose aims directly (not a reach).
|
|
362
|
+
const flips: Flip[] = [];
|
|
363
|
+
if (opts.flip !== 'off') {
|
|
364
|
+
const threshold = opts.flip;
|
|
365
|
+
for (const c of map.chains) {
|
|
366
|
+
if (c.reach) continue;
|
|
367
|
+
c.bones.forEach((b, k) => {
|
|
368
|
+
for (const s of spreadFlips(targets.get(b)!, threshold)) {
|
|
369
|
+
flips.push({ chain: c.name, bone: b, joints: [c.joints[k]!, c.joints[k + 1]!], ...s });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
332
375
|
// Hinges: each joint strictly inside a chain (a reach has one bone, so none).
|
|
333
376
|
const bends: BendEvent[] = [];
|
|
334
377
|
for (const c of map.chains) {
|
|
@@ -409,7 +452,90 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
409
452
|
}
|
|
410
453
|
if (translateTrack !== null) tracks.push(translateTrack);
|
|
411
454
|
|
|
412
|
-
return { animation: { duration: keyTimes[n - 1]!, tracks }, times: keyTimes, targets, origin, holds, bends, scale };
|
|
455
|
+
return { animation: { duration: keyTimes[n - 1]!, tracks }, times: keyTimes, targets, origin, holds, bends, flips, scale };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type Spread = Omit<Flip, 'chain' | 'bone' | 'joints'>;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The flip guard on one angle series (degrees), in place. A turn greater than
|
|
462
|
+
* `threshold` from frame k−1 to frame k is spread: for the smallest h ≥ 1, the
|
|
463
|
+
* angles of frames k−h…k+h−1 are replaced by the straight line from frame
|
|
464
|
+
* k−h−1 to frame k+h, taken through the unwrapped series (each one-frame turn
|
|
465
|
+
* the short way round, so the jump keeps its own direction), such that every
|
|
466
|
+
* one-frame turn in frames k−h…k+h is at or under the threshold. It is centred
|
|
467
|
+
* on the jump, so the step happens when the input put it, spread over 2h frames,
|
|
468
|
+
* and both ends are the input's own angles. A window that would cross the first
|
|
469
|
+
* or the last frame stops there (`clipped`). Windows that overlap — one replaces
|
|
470
|
+
* a frame the other ends on — merge into one line over both, widened again
|
|
471
|
+
* while that line still turns faster than the threshold. Returns the spreads.
|
|
472
|
+
*/
|
|
473
|
+
export function spreadFlips(angles: number[], threshold: number): Spread[] {
|
|
474
|
+
const n = angles.length;
|
|
475
|
+
if (n < 2) return [];
|
|
476
|
+
const u: number[] = [angles[0]!];
|
|
477
|
+
for (let i = 1; i < n; i++) u.push(u[i - 1]! + wrap(angles[i]! - angles[i - 1]!));
|
|
478
|
+
const fits = (lo: number, hi: number) => Math.abs(u[hi]! - u[lo]!) <= threshold * (hi - lo);
|
|
479
|
+
interface Win { lo: number; hi: number; h: number; start: boolean; end: boolean; turns: Array<{ frame: number; jump: number }> }
|
|
480
|
+
const wins: Win[] = [];
|
|
481
|
+
for (let k = 1; k < n; k++) {
|
|
482
|
+
const jump = u[k]! - u[k - 1]!;
|
|
483
|
+
if (Math.abs(jump) <= threshold) continue;
|
|
484
|
+
let h = 1;
|
|
485
|
+
let lo = Math.max(0, k - h - 1);
|
|
486
|
+
let hi = Math.min(n - 1, k + h);
|
|
487
|
+
while (!fits(lo, hi) && (lo > 0 || hi < n - 1)) {
|
|
488
|
+
h++;
|
|
489
|
+
lo = Math.max(0, k - h - 1);
|
|
490
|
+
hi = Math.min(n - 1, k + h);
|
|
491
|
+
}
|
|
492
|
+
wins.push({ lo, hi, h, start: k - h - 1 < 0, end: k + h > n - 1, turns: [{ frame: k, jump }] });
|
|
493
|
+
}
|
|
494
|
+
// Merge overlapping windows; a merged line that turns too fast widens by one on each side, then merges again.
|
|
495
|
+
for (let changed = true; changed;) {
|
|
496
|
+
changed = false;
|
|
497
|
+
wins.sort((a, b) => a.lo - b.lo);
|
|
498
|
+
for (let i = 0; i + 1 < wins.length; i++) {
|
|
499
|
+
const a = wins[i]!;
|
|
500
|
+
const b = wins[i + 1]!;
|
|
501
|
+
if (b.lo >= a.hi) continue;
|
|
502
|
+
wins.splice(i, 2, { lo: a.lo, hi: Math.max(a.hi, b.hi), h: Math.max(a.h, b.h), start: a.start || b.start, end: a.end || b.end, turns: [...a.turns, ...b.turns].sort((x, y) => x.frame - y.frame) });
|
|
503
|
+
changed = true;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (changed) continue;
|
|
507
|
+
for (const w of wins) {
|
|
508
|
+
if (w.turns.length < 2 || fits(w.lo, w.hi) || (w.lo === 0 && w.hi === n - 1)) continue;
|
|
509
|
+
if (w.lo === 0) w.start = true;
|
|
510
|
+
if (w.hi === n - 1) w.end = true;
|
|
511
|
+
w.lo = Math.max(0, w.lo - 1);
|
|
512
|
+
w.hi = Math.min(n - 1, w.hi + 1);
|
|
513
|
+
w.h++;
|
|
514
|
+
changed = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return wins.map((w) => {
|
|
518
|
+
const step = (u[w.hi]! - u[w.lo]!) / (w.hi - w.lo);
|
|
519
|
+
for (let i = w.lo + 1; i < w.hi; i++) angles[i] = wrap(u[w.lo]! + step * (i - w.lo));
|
|
520
|
+
const top = w.turns.reduce((m, t) => (Math.abs(t.jump) > Math.abs(m.jump) ? t : m));
|
|
521
|
+
return {
|
|
522
|
+
jump: top.jump,
|
|
523
|
+
turns: w.turns,
|
|
524
|
+
frames: [w.lo + 1, w.hi - 1] as [number, number],
|
|
525
|
+
h: w.h,
|
|
526
|
+
step,
|
|
527
|
+
clipped: w.start && w.end ? 'both' : w.start ? 'start' : w.end ? 'end' : null,
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function flipLine(f: Flip, times: readonly number[], threshold: number): string {
|
|
533
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
534
|
+
const turns = f.turns.map((t) => `${sg(t.jump)} from frame ${t.frame - 1} to ${t.frame} (t=${times[t.frame - 1]!.toFixed(3)}–${times[t.frame]!.toFixed(3)}s)`).join(', ');
|
|
535
|
+
const span = f.frames[0] > f.frames[1] ? 'no frame (the pose has none between the two)' : f.frames[0] === f.frames[1] ? `frame ${f.frames[0]}` : `frames ${f.frames[0]}–${f.frames[1]}`;
|
|
536
|
+
const clip = f.clipped === null ? '' : f.clipped === 'both' ? '; the window was cut short by both ends of the pose' : `; the window was cut short at the ${f.clipped === 'start' ? 'first' : 'last'} frame of the pose`;
|
|
537
|
+
const still = Math.abs(f.step) > threshold ? ` — still over ${threshold}°: the pose is too short to spread it` : '';
|
|
538
|
+
return `chain "${f.chain}" bone "${f.bone}" (${f.joints[0]} → ${f.joints[1]}) turned ${turns}, over ${threshold}° in one frame; spread over ${span} (h=${f.h}), ${sg(f.step)} per frame${clip}${still}`;
|
|
413
539
|
}
|
|
414
540
|
|
|
415
541
|
/** Frames before the first with a direction take its value; none at all is a problem. */
|
package/src/rigc.ts
CHANGED
|
@@ -100,18 +100,27 @@ export function gate(rig: string, motion: Record<string, unknown>, dir: string,
|
|
|
100
100
|
mkdirSync(dir, { recursive: true });
|
|
101
101
|
const motionPath = join(dir, 'motion.json');
|
|
102
102
|
writeFileSync(motionPath, `${JSON.stringify(motion, null, 2)}\n`);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
return buildInto(rig, motionPath, join(dir, 'build'), profile, join(dir, 'build-report.json'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `rigc build` of `rig` with the motion spec at `motionPath` into `out`, under
|
|
108
|
+
* rigc's gate (it writes nothing when red). rigc records each source image's
|
|
109
|
+
* path relative to `out` (`images[].page` in the model document), so a build is
|
|
110
|
+
* only right where it was built: the run gates in a scratch directory, and the
|
|
111
|
+
* build `--build` keeps is built again, by rigc, where it will live (#18) —
|
|
112
|
+
* never copied out, which would carry a path climbing from the scratch
|
|
113
|
+
* directory to the filesystem root. --copy-images puts the pages beside it.
|
|
114
|
+
*/
|
|
115
|
+
export function buildInto(rig: string, motionPath: string, out: string, profile: string, report?: string): Gate {
|
|
116
|
+
const run = runRigc(['build', '--rig', rig, '--motion', motionPath, '--out', out, '--profile', profile, '--copy-images', ...(report === undefined ? [] : ['--report', report])]);
|
|
108
117
|
const lines = run.out.split('\n');
|
|
109
118
|
return {
|
|
110
119
|
run,
|
|
111
120
|
green: run.status === 0,
|
|
112
121
|
summary: lines.find((l) => /assertions: \d+ measured/.test(l))?.replace(/^[\s.]+/, '') ?? null,
|
|
113
122
|
fails: lines.filter((l) => /^\s*FAIL\s/.test(l) || l.startsWith('rigc compile error')).map((l) => l.trim()),
|
|
114
|
-
build,
|
|
123
|
+
build: out,
|
|
115
124
|
};
|
|
116
125
|
}
|
|
117
126
|
|
package/src/run.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { dirname, join } from 'node:path';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
9
|
import { readMap, resolveMap } from './map.ts';
|
|
10
10
|
import { convertCoco17, personTrack, readPoseSequence } from './pose.ts';
|
|
11
11
|
import { lineOf, Problems, Refusal, type Problem } from './problems.ts';
|
|
12
|
-
import { bendLine, holdLine, retarget, type RetargetOptions } from './retarget.ts';
|
|
13
|
-
import { baseMotionOf, folds, gate, readBuild, resolveRig, restOf } from './rigc.ts';
|
|
12
|
+
import { bendLine, flipLine, holdLine, retarget, type RetargetOptions } from './retarget.ts';
|
|
13
|
+
import { baseMotionOf, buildInto, folds, gate, readBuild, resolveRig, restOf } from './rigc.ts';
|
|
14
14
|
import { landed, measureLanding, measurePosture, poseFrames, postureLines, ANGLE_TOLERANCE, ORIGIN_TOLERANCE } from './verify.ts';
|
|
15
15
|
|
|
16
16
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -78,9 +78,10 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
78
78
|
resolveMap(map, rest, a.map);
|
|
79
79
|
|
|
80
80
|
const r = retarget(rest, map, frames, times, a);
|
|
81
|
-
say(`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}, smooth ${a.smooth}, bends ${a.bends} (tolerance ${a.bendTolerance}°)`);
|
|
81
|
+
say(`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}, smooth ${a.smooth}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, bends ${a.bends} (tolerance ${a.bendTolerance}°)`);
|
|
82
82
|
if (r.scale !== null) say(` SCALE ${r.scale.factor.toFixed(5)} rig units per picture px (rig ${r.scale.rig.toFixed(2)} / pose median ${r.scale.pose.toFixed(2)})`);
|
|
83
83
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
84
|
+
if (a.flip !== 'off') for (const fl of r.flips) say(` FLIP ${flipLine(fl, r.times, a.flip)}`);
|
|
84
85
|
for (const e of r.bends) say(` BEND ${bendLine(e)} — clamped straight`);
|
|
85
86
|
|
|
86
87
|
const motion = { ...base, animations: { ...animations, [a.name]: r.animation } };
|
|
@@ -108,17 +109,21 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
108
109
|
|
|
109
110
|
const dts = r.times.slice(1).map((t, i) => t - r.times[i]!).sort((x, y) => x - y);
|
|
110
111
|
const fps = dts.length === 0 ? 12 : Math.max(1, Math.min(60, Math.round(1 / dts[dts.length >> 1]!)));
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// A rig that draws nothing (bones only) has nothing to fold, and rigc's render refuses it.
|
|
113
|
+
const f = doc.slots.length === 0 ? { run: { args: [], status: 0, out: '' }, meshes: [], frames: '' } : folds(g1.build, a.name, join(scratch, 'folds'), fps);
|
|
114
|
+
if (doc.slots.length === 0) say(' FOLD the rig has no slots, so it draws nothing to fold; not measured');
|
|
115
|
+
else if (f.meshes === null) say(` FOLD rigc render --folds did not read (exit ${f.run.status}); folds not measured`);
|
|
113
116
|
else if (f.meshes.length === 0) say(` FOLD rigc render --folds at ${fps} fps: no mesh folds`);
|
|
114
117
|
else for (const m of f.meshes) say(` FOLD mesh "${m.attachment}" (slot "${m.slot}") folds on ${m.times.length} frame(s) at ${fps} fps: t=${spans(m.times, 1 / fps)}`);
|
|
115
118
|
|
|
116
|
-
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
117
119
|
if (a.build !== undefined) {
|
|
120
|
+
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
118
121
|
rmSync(a.build, { recursive: true, force: true });
|
|
119
|
-
|
|
122
|
+
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile);
|
|
123
|
+
if (!kept.green) refuse([{ code: 'RIG_GATE', text: `rigc build into --build ${a.build} is red where the same build was green in scratch (exit ${kept.run.status}); nothing written` }, ...kept.fails.map((x) => ({ code: 'RIGC', text: x }))]);
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
126
|
+
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
122
127
|
rmSync(a.render, { recursive: true, force: true });
|
|
123
128
|
cpSync(f.frames, a.render, { recursive: true });
|
|
124
129
|
}
|
|
@@ -129,10 +134,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
129
134
|
animation: a.name,
|
|
130
135
|
frames: seq.frames.length,
|
|
131
136
|
duration: r.animation.duration,
|
|
132
|
-
options: { minScore: a.minScore, smooth: a.smooth, bends: a.bends, bendTolerance: a.bendTolerance, profile: a.profile },
|
|
137
|
+
options: { minScore: a.minScore, smooth: a.smooth, flip: a.flip, bends: a.bends, bendTolerance: a.bendTolerance, profile: a.profile },
|
|
133
138
|
scale: r.scale,
|
|
134
139
|
holds: r.holds,
|
|
135
140
|
bends: r.bends,
|
|
141
|
+
flips: r.flips,
|
|
136
142
|
gate: g1.summary,
|
|
137
143
|
landings,
|
|
138
144
|
posture,
|