@rig-c/pose 0.10.0 → 0.11.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 +13 -1
- package/cli.ts +25 -2
- package/package.json +1 -1
- package/src/reduce.ts +361 -0
- package/src/retarget.ts +2 -0
- package/src/run.ts +61 -5
package/README.md
CHANGED
|
@@ -55,9 +55,10 @@ and what the run cannot decide for you it lists:
|
|
|
55
55
|
- `FEET` — when the map has both legs: the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
|
|
56
56
|
- `SHEAR` — with `--leg-scale`: every scaled bone and every bone under one, how far its art is sheared and how long its world axes are against setup. A reading.
|
|
57
57
|
- `IDLE`, `IDLE-DROPPED`, `IDLE-KEPT-OUT`, `MERGE` — the rig's idle against the new animation, and what `--carry-idle` and `--merge` put back ([below](#the-idle-what-the-retarget-drops)).
|
|
58
|
+
- `REDUCE` — with `--reduce` ([below](#few-keys---reduce)): per bone, its keys before and after and its largest error, with the frame; then the total.
|
|
58
59
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
59
60
|
|
|
60
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale` and `plantFeet` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, `legScale`, `pivot`, `feet` (`planted`, `legLength`, `floor`, `supports`, `jumps`, `changes`, and the posed `reading`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), and the demand lists:
|
|
61
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale` and `plantFeet` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, `legScale`, `pivot`, `feet` (`planted`, `legLength`, `floor`, `supports`, `jumps`, `changes`, and the posed `reading`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists:
|
|
61
62
|
|
|
62
63
|
- `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } }, moved: { on, off } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for. `moved.on` is the boundary the span starts on (null from the first frame), `moved.off` the one the base art or the next set comes back on (null to the end), each `{ frame: [selected, keyed], frames, seconds, window: [first, last], speed: { old, new } }`: signed frames and seconds, the frames it could move to, and the speed in degrees per frame on the old and the new frame. A boundary between two sets is the same entry in both.
|
|
63
64
|
- `uncovered: [{ slot, attachment, chains, times }]` — every entry of `folds` on the times its slot still shows that mesh, with the map's chains that move it (the deepest: the arm, not the trunk it hangs from). `rigc render --folds` reads only what the pose shows, so a mesh swapped out or hidden by a switch is not read on those frames at all; `uncovered` is the fold times less the switched ones.
|
|
@@ -135,6 +136,17 @@ On HQ's dancer (`--flip 90 --short 0.35 --bends clamp --carry-idle`) the idle ha
|
|
|
135
136
|
|
|
136
137
|
Each track is a rigc-motion track. One posing a channel the new animation (or a carried idle track, or an earlier fragment track) already keys is refused by name (`MERGE_CONFLICT`): a fragment adds a track, it never replaces one. So is a key before 0 or past the animation's duration (`MERGE_TIME`), and a file of any other shape (`MERGE_SPEC`). What the track means — its target, property and values — is rigc's to judge: its gate checks every merged track like any other (`RIG_GATE`). `MERGE` lists each track merged; the report lists them under `merged`.
|
|
137
138
|
|
|
139
|
+
### Few keys: `--reduce`
|
|
140
|
+
|
|
141
|
+
The retarget writes one key per pose frame on every mapped bone. That is faithful, but nobody can edit it by hand. **`--reduce <deg>`** (off by default) thins each mapped bone's `rotate` track to a few keys with curves, so that every bone's **world** aim stays within `deg` of the dense keys on every pose frame. **`--reduce-hip <units>`** does the same for the hip's `translate`, holding its world origin within `units`; without it, the hip stays dense.
|
|
142
|
+
|
|
143
|
+
- **Keys stay on the pose's own frame times.** Between two kept keys, the span is linear if a straight line is within the tolerance. Otherwise it gets one raw Bézier `curve`, with its handles at the span's thirds and its values fitted by least squares. The error is measured the way rig-c's poser evaluates a curve (its 10-piece polyline). A span still over the tolerance gets a key at its worst frame, and both halves are fitted again.
|
|
144
|
+
- **Errors do not pile up down a chain.** Bones are fitted top-down, and each bone is fitted against its dense key less the error its nearest mapped parent already carries. So a bone's world error is its own fit error.
|
|
145
|
+
- **Kept regardless:** the first and last frame; the frames either side of a switch boundary, on the chains the switch changes; and the first and last frame of every `FLIP` spread, `SHORT` span and `RANGE` run, on that bone.
|
|
146
|
+
- **The landing check holds the dense targets** with the tolerance added (0.01° + `deg`; 0.01 + `units`), and it is posed by rig-c as always.
|
|
147
|
+
- **On HQ's dancer** (`--smooth 7 --flip 90 --short 0.35 --bends clamp`; 313 frames, 11 mapped bones): `--reduce 1 --reduce-hip 1` takes the rotate keys from 3443 to 588 (5.86×), 296 of the spans curved, and the hip's translate keys from 313 to 96. The gate is green. rig-c's poser lands every bone within 0.9998° of the dense target (head) and the hip within 0.901 units. At 0.5° the keys drop 3.92×; at 2°, 8.38×.
|
|
148
|
+
- **Not combined with `--leg-scale` or `--plant-feet`.** Their keys land a bone through a scaled parent or an ankle's lever, which this tolerance does not bound (rig-c/pose#32).
|
|
149
|
+
|
|
138
150
|
Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2` bad invocation. `rig-pose --help` has every flag.
|
|
139
151
|
|
|
140
152
|
## The pose: `rig-pose-sequence/1`
|
package/cli.ts
CHANGED
|
@@ -75,6 +75,15 @@ retarget
|
|
|
75
75
|
"tracks": [ <rigc-motion track> … ] } at absolute times from its start (blinks read
|
|
76
76
|
off the video); a track on a target and property it already keys is refused
|
|
77
77
|
(MERGE_CONFLICT), and so is a key past its duration (MERGE_TIME)
|
|
78
|
+
--reduce <deg> thin each mapped bone's rotate keys to few keys with curves (linear, or one raw
|
|
79
|
+
Bezier per span with handles at its thirds), on the pose's own frame times, so every
|
|
80
|
+
bone's world aim stays within deg of the dense keys on every frame; fitted top-down,
|
|
81
|
+
each bone against its parent's reduced error, so errors do not pile up down a chain.
|
|
82
|
+
Kept regardless: the first and last frame, the frames either side of a switch boundary
|
|
83
|
+
on the chains it changes, and the edges of every FLIP spread, SHORT span and RANGE run
|
|
84
|
+
(REDUCE lines; default: off). Not with --leg-scale or --plant-feet (rig-c/pose#32)
|
|
85
|
+
--reduce-hip <units> with --reduce, thin the hip's translate keys too, its world origin within units
|
|
86
|
+
of the dense keys (default: the hip stays dense)
|
|
78
87
|
--switch-tolerance <d> a set of alternate drawings (the map's "alternates") may show on a frame where
|
|
79
88
|
every segment of its chains is within d degrees of its key skeleton (default: 25)
|
|
80
89
|
--switch-min <s> a run of one set shorter than s seconds is dropped (default: 0.25)
|
|
@@ -87,7 +96,7 @@ retarget
|
|
|
87
96
|
--render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
|
|
88
97
|
--report <file> write the run's measurements (holds, short, endOn, flips, bends, landings, folds,
|
|
89
98
|
switches, uncovered, turns, range, artRange,
|
|
90
|
-
idleDropped, idleKeptOut, merged) as JSON
|
|
99
|
+
idleDropped, idleKeptOut, merged, reduce) as JSON
|
|
91
100
|
|
|
92
101
|
It writes nothing until rigc's gate is green with the animation in, every mapped bone lands on
|
|
93
102
|
the angle the pose asks for (posed by rig-c's own poser), and no hinge bends the wrong way.
|
|
@@ -191,7 +200,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
|
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
194
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge'], ['coco', 'plant-feet', 'carry-idle']);
|
|
203
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip'], ['coco', 'plant-feet', 'carry-idle']);
|
|
195
204
|
const bends = f.get('bends') ?? 'refuse';
|
|
196
205
|
if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
|
|
197
206
|
const switchAt = f.get('switch-at') ?? 'first';
|
|
@@ -228,9 +237,23 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
228
237
|
idle: f.get('idle'),
|
|
229
238
|
carryIdle: f.has('carry-idle'),
|
|
230
239
|
merge: f.get('merge'),
|
|
240
|
+
...reduceOf(f),
|
|
231
241
|
};
|
|
232
242
|
}
|
|
233
243
|
|
|
244
|
+
/** --reduce and --reduce-hip (rig-c/pose#32): off unless given; the hip only with the bones. */
|
|
245
|
+
function reduceOf(f: Map<string, string>): { reduce?: number; reduceHip?: number } {
|
|
246
|
+
if (!f.has('reduce')) {
|
|
247
|
+
if (f.has('reduce-hip')) throw new Usage('--reduce-hip is given without --reduce; the hip is thinned with the bones, not alone');
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
const reduce = num(f, 'reduce', 0, (n) => n > 0 && n < 180, 'degrees in (0, 180)');
|
|
251
|
+
for (const other of ['leg-scale', 'plant-feet'] as const) {
|
|
252
|
+
if (f.has(other) && f.get(other) !== 'off') throw new Usage(`--reduce is given with --${other}; their keys land a bone through a scaled parent or an ankle's lever, which the reduction's tolerance does not bound (rig-c/pose#32 round 1)`);
|
|
253
|
+
}
|
|
254
|
+
return f.has('reduce-hip') ? { reduce, reduceHip: num(f, 'reduce-hip', 0, (n) => n > 0, 'rig units > 0') } : { reduce };
|
|
255
|
+
}
|
|
256
|
+
|
|
234
257
|
function compareArgs(argv: string[]): CompareArgs {
|
|
235
258
|
const f = parseFlags(argv, ['reference', 'render', 'map', 'retarget-report', 'reference-hands', 'render-hands', 'cloth-reference', 'cloth-render', 'cloth-windows', 'report', 'sheet'], []);
|
|
236
259
|
const pair = (x: string, y: string) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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/reduce.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dense keys thinned to few keys with curves, within a stated tolerance
|
|
3
|
+
* (rig-c/pose#32, `--reduce`): one key per pose frame on every mapped bone's
|
|
4
|
+
* `rotate` track and on the hip's `translate`, in; the keys a curve needs to
|
|
5
|
+
* stay within the tolerance on every frame, out.
|
|
6
|
+
*
|
|
7
|
+
* Pure: numbers in, keys out. The method was fixed on the issue before any
|
|
8
|
+
* measurement. Keys sit on the pose's own frame times; between two kept keys a
|
|
9
|
+
* span is linear when every frame inside is within the tolerance, else it
|
|
10
|
+
* carries one raw Bézier `curve` whose handle times sit at the span's thirds and
|
|
11
|
+
* whose handle values are a least-squares fit; a span still over the tolerance
|
|
12
|
+
* takes a key at its worst frame and both halves are fitted again.
|
|
13
|
+
*
|
|
14
|
+
* Top-down along the tree. A child's `rotate` key adds to the turn of the
|
|
15
|
+
* mapped bone above it, so the reduced above's error turns the child's world
|
|
16
|
+
* aim too. A bone is fitted against its dense key minus the error its above
|
|
17
|
+
* carries (`target = dense − e_above`), and its own residual against that
|
|
18
|
+
* target is its world error: errors do not pile up down a chain. A knot's value
|
|
19
|
+
* is the target's, not the dense key's.
|
|
20
|
+
*
|
|
21
|
+
* Every error is read after the numbers are rounded as written and evaluated as
|
|
22
|
+
* rig-c's poser evaluates them (`curveAt`): the curve is the poser's ten-piece
|
|
23
|
+
* polyline, not the cubic. The poser is the judge — run.ts measures the landing
|
|
24
|
+
* of the gated build against the dense targets with the tolerance added.
|
|
25
|
+
*/
|
|
26
|
+
import { f32 } from 'rig-c/compile';
|
|
27
|
+
|
|
28
|
+
/** A key as the reducer writes it: a time, its values, and a raw curve on the segment to the next key. */
|
|
29
|
+
export interface ReducedKey {
|
|
30
|
+
t: number;
|
|
31
|
+
v: number[];
|
|
32
|
+
curve?: number[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReduceBone {
|
|
36
|
+
bone: string;
|
|
37
|
+
/** The nearest mapped bone above it (whose rotate track it adds to), or null. Always one of the other bones given, or null. */
|
|
38
|
+
above: string | null;
|
|
39
|
+
/** The dense key value on every frame (degrees, unwrapped, already rounded to 0.001). */
|
|
40
|
+
dense: number[];
|
|
41
|
+
/** Frame indices that must keep a key regardless (the first and last always do). */
|
|
42
|
+
keep: ReadonlySet<number>;
|
|
43
|
+
}
|
|
44
|
+
export interface ReduceHip {
|
|
45
|
+
bone: string;
|
|
46
|
+
/** Dense translate key on every frame, parent-local units, rounded to 0.001. */
|
|
47
|
+
dense: Array<[number, number]>;
|
|
48
|
+
keep: ReadonlySet<number>;
|
|
49
|
+
/** The hip's parent world matrix [a, b, c, d] at setup (wx = a·lx + b·ly, wy = c·lx + d·ly); null when it has none (identity). The tolerance is a world distance. */
|
|
50
|
+
parent: [number, number, number, number] | null;
|
|
51
|
+
tolerance: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ReduceRow {
|
|
54
|
+
bone: string;
|
|
55
|
+
kind: 'angle' | 'origin';
|
|
56
|
+
/** Dense key count. */
|
|
57
|
+
before: number;
|
|
58
|
+
/** Reduced key count. */
|
|
59
|
+
after: number;
|
|
60
|
+
/** The largest error over all frames: degrees of world aim for a bone, world units for the hip. */
|
|
61
|
+
worst: number;
|
|
62
|
+
/** The first frame where it is. */
|
|
63
|
+
frame: number;
|
|
64
|
+
}
|
|
65
|
+
export interface Reduced {
|
|
66
|
+
rotate: Map<string, ReducedKey[]>;
|
|
67
|
+
hip: ReducedKey[] | null;
|
|
68
|
+
/** The bones in the order given, then the hip. */
|
|
69
|
+
rows: ReduceRow[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// rig-c's evaluation
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** rig-c's `BEZIER_SIXTH` (node_modules/rig-c/src/core/animation.ts). */
|
|
77
|
+
const BEZIER_SIXTH = 0.16666667;
|
|
78
|
+
/** Pieces of the poser's polyline per Bézier segment — rig-c's `BEZIER_PIECES`. */
|
|
79
|
+
export const REDUCE_PIECES = 10;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copied verbatim from rig-c `src/core/animation.ts` (`bezierPolyline`, not
|
|
83
|
+
* exported by `rig-c/core`): the nine interior points of one channel's Bézier
|
|
84
|
+
* segment, `[t1, v1, …, t9, v9]`, each float32, by forward differences. The
|
|
85
|
+
* poser is the judge; this copy only lets the fit see what it will see.
|
|
86
|
+
*/
|
|
87
|
+
export function bezierPolyline(t0: number, v0: number, cx1: number, cy1: number, cx2: number, cy2: number, t3: number, v3: number): number[] {
|
|
88
|
+
const walk = (p0: number, p1: number, p2: number, p3: number): number[] => {
|
|
89
|
+
const second = (p0 - 2 * p1 + p2) * 0.03;
|
|
90
|
+
const third = ((p1 - p2) * 3 - p0 + p3) * 0.006;
|
|
91
|
+
let first = (p1 - p0) * 0.3 + second + third * BEZIER_SIXTH;
|
|
92
|
+
let d2 = second * 2 + third;
|
|
93
|
+
let at = p0;
|
|
94
|
+
const out: number[] = [];
|
|
95
|
+
for (let i = 1; i < REDUCE_PIECES; i++) {
|
|
96
|
+
at += first;
|
|
97
|
+
first += d2;
|
|
98
|
+
d2 += third;
|
|
99
|
+
out.push(Math.fround(at));
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
};
|
|
103
|
+
const ts = walk(t0, cx1, cx2, t3);
|
|
104
|
+
const vs = walk(v0, cy1, cy2, v3);
|
|
105
|
+
const out: number[] = [];
|
|
106
|
+
for (let i = 0; i < ts.length; i++) out.push(ts[i]!, vs[i]!);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const F32 = new Float32Array(1);
|
|
111
|
+
const F32_BITS = new Uint32Array(F32.buffer);
|
|
112
|
+
|
|
113
|
+
/** A key time as the built model states it — rig-c's `keyTime`: its own float32 name, else the float32 at or below it, named. */
|
|
114
|
+
function statedTime(t: number): number {
|
|
115
|
+
const named = f32(t);
|
|
116
|
+
if (named === t) return named;
|
|
117
|
+
let f = Math.fround(t);
|
|
118
|
+
if (f > t) {
|
|
119
|
+
F32[0] = f;
|
|
120
|
+
if (f > 0) F32_BITS[0]! -= 1;
|
|
121
|
+
else if (f < 0) F32_BITS[0]! += 1;
|
|
122
|
+
else F32_BITS[0] = 0x80000001;
|
|
123
|
+
f = F32[0]!;
|
|
124
|
+
}
|
|
125
|
+
return f32(f);
|
|
126
|
+
}
|
|
127
|
+
/** A key time as the poser compares it: the float32 of the stated time. */
|
|
128
|
+
const storedTime = (t: number): number => Math.fround(statedTime(t));
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One channel of one segment, as the poser holds it: null for a linear
|
|
132
|
+
* segment, else the polyline `[t0, v0, …nine points…, t3, v3]`. The recurrence
|
|
133
|
+
* runs on the numbers the built model states (rig-c's `f32` of each, a key
|
|
134
|
+
* time by `keyTime`); the two ends are the keys as stored, float32.
|
|
135
|
+
*/
|
|
136
|
+
function segmentPoints(a: ReducedKey, b: ReducedKey, channel: number): number[] | null {
|
|
137
|
+
if (a.curve === undefined) return null;
|
|
138
|
+
const c = a.curve.slice(channel * 4, channel * 4 + 4).map(f32);
|
|
139
|
+
const ta = statedTime(a.t);
|
|
140
|
+
const tb = statedTime(b.t);
|
|
141
|
+
const va = f32(a.v[channel]!);
|
|
142
|
+
const vb = f32(b.v[channel]!);
|
|
143
|
+
const inner = bezierPolyline(ta, va, c[0]!, c[1]!, c[2]!, c[3]!, tb, vb);
|
|
144
|
+
return [Math.fround(ta), Math.fround(va), ...inner, Math.fround(tb), Math.fround(vb)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `segmentPoints`' segment at `t` — rig-c's `channelAt` past the key search. */
|
|
148
|
+
function pointsAt(a: ReducedKey, b: ReducedKey, channel: number, points: readonly number[] | null, t: number): number {
|
|
149
|
+
if (points === null) {
|
|
150
|
+
const ta = storedTime(a.t);
|
|
151
|
+
const va = Math.fround(a.v[channel]!);
|
|
152
|
+
return va + ((t - ta) / (storedTime(b.t) - ta)) * (Math.fround(b.v[channel]!) - va);
|
|
153
|
+
}
|
|
154
|
+
let i = 2;
|
|
155
|
+
while (i < points.length - 2 && points[i]! < t) i += 2;
|
|
156
|
+
const x0 = points[i - 2]!;
|
|
157
|
+
const y0 = points[i - 1]!;
|
|
158
|
+
return y0 + ((t - x0) / (points[i]! - x0)) * (points[i + 1]! - y0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Channel `channel` of the segment from key `a` to key `b` (`a.curve` shapes it) at `t`, as rig-c's poser evaluates it. */
|
|
162
|
+
export function curveAt(a: ReducedKey, b: ReducedKey, channel: number, t: number): number {
|
|
163
|
+
return pointsAt(a, b, channel, segmentPoints(a, b, channel), t);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every channel of a whole track at each sample time: the key search (last key stored at or before `t`), then the segment. */
|
|
167
|
+
function trackAt(keys: readonly ReducedKey[], samples: readonly number[], channels: number): number[][] {
|
|
168
|
+
const stored = keys.map((k) => storedTime(k.t));
|
|
169
|
+
const points = keys.map((k, j) => (j + 1 < keys.length ? Array.from({ length: channels }, (_, c) => segmentPoints(k, keys[j + 1]!, c)) : []));
|
|
170
|
+
let j = 0;
|
|
171
|
+
return samples.map((t) => {
|
|
172
|
+
while (j + 1 < keys.length && stored[j + 1]! <= t) j++;
|
|
173
|
+
const a = keys[j]!;
|
|
174
|
+
const b = keys[j + 1];
|
|
175
|
+
// Before the first key the track does not pose; the first key is on frame 0, stored at or below it.
|
|
176
|
+
if (b === undefined || t < stored[j]!) return a.v.map((v) => Math.fround(v));
|
|
177
|
+
return Array.from({ length: channels }, (_, c) => pointsAt(a, b, c, points[j]![c]!, t));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// the fit
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const r3 = (v: number): number => {
|
|
186
|
+
const r = Math.round(v * 1000) / 1000;
|
|
187
|
+
return Object.is(r, -0) ? 0 : r;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** 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
|
+
interface Fit {
|
|
192
|
+
targets: readonly (readonly number[])[];
|
|
193
|
+
keep: ReadonlySet<number>;
|
|
194
|
+
tolerance: number;
|
|
195
|
+
/** The error at frame `i` of `values` against the target. */
|
|
196
|
+
error: (i: number, values: readonly number[]) => number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The frame with the largest error over `frames` (the first, on a tie), or null when there is none. */
|
|
200
|
+
function worstOf(frames: readonly number[], errorAt: (i: number) => number): { worst: number; frame: number } | null {
|
|
201
|
+
let best: { worst: number; frame: number } | null = null;
|
|
202
|
+
for (const i of frames) {
|
|
203
|
+
const e = errorAt(i);
|
|
204
|
+
if (best === null || e > best.worst) best = { worst: e, frame: i };
|
|
205
|
+
}
|
|
206
|
+
return best;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The least-squares handle values of one channel, or null when the normal equations are singular. */
|
|
210
|
+
function solveHandles(a: ReducedKey, b: ReducedKey, channel: number, hx1: number, hx2: number, interior: readonly number[], samples: readonly number[], target: (i: number) => number): [number, number] | null {
|
|
211
|
+
const model = (cy1: number, cy2: number): number[] => {
|
|
212
|
+
const c = new Array<number>(a.v.length * 4).fill(0);
|
|
213
|
+
c.splice(channel * 4, 4, hx1, cy1, hx2, cy2);
|
|
214
|
+
const k: ReducedKey = { t: a.t, v: a.v, curve: c };
|
|
215
|
+
const points = segmentPoints(k, b, channel);
|
|
216
|
+
return interior.map((i) => pointsAt(k, b, channel, points, samples[i]!));
|
|
217
|
+
};
|
|
218
|
+
const f00 = model(0, 0);
|
|
219
|
+
const f10 = model(1, 0);
|
|
220
|
+
const f01 = model(0, 1);
|
|
221
|
+
let s11 = 0;
|
|
222
|
+
let s12 = 0;
|
|
223
|
+
let s22 = 0;
|
|
224
|
+
let r1 = 0;
|
|
225
|
+
let r2 = 0;
|
|
226
|
+
interior.forEach((i, j) => {
|
|
227
|
+
const g1 = f10[j]! - f00[j]!;
|
|
228
|
+
const g2 = f01[j]! - f00[j]!;
|
|
229
|
+
const r = target(i) - f00[j]!;
|
|
230
|
+
s11 += g1 * g1;
|
|
231
|
+
s12 += g1 * g2;
|
|
232
|
+
s22 += g2 * g2;
|
|
233
|
+
r1 += g1 * r;
|
|
234
|
+
r2 += g2 * r;
|
|
235
|
+
});
|
|
236
|
+
const det = s11 * s22 - s12 * s12;
|
|
237
|
+
if (!(s11 > 0 && s22 > 0) || !(det > 1e-12 * s11 * s22)) return null;
|
|
238
|
+
return [(r1 * s22 - r2 * s12) / det, (r2 * s11 - r1 * s12) / det];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A track's keys: the kept frames, then a knot at the worst frame of every span over the tolerance, until none is. */
|
|
242
|
+
function fitTrack(times: readonly number[], samples: readonly number[], fit: Fit): ReducedKey[] {
|
|
243
|
+
const n = times.length;
|
|
244
|
+
const channels = fit.targets[0]!.length;
|
|
245
|
+
const keyOn = (i: number): ReducedKey => ({ t: times[i]!, v: fit.targets[i]!.map(r3) });
|
|
246
|
+
const knots = [...new Set([0, n - 1, ...fit.keep])].sort((x, y) => x - y);
|
|
247
|
+
const curves = new Map<number, number[]>();
|
|
248
|
+
const done: number[] = [];
|
|
249
|
+
const spans: Array<[number, number]> = [];
|
|
250
|
+
for (let j = knots.length - 1; j > 0; j--) spans.push([knots[j - 1]!, knots[j]!]);
|
|
251
|
+
done.push(...knots);
|
|
252
|
+
while (spans.length > 0) {
|
|
253
|
+
const [k0, k1] = spans.pop()!;
|
|
254
|
+
if (k1 - k0 <= 1) continue;
|
|
255
|
+
const a = keyOn(k0);
|
|
256
|
+
const b = keyOn(k1);
|
|
257
|
+
const interior = Array.from({ length: k1 - k0 - 1 }, (_, j) => k0 + 1 + j);
|
|
258
|
+
const errorWith = (k: ReducedKey) => {
|
|
259
|
+
const points = Array.from({ length: channels }, (_, c) => segmentPoints(k, b, c));
|
|
260
|
+
return worstOf(interior, (i) => fit.error(i, points.map((p, c) => pointsAt(k, b, c, p, samples[i]!))))!;
|
|
261
|
+
};
|
|
262
|
+
const linear = errorWith(a);
|
|
263
|
+
if (linear.worst <= fit.tolerance) continue;
|
|
264
|
+
let split = linear.frame;
|
|
265
|
+
const hx1 = times[k0]! + (times[k1]! - times[k0]!) / 3;
|
|
266
|
+
const hx2 = times[k0]! + (2 * (times[k1]! - times[k0]!)) / 3;
|
|
267
|
+
const handles = Array.from({ length: channels }, (_, c) => solveHandles(a, b, c, hx1, hx2, interior, samples, (i) => fit.targets[i]![c]!));
|
|
268
|
+
if (handles.every((h) => h !== null)) {
|
|
269
|
+
const curve = handles.flatMap(([cy1, cy2]) => [hx1, r3(cy1), hx2, r3(cy2)]);
|
|
270
|
+
const bent = errorWith({ ...a, curve });
|
|
271
|
+
if (bent.worst <= fit.tolerance) {
|
|
272
|
+
curves.set(k0, curve);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
split = bent.frame;
|
|
276
|
+
}
|
|
277
|
+
done.push(split);
|
|
278
|
+
spans.push([split, k1], [k0, split]);
|
|
279
|
+
}
|
|
280
|
+
return [...new Set(done)].sort((x, y) => x - y).map((i) => {
|
|
281
|
+
const k = keyOn(i);
|
|
282
|
+
const curve = curves.get(i);
|
|
283
|
+
return curve === undefined ? k : { ...k, curve };
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The frame indices `keep` names, each checked against the frame count. */
|
|
288
|
+
function keptFrames(keep: ReadonlySet<number>, n: number, label: string): Set<number> {
|
|
289
|
+
for (const i of keep) {
|
|
290
|
+
if (!Number.isInteger(i) || i < 0 || i >= n) throw new Error(`reduce: ${label} keeps frame ${i}, outside the ${n} frame(s) 0–${n - 1}`);
|
|
291
|
+
}
|
|
292
|
+
return new Set(keep);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Every bone's rotate track (and the hip's translate) reduced within the
|
|
297
|
+
* tolerance, top-down along `above`; the rows say each track's key counts and
|
|
298
|
+
* its worst world error, measured on the reduced keys as the poser evaluates
|
|
299
|
+
* them.
|
|
300
|
+
*/
|
|
301
|
+
export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[], tolerance: number, hip: ReduceHip | null): Reduced {
|
|
302
|
+
const n = times.length;
|
|
303
|
+
if (n === 0) throw new Error('reduce: no frames to reduce');
|
|
304
|
+
if (!(Number.isFinite(tolerance) && tolerance > 0)) throw new Error(`reduce: the tolerance is ${tolerance}°, not a positive number`);
|
|
305
|
+
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
|
+
// The poser samples at the running sum of the frame steps (verify.ts `poseFrames`).
|
|
307
|
+
const samples: number[] = [times[0]!];
|
|
308
|
+
for (let i = 1; i < n; i++) samples.push(samples[i - 1]! + (times[i]! - times[i - 1]!));
|
|
309
|
+
|
|
310
|
+
const byName = new Map<string, ReduceBone>();
|
|
311
|
+
for (const b of bones) {
|
|
312
|
+
if (byName.has(b.bone)) throw new Error(`reduce: bone "${b.bone}" is given twice`);
|
|
313
|
+
if (b.dense.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${b.dense.length} dense key(s) for ${n} frame(s)`);
|
|
314
|
+
byName.set(b.bone, b);
|
|
315
|
+
}
|
|
316
|
+
for (const b of bones) {
|
|
317
|
+
if (b.above !== null && !byName.has(b.above)) throw new Error(`reduce: bone "${b.bone}" adds to bone "${b.above}", which is not among the bones reduced`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Top-down: a bone once its above is done, in the order given otherwise.
|
|
321
|
+
const errors = new Map<string, number[]>();
|
|
322
|
+
const rotate = new Map<string, ReducedKey[]>();
|
|
323
|
+
const rowOf = new Map<string, ReduceRow>();
|
|
324
|
+
const pending = [...bones];
|
|
325
|
+
while (pending.length > 0) {
|
|
326
|
+
const at = pending.findIndex((b) => b.above === null || errors.has(b.above));
|
|
327
|
+
if (at < 0) throw new Error(`reduce: bones ${pending.map((b) => `"${b.bone}"`).join(', ')} each add to another of them — a loop, not a tree`);
|
|
328
|
+
const b = pending.splice(at, 1)[0]!;
|
|
329
|
+
const eAbove = b.above === null ? null : errors.get(b.above)!;
|
|
330
|
+
const targets = b.dense.map((d, i) => [eAbove === null ? d : d - eAbove[i]!]);
|
|
331
|
+
const keys = fitTrack(times, samples, {
|
|
332
|
+
targets,
|
|
333
|
+
keep: keptFrames(b.keep, n, `bone "${b.bone}"`),
|
|
334
|
+
tolerance,
|
|
335
|
+
error: (i, v) => Math.abs(v[0]! - targets[i]![0]!),
|
|
336
|
+
});
|
|
337
|
+
const e = trackAt(keys, samples, 1).map((v, i) => v[0]! - targets[i]![0]!);
|
|
338
|
+
errors.set(b.bone, e);
|
|
339
|
+
rotate.set(b.bone, keys);
|
|
340
|
+
const worst = worstOf(e.map((_, i) => i), (i) => Math.abs(e[i]!))!;
|
|
341
|
+
rowOf.set(b.bone, { bone: b.bone, kind: 'angle', before: n, after: keys.length, worst: worst.worst, frame: worst.frame });
|
|
342
|
+
}
|
|
343
|
+
const rows = bones.map((b) => rowOf.get(b.bone)!);
|
|
344
|
+
const ordered = new Map(bones.map((b) => [b.bone, rotate.get(b.bone)!]));
|
|
345
|
+
|
|
346
|
+
let hipKeys: ReducedKey[] | null = null;
|
|
347
|
+
if (hip !== null) {
|
|
348
|
+
if (hip.dense.length !== n) throw new Error(`reduce: hip "${hip.bone}" has ${hip.dense.length} dense key(s) for ${n} frame(s)`);
|
|
349
|
+
const [ma, mb, mc, md] = hip.parent ?? [1, 0, 0, 1];
|
|
350
|
+
const world = (i: number, v: readonly number[]): number => {
|
|
351
|
+
const dx = v[0]! - hip.dense[i]![0];
|
|
352
|
+
const dy = v[1]! - hip.dense[i]![1];
|
|
353
|
+
return Math.hypot(ma * dx + mb * dy, mc * dx + md * dy);
|
|
354
|
+
};
|
|
355
|
+
hipKeys = fitTrack(times, samples, { targets: hip.dense, keep: keptFrames(hip.keep, n, `hip "${hip.bone}"`), tolerance: hip.tolerance, error: world });
|
|
356
|
+
const at = trackAt(hipKeys, samples, 2);
|
|
357
|
+
const worst = worstOf(at.map((_, i) => i), (i) => world(i, at[i]!))!;
|
|
358
|
+
rows.push({ bone: hip.bone, kind: 'origin', before: n, after: hipKeys.length, worst: worst.worst, frame: worst.frame });
|
|
359
|
+
}
|
|
360
|
+
return { rotate: ordered, hip: hipKeys, rows };
|
|
361
|
+
}
|
package/src/retarget.ts
CHANGED
|
@@ -173,6 +173,8 @@ export interface MotionKey {
|
|
|
173
173
|
v: number[];
|
|
174
174
|
/** `stepped`: the value holds until the next key (the offsets' translate keys). */
|
|
175
175
|
ease?: 'stepped';
|
|
176
|
+
/** A raw Bézier to the next key: four absolute (time, value) numbers per channel (`--reduce`, #32). */
|
|
177
|
+
curve?: number[];
|
|
176
178
|
}
|
|
177
179
|
export interface MotionTrack {
|
|
178
180
|
bone: string;
|
package/src/run.ts
CHANGED
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
|
-
import { readMap, resolveMap, type JointMap } from './map.ts';
|
|
10
|
+
import { readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
11
|
import { convertCoco17, personTrack, readPoseSequence } from './pose.ts';
|
|
12
12
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
13
13
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
14
|
-
import { bendLine, endOnLine, flipLine, holdLine, rangeLine, retarget, shortLine, turnLine, type RetargetOptions } from './retarget.ts';
|
|
14
|
+
import { bendLine, endOnLine, flipLine, holdLine, rangeLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions } from './retarget.ts';
|
|
15
15
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, slotsOf } from './rigc.ts';
|
|
16
16
|
import { artRangeLine, chainsMoving, switchLine, uncoveredFolds } from './switch.ts';
|
|
17
17
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
18
18
|
import { legsOf } from './legs.ts';
|
|
19
|
+
import { reduceKeys, type ReduceBone, type Reduced } from './reduce.ts';
|
|
19
20
|
import type { LegScale } from './legs.ts';
|
|
20
21
|
|
|
21
22
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -42,6 +43,10 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
42
43
|
carryIdle?: boolean;
|
|
43
44
|
/** A rig-pose-tracks/1 fragment merged into the new animation. */
|
|
44
45
|
merge?: string;
|
|
46
|
+
/** Thin the mapped bones' rotate keys within this many degrees (#32). */
|
|
47
|
+
reduce?: number;
|
|
48
|
+
/** With reduce: thin the hip's translate keys within this many rig units. */
|
|
49
|
+
reduceHip?: number;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export interface RunResult {
|
|
@@ -99,7 +104,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
99
104
|
resolveMap(map, rest, a.map, slots);
|
|
100
105
|
|
|
101
106
|
const r = retarget(rest, map, frames, times, a, slots);
|
|
102
|
-
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}°`}, 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.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}`);
|
|
107
|
+
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}°`}, 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.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
103
108
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
104
109
|
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)})`);
|
|
105
110
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
@@ -129,6 +134,14 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
129
134
|
say(` FEET steps (the supporting ankle > 0.03 leg lengths a frame; re-planted after 2 frames < 0.01): ${f.steps.map((x) => `${x.side} ${span(x.frames)} → ${x.to === null ? 'the pose\'s travel' : `${x.to} supports`}, ${x.replant === null ? 'never re-planted' : `re-planted at ${x.replant}`}`).join('; ') || 'none'}`);
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
138
|
+
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip);
|
|
139
|
+
if (reduced !== null) {
|
|
140
|
+
for (const row of reduced.rows) say(` REDUCE ${row.kind === 'angle' ? 'bone' : 'hip'} "${row.bone}" ${row.kind === 'angle' ? 'rotate' : 'translate'}: ${row.before} → ${row.after} key(s); worst ${row.kind === 'angle' ? 'aim' : 'origin'} error ${row.worst.toPrecision(3)}${row.kind === 'angle' ? '°' : ' units'} at frame ${row.frame} (t=${r.times[row.frame]!.toFixed(3)}s)`);
|
|
141
|
+
const sum = (k: 'before' | 'after') => reduced.rows.filter((x) => x.kind === 'angle').reduce((t, x) => t + x[k], 0);
|
|
142
|
+
say(` REDUCE rotate keys ${sum('before')} → ${sum('after')} (${(sum('before') / sum('after')).toFixed(2)}×) within ${a.reduce}°${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
// The idle against the new animation (#43 item 2): what it drops, and with --carry-idle, looped back in.
|
|
133
146
|
const groups = (isObject(base.groups) ? base.groups : {}) as Record<string, string[]>;
|
|
134
147
|
const easings = (isObject(base.easings) ? base.easings : {}) as Record<string, number[]>;
|
|
@@ -179,7 +192,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
179
192
|
|
|
180
193
|
const doc = readBuild(g1.build);
|
|
181
194
|
const posed = poseFrames(doc, a.name, r.times);
|
|
182
|
-
|
|
195
|
+
// A reduced track is held to the dense target plus its tolerance (#32).
|
|
196
|
+
const landings = measureLanding(doc, a.name, map, r, posed).map((l) => (l.kind === 'angle' && a.reduce !== undefined ? { ...l, tolerance: l.tolerance + a.reduce } : l.kind === 'origin' && a.reduceHip !== undefined ? { ...l, tolerance: l.tolerance + a.reduceHip } : l));
|
|
183
197
|
const p = new Problems();
|
|
184
198
|
// A carried idle track on a bone above a mapped one moves it where the keys did not ask (the key formula holds unmapped bones at setup).
|
|
185
199
|
const carriedAbove = (bone: string): string => {
|
|
@@ -249,7 +263,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
249
263
|
animation: a.name,
|
|
250
264
|
frames: seq.frames.length,
|
|
251
265
|
duration: r.animation.duration,
|
|
252
|
-
options: { minScore: a.minScore, smooth: a.smooth, 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, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null },
|
|
266
|
+
options: { minScore: a.minScore, smooth: a.smooth, 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, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off' },
|
|
253
267
|
scale: r.scale,
|
|
254
268
|
holds: r.holds,
|
|
255
269
|
short: r.short,
|
|
@@ -272,6 +286,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
272
286
|
idleDropped: idle === null ? null : idle.dropped,
|
|
273
287
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
274
288
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
289
|
+
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows },
|
|
275
290
|
}, null, 2)}\n`);
|
|
276
291
|
}
|
|
277
292
|
say(` wrote ${a.out} — animations: ${Object.keys(motion.animations).join(', ')}`);
|
|
@@ -281,6 +296,47 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
281
296
|
}
|
|
282
297
|
}
|
|
283
298
|
|
|
299
|
+
/**
|
|
300
|
+
* The retarget's rotate tracks (and with a hip tolerance, its translate) thinned
|
|
301
|
+
* in place (#32). Kept regardless, per bone: the frames either side of each
|
|
302
|
+
* switch boundary on the chains it changes, and the edges of every FLIP spread,
|
|
303
|
+
* SHORT span and RANGE run on it (the first and last frame the reducer keeps).
|
|
304
|
+
*/
|
|
305
|
+
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined): Reduced {
|
|
306
|
+
const n = r.times.length;
|
|
307
|
+
const keep = new Map<string, Set<number>>();
|
|
308
|
+
const add = (bone: string, ...frames: number[]) => {
|
|
309
|
+
const s = keep.get(bone) ?? new Set<number>();
|
|
310
|
+
for (const f of frames) if (f >= 0 && f < n) s.add(f);
|
|
311
|
+
keep.set(bone, s);
|
|
312
|
+
};
|
|
313
|
+
for (const sw of r.switches) {
|
|
314
|
+
for (const c of map.chains.filter((x) => sw.chains.includes(x.name))) for (const b of c.bones) add(b, sw.frames[0] - 1, sw.frames[0], sw.frames[1], sw.frames[1] + 1);
|
|
315
|
+
}
|
|
316
|
+
for (const x of [...r.flips, ...r.short, ...r.range]) add(x.bone, x.frames[0], x.frames[1]);
|
|
317
|
+
const mapped = new Set(r.targets.keys());
|
|
318
|
+
const above = (b: string): string | null => {
|
|
319
|
+
for (let p = rest.get(b)!.parent; p !== null; p = rest.get(p)!.parent) if (mapped.has(p)) return p;
|
|
320
|
+
return null;
|
|
321
|
+
};
|
|
322
|
+
const rotate = new Map<string, MotionTrack>();
|
|
323
|
+
for (const tr of r.animation.tracks) if ('bone' in tr && tr.property === 'rotate') rotate.set(tr.bone, tr);
|
|
324
|
+
const bones: ReduceBone[] = [...rotate.values()].map((tr) => ({ bone: tr.bone, above: above(tr.bone), dense: tr.keys.map((k) => k.v[0]!), keep: keep.get(tr.bone) ?? new Set<number>() }));
|
|
325
|
+
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));
|
|
326
|
+
const parent = hipTrack === undefined ? null : rest.get(hipTrack.bone)!.parent;
|
|
327
|
+
const pm = parent === null ? null : rest.get(parent)!;
|
|
328
|
+
const out = reduceKeys(r.times, bones, tolerance, hipTrack === undefined ? null : {
|
|
329
|
+
bone: hipTrack.bone,
|
|
330
|
+
dense: hipTrack.keys.map((k) => [k.v[0]!, k.v[1]!]),
|
|
331
|
+
keep: new Set<number>(),
|
|
332
|
+
parent: pm === null ? null : [pm.a, pm.b, pm.c, pm.d],
|
|
333
|
+
tolerance: hipTolerance!,
|
|
334
|
+
});
|
|
335
|
+
for (const [bone, keys] of out.rotate) rotate.get(bone)!.keys = keys;
|
|
336
|
+
if (hipTrack !== undefined && out.hip !== null) hipTrack.keys = out.hip;
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
284
340
|
function spans(ts: number[], step: number): string {
|
|
285
341
|
const out: string[] = [];
|
|
286
342
|
let s = ts[0]!;
|