@rig-c/pose 0.29.1 → 0.30.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 +43 -40
- package/package.json +1 -1
- package/src/legs.ts +8 -1
- package/src/report.ts +88 -0
- package/src/retarget.ts +190 -85
- package/src/run.ts +64 -46
package/README.md
CHANGED
|
@@ -40,13 +40,13 @@ The steps, in this order, each printed with its figures:
|
|
|
40
40
|
|
|
41
41
|
A **median** is taken per coordinate (x and y apart), over the window's frames on which the joint has a position, its own frame included, whatever its state. An even count takes the mean of the middle two. **`--loop`** says the sequence is one cycle, such as a gait from strike to strike. Every window then wraps, the last frame running into the first, so nothing is untested; a cycle shorter than its widest window is refused (`CLEAN_LOOP`). **Nothing is invented.** A joint with no position on a frame gets none: `retarget` holds it and prints a `HOLD` line. States and scores are written as they were read. `--coco` reads COCO-17 (below) and converts it first. `--person` picks the person when there is more than one, and only that person is written.
|
|
42
42
|
|
|
43
|
-
The output carries the record, here
|
|
43
|
+
The output carries the record, here `dance-a`'s from the table below:
|
|
44
44
|
|
|
45
45
|
```json
|
|
46
46
|
"cleaned": { "spec": "rig-pose-clean/1",
|
|
47
47
|
"params": { "loop": false, "legs": 5, "rest": 7 },
|
|
48
48
|
"spikes": { "found": [], "untested": [0, 1, 311, 312] },
|
|
49
|
-
"jitter": { "before": 7.
|
|
49
|
+
"jitter": { "before": 7.91, "after": 4.62 } }
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`jitter` is the mean |second difference| of y, in px/f², over every joint and every three consecutive frames that have a position, wrapping with `--loop`. It is measured on the sequence as read and on the cleaned positions. `retarget` prints the record as its `CLEAN` line and copies it into `--report`.
|
|
@@ -89,17 +89,17 @@ Both hips are marked `inferred` on every frame and turned about their midpoint t
|
|
|
89
89
|
|
|
90
90
|
`tools/pelvis_selftest.py` is its synthetic check. A planted wrist on a hand in front of the waist reads "covered", and with the hand moved away the edge's angle is read again. The misread guard, the interpolation, the detector-gain scaling on long covered spans (`#106`, with `--hold-covered` as its planted mutant) and the `unmeasured` path (`r < 0.9`) are checked frame by frame. Its cover mutant (`--no-cover-check`) must read a line through the hand, and the check asserts that it does.
|
|
91
91
|
|
|
92
|
-
**Measured** on
|
|
92
|
+
**Measured** on the reference fixtures ([#67](https://github.com/rig-c/pose/issues/67)'s bars):
|
|
93
93
|
|
|
94
|
-
| Clip | Run | Jitter
|
|
94
|
+
| Clip | Run | Jitter reduction | Against |
|
|
95
95
|
| --- | --- | --- | --- |
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
96
|
+
| `dance-a`, 13 s non-loop | defaults | −42 % (7.91 → 4.62 px/f²) | hand-cleaned reference: 4.62; positions within 0.071 px |
|
|
97
|
+
| `walk-a`, one gait cycle | `--loop` | −66 % | hand-cleaned reference (within 0.001 px/f² outside the arms) |
|
|
98
|
+
| `run-a`, one gait cycle | `--loop` | −53 % | hand-cleaned reference |
|
|
99
|
+
| `walk-b`, 90 frames, held out | defaults | −67 % | the bar: −30 % |
|
|
100
|
+
| `run-b`, 90 frames, held out | defaults | −75 % | the bar: −30 % |
|
|
101
101
|
|
|
102
|
-
A retarget of the cleaned
|
|
102
|
+
A retarget of the cleaned `dance-a` fixture finds the same jumps (`FEET`) as the same run on the hand-cleaned reference; its hip rotate sd over frames 94–175 is 4.574, against 4.58.
|
|
103
103
|
|
|
104
104
|
## What it does
|
|
105
105
|
|
|
@@ -147,7 +147,10 @@ and what the run cannot decide for you it lists:
|
|
|
147
147
|
- `BODY` — per-frame body yaw read from the camera-normalised shoulder width (`r_shoulder + l_shoulder`) and hip width (`r_hip + l_hip`) projected perpendicular to the trunk against the sequence's standing width (`yaw ≈ acos(w / w_standing)`, [#173](https://github.com/rig-c/pose/issues/173)): its overall and opening (`frames 0–47`) range, every span with `|yaw| ≥ 45°`, and, when `body.yaw` or `body.sets` is declared in the map, its knee-mapped dial keys and every yaw-switched set's entered spans, swap count, and any `hides` slot keyed by another track, written as `body` in `--report`.
|
|
148
148
|
- `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.
|
|
149
149
|
|
|
150
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `frames`, `duration`, `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `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. The fields a run is graded by:
|
|
150
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `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. The fields a run is graded by:
|
|
151
|
+
|
|
152
|
+
Report event objects with a pose `frame` or inclusive `frames` also carry exact `t` or `times` from `frameTimes`; existing `seconds` remain span lengths, with additive `spanSeconds`. Holds preserve their frame-index `from`/`to`. Existing scan joint/chain identities remain beside map-derived `bone` (a unique mapped target) or `bones` (all actual mapped targets); no joint-name heuristic chooses a target. `feet.jumps` stays an array of original frame pairs; `feet.jumpEvents` adds matching `{ frames, times }` objects. Fold and uncovered rows preserve their discrete `times` and gaps, declare `domain: "render"`, and use the separate root `render` domain, never fabricated nearest pose-frame indices. The run hashes map/pose from the exact buffers passed to their readers, snapshots the resolved rig file before its gate, and rechecks all three before user-output writes. The writer validates coordinates before output writes and never changes motion keys. The switch diagnostic `moved.frame` is a selected-to-keyed pair with signed `frames`/`seconds` displacement; it is preserved as diagnostic data, not reinterpreted as an inclusive interval. An explicitly supplied uniform writer clock requires both positive `fps` and seconds offset `frame0`; canonical pose input does not yet declare FPS, so the retarget producer always writes exact `frameTimes`.
|
|
153
|
+
|
|
151
154
|
|
|
152
155
|
| field | each entry |
|
|
153
156
|
| --- | --- |
|
|
@@ -172,49 +175,49 @@ Runs in `turns`, `range` and `artRange` merge across a gap of one frame, as `end
|
|
|
172
175
|
|
|
173
176
|
`--smooth <n>` is an alias kept since [#67](https://github.com/rig-c/pose/issues/67). The smoothing is `clean`'s (`--legs`, `--rest`), so `retarget` prints `--smooth is carried by clean; ignored` and smooths nothing: `--smooth 7` writes the same bytes as `--smooth 1`. The three paragraphs below were measured before #67, when `retarget` smoothed with a cubic Savitzky–Golay over `--smooth` frames. They are kept as the record of why the smoothing moved to the sequence.
|
|
174
177
|
|
|
175
|
-
**`--smooth 7` flattens the pelvis's rebound.** Measured on
|
|
178
|
+
**`--smooth 7` flattens the pelvis's rebound.** Measured on `dance-a` with planted feet and leg scaling ([#47](https://github.com/rig-c/pose/issues/47)): the rebound — the hip's peak upward acceleration at the bottom of each bounce, rig ÷ reference, median over a window, bar ≥ 0.7 — reads 0.40–0.52 with `--smooth 7` in every configuration measured (0.461 and 0.501 in the two windows, where `--smooth 1` reads 0.648 and 0.942). So the pelvis is read at `--smooth 1`, and `--smooth 7` is not advised for it. What follows is the earlier advice, for the limbs.
|
|
176
179
|
|
|
177
|
-
**`--smooth 7` at 24 fps, for the limbs.** 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.
|
|
180
|
+
**`--smooth 7` at 24 fps, for the limbs.** It was measured on a 13 s dance (`dance-a`) 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.
|
|
178
181
|
|
|
179
182
|
**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 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`.
|
|
180
183
|
|
|
181
|
-
The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/issues/15)). On
|
|
184
|
+
The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/issues/15)). On `dance-a` (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.
|
|
182
185
|
|
|
183
|
-
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.
|
|
186
|
+
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. With a 90° flip threshold on `dance-a`, no bone of the gated build turns more than 90° in one frame (without it, 148.6°), and every bone still lands.
|
|
184
187
|
|
|
185
188
|
**The short hold.** A segment pointing at the camera is short in the picture, and its picture direction is then detector noise: the rig turns the bone to follow it. So after holds, and before the flip guard, every mapped segment is measured on every frame (not a `reach`) — its picture length, between the same two points its angle is read from, over its own median length across the pose. On a frame under `--short <fraction>` its angle is not read from the pose: it is interpolated, the short way round, from the last frame at or above the cutoff to the next one, and held flat at the first or last frame of the pose. A horizontal line across the body from a left joint to its right twin (`a>b`, such as the hip line or the eye line) also holds its last confident angle and interpolates across the span whenever its picture length falls under `--short` or its 2D reading reverses sign within one frame past `--flip` (when the left and right joints cross in 2D during a full side turn, [#184](https://github.com/rig-c/pose/issues/184)); its hold is printed as `HOLD chain "..."` and is not counted toward limb `END-ON` runs. The flip guard, the hinge check and the keys all see the held angle. `--short off` (the default) reads every frame unless an across-body line reverses past `--flip`. The report lists each span under `short` (chain, bone, segment, first and last frame, the lowest fraction, and `across` / `reversed` when across-body).
|
|
186
189
|
|
|
187
|
-
The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/issues/22)), over both upper arms, forearms, thighs and shins. On
|
|
190
|
+
The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/issues/22)), over both upper arms, forearms, thighs and shins. On `dance-a` (24 fps, 2,504 segment-frames) the fraction's 0.5th percentile is 0.51, its 5th 0.82, and 7 segment-frames (0.28%) are under 0.35; on `dance-b` (a 16 s sequence at 30 fps, 3,936 segment-frames) they are 0.27 and 0.75, and 34 (0.86%). Every frame under 0.35 was looked at, and was one of two kinds:
|
|
188
191
|
|
|
189
|
-
- **end-on, lasting several frames** — the limb really points at the camera: on
|
|
190
|
-
- **blur collapse, lasting 1–3 frames** — a fast forearm whose elbow and wrist the detector both puts on the blurred hand (eleven spans on
|
|
192
|
+
- **end-on, lasting several frames** — the limb really points at the camera: on `dance-b` the right upper arm folded across the chest (frames 395–401, 0.09–0.25), and the right thigh and shin of a knee raised at the camera (365–370, 0.16–0.34); on `dance-a` the left upper arm with the elbow at the camera (frames 30–37, 0.27–0.35, S2 below);
|
|
193
|
+
- **blur collapse, lasting 1–3 frames** — a fast forearm whose elbow and wrist the detector both puts on the blurred hand (eleven spans on `dance-b`).
|
|
191
194
|
|
|
192
|
-
Both have a meaningless direction; only the first is a pose the rig cannot draw. **0.35** is the smallest round value that takes in every end-on frame (the highest is 0.35, on
|
|
195
|
+
Both have a meaningless direction; only the first is a pose the rig cannot draw. **0.35** is the smallest round value that takes in every end-on frame (the highest is 0.35, on `dance-a`) without reaching normal motion (the 5th percentiles are 0.75 and 0.82). A run under the cutoff — spans merged across a gap of one frame — lasting **≥ 0.15 s** (3.6 frames at 24 fps, 4.5 at 30) is the end-on kind: it excludes every blur collapse above (at most 3 frames, 0.10 s at 30 fps) and keeps every end-on span (6–8 frames). The run prints it as `END-ON` and the report lists it under `endOn` (chain, bone, segment, frames, seconds, the lowest fraction): it is the list of moments that need an alternate drawing ([#6](https://github.com/rig-c/pose/issues/6)).
|
|
193
196
|
|
|
194
|
-
Re-checked on
|
|
197
|
+
Re-checked on a @rig-c/parts fixture rig with the short hold, the flip guard and clamped bends. On `dance-b` the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with the flip guard alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On `dance-a` `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands. When `dance-b`'s pelvis is mapped (`r_hip → l_hip`), its side-on turn (frames 463–491, body yaw +79.5°) foreshortens the hip line to 0.17 of standing and crosses `r_hip.x` past `l_hip.x` on frames 482–484 (+177.4° / −179.4°); `holdAcross` ([#184](https://github.com/rig-c/pose/issues/184)) holds `hip.rotate` across the unconfident span (`[−2.2°, +2.6°]` over 463–491, `0` `FLIP`s) without listing horizontal lines as limb `END-ON`. The default stays off: a default needs its own criterion and a third sequence.
|
|
195
198
|
|
|
196
199
|
### The legs: foreshortening and planted feet
|
|
197
200
|
|
|
198
201
|
A knee bending toward the camera is seen from the front only as a shorter leg, and a rig with rigid legs cannot lower its hip without lifting its feet ([#47](https://github.com/rig-c/pose/issues/47)). Two flags, both off by default:
|
|
199
202
|
|
|
200
|
-
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf` derived from posture-invariant measures — `max(r_thigh + r_shin, l_thigh + l_shin)`, shoulder width and eye width, never the trunk — with frame-to-frame step ≤ 1 % and frozen spans ≥ 5 frames held at their median, so neither camera zoom nor a forward trunk bend reads as the camera moving, and `--camera <file>` writes that track as `rig-pose-camera/1` without changing `motion.json`, [#159](https://github.com/rig-c/pose/issues/159), [#161](https://github.com/rig-c/pose/issues/161), [#163](https://github.com/rig-c/pose/issues/163), [#172](https://github.com/rig-c/pose/issues/172)), clamped to [min, max]. When the camera-normalised trunk (`neck → mid_hip` or `mid_shoulder → mid_hip`) foreshortens or extends by more than 4 % from its standing length, the trunk bone is keyed in `scaley` (for a `+y`-aimed trunk, `scalex` for `+x`) clamped to the same `[min, max]` bounds, carrying the trunk and head's depth bend while keeping the aimed rotation ([#172](https://github.com/rig-c/pose/issues/172)). The
|
|
203
|
+
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf` derived from posture-invariant measures — `max(r_thigh + r_shin, l_thigh + l_shin)`, shoulder width and eye width, never the trunk — with frame-to-frame step ≤ 1 % and frozen spans ≥ 5 frames held at their median, so neither camera zoom nor a forward trunk bend reads as the camera moving, and `--camera <file>` writes that track as `rig-pose-camera/1` without changing `motion.json`, [#159](https://github.com/rig-c/pose/issues/159), [#161](https://github.com/rig-c/pose/issues/161), [#163](https://github.com/rig-c/pose/issues/163), [#172](https://github.com/rig-c/pose/issues/172)), clamped to [min, max]. When the camera-normalised trunk (`neck → mid_hip` or `mid_shoulder → mid_hip`) foreshortens or extends by more than 4 % from its standing length, the trunk bone is keyed in `scaley` (for a `+y`-aimed trunk, `scalex` for `+x`) clamped to the same `[min, max]` bounds, carrying the trunk and head's depth bend while keeping the aimed rotation ([#172](https://github.com/rig-c/pose/issues/172)). The bounds are `<min,max>` with `0 < min ≤ 1 ≤ max`. A segment the map aims other than `"x"` is refused (`LEGS_MAP`). `LEG-SCALE` lists each segment's frames clamped at either limit.
|
|
201
204
|
|
|
202
|
-
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. A child (or its intermediate control bone) with `noScale` or `noScaleOrReflection` ([#153](https://github.com/rig-c/pose/issues/153)) still has its direction carried through the parent's scaled matrix, so its angle is skewed the same way, while rig-c normalises that direction to unit length so the parent's scale is not inherited. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' put its world x axis on its target at the scale asked (under `noScale`, s' is the bone's own asked scale s directly). On
|
|
205
|
+
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. A child (or its intermediate control bone) with `noScale` or `noScaleOrReflection` ([#153](https://github.com/rig-c/pose/issues/153)) still has its direction carried through the parent's scaled matrix, so its angle is skewed the same way, while rig-c normalises that direction to unit length so the parent's scale is not inherited. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' put its world x axis on its target at the scale asked (under `noScale`, s' is the bone's own asked scale s directly). On `dance-a` the unit-scale formula missed the shins by up to 2.4°, their length by 0.16 of setup and the ankles by 38 units; solved, every bone lands. A `normal` child's art is still sheared by its parent's scale, and a bone under it (a foot) is squashed along the shin: `SHEAR` reads both.
|
|
203
206
|
|
|
204
207
|
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
|
|
205
208
|
|
|
206
209
|
- The supporting foot is the one whose reference ankle is lower in the picture. It changes only when the other ankle has been at least **0.02 leg lengths** lower for at least **0.1 s** (timed as `END-ON` runs are), and once confirmed takes support from the frame it crossed that margin ([#102](https://github.com/rig-c/pose/issues/102)); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
|
|
207
210
|
- The new support is held where it is on the frame it takes over, and the hip blends in y from the old derivation to the new over **3 frames** while taking the new derivation's x immediately ([#102](https://github.com/rig-c/pose/issues/102)), so the planted foot does not slide across the blend.
|
|
208
|
-
- A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames**, starting from the first immediately preceding push-off frame where both ankles are already above their rolling floor while mid_hip lags ([#142](https://github.com/rig-c/pose/issues/142)) — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). At a walking hand-over the front foot has not landed yet while the back ankle lifts its heel and the hip rises 0.051–0.065 leg lengths; when the front foot lands while the back ankle stays above its floor, a back ankle that holds its height in y (< **0.01** leg lengths a frame for **2 frames**) is on a raised heel rather than in flight and is not read as a jump ([#101](https://github.com/rig-c/pose/issues/101)). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence
|
|
211
|
+
- A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames**, starting from the first immediately preceding push-off frame where both ankles are already above their rolling floor while mid_hip lags ([#142](https://github.com/rig-c/pose/issues/142)) — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). At a walking hand-over the front foot has not landed yet while the back ankle lifts its heel and the hip rises 0.051–0.065 leg lengths; when the front foot lands while the back ankle stays above its floor, a back ankle that holds its height in y (< **0.01** leg lengths a frame for **2 frames**) is on a raised heel rather than in flight and is not read as a jump ([#101](https://github.com/rig-c/pose/issues/101)). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence the subject in `dance-a` standing still (frames 0–29 and 277–312, mid_hip moving 10 and 14 px) read as jumping, 152–155 of 313 frames in all.
|
|
209
212
|
- **In place** ([#92](https://github.com/rig-c/pose/issues/92)). On a sequence with `"travel": "in-place"`, the supporting foot of a figure that does not travel moves back in the picture while the body stays. Holding it where it planted turns that slide into the body's travel, so the hip walks off and never comes back. Under the declaration each plant's anchor follows its own ankle's picture x, scaled into the rig, from the frame it planted, and keeps its y on the one floor. No step releases a foot, since a foot that follows its track has no plant to drag. Every foot may take support, and the lower one does, by the rules above. The loop is then closed: the hip's x on frame i moves by −drift · i / (n − 1), where drift is its x on the last frame less its x on the first. The last frame's hip x is then the first's, and the loop carries no net travel. The y is the floor's and is not touched. `FEET` prints the drift closed, in place of the steps line, and the report's `feet.closure` carries `{ drift, frames }` (`null` otherwise). On the tests' synthetic in-place loop (40 frames, each supporting ankle sliding back 2 px a frame, the hips still), the hip moves less than 10⁻⁹ px. The same joints without the declaration put it 68 px forward over the loop. With a slide of 0.04 leg lengths a frame, both feet are released as stepping and never re-plant without the declaration, and the supports alternate with it. Jumps are read as before.
|
|
210
|
-
- A **step** releases the plant too: while the supporting ankle moves more than **0.03** leg lengths a frame, the other foot takes support if it is planted (not itself released or moving), and otherwise the hip follows the pose's travel. The released ankle may support again once it has moved less than **0.01** leg lengths a frame for **2 frames**. On
|
|
213
|
+
- A **step** releases the plant too: while the supporting ankle moves more than **0.03** leg lengths a frame, the other foot takes support if it is planted (not itself released or moving), and otherwise the hip follows the pose's travel. The released ankle may support again once it has moved less than **0.01** leg lengths a frame for **2 frames**. On `dance-a` the plant held the supporting ankle through a real step (frames 176–181, 579 → 718 px) and threw the hip sideways.
|
|
211
214
|
- **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113), [#119](https://github.com/rig-c/pose/issues/119)). On a sequence not declared `"in-place"`, a foot whose ankle is at least **0.02 leg lengths** higher in the picture than the other's never takes support: not when a plant starts, and not when a step releases the other foot. Before, a step handed support to the only foot free to take it, even a folded, lifted one, and the hip was then placed under a foot in the air. A supporting foot that the other ankle has been that much lower than for **0.1 s** hands support to it when it may take it, as before; when the lower foot may not (it is released and not yet still, or moving), the supporting foot is released, and the hip follows the pose's travel until a foot may take support. On a frame after a jump or a step's release where the only foot free under #113 is the higher one (`cause: "higher"`), the lower foot supports **tracked, not planted** ([#119](https://github.com/rig-c/pose/issues/119)): the hip puts it on #85's floor and follows its scaled ankle track (`FEET tracked`, `feet.tracked`) until it has been still for 2 frames (`< 0.01`), when the normal plant takes over at that position. Steps and jumps are read as before, and under `"in-place"` #92's reading stands. `FEET support read from the lower ankle` names every run of frames whose support differs from the reading before #113, the side before and after (`none`: no foot supports), and for each `none` why: `released`, the supporting foot let go because the lower ankle could not take support (released by a step, or moving), or the only foot free to take support was the higher one. The report's `feet.lowered` holds them as `{ frames, was, now, cause }` (absent in place). On the tests' synthetic step under a lifted leg (the lifted ankle 0.2 leg lengths up, the grounded one stepping 0.05 leg lengths on frame 10 and still from 11), the lifted leg took support on frames 10–11 before; now frames 10–11 track the lower foot on the floor (`feet.tracked`) and the grounded one is planted from 12, when it is still again.
|
|
212
215
|
- **One floor** ([#85](https://github.com/rig-c/pose/issues/85)). A 2D rig has no depth, so its floor is one line, and a planted foot is on it. The picture disagrees: a step toward the camera puts the foot lower in the frame, and a camera residual puts a whole span higher, so a plant held where the picture puts each span leaves the floor stepping between spans. Every support span's supporting ankle is therefore moved in y onto one floor, the hip and everything above with it. A span's *planted height* is the world y (on the rig, posed by rig-c) its supporting ankle would be held at; the floor is the median of the spans' planted heights, or **`--floor <y>`** (world units, y up; only with `--plant-feet`, exit 2 without it). Each span's *shift* is floor − its planted height. A change of support still blends the hip over the 3 frames, now between two spans on the floor, and a planted foot does not leave the floor: on each blend frame the **supporting** leg is stretched along its chain about its root, by the factor `--stance` uses, k = (root y − floor y) / (root y − ankle y), held within **[0.92, 1.08]**, its lower bound on the frame the supporting leg's own range as `--stance`'s ([#95](https://github.com/rig-c/pose/issues/95); where that went below 0.92, a `FEET bound from the leg's own range` line, the report's `feet.blend.range`, and each blend run's `lower`). A frame that needs more is held at the bound, the rest left to the blend, and listed `FEET past the bounds` with the k needed and how far the foot was left from the floor. An ankle already within 0.01 units (the landing check's origin tolerance) is not stretched. The stretched scales are scale targets, so the landing check holds them; `--stance` then stretches the free leg, a different chain, to the supporting ankle. A jump follows the pose's travel from the hip it left, which is on the floor, carried linearly from take-off so the landing frame meets the floor and clamped so no frame inside the flight puts a foot below it ([#103](https://github.com/rig-c/pose/issues/103)), and is held the same way over its first 3 planted frames, with the flight's carry held across the landing blend so it fades out with the blend weights rather than dropping on the first frame after landing ([#147](https://github.com/rig-c/pose/issues/147)). `--stance` is unchanged: it stretches the free leg to the supporting ankle, which is on the floor. On the tests' synthetic stick, three spans 0, 6 and 1 px apart in the picture (0, 9 and 1.5 units on the rig) plant within 0.0001 units of one floor. With the shift forced to 0 they stay 9 units apart; without the stretch, the supporting ankle leaves the floor by 6.75 units on the first blend frame.
|
|
213
216
|
- `FEET` lists the supports, the jumps and the steps (each with the side, its frames, who took support and where it re-planted; in place, the loop's closure instead), and with `--plant-feet` the floor: its world y and where it came from, each span's shift in units and in that side's rig leg at setup (thigh origin → ankle), and the shifts' range, then one line per run of blend frames whose supporting leg was stretched (`blend held on the floor`) and per run `past the bounds`. The report carries them under `feet` (`supports`, `jumps`, `steps`, `changes`, `floor`, `shifts`, `blend`, `closure`).
|
|
214
|
-
- **`--stance <leg lengths>`** (only with `--plant-feet`, which it is refused without; off by default) plants **both** feet when the free one is near the floor ([#60](https://github.com/rig-c/pose/issues/60)). On a frame with a supporting side (so outside every jump), the free ankle's height above the supporting one is read in the picture, in the free side's leg length — the leg length `FEET` uses. The tolerance is a **distance** ([#114](https://github.com/rig-c/pose/issues/114)): within it, above the supporting ankle or below it, the frame is **planted both**: every bone of the free leg's chain has its `scalex` key multiplied by k = (root y − floor y) / (root y − ankle y) on the posed rig, the root being the chain's first bone's origin and the floor the supporting ankle, so the foot reaches the floor. A free ankle within the tolerance below the supporting one is planted too: its k is under 1, since a foot slightly under the floor is the same fault as one floating above it. A free ankle **past the tolerance below** the supporting one is not a stance but a support error (the foot read as supporting is the one in the air), so that frame is never planted both and keeps the pose's height; it is named on a `STANCE past the tolerance below` line with the side, the frames and the largest distance, and read `"below"` in the report's `stance.runs`. Before #114 the gap was signed, so any free ankle below the supporting one counted as within, and a lifted foot read as supporting had the grounded leg shrunk up to it. On the tests' synthetic stance, a free ankle 0.5 leg lengths below is read below and not planted; one 0.03 below is planted with k under 1. k is held within **[0.92, 1.08]**, except that the lower bound comes from the leg's own range in the sequence ([#95](https://github.com/rig-c/pose/issues/95)): per side, r = the leg's picture length hip → ankle on the frame ÷ its straight length (the 95th percentile of that length, as `--leg-scale` reads a segment's), and the frame's lower bound is min(0.92, r), so the rig leg may fold as far as the picture folds it on that frame and never less far than 0.92. The upper bound stays 1.08, since a picture leg does not read longer than straight. A sequence whose legs never fold below 0.92 keeps exactly [0.92, 1.08]. Where the range set a bound below 0.92 on a planted-both frame, one `STANCE bound from the leg's own range` line gives each side's lowest, as does the report's `stance.range`. A frame past its bound is listed `past the bounds` with the bound it had, held at the bound and not forced. Past the tolerance above, the foot is **lifted** and keeps the pose's height. The state is read with **hysteresis** and a **shortest run** ([#87](https://github.com/rig-c/pose/issues/87)), on the distance: a planted foot leaves the stance when its distance from the supporting one goes past the tolerance, and one past it is planted again only under **0.8** of it (both figures are on the `STANCE` line). A change of state takes effect only when its condition holds for **0.1 s** (by the frames' times). A shorter crossing with the other state on both sides of it keeps their state and is listed `absorbed` with its frames and the height that crossed (`absorbed: true` in the report's `stance.runs`, beside the run that took it in). One that runs to the end of a support span is taken, since nothing of the other state follows it. A threshold read frame by frame turned noise at the threshold into a pop: one frame at 0.0708 against 0.07 dropped the stretch for one frame and the foot jumped up and back. The stretched scales are the scale targets, so the landing check holds them. Measured
|
|
217
|
+
- **`--stance <leg lengths>`** (only with `--plant-feet`, which it is refused without; off by default) plants **both** feet when the free one is near the floor ([#60](https://github.com/rig-c/pose/issues/60)). On a frame with a supporting side (so outside every jump), the free ankle's height above the supporting one is read in the picture, in the free side's leg length — the leg length `FEET` uses. The tolerance is a **distance** ([#114](https://github.com/rig-c/pose/issues/114)): within it, above the supporting ankle or below it, the frame is **planted both**: every bone of the free leg's chain has its `scalex` key multiplied by k = (root y − floor y) / (root y − ankle y) on the posed rig, the root being the chain's first bone's origin and the floor the supporting ankle, so the foot reaches the floor. A free ankle within the tolerance below the supporting one is planted too: its k is under 1, since a foot slightly under the floor is the same fault as one floating above it. A free ankle **past the tolerance below** the supporting one is not a stance but a support error (the foot read as supporting is the one in the air), so that frame is never planted both and keeps the pose's height; it is named on a `STANCE past the tolerance below` line with the side, the frames and the largest distance, and read `"below"` in the report's `stance.runs`. Before #114 the gap was signed, so any free ankle below the supporting one counted as within, and a lifted foot read as supporting had the grounded leg shrunk up to it. On the tests' synthetic stance, a free ankle 0.5 leg lengths below is read below and not planted; one 0.03 below is planted with k under 1. k is held within **[0.92, 1.08]**, except that the lower bound comes from the leg's own range in the sequence ([#95](https://github.com/rig-c/pose/issues/95)): per side, r = the leg's picture length hip → ankle on the frame ÷ its straight length (the 95th percentile of that length, as `--leg-scale` reads a segment's), and the frame's lower bound is min(0.92, r), so the rig leg may fold as far as the picture folds it on that frame and never less far than 0.92. The upper bound stays 1.08, since a picture leg does not read longer than straight. A sequence whose legs never fold below 0.92 keeps exactly [0.92, 1.08]. Where the range set a bound below 0.92 on a planted-both frame, one `STANCE bound from the leg's own range` line gives each side's lowest, as does the report's `stance.range`. A frame past its bound is listed `past the bounds` with the bound it had, held at the bound and not forced. Past the tolerance above, the foot is **lifted** and keeps the pose's height. The state is read with **hysteresis** and a **shortest run** ([#87](https://github.com/rig-c/pose/issues/87)), on the distance: a planted foot leaves the stance when its distance from the supporting one goes past the tolerance, and one past it is planted again only under **0.8** of it (both figures are on the `STANCE` line). A change of state takes effect only when its condition holds for **0.1 s** (by the frames' times). A shorter crossing with the other state on both sides of it keeps their state and is listed `absorbed` with its frames and the height that crossed (`absorbed: true` in the report's `stance.runs`, beside the run that took it in). One that runs to the end of a support span is taken, since nothing of the other state follows it. A threshold read frame by frame turned noise at the threshold into a pop: one frame at 0.0708 against a 0.07 threshold dropped the stretch for one frame and the foot jumped up and back. The stretched scales are the scale targets, so the landing check holds them. Measured on `dance-a`, frames 94–175 outside the jumps: the free ankle's height above the supporting one on the posed build went from median 18.3 and p90 25.4 units to 0.0 and 0.0. On `dance-b` (492 frames at 30 fps) the 29 frames labelled by eye as both feet flat, outside the jumps, read 0.0 and 0.0. Of the 6 labelled one foot lifted, 5 stay above the tolerance; the sixth (frame 360) was 0.042 leg lengths up in the picture, within the tolerance, and so planted; without `--stance` it stood 6.5 units up, also under the tolerance (22.6 units there). That is 83 %, under the 90 % the card asked.
|
|
215
218
|
- The map needs one chain ending at each ankle whose last bone is aimed `"x"` (the ankle is that bone's tip), and a `translate` the legs hang from (`LEGS_MAP`). A `range`, or a set's `turn` or `offsets`, on a bone between the hip and an ankle is refused (`LEGS_RANGE`): the plant is derived before the range clamp, and a clamp after it would move the planted foot.
|
|
216
219
|
|
|
217
|
-
Both are checked on the gated build: every scale lands, each ankle lands where the keys put it, and the hip's origin where the plant put it. The figures were fixed in #47 before
|
|
220
|
+
Both are checked on the gated build: every scale lands, each ankle lands where the keys put it, and the hip's origin where the plant put it. The figures were fixed in #47 before `dance-a` was measured; the defaults stay off until they pass.
|
|
218
221
|
|
|
219
222
|
### The idle: what the retarget drops
|
|
220
223
|
|
|
@@ -231,7 +234,7 @@ A rig's motion spec usually has an `idle` — a blink, a breath, hair and hem sw
|
|
|
231
234
|
|
|
232
235
|
Every bone still has to land with the carried tracks in. A carried translate above the translated bone moves its origin off the pose's travel, and the run refuses with `POSE_LANDS`, naming the carried tracks above the bone that missed.
|
|
233
236
|
|
|
234
|
-
On
|
|
237
|
+
On `dance-a` (with `--carry-idle`) the idle has 26 tracks. Kept out: the four arm rotates, which the dance keys, and a control bone's scalex above the chest, the neck's rotate and a head control's rotate, which sit above the mapped chest, head and arms — carried, they missed the chest, the head and the arms by 0.16°–1.37°. Carried: the other 19, the blink (an eyes group's scaley) and the breath (the chest control's translatey) among them. The gate is green, every bone lands (worst 0.00167°), and rig-c's poser shows the blink at every cycle (scaleY 0.12 at 2.37 s, 5.70 s, 9.08 s and 12.41 s, against the idle's at t mod 3.333 s within 8.9e-6) and the breath's offset (5.02 at 0.833, 4.167, 7.5, 10.833 s; 0 at 2.5, 5.833, 9.167, 12.5 s; 1.035 at the cut, 13 s).
|
|
235
238
|
|
|
236
239
|
**`--merge <file>`** merges a timed track fragment into the new animation, at absolute times from its start — blinks read off the video, for example:
|
|
237
240
|
|
|
@@ -249,7 +252,7 @@ The retarget writes one key per pose frame on every mapped bone. That is faithfu
|
|
|
249
252
|
- **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.
|
|
250
253
|
- **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.
|
|
251
254
|
- **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.
|
|
252
|
-
- **On
|
|
255
|
+
- **On `dance-a`** (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×.
|
|
253
256
|
- **It runs last, on the final keys** (rig-c/pose#94): after `--plant-feet`'s plant, its one floor, the blend and `--stance` stretches and the in-place closure. Their keys are reduced like any other, and every landing check is run again on the reduced motion.
|
|
254
257
|
- A scaled bone's `scalex` track is thinned within 5e-5 of its setup length: half of the scale landing check's 1e-4, so a reduced curve cannot fail that check.
|
|
255
258
|
- A mapped bone under a scaled bone (keyed through its parent's posed matrix by `--leg-scale`) is solved again through its parent as reduced. Its rotate is then thinned with the error read as the world aim it lands on, and its `scalex` with the error read as the world length it lands on. Its world error is its own fit error, as down any other chain.
|
|
@@ -312,7 +315,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
312
315
|
- A **point** of the rig is a bone's origin (`name`), the end of its x axis (`name:tip`), or the midpoint of two of those (`"a+b"`: `"sleeve_r0+sleeve_l0"` is the shoulders' midpoint). An aim's points must be the bone itself or descend from it.
|
|
313
316
|
- `reach: true`, on a one-bone chain from the translated joint (a trunk from `mid_hip`): the bone is aimed from where its origin is posed — the hip moved and turned — at where the pose puts the segment's end relative to mid_hip, scaled. Without it, a tilted pelvis carries the chest's origin sideways and the trunk, parallel to the pose's, misses the shoulders. Its aim is one point, from the bone's own origin: `{ "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "mid_shoulder"], "aim": ["sleeve_r0+sleeve_l0"], "reach": true }`. The chain must descend from the translated bone.
|
|
314
317
|
- `bend`: every joint inside a chain is a hinge. `"rest"` (default) takes the side it may bend from the rig's rest pose and refuses a rest that is straight (under 0.5°); `"ccw"`/`"cw"` state it; `"free"` skips the check.
|
|
315
|
-
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On a second dance with knee bends
|
|
318
|
+
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (`dance-a`, 313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On `dance-b` (a second dance with deeper knee bends, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
|
|
316
319
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
317
320
|
- `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
|
|
318
321
|
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides` (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
@@ -361,20 +364,20 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
361
364
|
|
|
362
365
|
**Where a switch is keyed: `--switch-at`.** By default (`first`) a boundary — the frame a set starts showing on, and the frame the base art or the next set comes back on — is where the selection above puts it: the first frame within the tolerance, often a frame where the arm is slowing into the pose, so the change of drawing can be seen on a nearly still arm. `--switch-at peak` moves each boundary to the frame within ±`--switch-min`/2 (±3 frames at 24 fps with the default 0.25 s) where the chains it changes turn fastest, so the cut lands in motion ([#31](https://github.com/rig-c/pose/issues/31)). The speed on a frame is the largest |angular speed| in degrees per frame, a central difference of the target world angles (one-sided on the first and last frame), over every segment of the leaving set's and the arriving set's chains; it is read on the same targets as the distance, before the range clamp and any set's `turn`, which depend on where the spans are. The window is clipped so every span keeps `--switch-min`, a gap between two spans of one set keeps it too (the bridging holds), no boundary reaches its neighbour, and a set with `frames` shows only inside them. In it, a frame qualifies only if the set the move newly shows is within **2 × `--switch-tolerance`** of its key skeleton on every frame it newly shows it on; the boundary goes to the fastest qualifying frame, and stays when none qualifies. A tie goes to the frame nearest the original, then the earlier. A boundary between two sets moves once.
|
|
363
366
|
|
|
364
|
-
The bound exists because speed alone rewarded showing art far from its pose. Unbounded, on
|
|
367
|
+
The bound exists because speed alone rewarded showing art far from its pose. Unbounded, on `dance-a`, the peak sat mid-swing: the largest distance over the frames a set showed rose from 25° to 135° (K2), 24° to 64° (K4), 21° to 59° (K1) and 21° to 38° (K3), the sets' `turn` held the arms hard against the pose (`ART-RANGE` runs 24 → 36), and the rig snapped to a hands-low drawing two frames before the arms came down. Bounded, every set stays within 50° (the largest is 43°, K4) and the median speed at the boundaries still rises from 9.7 to 14.2°/frame, but `ART-RANGE` runs still rise, 24 → 31 (K4 1 → 3, K1 6 → 8, K2 6 → 7, K3 2 → 4). That is why `first` is the default and `peak` an option ([#31](https://github.com/rig-c/pose/issues/31), round 2).
|
|
365
368
|
|
|
366
369
|
Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's old and new frame (the same under `first`), the frames and seconds it moved, the speed on both and its window, and the report carries the same under `moved`.
|
|
367
370
|
|
|
368
371
|
**What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. Between two sets that both draw an arm behind the torso, a frame that belongs to neither set stays behind (the set nearer in hand position and segment distance, not frame count), with any adjacent bridge or `SHORT` ramp interpolated in joint space (parent-local angle for child bones) and any extended hold of a tight set included in `chainHolds`/`covered` so the held art is clamped and exempted from `capShortSteps` and `range`; the default front slot is for a frame no set can claim **and** whose arm is read in front — not a fallback ([#136](https://github.com/rig-c/pose/issues/136), [#139](https://github.com/rig-c/pose/issues/139), [#162](https://github.com/rig-c/pose/issues/162), [#180](https://github.com/rig-c/pose/issues/180), [#187](https://github.com/rig-c/pose/issues/187), [#206](https://github.com/rig-c/pose/issues/206)). A switch onto frame *i* is keyed halfway between frames *i*−1 and *i*: a stepped key on the frame's own time fires a frame late wherever a player's accumulated time lands just below it (rig-c's docs/AUTHORING.md §4.5), and halfway both the pose's times and a renderer's `i / fps` see it on frame *i*. Before the first key a slot shows its setup attachment, so no key is written at 0 unless a span starts there. Offsets are `translate` keys at the same times, `stepped`. rig-c's gate checks these keys like any other.
|
|
369
372
|
|
|
370
|
-
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on
|
|
373
|
+
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on `dance-a` (13 s front-facing) their picture length never drops under 0.81 of its median and none reverses; on `dance-b` the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds, held as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
|
|
371
374
|
|
|
372
375
|
|
|
373
|
-
**A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on
|
|
376
|
+
**A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on `dance-a`, the hands clasped behind the back and the hands low in front differ by only 2–21°, and the wrist and hand-point scores do not separate them either; the cue that might is #33's depth state. It is recorded in the map as a stopgap, not a selection rule. A window **restricts** a set; it never forces one. Where the set does not show inside its window, the run says why in a `FRAMES` line: the stretch, and whether the pose there was past the tolerance (with the largest distance), or within it but the run was dropped.
|
|
374
377
|
|
|
375
378
|
**How far a set's art may turn: `turn`.** A drawing reads right only near the angle it was painted at. A set may state, per bone with a `drawn` angle, how far its art may turn from it, `"turn": { "sleeve_r0": [-20, 20] }`. While the set shows, the bone is held within it: its target becomes drawn + clamp(wrap(target − drawn), min, max), each bone on its own world angle, so a forearm under a held upper arm keeps its own target. The landing check measures against the held angle. The overflow the pose asked for is listed as `ART-RANGE`. A `turn` on a bone the set has no `drawn` angle for is refused (`MAP_ALT_TURN`).
|
|
376
379
|
|
|
377
|
-
**A raised arm placed against the head: `--raised-by-head`.** The pose's angles put a hand where it is against the
|
|
380
|
+
**A raised arm placed against the head: `--raised-by-head`.** The pose's angles put a hand where it is against the subject's own shoulder. On a rig whose shoulders sit nearer its head, or whose head is larger, the same angles put a hand raised beside the head on top of it. With `--raised-by-head` (off by default, [#62](https://github.com/rig-c/pose/issues/62), [#179](https://github.com/rig-c/pose/issues/179)) a set places a chain by the head when its key skeleton (`pose`) puts the chain's wrist within `2 × eye_dist` (`RAISED_WRIST_EYES = 2`) of the head centre (the midpoint of `r_eye` and `l_eye`, or `nose` when the eyes are absent from the key skeleton); a chain whose wrist sits farther keeps its shoulder-based pose angles regardless of its upper arm's `drawn` angle. While that set shows, the arm's wrist goes where the reference's is **against the head**:
|
|
378
381
|
|
|
379
382
|
- The unit is the **eye distance**: on each side, its median over the span. That is the reference's |r_eye − l_eye|, and on the rig the distance between the two points the head chain's `"a>b"` aim names. The head centre is the eyes' midpoint on each frame. The head chain is the one carrying the eye line (r_eye → l_eye, either way round). The flag is refused without one, or with one not aimed `"a>b"` (`RAISED_HEAD`).
|
|
380
383
|
- The target is rig head centre + (reference wrist − reference head centre) × (rig eye distance ÷ reference eye distance), the picture's y turned up, per frame.
|
|
@@ -426,8 +429,8 @@ The figures and bars below are fixed. They were set in [#38](https://github.com/
|
|
|
426
429
|
Why these bars ([#38](https://github.com/rig-c/pose/issues/38)):
|
|
427
430
|
|
|
428
431
|
- **20° for a segment's direction.** HQ's rule for a set's art is `drawn` ± 20°: the art may turn up to 20° from the angle it was painted at and still read. A bigger difference is an arm that looks different.
|
|
429
|
-
- **0.6 and 0.4 for a hand.** On
|
|
430
|
-
- **15°** for the head's tilt, **0.05 / 0.02** hip widths per frame for a sliding foot, and **0.5 ×** of the reference's range for a missing bounce were fixed with the others before
|
|
432
|
+
- **0.6 and 0.4 for a hand.** On `dance-a`, the hands in front (S4) score 0.53–0.88 and the hands hidden behind the back (S1) score 0.41–0.57. 0.6 and 0.4 sit outside that overlap, one on each side.
|
|
433
|
+
- **15°** for the head's tilt, **0.05 / 0.02** hip widths per frame for a sliding foot, and **0.5 ×** of the reference's range for a missing bounce were fixed with the others before `dance-a` was measured.
|
|
431
434
|
|
|
432
435
|
**Units and segments.** The map says which segments are compared. An **arm** segment joins two of one side's shoulder, elbow and wrist, and a **leg** segment two of one side's hip, knee and ankle. A segment's name is `chain/bone`. Other chains, such as the trunk, the pelvis or the head, are not compared as segments. The head is measured on its eye line whatever the map says, and the body on its torso and hip. A mapped arm or leg segment whose point has a position on no frame of one side is refused (`COMPARE_JOINT_MISSING`, naming the chain, the bone, the point and the side).
|
|
433
436
|
|
|
@@ -443,13 +446,13 @@ Why these bars ([#38](https://github.com/rig-c/pose/issues/38)):
|
|
|
443
446
|
|
|
444
447
|
**Body.** Each figure is read over the whole run, so the unit gives at most one finding per measure, `no bounce`, spanning every frame. Its figure is the ratio of the ranges, render ÷ reference. Its worst frame, the one the sheet draws, is the reference's frame farthest from its median. The summary line always gives both ranges.
|
|
445
448
|
|
|
446
|
-
**Cloth.** Cloth is measured only when both cloth tracks are given, with `--cloth-windows`. Without them the unit reads `not measured`. A cloth track is HQ's `rig-c-cloth-track/0`: `{ "spec": "rig-c-cloth-track/0", "subject"?, "method"?, "source"?, "fps"?, "frames_count"?, "units"?, "hipWidth": { "median", "unit": "picture px" | "rig world units" }, "origin"?, "axes"?, "skirtSwing"?, "frames": [{ "i", "t", "hip_x", "hip_y", "hip_w", "tilt_deg", "hem_mid", "hem_l", "hem_r", "hem_l_y", "hem_r_y", "hem_band_cx", "skirt_cx", "skirt_top", "skirt_bottom" }] }`, the frame keys all numbers. Every position is already divided by the subject's own median hip width, with x to the right. `hip_x` is the hips' midpoint and `hem_mid` the midpoint of the hem's leftmost and rightmost points in the lower half of the skirt ([#34](https://github.com/rig-c/pose/issues/34)'s hem); only those two and `t` are read. Any other spec, or a key outside that list, is refused (`COMPARE_CLOTH`). The three figures are #34's, as HQ's skirt criteria define them, ported from HQ's measuring script. On
|
|
449
|
+
**Cloth.** Cloth is measured only when both cloth tracks are given, with `--cloth-windows`. Without them the unit reads `not measured`. A cloth track is HQ's `rig-c-cloth-track/0`: `{ "spec": "rig-c-cloth-track/0", "subject"?, "method"?, "source"?, "fps"?, "frames_count"?, "units"?, "hipWidth": { "median", "unit": "picture px" | "rig world units" }, "origin"?, "axes"?, "skirtSwing"?, "frames": [{ "i", "t", "hip_x", "hip_y", "hip_w", "tilt_deg", "hem_mid", "hem_l", "hem_r", "hem_l_y", "hem_r_y", "hem_band_cx", "skirt_cx", "skirt_top", "skirt_bottom" }] }`, the frame keys all numbers. Every position is already divided by the subject's own median hip width, with x to the right. `hip_x` is the hips' midpoint and `hem_mid` the midpoint of the hem's leftmost and rightmost points in the lower half of the skirt ([#34](https://github.com/rig-c/pose/issues/34)'s hem); only those two and `t` are read. Any other spec, or a key outside that list, is refused (`COMPARE_CLOTH`). The three figures are #34's, as HQ's skirt criteria define them, ported from HQ's measuring script. On the per-frame record of `dance-a` they reproduce its numbers exactly (reference A 0.958 / 2.634 / 0.061, B 0.696 / 1.409 / 0.000). Read through `rig-c-cloth-track/0`, whose values are rounded to 5 decimals, the reference's lag in A reads 0.959.
|
|
447
450
|
|
|
448
451
|
- Both signals are smoothed with a Gaussian, σ = 1 frame over ± 4 frames, ends padded, before differentiating (numpy's `gradient`).
|
|
449
452
|
- **lag**, in frames, hem after hip positive: the peak of the normalised cross-correlation of hip-x velocity with hem-x velocity over the window, searched over −6…+12 frames and refined by a parabola.
|
|
450
453
|
- **swing ratio**: per hip stroke, the hem's excursion over the hip's. A stroke runs from one hip reversal (a zero crossing of hip velocity) to the next and moves at least the stroke threshold. A wiggle under the threshold is dropped as a pair. The hem is read `round(lag)` frames after each reversal. The figure is the median over the window's strokes.
|
|
451
454
|
- **overshoot**: per stroke, how far the hem keeps travelling in the stroke's direction after the hips turn, within half of the next stroke (or 12 frames after the last), as a fraction of the hip stroke. The figure is the median.
|
|
452
|
-
- **The stroke threshold** is 0.2 *reference* hip widths: one physical size on both sides (the criteria's Revision 1), expressed in the render's own hip widths. A render track in rig world units is carried by the retarget's scale, `scale.factor` in `--retarget-report` (rig units per reference px): 0.2 × reference hip width × factor ÷ render hip width. On
|
|
455
|
+
- **The stroke threshold** is 0.2 *reference* hip widths: one physical size on both sides (the criteria's Revision 1), expressed in the render's own hip widths. A render track in rig world units is carried by the retarget's scale, `scale.factor` in `--retarget-report` (rig units per reference px): 0.2 × reference hip width × factor ÷ render hip width. On `dance-a` that is 0.2 × 128.1 × 0.54152 ÷ 92.202 = 0.1505, the criteria's own figure. Without the report it is refused (`COMPARE_CLOTH_SCALE`). A render track in picture px is carried by the torso, as the retarget scales the rig: 0.2 × (reference hip width ÷ reference torso) ÷ (render hip width ÷ render torso), each torso the sequence's median mid_hip → mid_shoulder. A reference track not in picture px is refused (`COMPARE_CLOTH_UNITS`). The `CLOTH` lines print both thresholds and how the render's was carried.
|
|
453
456
|
- **The bands**, per window, around the reference's own figure: lag ± 1.0 frame; swing ratio ± 25 %; overshoot ± 0.10, absolute, floored at 0. A render figure outside its band is a finding over the window's frames. A window with no stroke on one side is listed as not measured.
|
|
454
457
|
- A window is `t0-t1` in seconds, and windows are named A, B, … in the order given. A window's frames are `round((t − t₀) × fps)` up to the end, exclusive. A window of 12 frames or fewer is refused, since the lag search reaches 12.
|
|
455
458
|
|
|
@@ -467,7 +470,7 @@ Numbers are rounded to 4 decimals, once, at the writer.
|
|
|
467
470
|
|
|
468
471
|
`--sheet <png>` draws the reference skeleton beside the render skeleton at each finding's worst frame, one row per finding, with the finding's own points marked in yellow. It never draws a video frame. A cloth finding is a figure over a window, with no single frame, so it has no row. When no finding has a frame, no sheet is written and the run says so.
|
|
469
472
|
|
|
470
|
-
**The control** is the reference compared with itself. It must report no finding in any unit, and every summary line says `0 findings`. On
|
|
473
|
+
**The control** is the reference compared with itself. It must report no finding in any unit, and every summary line says `0 findings`. On `dance-a` (313 frames, RTMPose, with its whole-body hand points and the retarget's report) it does.
|
|
471
474
|
|
|
472
475
|
Exit codes: `0` compared, whether it found anything or not, since a finding is not a failure; `1` refused, with every reason printed; `2` bad invocation.
|
|
473
476
|
|
|
@@ -477,9 +480,9 @@ Exit codes: `0` compared, whether it found anything or not, since a finding is n
|
|
|
477
480
|
|
|
478
481
|
## Why alternate drawings (0.2)
|
|
479
482
|
|
|
480
|
-
A flat painting turned by its bones matches each limb's direction, not its depth. On a
|
|
483
|
+
A flat painting turned by its bones matches each limb's direction, not its depth. On `dance-a` (a 13 s front-facing dance), retargeted onto a @rig-c/parts rig, five moments were found where it cannot follow. They were found by measuring the pose against the rig and checked against the video:
|
|
481
484
|
|
|
482
|
-
| | t (s) | what the
|
|
485
|
+
| | t (s) | what the subject does | why bone rotation cannot follow |
|
|
483
486
|
| --- | --- | --- | --- |
|
|
484
487
|
| S1 | 0.0–1.1 | hands clasped behind the back | depth order: the forearms go behind the body |
|
|
485
488
|
| S2 | 1.0–2.25 | a hand to the ear, then the forearm toward the camera | foreshortening (the upper arm at 0.27 of its length) and depth order |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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/legs.ts
CHANGED
|
@@ -772,6 +772,8 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
772
772
|
let mode: Mode | null = null;
|
|
773
773
|
let from: Mode | null = null;
|
|
774
774
|
let blendAt = -1;
|
|
775
|
+
let blendEnd = -1;
|
|
776
|
+
let blendToFlight = false;
|
|
775
777
|
for (let i = 0; i < n; i++) {
|
|
776
778
|
const s = feet.support[i] ?? null;
|
|
777
779
|
const tr = s === null ? (tracked[i] ?? null) : null;
|
|
@@ -793,6 +795,10 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
793
795
|
from = mode;
|
|
794
796
|
mode = plant(i, s, [here[0] + o[0], here[1] + o[1]]);
|
|
795
797
|
blendAt = i;
|
|
798
|
+
blendEnd = i;
|
|
799
|
+
while (blendEnd + 1 < n && (feet.support[blendEnd + 1] ?? null) === s) blendEnd++;
|
|
800
|
+
blendToFlight =
|
|
801
|
+
blendEnd + 1 < n && (feet.support[blendEnd + 1] ?? null) === null && (tracked[blendEnd + 1] ?? null) === null;
|
|
796
802
|
}
|
|
797
803
|
} else if (tr !== null) {
|
|
798
804
|
if (mode!.kind !== 'track' || mode!.side !== tr) {
|
|
@@ -809,7 +815,8 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
809
815
|
}
|
|
810
816
|
const to = of(mode!, i);
|
|
811
817
|
if (from !== null && i - blendAt < PLANT_BLEND) {
|
|
812
|
-
const
|
|
818
|
+
const stepIn = i - blendAt + 1;
|
|
819
|
+
const w = (blendToFlight && blendEnd - blendAt < PLANT_BLEND ? Math.min(stepIn, blendEnd - i + 1) : stepIn) / (PLANT_BLEND + 1);
|
|
813
820
|
const f = of(from, i);
|
|
814
821
|
// #102: blend y only; x takes the new derivation immediately so the planted foot does not slide across the blend.
|
|
815
822
|
hip.push([to[0], f[1] + (to[1] - f[1]) * w]);
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Add report coordinates without touching the retarget or any source value. */
|
|
2
|
+
import type { JointMap } from './map.ts';
|
|
3
|
+
import { isObject, Problems, Refusal } from './problems.ts';
|
|
4
|
+
|
|
5
|
+
export type ReportClock={frameTimes:readonly number[]}|{fps:number;frame0:number;frames:number};
|
|
6
|
+
export interface RenderDomain {fps:number;frames:number}
|
|
7
|
+
export interface ReportInput {path:string;sha256:string}
|
|
8
|
+
export interface ReportInputs {rig:ReportInput;map:ReportInput;pose:ReportInput}
|
|
9
|
+
function fail(field:string,text:string):never {throw new Refusal([{code:'REPORT_TIMEBASE',text:`${field}: ${text}`}]);}
|
|
10
|
+
function finite(value:unknown,field:string):number {if(typeof value!=='number'||!Number.isFinite(value)||value<0)return fail(field,`found ${String(value)}; expected finite seconds >= 0`);return value;}
|
|
11
|
+
function integer(value:unknown,field:string):number {const n=finite(value,field);if(!Number.isSafeInteger(n))fail(field,`found ${n}; expected nonnegative safe integer`);return n;}
|
|
12
|
+
export function reportFrameTimes(clock:ReportClock):number[] {
|
|
13
|
+
if('frameTimes' in clock){
|
|
14
|
+
if(!Array.isArray(clock.frameTimes))fail('frameTimes','expected an explicit array');
|
|
15
|
+
const values=clock.frameTimes.map((time,i)=>finite(time,`frameTimes[${i}]`));
|
|
16
|
+
if(!values.length)fail('frameTimes','expected at least one explicit frame');
|
|
17
|
+
for(let i=1;i<values.length;i++)if(values[i]!<=values[i-1]!)fail(`frameTimes[${i}]`,`found ${values[i]}; must follow ${values[i-1]}`);return values;
|
|
18
|
+
}
|
|
19
|
+
const fps=finite(clock.fps,'fps');if(fps===0)fail('fps','expected positive explicit rate');const frame0=finite(clock.frame0,'frame0'),count=integer(clock.frames,'frames');
|
|
20
|
+
if(count===0)fail('frames','expected at least one frame');const times=Array.from({length:count},(_,i)=>frame0+i/fps);
|
|
21
|
+
for(let i=0;i<times.length;i++){finite(times[i],`frame ${i}`);if(i>0&×[i]!<=times[i-1]!)fail(`frame ${i}`,'uniform clock cannot represent distinct frame samples');}return times;
|
|
22
|
+
}
|
|
23
|
+
export function reportRenderDomain(input:unknown,animation:string):RenderDomain {
|
|
24
|
+
if(!isObject(input)||!Array.isArray(input.sets))fail('render.sets','expected renderer frame-set metadata');
|
|
25
|
+
const set=input.sets.find((value:unknown)=>isObject(value)&&value.animation===animation);
|
|
26
|
+
if(!isObject(set))fail('render.animation',`missing ${JSON.stringify(animation)} frame-set metadata`);
|
|
27
|
+
const fps=finite(set.fps,'render.fps'),frames=integer(set.sampled,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');return {fps,frames};
|
|
28
|
+
}
|
|
29
|
+
function clone(value:unknown):unknown {if(Array.isArray(value))return value.map(clone);if(isObject(value))return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,clone(item)]));return value;}
|
|
30
|
+
|
|
31
|
+
export function enrichReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null=null,inputs?:ReportInputs):Record<string,unknown> {
|
|
32
|
+
const times=reportFrameTimes(clock),report=clone(input) as Record<string,unknown>;
|
|
33
|
+
if(render!==null){const fps=finite(render.fps,'render.fps'),frames=integer(render.frames,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');}
|
|
34
|
+
if(report.frames!==times.length)fail('frames',`report has ${String(report.frames)}, clock has ${times.length}`);
|
|
35
|
+
const problems=new Problems();
|
|
36
|
+
const time=(frame:unknown,where:string):number=>{const n=integer(frame,where);if(n>=times.length)fail(where,`frame ${n} outside ${times.length} declared frames`);return times[n]!;};
|
|
37
|
+
const add=(row:Record<string,unknown>,key:string,value:unknown,where:string)=>{
|
|
38
|
+
if(row[key]!==undefined&&JSON.stringify(row[key])!==JSON.stringify(value))problems.add('REPORT_COORDINATE',`${where}.${key}: supplied ${JSON.stringify(row[key])}, expected ${JSON.stringify(value)}`);
|
|
39
|
+
else row[key]=value;
|
|
40
|
+
};
|
|
41
|
+
// A joint can influence several mapped segments. Preserve all actual matches;
|
|
42
|
+
// only a unique match may occupy the singular bone field.
|
|
43
|
+
const bonesFor=(joint:string)=>{const bones=new Set<string>();if(map.translate?.joint===joint)bones.add(map.translate.bone);for(const chain of map.chains)for(let i=0;i<chain.bones.length;i++)if(chain.joints[i]===joint||chain.joints[i+1]===joint)bones.add(chain.bones[i]!);return [...bones];};
|
|
44
|
+
const pair=(value:unknown,where:string):[number,number]=>{if(!Array.isArray(value)||value.length!==2)fail(where,'expected inclusive [first,last] frame pair');const first=integer(value[0],where+'[0]'),last=integer(value[1],where+'[1]');if(last<first)fail(where,`found [${first},${last}]; inclusive last must be >= first`);return [first,last];};
|
|
45
|
+
const poseEvent=(where:string)=>/^report\.(?:holds|short|endOn|bends|flips|turns|range|artRange|raised|byHip|landings|shear|switches|cleaned\.(?:holds|seams|spikes\.found)|feet\.(?:supports|changes|steps|lowered|tracked|shifts)|stance\.(?:runs|over))\[\d+\]$/.test(where);
|
|
46
|
+
const targets=(row:Record<string,unknown>,where:string)=>{
|
|
47
|
+
if(row.bone!==undefined||row.slot!==undefined)return;
|
|
48
|
+
const joints=typeof row.joint==='string'?[row.joint]:typeof row.point==='string'?[row.point]:Array.isArray(row.joints)?row.joints.filter((v):v is string=>typeof v==='string'):[];
|
|
49
|
+
const chain=typeof row.chain==='string'?map.chains.find(c=>c.name===row.chain):undefined;
|
|
50
|
+
const bones=[...new Set(chain?chain.bones:joints.flatMap(bonesFor))];
|
|
51
|
+
if(bones.length===1)add(row,'bone',bones[0],where);else if(bones.length>1)add(row,'bones',bones,where);
|
|
52
|
+
// Multiple affected targets remain explicit lists, never a guessed winner.
|
|
53
|
+
if(typeof row.point==='string')add(row,'joint',row.point,where);
|
|
54
|
+
};
|
|
55
|
+
const visit=(value:unknown,where:string):void=>{
|
|
56
|
+
if(Array.isArray(value)){value.forEach((row,i)=>visit(row,`${where}[${i}]`));return;}
|
|
57
|
+
if(!isObject(value))return;
|
|
58
|
+
// Preserve uncovered's discrete renderer times; they do not index the pose.
|
|
59
|
+
if(where.startsWith('report.uncovered[')||where.startsWith('report.folds[')){
|
|
60
|
+
if(!render)fail(where,'render-domain event requires declared renderer metadata');
|
|
61
|
+
add(value,'domain','render',where);return;
|
|
62
|
+
}
|
|
63
|
+
let coordinates=false;
|
|
64
|
+
if(poseEvent(where)&&value.frame!==undefined&&typeof value.frame!=='number')fail(where+'.frame','expected nonnegative integer pose frame');
|
|
65
|
+
if(typeof value.frame==='number'){add(value,'t',time(value.frame,where+'.frame'),where);coordinates=true;}
|
|
66
|
+
if((poseEvent(where)&&value.frames!==undefined)||(Array.isArray(value.frames)&&value.frames.length===2&&value.frames.every(frame=>typeof frame==='number'))){const frames=pair(value.frames,where+'.frames');add(value,'times',frames.map((frame,i)=>time(frame,`${where}.frames[${i}]`)),where);coordinates=true;}
|
|
67
|
+
if(where.startsWith('report.holds[')&&typeof value.from==='number'&&typeof value.to==='number'){
|
|
68
|
+
pair([value.from,value.to],where+'.from/to');add(value,'frames',[value.from,value.to],where);add(value,'times',[time(value.from,where+'.from'),time(value.to,where+'.to')],where);coordinates=true;
|
|
69
|
+
}
|
|
70
|
+
if(coordinates){if(typeof value.seconds==='number')add(value,'spanSeconds',value.seconds,where);targets(value,where);}
|
|
71
|
+
// Recurse over the original fields only; added time/target arrays are data.
|
|
72
|
+
for(const [key,item] of Object.entries(value))if(!['t','times','bones','spanSeconds'].includes(key))visit(item,where+'.'+key);
|
|
73
|
+
};
|
|
74
|
+
visit(report,'report');
|
|
75
|
+
if(isObject(report.feet)&&Array.isArray(report.feet.jumps)){
|
|
76
|
+
add(report.feet,'jumpEvents',report.feet.jumps.map((frames:unknown,index:number)=>{
|
|
77
|
+
const interval=pair(frames,`feet.jumps[${index}]`);
|
|
78
|
+
return {frames:[...interval],times:interval.map((frame,i)=>time(frame,`feet.jumps[${index}][${i}]`))};
|
|
79
|
+
}),'report.feet');
|
|
80
|
+
}
|
|
81
|
+
// The actual producer uses frameTimes. Uniform clocks exist only when a
|
|
82
|
+
// caller explicitly supplies fps/frame0, never inferred from samples.
|
|
83
|
+
if('frameTimes' in clock)add(report,'frameTimes',[...times],'report');else{add(report,'fps',clock.fps,'report');add(report,'frame0',clock.frame0,'report');}
|
|
84
|
+
add(report,'render',render,'report');
|
|
85
|
+
if(inputs!==undefined){if(!isObject(inputs))throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);for(const key of ['rig','map','pose'] as const){const entry=inputs[key];if(!isObject(entry)||typeof entry.path!=='string'||!entry.path||entry.path.startsWith('/')||entry.path.charCodeAt(0)===92||/^[A-Za-z]:/.test(entry.path)||typeof entry.sha256!=='string'||! /^[a-f0-9]{64}$/.test(entry.sha256))throw new Refusal([{code:'REPORT_INPUTS',text:`inputs.${key}: expected invocation-relative consumed file path and exact SHA-256`}]);}add(report,'inputs',clone(inputs),'report');}
|
|
86
|
+
problems.throwIfAny();return report;
|
|
87
|
+
}
|
|
88
|
+
export function writeReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null,inputs:ReportInputs):string {if(inputs===undefined)throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);return JSON.stringify(enrichReport(input,map,clock,render,inputs),null,2)+'\n';}
|
package/src/retarget.ts
CHANGED
|
@@ -1582,9 +1582,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1582
1582
|
}
|
|
1583
1583
|
};
|
|
1584
1584
|
/**
|
|
1585
|
-
* #188: For a `set.symmetric === true` gesture set,
|
|
1586
|
-
* on frame `i`
|
|
1587
|
-
*
|
|
1585
|
+
* #188 / #207: For a `set.symmetric === true` gesture set, solve its two two-bone arm chains
|
|
1586
|
+
* on frame `i` in the chest-local frame so the diamond is symmetric about the torso axis and
|
|
1587
|
+
* torso lean carries the whole diamond without collapsing one arm into the ±20° turn clamp.
|
|
1588
1588
|
*/
|
|
1589
1589
|
const solveSymmetricFrame = (set: (typeof map.alternates)[number], i: number): { cR: Chain; cL: Chain; wR: Pt; wL: Pt; hc: Pt; ed: number } | null => {
|
|
1590
1590
|
if (set.chains.length !== 2) return null;
|
|
@@ -1626,6 +1626,21 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1626
1626
|
ed = d;
|
|
1627
1627
|
}
|
|
1628
1628
|
}
|
|
1629
|
+
let hc0: Pt = [(SR0[0] + SL0[0]) / 2, (SR0[1] + SL0[1]) / 2 + 0.85 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1])];
|
|
1630
|
+
let ed0 = Math.max(1e-3, 0.30 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1]));
|
|
1631
|
+
if (gestureHead !== null) {
|
|
1632
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
1633
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
1634
|
+
if (ha0 !== null && hb0 !== null) {
|
|
1635
|
+
const d0 = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1636
|
+
if (d0 > 1e-6) {
|
|
1637
|
+
hc0 = [(ha0[0] + hb0[0]) / 2, (ha0[1] + hb0[1]) / 2];
|
|
1638
|
+
ed0 = d0;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const above0 = mappedAbove(bR0);
|
|
1643
|
+
const chestTurn = above0 === null ? 0 : turn(above0, i);
|
|
1629
1644
|
const drawnR0 = set.drawn[bR0]!;
|
|
1630
1645
|
const drawnR1 = set.drawn[bR1]!;
|
|
1631
1646
|
const drawnL0 = set.drawn[bL0]!;
|
|
@@ -1634,19 +1649,39 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1634
1649
|
const turnR1 = set.turn[bR1] ?? [-20, 20];
|
|
1635
1650
|
const turnL0 = set.turn[bL0] ?? [-20, 20];
|
|
1636
1651
|
const turnL1 = set.turn[bL1] ?? [-20, 20];
|
|
1637
|
-
const
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
let
|
|
1642
|
-
|
|
1652
|
+
const erDrawnX = SR0[0] + lR0 * Math.cos((drawnR0 + dR0) / DEG);
|
|
1653
|
+
const elDrawnX = SL0[0] + lL0 * Math.cos((drawnL0 + dL0) / DEG);
|
|
1654
|
+
let aR0_c: number;
|
|
1655
|
+
let aL0_c: number;
|
|
1656
|
+
let aR1_c: number;
|
|
1657
|
+
let aL1_c: number;
|
|
1658
|
+
if (Math.abs(wrap(drawnR0 + drawnL0 - 180)) > 1 && (hc0[0] - erDrawnX > lR1 || elDrawnX - hc0[0] > lL1)) {
|
|
1659
|
+
const eyTarget = hc0[1] - 0.30 * ed0;
|
|
1660
|
+
const sR = Math.max(-1, Math.min(1, (eyTarget - SR0[1]) / Math.max(1e-6, lR0)));
|
|
1661
|
+
const sL = Math.max(-1, Math.min(1, (eyTarget - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1662
|
+
const wantR0 = wrap(180 - Math.asin(sR) * DEG - dR0);
|
|
1663
|
+
const wantL0 = wrap(Math.asin(sL) * DEG - dL0);
|
|
1664
|
+
const offR0_c = Math.min(Math.max(wrap(wantR0 - drawnR0), turnR0[0]), turnR0[1]);
|
|
1665
|
+
const offL0_c = Math.min(Math.max(wrap(wantL0 - drawnL0), turnL0[0]), turnL0[1]);
|
|
1666
|
+
aR0_c = wrap(drawnR0 + offR0_c);
|
|
1667
|
+
aL0_c = wrap(drawnL0 + offL0_c);
|
|
1668
|
+
const offR1_c = Math.min(Math.max(-0.35 * offR0_c, turnR1[0]), turnR1[1]);
|
|
1669
|
+
const offL1_c = Math.min(Math.max(-0.35 * offL0_c, turnL1[0]), turnL1[1]);
|
|
1670
|
+
aR1_c = wrap(drawnR1 + offR1_c);
|
|
1671
|
+
aL1_c = wrap(drawnL1 + offL1_c);
|
|
1672
|
+
} else {
|
|
1673
|
+
const rawR0 = wrap((rawTargets.get(bR0)?.[i] ?? drawnR0) - chestTurn);
|
|
1674
|
+
const rawL0 = wrap((rawTargets.get(bL0)?.[i] ?? drawnL0) - chestTurn);
|
|
1675
|
+
const offR0 = wrap(rawR0 - drawnR0);
|
|
1676
|
+
const offL0 = wrap(rawL0 - drawnL0);
|
|
1677
|
+
const cOpen = Math.max(-4, Math.min(2, 0.15 * 0.5 * (offR0 - offL0)));
|
|
1643
1678
|
const evalDiff = (deg: number) => {
|
|
1644
1679
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1645
1680
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1646
|
-
const ar = drawnR0 + Math.min(Math.max(
|
|
1647
|
-
const al = drawnL0 + Math.min(Math.max(-
|
|
1648
|
-
const eyr =
|
|
1649
|
-
const eyl =
|
|
1681
|
+
const ar = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1682
|
+
const al = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1683
|
+
const eyr = SR0[1] + lR0 * Math.sin((ar + dR0) / DEG);
|
|
1684
|
+
const eyl = SL0[1] + lL0 * Math.sin((al + dL0) / DEG);
|
|
1650
1685
|
return eyl - eyr;
|
|
1651
1686
|
};
|
|
1652
1687
|
let lo = -45;
|
|
@@ -1671,68 +1706,73 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1671
1706
|
}
|
|
1672
1707
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1673
1708
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const eyR0 =
|
|
1677
|
-
const eyL0 =
|
|
1709
|
+
aR0_c = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1710
|
+
aL0_c = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1711
|
+
const eyR0 = SR0[1] + lR0 * Math.sin((aR0_c + dR0) / DEG);
|
|
1712
|
+
const eyL0 = SL0[1] + lL0 * Math.sin((aL0_c + dL0) / DEG);
|
|
1678
1713
|
if (Math.abs(eyL0 - eyR0) > 1e-4) {
|
|
1679
|
-
const sL = Math.max(-1, Math.min(1, (eyR0 -
|
|
1714
|
+
const sL = Math.max(-1, Math.min(1, (eyR0 - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1680
1715
|
const candL = wrap(Math.asin(sL) * DEG - dL0);
|
|
1681
1716
|
const tL = wrap(candL - drawnL0);
|
|
1682
|
-
if (tL >= turnL0[0] && tL <= turnL0[1])
|
|
1717
|
+
if (tL >= turnL0[0] && tL <= turnL0[1]) aL0_c = candL;
|
|
1683
1718
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
if (err < bestErr) {
|
|
1720
|
-
bestErr = err;
|
|
1721
|
-
bestA = candA;
|
|
1719
|
+
aR0_c = wrap(aR0_c);
|
|
1720
|
+
aL0_c = wrap(aL0_c);
|
|
1721
|
+
const rR0_c = (aR0_c + dR0) / DEG;
|
|
1722
|
+
const rL0_c = (aL0_c + dL0) / DEG;
|
|
1723
|
+
const exR_c = SR0[0] + lR0 * Math.cos(rR0_c);
|
|
1724
|
+
const eyR_c = SR0[1] + lR0 * Math.sin(rR0_c);
|
|
1725
|
+
const exL_c = SL0[0] + lL0 * Math.cos(rL0_c);
|
|
1726
|
+
const eyL_c = SL0[1] + lL0 * Math.sin(rL0_c);
|
|
1727
|
+
const aimFore0 = (ex: number, ey: number, l1: number, d1: number, drawn1: number, turn1: readonly [number, number], tx: number, ty: number): number => {
|
|
1728
|
+
const want = wrap(Math.atan2(ty - ey, tx - ex) * DEG - d1);
|
|
1729
|
+
const off = Math.min(Math.max(wrap(want - drawn1), turn1[0]), turn1[1]);
|
|
1730
|
+
let a1 = wrap(drawn1 + off);
|
|
1731
|
+
const r1 = (a1 + d1) / DEG;
|
|
1732
|
+
const wx = ex + l1 * Math.cos(r1);
|
|
1733
|
+
const wy = ey + l1 * Math.sin(r1);
|
|
1734
|
+
const xMin = hc0[0] - 0.30 * ed0;
|
|
1735
|
+
const xMax = hc0[0] + 0.30 * ed0;
|
|
1736
|
+
const yMin = hc0[1] + 0.15 * ed0;
|
|
1737
|
+
const yMax = hc0[1] + 0.95 * ed0;
|
|
1738
|
+
if (wx < xMin || wx > xMax || wy < yMin || wy > yMax) {
|
|
1739
|
+
let bestA = a1;
|
|
1740
|
+
let bestErr = Infinity;
|
|
1741
|
+
for (let step = -200; step <= 200; step++) {
|
|
1742
|
+
const candOff = turn1[0] + ((turn1[1] - turn1[0]) * (step + 200)) / 400;
|
|
1743
|
+
const candA = wrap(drawn1 + candOff);
|
|
1744
|
+
const cr = (candA + d1) / DEG;
|
|
1745
|
+
const cwx = ex + l1 * Math.cos(cr);
|
|
1746
|
+
const cwy = ey + l1 * Math.sin(cr);
|
|
1747
|
+
const ox = cwx < xMin ? xMin - cwx : cwx > xMax ? cwx - xMax : 0;
|
|
1748
|
+
const oy = cwy < yMin ? yMin - cwy : cwy > yMax ? cwy - yMax : 0;
|
|
1749
|
+
const err = (ox + oy) * 1000 + Math.hypot(cwx - tx, cwy - ty);
|
|
1750
|
+
if (err < bestErr) {
|
|
1751
|
+
bestErr = err;
|
|
1752
|
+
bestA = candA;
|
|
1753
|
+
}
|
|
1722
1754
|
}
|
|
1755
|
+
a1 = bestA;
|
|
1723
1756
|
}
|
|
1724
|
-
a1
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
const
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1757
|
+
return a1;
|
|
1758
|
+
};
|
|
1759
|
+
aR1_c = aimFore0(exR_c, eyR_c, lR1, dR1, drawnR1, turnR1, hc0[0] - 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1760
|
+
aL1_c = aimFore0(exL_c, eyL_c, lL1, dL1, drawnL1, turnL1, hc0[0] + 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1761
|
+
}
|
|
1762
|
+
const carry = 0.35 * chestTurn;
|
|
1763
|
+
const aR0 = wrap(aR0_c + carry);
|
|
1764
|
+
const aR1 = wrap(aR1_c + carry);
|
|
1765
|
+
const aL0 = wrap(aL0_c + carry);
|
|
1766
|
+
const aL1 = wrap(aL1_c + carry);
|
|
1767
|
+
const rR0 = (aR0 + dR0) / DEG;
|
|
1768
|
+
const rR1 = (aR1 + dR1) / DEG;
|
|
1769
|
+
const rL0 = (aL0 + dL0) / DEG;
|
|
1770
|
+
const rL1 = (aL1 + dL1) / DEG;
|
|
1771
|
+
const wR: Pt = [sr[0] + lR0 * Math.cos(rR0) + lR1 * Math.cos(rR1), sr[1] + lR0 * Math.sin(rR0) + lR1 * Math.sin(rR1)];
|
|
1772
|
+
const wL: Pt = [sl[0] + lL0 * Math.cos(rL0) + lL1 * Math.cos(rL1), sl[1] + lL0 * Math.sin(rL0) + lL1 * Math.sin(rL1)];
|
|
1773
|
+
targets.get(bR0)![i] = aR0;
|
|
1734
1774
|
targets.get(bR1)![i] = aR1;
|
|
1735
|
-
targets.get(bL0)![i] =
|
|
1775
|
+
targets.get(bL0)![i] = aL0;
|
|
1736
1776
|
targets.get(bL1)![i] = aL1;
|
|
1737
1777
|
return { cR, cL, wR, wL, hc, ed };
|
|
1738
1778
|
};
|
|
@@ -1889,6 +1929,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1889
1929
|
const heldMiss: number[] = [];
|
|
1890
1930
|
const isHeadAnchored = HEAD_ANCHOR_FRAME !== 'off' && set.anchor?.includes(c.name) === true;
|
|
1891
1931
|
if (isHeadAnchored) job.saturated = [];
|
|
1932
|
+
let prevUpperClampSign: -1 | 0 | 1 = 0;
|
|
1892
1933
|
for (const i of span) {
|
|
1893
1934
|
const h = pairOn(anchor, i);
|
|
1894
1935
|
const rc = refCentre(i);
|
|
@@ -1928,8 +1969,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1928
1969
|
let a1Cl = wrap(sol0.fore - d1);
|
|
1929
1970
|
if (turn0 !== undefined && set.drawn[b0] !== undefined) {
|
|
1930
1971
|
const tt0 = wrap(a0Cl - set.drawn[b0]!);
|
|
1931
|
-
|
|
1972
|
+
let cl0 = Math.min(Math.max(tt0, turn0[0]), turn0[1]);
|
|
1932
1973
|
if (Math.abs(cl0 - tt0) > RANGE_SLACK) {
|
|
1974
|
+
const sign0: 1 | -1 = cl0 < tt0 ? 1 : -1;
|
|
1975
|
+
if (place0.how === 'in' && prevUpperClampSign !== 0 && sign0 !== prevUpperClampSign) {
|
|
1976
|
+
cl0 = prevUpperClampSign > 0 ? turn0[1] : turn0[0];
|
|
1977
|
+
}
|
|
1933
1978
|
a0Cl = wrap(set.drawn[b0]! + cl0);
|
|
1934
1979
|
const r0Cl = (a0Cl + d0) / DEG;
|
|
1935
1980
|
const ECl: Pt = [shoulder[0] + l0 * Math.cos(r0Cl), shoulder[1] + l0 * Math.sin(r0Cl)];
|
|
@@ -1966,26 +2011,35 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1966
2011
|
upperAsked.push(0);
|
|
1967
2012
|
upperOver.push(0);
|
|
1968
2013
|
heldMiss.push(0);
|
|
2014
|
+
prevUpperClampSign = 0;
|
|
1969
2015
|
continue;
|
|
1970
2016
|
}
|
|
1971
2017
|
let a0 = wrap(sol.upper - d0);
|
|
1972
2018
|
let a1 = wrap(sol.fore - d1);
|
|
1973
2019
|
// The set's turn holds the upper arm; the forearm is then re-aimed at the placed wrist from where the held upper arm puts the elbow.
|
|
1974
2020
|
let held = false;
|
|
1975
|
-
|
|
1976
|
-
upperAsked.push(tt);
|
|
1977
|
-
upperOver.push(turn0 === undefined ? 0 : overflow(tt, turn0));
|
|
2021
|
+
let tt = turn0 === undefined ? 0 : wrap(a0 - set.drawn[b0]!);
|
|
1978
2022
|
if (turn0 !== undefined) {
|
|
1979
|
-
|
|
2023
|
+
let cl = Math.min(Math.max(tt, turn0[0]), turn0[1]);
|
|
1980
2024
|
if (Math.abs(cl - tt) > RANGE_SLACK) {
|
|
2025
|
+
const sign: 1 | -1 = cl < tt ? 1 : -1;
|
|
2026
|
+
if (place.how === 'in' && prevUpperClampSign !== 0 && sign !== prevUpperClampSign) {
|
|
2027
|
+
cl = prevUpperClampSign > 0 ? turn0[1] : turn0[0];
|
|
2028
|
+
tt += prevUpperClampSign * 360;
|
|
2029
|
+
}
|
|
1981
2030
|
held = true;
|
|
2031
|
+
prevUpperClampSign = cl < tt ? 1 : -1;
|
|
1982
2032
|
a0 = wrap(set.drawn[b0]! + cl);
|
|
1983
2033
|
job.upperHeld.push(i);
|
|
1984
2034
|
const r = (a0 + d0) / DEG;
|
|
1985
2035
|
const E: Pt = [shoulder[0] + l0 * Math.cos(r), shoulder[1] + l0 * Math.sin(r)];
|
|
1986
2036
|
a1 = wrap(Math.atan2(P[1] - E[1], P[0] - E[0]) * DEG - d1);
|
|
2037
|
+
} else {
|
|
2038
|
+
prevUpperClampSign = 0;
|
|
1987
2039
|
}
|
|
1988
2040
|
}
|
|
2041
|
+
upperAsked.push(tt);
|
|
2042
|
+
upperOver.push(turn0 === undefined ? 0 : overflow(tt, turn0));
|
|
1989
2043
|
// A forearm the turn still holds: the re-aim failed within the art's range — ART-RANGE.
|
|
1990
2044
|
const asked = turn1 === undefined ? 0 : wrap(a1 - set.drawn[b1]!);
|
|
1991
2045
|
const over = turn1 === undefined ? 0 : overflow(asked, turn1);
|
|
@@ -2193,7 +2247,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2193
2247
|
interpolateChainInJointSpace(ch, lo - 1, hi + 1);
|
|
2194
2248
|
}
|
|
2195
2249
|
}
|
|
2196
|
-
// #188 / #191 / #206: Cover the full shown/bridged span of symmetric sets, support-arm sets, and head-anchored chains after short interpolation.
|
|
2250
|
+
// #188 / #191 / #206 / #218: Cover the full shown/bridged span of symmetric sets, support-arm sets, and head-anchored chains after short interpolation.
|
|
2197
2251
|
for (let sIdx = 0; sIdx < sw.switches.length; sIdx++) {
|
|
2198
2252
|
const s = sw.switches[sIdx]!;
|
|
2199
2253
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
@@ -2202,9 +2256,21 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2202
2256
|
setSlots.length > 0 && setSlots.every(([sl, att]) => sw.shown.get(sl)?.[i] === att);
|
|
2203
2257
|
let loShow = s.frames[0];
|
|
2204
2258
|
while (loShow - 1 >= 0 && isSetShownAt(loShow - 1)) loShow--;
|
|
2259
|
+
if (
|
|
2260
|
+
loShow === s.frames[0] &&
|
|
2261
|
+
s.frames[0] > 0 &&
|
|
2262
|
+
set.anchor === undefined &&
|
|
2263
|
+
set.symmetric !== true &&
|
|
2264
|
+
set.support === undefined &&
|
|
2265
|
+
setSlots.some(([sl, att]) => sw.shown.get(sl)?.[s.frames[0] - 1] === att)
|
|
2266
|
+
) {
|
|
2267
|
+
loShow = s.frames[0] - 1;
|
|
2268
|
+
}
|
|
2205
2269
|
let hiShow = s.frames[1];
|
|
2206
2270
|
while (hiShow + 1 < n && isSetShownAt(hiShow + 1)) hiShow++;
|
|
2207
2271
|
if (set.anchor !== undefined && HEAD_ANCHOR_FRAME !== 'off') {
|
|
2272
|
+
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2273
|
+
const i0Bridge = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] : -1;
|
|
2208
2274
|
for (const cn of set.anchor) {
|
|
2209
2275
|
const c = map.chains.find((x) => x.name === cn);
|
|
2210
2276
|
if (!c || c.bones.length < 2) continue;
|
|
@@ -2229,12 +2295,22 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2229
2295
|
t1[i] = ha.a1;
|
|
2230
2296
|
}
|
|
2231
2297
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2298
|
+
if (i0Bridge >= 0 && prevSw!.chains.includes(cn) && s.frames[0] - i0Bridge >= 2) {
|
|
2299
|
+
const i1Bridge = s.frames[0];
|
|
2300
|
+
const steps = i1Bridge - i0Bridge;
|
|
2301
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2302
|
+
const b = c.bones[bIdx]!;
|
|
2303
|
+
const tg = targets.get(b)!;
|
|
2304
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2305
|
+
const startA = parentTg !== undefined ? wrap(tg[i0Bridge]! - parentTg[i0Bridge]!) : tg[i0Bridge]!;
|
|
2306
|
+
const endA = parentTg !== undefined ? wrap(tg[i1Bridge]! - parentTg[i1Bridge]!) : tg[i1Bridge]!;
|
|
2307
|
+
const total = wrap(endA - startA);
|
|
2308
|
+
for (let k = i0Bridge + 1; k < i1Bridge; k++) {
|
|
2309
|
+
const localA = wrap(startA + ((k - i0Bridge) / steps) * total);
|
|
2310
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
} else if (loC >= 2 && covered.get(cn)?.[loC - 1] === true) {
|
|
2238
2314
|
t0[loC - 1] = t0[loC - 2]!;
|
|
2239
2315
|
t1[loC - 1] = t1[loC - 2]!;
|
|
2240
2316
|
}
|
|
@@ -2335,14 +2411,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2335
2411
|
}
|
|
2336
2412
|
}
|
|
2337
2413
|
if (set.anchor === undefined && set.symmetric !== true && set.support === undefined && loShow < s.frames[0]) {
|
|
2414
|
+
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2415
|
+
const i0 = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] : -1;
|
|
2338
2416
|
for (const cn of s.chains) {
|
|
2339
2417
|
const c = map.chains.find((x) => x.name === cn);
|
|
2340
2418
|
if (!c || c.bones.length < 2) continue;
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2419
|
+
const chainSlots = setSlots.filter(([sl]) => {
|
|
2420
|
+
const sb = slots.get(sl)?.bone;
|
|
2421
|
+
return sb !== undefined && c.bones.includes(sb);
|
|
2422
|
+
});
|
|
2423
|
+
const chainShownInBridge =
|
|
2424
|
+
chainSlots.length === 0 ||
|
|
2425
|
+
chainSlots.some(([sl, att]) => {
|
|
2426
|
+
for (let k = Math.max(0, i0 + 1); k < s.frames[0]; k++) if (sw.shown.get(sl)?.[k] === att) return true;
|
|
2427
|
+
return false;
|
|
2428
|
+
});
|
|
2429
|
+
if (i0 >= 0 && prevSw!.chains.includes(cn) && s.frames[0] - i0 >= 2 && chainShownInBridge) {
|
|
2430
|
+
const i1 = s.frames[0];
|
|
2431
|
+
const steps = i1 - i0;
|
|
2432
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2433
|
+
const b = c.bones[bIdx]!;
|
|
2434
|
+
const tg = targets.get(b)!;
|
|
2435
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2436
|
+
const startA = parentTg !== undefined ? wrap(tg[i0]! - parentTg[i0]!) : tg[i0]!;
|
|
2437
|
+
const endA = parentTg !== undefined ? wrap(tg[i1]! - parentTg[i1]!) : tg[i1]!;
|
|
2438
|
+
const total = wrap(endA - startA);
|
|
2439
|
+
for (let k = i0 + 1; k < i1; k++) {
|
|
2440
|
+
const localA = wrap(startA + ((k - i0) / steps) * total);
|
|
2441
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
const b1 = c.bones[1]!;
|
|
2446
|
+
const t1 = targets.get(b1)!;
|
|
2447
|
+
const entryWorld1 = t1[s.frames[0]]!;
|
|
2448
|
+
for (let i = loShow; i < s.frames[0]; i++) {
|
|
2449
|
+
t1[i] = entryWorld1;
|
|
2450
|
+
}
|
|
2346
2451
|
}
|
|
2347
2452
|
}
|
|
2348
2453
|
}
|
package/src/run.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* measure where the rig landed, read the folds (and which no switch covers),
|
|
5
5
|
* and write — only when every check passed.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { tmpdir } from 'node:os';
|
|
9
|
-
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
10
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
12
|
import { cleanLine, inferredSource } from './clean.ts';
|
|
12
13
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
@@ -19,6 +20,7 @@ import { landed, measureFeet, measureLanding, measurePosture, measureShear, pose
|
|
|
19
20
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
21
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
22
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
23
|
+
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
22
24
|
import { bodyLine, faceLine } from './face.ts';
|
|
23
25
|
|
|
24
26
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -82,6 +84,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
82
84
|
}
|
|
83
85
|
args.throwIfAny();
|
|
84
86
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
87
|
+
const rigSnapshot = readReportInput(input.rig);
|
|
85
88
|
const base = baseMotionOf(input);
|
|
86
89
|
const animations = base.animations as Record<string, unknown>;
|
|
87
90
|
if (a.name in animations) refuse([{ code: 'MOTION_NAME_FREE', text: `the rig's motion spec already has an animation "${a.name}" (it has ${Object.keys(animations).join(', ')}); name this one with --name` }]);
|
|
@@ -90,8 +93,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
90
93
|
refuse([{ code: 'IDLE_NAME', text: `${a.idle !== undefined ? `--idle "${idleName}"` : '--carry-idle reads the animation "idle", and'} the rig's motion spec ${input.motion === null ? '— there is none, so it has no animations' : `${input.motion} has no animation "${idleName}" (it has ${Object.keys(animations).join(', ') || 'none'})`}; name the rig's idle with --idle` }]);
|
|
91
94
|
}
|
|
92
95
|
const fragment = a.merge === undefined ? [] : readFragment(parseJson(readFileSync(a.merge, 'utf8'), a.merge, 'MERGE_JSON'), a.merge);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
96
|
+
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
97
|
+
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
98
|
+
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
95
99
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
96
100
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
97
101
|
const frames = personTrack(seq, a.person, a.pose);
|
|
@@ -305,48 +309,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
305
309
|
for (const u of uncovered) say(` UNCOVERED mesh "${u.attachment}" (slot "${u.slot}"; chains ${u.chains.length === 0 ? 'none' : u.chains.join(', ')}) folds with no switch covering it on ${u.times.length} frame(s): t=${spans(u.times, 1 / fps)}`);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
|
-
if (a.
|
|
309
|
-
|
|
310
|
-
rmSync(a.build, { recursive: true, force: true });
|
|
311
|
-
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
312
|
-
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 }))]);
|
|
313
|
-
}
|
|
314
|
-
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
315
|
-
if (a.camera !== undefined) {
|
|
316
|
-
const ss = r.figureScale.frames.map((x) => x.s);
|
|
317
|
-
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
318
|
-
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
319
|
-
writeFileSync(a.camera, `${JSON.stringify({
|
|
320
|
-
spec: 'rig-pose-camera/1',
|
|
321
|
-
rig: input.name,
|
|
322
|
-
animation: a.name,
|
|
323
|
-
frames: r.figureScale.frames.map((f, i) => {
|
|
324
|
-
const cam = seq.camera?.[i];
|
|
325
|
-
const camS = cam?.s ?? 1;
|
|
326
|
-
return {
|
|
327
|
-
frame: f.frame,
|
|
328
|
-
t: f.t,
|
|
329
|
-
s: f.s,
|
|
330
|
-
rawS: f.rawS,
|
|
331
|
-
tx: cam?.tx ?? 0,
|
|
332
|
-
ty: cam?.ty ?? 0,
|
|
333
|
-
videoS: Number((f.s * camS).toFixed(6)),
|
|
334
|
-
};
|
|
335
|
-
}),
|
|
336
|
-
summary: {
|
|
337
|
-
minS,
|
|
338
|
-
maxS,
|
|
339
|
-
maxStep: r.figureScale.maxStep,
|
|
340
|
-
endOverStart: r.figureScale.endOverStart,
|
|
341
|
-
},
|
|
342
|
-
}, null, 2)}\n`);
|
|
343
|
-
}
|
|
344
|
-
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
345
|
-
rmSync(a.render, { recursive: true, force: true });
|
|
346
|
-
cpSync(f.frames, a.render, { recursive: true });
|
|
347
|
-
}
|
|
348
|
-
if (a.report !== undefined) {
|
|
349
|
-
writeFileSync(a.report, `${JSON.stringify({
|
|
312
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
313
|
+
const reportText = a.report === undefined ? null : writeReport({
|
|
350
314
|
spec: 'rig-pose-report/1',
|
|
351
315
|
rigc,
|
|
352
316
|
rig: input.name,
|
|
@@ -403,8 +367,51 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
403
367
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
404
368
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
405
369
|
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows },
|
|
370
|
+
}, map, { frameTimes: r.times }, f.meshes !== null && doc.slots.length > 0 ? reportRenderDomain(JSON.parse(readFileSync(join(f.frames, '..', 'frames.json'), 'utf8')), a.name) : null, {rig:rigSnapshot.identity,map:mapSnapshot.identity,pose:poseSnapshot.identity});
|
|
371
|
+
if (a.build !== undefined) {
|
|
372
|
+
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
373
|
+
rmSync(a.build, { recursive: true, force: true });
|
|
374
|
+
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
375
|
+
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 }))]);
|
|
376
|
+
}
|
|
377
|
+
// The kept build reads the rig again; its bytes must still match the report.
|
|
378
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
379
|
+
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
380
|
+
if (a.camera !== undefined) {
|
|
381
|
+
const ss = r.figureScale.frames.map((x) => x.s);
|
|
382
|
+
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
383
|
+
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
384
|
+
writeFileSync(a.camera, `${JSON.stringify({
|
|
385
|
+
spec: 'rig-pose-camera/1',
|
|
386
|
+
rig: input.name,
|
|
387
|
+
animation: a.name,
|
|
388
|
+
frames: r.figureScale.frames.map((f, i) => {
|
|
389
|
+
const cam = seq.camera?.[i];
|
|
390
|
+
const camS = cam?.s ?? 1;
|
|
391
|
+
return {
|
|
392
|
+
frame: f.frame,
|
|
393
|
+
t: f.t,
|
|
394
|
+
s: f.s,
|
|
395
|
+
rawS: f.rawS,
|
|
396
|
+
tx: cam?.tx ?? 0,
|
|
397
|
+
ty: cam?.ty ?? 0,
|
|
398
|
+
videoS: Number((f.s * camS).toFixed(6)),
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
summary: {
|
|
402
|
+
minS,
|
|
403
|
+
maxS,
|
|
404
|
+
maxStep: r.figureScale.maxStep,
|
|
405
|
+
endOverStart: r.figureScale.endOverStart,
|
|
406
|
+
},
|
|
406
407
|
}, null, 2)}\n`);
|
|
407
408
|
}
|
|
409
|
+
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
410
|
+
rmSync(a.render, { recursive: true, force: true });
|
|
411
|
+
cpSync(f.frames, a.render, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
if (a.report !== undefined && reportText !== null) writeFileSync(a.report, reportText);
|
|
414
|
+
|
|
408
415
|
if (a.camera !== undefined) say(` wrote ${a.camera} — camera track (${r.figureScale.frames.length} frames)`);
|
|
409
416
|
say(` wrote ${a.out} — animations: ${Object.keys(motion.animations).join(', ')}`);
|
|
410
417
|
return { lines, written: true };
|
|
@@ -794,3 +801,14 @@ export function notCleaned(pose: string, coco: boolean): string {
|
|
|
794
801
|
const what = coco ? `${pose} is COCO-17 (--coco), which carries no "cleaned" record` : `${pose} has no "cleaned" record`;
|
|
795
802
|
return `${what}; retarget reads a sequence rig-pose clean wrote (spikes, occlusion, smoothing; there is no flag to skip it). Run \`rig-pose clean ${coco ? '--coco ' : ''}--pose ${pose} --out ${out}\` (add --loop for one cycle of a gait) and retarget --pose ${out}`;
|
|
796
803
|
}
|
|
804
|
+
|
|
805
|
+
/** File-bound identities are assembled only in this existing IO boundary. */
|
|
806
|
+
export interface ReportInputSnapshot {file:string;bytes:Buffer;identity:ReportInput;size:number;mtimeMs:number}
|
|
807
|
+
export function readReportInput(file:string):ReportInputSnapshot {
|
|
808
|
+
const path=realpathSync(resolve(file)),before=statSync(path),bytes=readFileSync(path),after=statSync(path);
|
|
809
|
+
if(!before.isFile()||before.size!==after.size||before.mtimeMs!==after.mtimeMs)throw new Refusal([{code:'REPORT_INPUTS',text:`${file}: input changed while reading; expected one stable regular file`}]);
|
|
810
|
+
return {file:resolve(file),bytes,identity:{path:relative(process.cwd(),path).split('\\').join('/'),sha256:createHash('sha256').update(bytes).digest('hex')},size:after.size,mtimeMs:after.mtimeMs};
|
|
811
|
+
}
|
|
812
|
+
export function assertReportInputs(inputs:readonly ReportInputSnapshot[]):void {
|
|
813
|
+
for(const expected of inputs){const current=readReportInput(expected.file);if(current.identity.path!==expected.identity.path||current.identity.sha256!==expected.identity.sha256)throw new Refusal([{code:'REPORT_INPUTS_CHANGED',text:`${expected.identity.path}: consumed input changed before output writes; expected SHA-256 ${expected.identity.sha256}, found ${current.identity.sha256}`}]);}
|
|
814
|
+
}
|