@rig-c/pose 0.20.4 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/face.ts +221 -0
- package/src/legs.ts +60 -22
- package/src/map.ts +65 -2
- package/src/reduce.ts +24 -6
- package/src/retarget.ts +113 -12
- package/src/rigc.ts +10 -3
- package/src/run.ts +36 -6
- package/src/verify.ts +25 -1
package/README.md
CHANGED
|
@@ -143,6 +143,7 @@ and what the run cannot decide for you it lists:
|
|
|
143
143
|
- `SHEAR` — with `--leg-scale`: every scaled bone and every bone under one, how far its art is sheared and how long its world axes are against setup. A reading.
|
|
144
144
|
- `IDLE`, `IDLE-DROPPED`, `IDLE-KEPT-OUT`, `MERGE` — the rig's idle against the new animation, and what `--carry-idle` and `--merge` put back ([below](#the-idle-what-the-retarget-drops)).
|
|
145
145
|
- `REDUCE` — with `--reduce` ([below](#few-keys---reduce)): per bone, its keys before and after and its largest error, with the frame; then the total.
|
|
146
|
+
- `FACE` — with `face` in the map ([#138](https://github.com/rig-c/pose/issues/138)): per-frame yaw and pitch read from `nose` against the eye midpoint (`r_eye + l_eye`) and, when present, the ear midpoint (`r_ear + l_ear`), de-rolled by the eye line, then compressed into each configured axis's `[min, max]` range (`scale` and correlation `r`), and written as `face` in `--report`.
|
|
146
147
|
- `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.
|
|
147
148
|
|
|
148
149
|
`--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`, `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:
|
|
@@ -195,19 +196,19 @@ Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. O
|
|
|
195
196
|
|
|
196
197
|
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:
|
|
197
198
|
|
|
198
|
-
**`--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
|
|
199
|
+
**`--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 the rolling 25-frame median trunk length over the sequence median so residual camera zoom does not shrink earlier phrases against a zoomed-in ending, and the shared thigh + shin straight lengths are capped at the larger 95th-percentile whole-leg `hip → ankle` length so a drifting knee split does not inflate their sum, [#159](https://github.com/rig-c/pose/issues/159)), clamped to [min, max]. The card's range is `0.8,1.05`. A segment the map aims other than `"x"` is refused (`LEGS_MAP`). `LEG-SCALE` lists each segment's frames clamped at either limit.
|
|
199
200
|
|
|
200
|
-
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 φ)|. 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'
|
|
201
|
+
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 HQ's dancer 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.
|
|
201
202
|
|
|
202
203
|
**`--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:
|
|
203
204
|
|
|
204
205
|
- 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.
|
|
205
206
|
- 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.
|
|
206
|
-
- 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
|
|
207
|
+
- 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 HQ's dancer 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.
|
|
207
208
|
- **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.
|
|
208
209
|
- 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 the dancer the plant held the supporting ankle through a real step (frames 176–181, 579 → 718 px) and threw the hip sideways.
|
|
209
210
|
- **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.
|
|
210
|
-
- **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. `--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.
|
|
211
|
+
- **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.
|
|
211
212
|
- `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`).
|
|
212
213
|
- **`--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 with `--stance 0.07` and HQ's flags (`--smooth 7 --flip 90 --short 0.35 --bends clamp --leg-scale 0.8,1.05 --plant-feet --carry-idle`): on HQ's dancer, 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 a second sequence (Toca, 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.
|
|
213
214
|
- 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.
|
|
@@ -312,6 +313,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
312
313
|
⚠️ **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 (Toca Toca, 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.
|
|
313
314
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
314
315
|
- `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.
|
|
316
|
+
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] } }` (`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), compresses each axis into its declared range so peaks are scaled rather than clipped flat, and keys the named dial bone's `rotate` track (`FACE`).
|
|
315
317
|
|
|
316
318
|
### A joint's range: `range`
|
|
317
319
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/face.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Face direction (`rig-c/pose#138`): read yaw and pitch per frame from the five
|
|
3
|
+
* body-18 face points (`nose` against the eye midpoint `r_eye + l_eye` and,
|
|
4
|
+
* when present, the ear midpoint `r_ear + l_ear`, de-rolled by the eye line),
|
|
5
|
+
* and map each configured axis into its declared range by compression (never
|
|
6
|
+
* clipping, so motion changes at the extremes survive).
|
|
7
|
+
*/
|
|
8
|
+
import type { FaceAxis, FaceMap } from './map.ts';
|
|
9
|
+
|
|
10
|
+
type Pt = [number, number];
|
|
11
|
+
|
|
12
|
+
const DEG = 180 / Math.PI;
|
|
13
|
+
|
|
14
|
+
export interface FaceSpan {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
range: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FaceAxisReading {
|
|
21
|
+
bone: string;
|
|
22
|
+
limits: [number, number];
|
|
23
|
+
read: FaceSpan;
|
|
24
|
+
mapped: FaceSpan;
|
|
25
|
+
/** Compression factors [kNeg, kPos] in (0, 1] applied to negative and positive read angles. */
|
|
26
|
+
scale: [number, number];
|
|
27
|
+
/** Pearson correlation between the read angle and the mapped keys over the clip. */
|
|
28
|
+
corr: number;
|
|
29
|
+
/** Per-frame read angle in degrees, before range compression. */
|
|
30
|
+
raw: number[];
|
|
31
|
+
/** Per-frame mapped rotate key in degrees, rounded to 0.001°. */
|
|
32
|
+
keys: number[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FaceReading {
|
|
36
|
+
ears: boolean;
|
|
37
|
+
read: {
|
|
38
|
+
yaw: FaceSpan;
|
|
39
|
+
pitch: FaceSpan;
|
|
40
|
+
rawYaw: number[];
|
|
41
|
+
rawPitch: number[];
|
|
42
|
+
};
|
|
43
|
+
yaw?: FaceAxisReading;
|
|
44
|
+
pitch?: FaceAxisReading;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const r3 = (v: number): number => {
|
|
48
|
+
const r = Math.round(v * 1000) / 1000;
|
|
49
|
+
return Object.is(r, -0) ? 0 : r;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function median(xs: readonly number[]): number {
|
|
53
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
54
|
+
const m = s.length >> 1;
|
|
55
|
+
return s.length % 2 === 1 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function spanOf(xs: readonly number[]): FaceSpan {
|
|
59
|
+
let min = Infinity;
|
|
60
|
+
let max = -Infinity;
|
|
61
|
+
for (const x of xs) {
|
|
62
|
+
if (x < min) min = x;
|
|
63
|
+
if (x > max) max = x;
|
|
64
|
+
}
|
|
65
|
+
return { min: r3(min), max: r3(max), range: r3(max - min) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function corrOf(a: readonly number[], b: readonly number[]): number {
|
|
69
|
+
if (a.length < 2 || a.length !== b.length) return 1;
|
|
70
|
+
const ma = a.reduce((x, y) => x + y, 0) / a.length;
|
|
71
|
+
const mb = b.reduce((x, y) => x + y, 0) / b.length;
|
|
72
|
+
let num = 0;
|
|
73
|
+
let da = 0;
|
|
74
|
+
let db = 0;
|
|
75
|
+
for (let i = 0; i < a.length; i++) {
|
|
76
|
+
const xa = a[i]! - ma;
|
|
77
|
+
const xb = b[i]! - mb;
|
|
78
|
+
num += xa * xb;
|
|
79
|
+
da += xa * xa;
|
|
80
|
+
db += xb * xb;
|
|
81
|
+
}
|
|
82
|
+
const den = Math.sqrt(da * db);
|
|
83
|
+
return den > 1e-9 ? r3(num / den) : 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read per-frame yaw and pitch (degrees) from `nose`, `r_eye`, `l_eye`, and
|
|
88
|
+
* (when both are present) `r_ear` and `l_ear`.
|
|
89
|
+
*
|
|
90
|
+
* Sign convention (matching `rig-c` gallery `look` and the Part 1 range sweep):
|
|
91
|
+
* - `yaw > 0`: nose turned toward the subject's right (`r_eye`, viewer-left).
|
|
92
|
+
* - `pitch > 0`: nose tilted down (chin down / nod down).
|
|
93
|
+
*/
|
|
94
|
+
export function readFaceAngles(
|
|
95
|
+
nose: readonly Pt[],
|
|
96
|
+
rEye: readonly Pt[],
|
|
97
|
+
lEye: readonly Pt[],
|
|
98
|
+
rEar: readonly Pt[] | null,
|
|
99
|
+
lEar: readonly Pt[] | null,
|
|
100
|
+
): { ears: boolean; yaw: number[]; pitch: number[] } {
|
|
101
|
+
const n = nose.length;
|
|
102
|
+
const ears = rEar !== null && lEar !== null && rEar.length === n && lEar.length === n;
|
|
103
|
+
const xne: number[] = [];
|
|
104
|
+
const xna: number[] = [];
|
|
105
|
+
const yne: number[] = [];
|
|
106
|
+
const yna: number[] = [];
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < n; i++) {
|
|
109
|
+
const N = nose[i]!;
|
|
110
|
+
const Er = rEye[i]!;
|
|
111
|
+
const El = lEye[i]!;
|
|
112
|
+
const ex = El[0] - Er[0];
|
|
113
|
+
const ey = El[1] - Er[1];
|
|
114
|
+
const de = Math.hypot(ex, ey);
|
|
115
|
+
if (!(de > 1e-9)) {
|
|
116
|
+
xne.push(xne[i - 1] ?? 0);
|
|
117
|
+
yne.push(yne[i - 1] ?? 0);
|
|
118
|
+
if (ears) {
|
|
119
|
+
xna.push(xna[i - 1] ?? 0);
|
|
120
|
+
yna.push(yna[i - 1] ?? 0);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const ux = ex / de;
|
|
125
|
+
const uy = ey / de;
|
|
126
|
+
const vx = -uy;
|
|
127
|
+
const vy = ux;
|
|
128
|
+
const mex = 0.5 * (Er[0] + El[0]);
|
|
129
|
+
const mey = 0.5 * (Er[1] + El[1]);
|
|
130
|
+
xne.push(((mex - N[0]) * ux + (mey - N[1]) * uy) / de);
|
|
131
|
+
yne.push(((N[0] - mex) * vx + (N[1] - mey) * vy) / de);
|
|
132
|
+
if (ears) {
|
|
133
|
+
const Ar = rEar[i]!;
|
|
134
|
+
const Al = lEar[i]!;
|
|
135
|
+
const max = 0.5 * (Ar[0] + Al[0]);
|
|
136
|
+
const may = 0.5 * (Ar[1] + Al[1]);
|
|
137
|
+
const da = Math.max(de, (Al[0] - Ar[0]) * ux + (Al[1] - Ar[1]) * uy);
|
|
138
|
+
xna.push(((max - N[0]) * ux + (may - N[1]) * uy) / da);
|
|
139
|
+
yna.push(((N[0] - max) * vx + (N[1] - may) * vy) / de);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const yne0 = median(yne);
|
|
144
|
+
const yna0 = ears ? median(yna) : 0;
|
|
145
|
+
const yaw: number[] = [];
|
|
146
|
+
const pitch: number[] = [];
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
const ty = ears ? (2 * xne[i]! + xna[i]!) / 2 : 2 * xne[i]!;
|
|
149
|
+
const tp = ears ? (yne[i]! - yne0 + (yna[i]! - yna0)) / 2 : yne[i]! - yne0;
|
|
150
|
+
yaw.push(Math.atan(ty) * DEG);
|
|
151
|
+
pitch.push(Math.atan(tp) * DEG);
|
|
152
|
+
}
|
|
153
|
+
return { ears, yaw, pitch };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compress a read angle series into `axis.range` (`[min, max]` with `min <= 0 <= max`):
|
|
158
|
+
* negative readings scale by `kNeg = min(1, limits[0] / rawMin)` and positive
|
|
159
|
+
* readings by `kPos = min(1, limits[1] / rawMax)`, so peaks above the range are
|
|
160
|
+
* compressed to the limit rather than clipped flat, while motion already inside
|
|
161
|
+
* the range keeps unit gain (`1`).
|
|
162
|
+
*/
|
|
163
|
+
export function mapFaceAxis(raw: readonly number[], axis: FaceAxis): FaceAxisReading {
|
|
164
|
+
const limits = axis.range;
|
|
165
|
+
let rawMin = Infinity;
|
|
166
|
+
let rawMax = -Infinity;
|
|
167
|
+
for (const x of raw) {
|
|
168
|
+
if (x < rawMin) rawMin = x;
|
|
169
|
+
if (x > rawMax) rawMax = x;
|
|
170
|
+
}
|
|
171
|
+
const kNeg = rawMin < limits[0] && rawMin < -1e-9 ? limits[0] / rawMin : 1;
|
|
172
|
+
const kPos = rawMax > limits[1] && rawMax > 1e-9 ? limits[1] / rawMax : 1;
|
|
173
|
+
const keys = raw.map((x) => r3(x < 0 ? x * kNeg : x * kPos));
|
|
174
|
+
return {
|
|
175
|
+
bone: axis.bone,
|
|
176
|
+
limits: [limits[0], limits[1]],
|
|
177
|
+
read: spanOf(raw),
|
|
178
|
+
mapped: spanOf(keys),
|
|
179
|
+
scale: [r3(kNeg), r3(kPos)],
|
|
180
|
+
corr: corrOf(raw, keys),
|
|
181
|
+
raw: [...raw],
|
|
182
|
+
keys,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function readFace(
|
|
187
|
+
face: FaceMap,
|
|
188
|
+
nose: readonly Pt[],
|
|
189
|
+
rEye: readonly Pt[],
|
|
190
|
+
lEye: readonly Pt[],
|
|
191
|
+
rEar: readonly Pt[] | null,
|
|
192
|
+
lEar: readonly Pt[] | null,
|
|
193
|
+
): FaceReading {
|
|
194
|
+
const { ears, yaw, pitch } = readFaceAngles(nose, rEye, lEye, rEar, lEar);
|
|
195
|
+
return {
|
|
196
|
+
ears,
|
|
197
|
+
read: {
|
|
198
|
+
yaw: spanOf(yaw),
|
|
199
|
+
pitch: spanOf(pitch),
|
|
200
|
+
rawYaw: yaw,
|
|
201
|
+
rawPitch: pitch,
|
|
202
|
+
},
|
|
203
|
+
...(face.yaw === undefined ? {} : { yaw: mapFaceAxis(yaw, face.yaw) }),
|
|
204
|
+
...(face.pitch === undefined ? {} : { pitch: mapFaceAxis(pitch, face.pitch) }),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const f1 = (x: number): string => x.toFixed(1);
|
|
209
|
+
const sg = (x: number): string => `${x >= 0 ? '+' : '−'}${Math.abs(x).toFixed(1)}`;
|
|
210
|
+
|
|
211
|
+
export function faceLine(f: FaceReading): string {
|
|
212
|
+
const joints = f.ears ? 'nose against r_eye + l_eye, r_ear + l_ear' : 'nose against r_eye + l_eye (ears absent)';
|
|
213
|
+
const readPart = `read yaw ${sg(f.read.yaw.min)}° … ${sg(f.read.yaw.max)}° (${f1(f.read.yaw.range)}°), pitch ${sg(f.read.pitch.min)}° … ${sg(f.read.pitch.max)}° (${f1(f.read.pitch.range)}°)`;
|
|
214
|
+
const axes: string[] = [];
|
|
215
|
+
for (const name of ['yaw', 'pitch'] as const) {
|
|
216
|
+
const ax = f[name];
|
|
217
|
+
if (ax === undefined) continue;
|
|
218
|
+
axes.push(`${name} → bone "${ax.bone}" [${ax.limits[0]}, ${ax.limits[1]}]°: ${sg(ax.mapped.min)}° … ${sg(ax.mapped.max)}° (scale ${ax.scale[0].toFixed(3)}/${ax.scale[1].toFixed(3)}, r=${ax.corr.toFixed(3)})`);
|
|
219
|
+
}
|
|
220
|
+
return `${joints}: ${readPart}; mapped ${axes.join(', ')}`;
|
|
221
|
+
}
|
package/src/legs.ts
CHANGED
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
* x axis, and a child with the `normal` inherit mode takes the parent's whole
|
|
13
13
|
* world matrix — so under a thigh scaled by s, a shin at angle φ in the thigh's
|
|
14
14
|
* axes points at atan2(sin φ, s·cos φ), not φ, and its own length is scaled by
|
|
15
|
-
* |(s·cos φ, sin φ)|.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (`
|
|
15
|
+
* |(s·cos φ, sin φ)|. A child with `noScale` or `noScaleOrReflection`
|
|
16
|
+
* (rig-c/pose#153) still has its direction carried through the parent's scaled
|
|
17
|
+
* matrix, so its angle is skewed the same way, while rig-c normalises that
|
|
18
|
+
* direction to unit length so the parent's scale is not inherited. The rotate
|
|
19
|
+
* key `turn − turn above` assumes unit scale. A mapped bone below a scaled bone
|
|
20
|
+
* is therefore keyed through its parent's posed matrix (`posePath`): with
|
|
21
|
+
* rig-c's semantics, a `rotate` key k and a `scalex` key s' put its world x
|
|
22
|
+
* axis on the target angle at the asked length (under `noScale`, s' is the
|
|
23
|
+
* bone's own asked scale s directly). Its art is still sheared by a `normal`
|
|
24
|
+
* parent's scale; that is measured, not hidden (`shear` in the report).
|
|
23
25
|
*
|
|
24
26
|
* **Planted feet.** The hip is no longer the pose's mid_hip travel scaled. On
|
|
25
27
|
* every frame one foot supports: the one whose reference ankle is lower in the
|
|
@@ -35,7 +37,9 @@
|
|
|
35
37
|
* picture y over ± `FLOOR_SECONDS`), for ≥ `JUMP_FRAMES` frames, excluding a
|
|
36
38
|
* hand-over whose back ankle stays up and holds its height in y (`< STILL_SPEED`
|
|
37
39
|
* for `STILL_FRAMES` frames) when the front foot lands (a raised heel,
|
|
38
|
-
* rig-c/pose#101)
|
|
40
|
+
* rig-c/pose#101), and starting from the first preceding push-off frame where
|
|
41
|
+
* both ankles are already above their rolling floor while mid_hip lags
|
|
42
|
+
* (rig-c/pose#142) — releases the plant: the hip follows the pose's travel from
|
|
39
43
|
* where it was, and re-plants on landing. A step does too: while the supporting ankle moves more than
|
|
40
44
|
* `STEP_SPEED` leg lengths a frame its plant is released, the other foot takes
|
|
41
45
|
* support if it is planted (not released, not moving) and otherwise the hip
|
|
@@ -162,12 +166,37 @@ export interface PathPose {
|
|
|
162
166
|
solved: Map<string, { rotate: number; scale: number }>;
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
/**
|
|
170
|
+
* World 2×2 of a `noScale` or `noScaleOrReflection` bone under `Wp` at rotate
|
|
171
|
+
* key `k` (degrees) and scalex key `sk` (rig-c/pose#153), matching rig-c's
|
|
172
|
+
* `modeMatrix` (`rigc/src/core/world.ts`): the bone's local x direction is
|
|
173
|
+
* rotated by `k`, carried through `Wp` and normalised to unit length before its
|
|
174
|
+
* setup local frame (`F0`) and `sk` are applied.
|
|
175
|
+
*/
|
|
176
|
+
export function poseNoScale(Wp: M2, Wp0: M2, W0: M2, inherit: 'noScale' | 'noScaleOrReflection', k: number, sk: number): M2 {
|
|
177
|
+
const L0 = mul(inv(Wp0), W0);
|
|
178
|
+
const l0: Pt = [L0[0], L0[2]];
|
|
179
|
+
const len0 = Math.hypot(W0[0], W0[2]);
|
|
180
|
+
const u0: Pt = [W0[0] / len0, W0[2] / len0];
|
|
181
|
+
const flip0 = inherit === 'noScale' && Wp0[0] * Wp0[3] - Wp0[1] * Wp0[2] < 0 ? -1 : 1;
|
|
182
|
+
const turned0: M2 = [u0[0], -u0[1] * flip0, u0[1], u0[0] * flip0];
|
|
183
|
+
const F0 = mul(inv(turned0), W0);
|
|
184
|
+
const u = apply(Wp, apply(rot(k), l0));
|
|
185
|
+
const invLen = 1 / Math.sqrt(u[0] * u[0] + u[1] * u[1]);
|
|
186
|
+
const ux = u[0] * invLen;
|
|
187
|
+
const uy = u[1] * invLen;
|
|
188
|
+
const flip = inherit === 'noScale' && Wp[0] * Wp[3] - Wp[1] * Wp[2] < 0 ? -1 : 1;
|
|
189
|
+
const turned: M2 = [ux, -uy * flip, uy, ux * flip];
|
|
190
|
+
return mul(mul(turned, F0), scaleX(sk));
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
/**
|
|
166
194
|
* Pose a path of bones, parents first, the first the path's top (its origin is
|
|
167
195
|
* (0, 0)). A mapped bone with no scaled bone above it turns rigidly from setup
|
|
168
196
|
* (R(turn) · setup world · diag(scale, 1)); a mapped bone under a scaled bone
|
|
169
197
|
* is solved through its parent's posed matrix (the header); an unmapped bone
|
|
170
|
-
* keeps its setup local matrix
|
|
198
|
+
* keeps its setup local matrix (or its `noScale` unit frame, #153). Every setup
|
|
199
|
+
* matrix and origin is rig-c's.
|
|
171
200
|
*/
|
|
172
201
|
export function posePath(rest: Rest, path: readonly string[], posing: (b: string) => Posing | null, scaleOf: (b: string) => number | null): PathPose | string {
|
|
173
202
|
const origin = new Map<string, Pt>();
|
|
@@ -197,7 +226,8 @@ export function posePath(rest: Rest, path: readonly string[], posing: (b: string
|
|
|
197
226
|
origin.set(b, [po[0] + o[0], po[1] + o[1]]);
|
|
198
227
|
const L0 = mul(inv(Wp0), W0);
|
|
199
228
|
const under = scaledAbove.has(p);
|
|
200
|
-
|
|
229
|
+
const noScale = rb.inherit === 'noScale' || rb.inherit === 'noScaleOrReflection' ? rb.inherit : null;
|
|
230
|
+
if (pz === null) m.set(b, noScale !== null ? poseNoScale(Wp, Wp0, W0, noScale, 0, 1) : mul(Wp, L0));
|
|
201
231
|
else if (!under) m.set(b, mul(mul(rot(pz.turn), W0), scaleX(s)));
|
|
202
232
|
else {
|
|
203
233
|
if (!pz.aimX) return `bone "${b}" is mapped under a scaled bone and aimed other than along its x axis; only an "x" aim is solved through a scaled parent`;
|
|
@@ -206,8 +236,9 @@ export function posePath(rest: Rest, path: readonly string[], posing: (b: string
|
|
|
206
236
|
const v = apply(inv(Wp), X);
|
|
207
237
|
const l0: Pt = [L0[0], L0[2]];
|
|
208
238
|
const k = (Math.atan2(v[1], v[0]) - Math.atan2(l0[1], l0[0])) * DEG;
|
|
209
|
-
|
|
210
|
-
|
|
239
|
+
// Under noScale / noScaleOrReflection (#153), rig-c normalises the direction carried through Wp to unit length, so the parent's scale is not inherited and the bone's own scalex is s directly.
|
|
240
|
+
const sk = noScale !== null ? s : Math.hypot(v[0], v[1]) / Math.hypot(l0[0], l0[1]);
|
|
241
|
+
m.set(b, noScale !== null ? poseNoScale(Wp, Wp0, W0, noScale, k, sk) : mul(mul(mul(Wp, rot(k)), L0), scaleX(sk)));
|
|
211
242
|
solved.set(b, { rotate: k, scale: sk });
|
|
212
243
|
}
|
|
213
244
|
if (under || scaleOf(b) !== null) scaledAbove.add(b);
|
|
@@ -268,7 +299,7 @@ export interface LegScale {
|
|
|
268
299
|
chain: string;
|
|
269
300
|
bone: string;
|
|
270
301
|
joints: [string, string];
|
|
271
|
-
/** The straight length: the
|
|
302
|
+
/** The straight length: the larger of the two sides' 95th-percentile picture lengths for this segment (#148), px. */
|
|
272
303
|
straight: number;
|
|
273
304
|
/** The picture length over the straight length, before the clamp: lowest, median, highest. */
|
|
274
305
|
ratio: { min: number; median: number; max: number };
|
|
@@ -290,9 +321,8 @@ export interface LegScale {
|
|
|
290
321
|
support?: SupportScaleRun[];
|
|
291
322
|
}
|
|
292
323
|
|
|
293
|
-
/** One segment's scale per frame: picture length ÷
|
|
294
|
-
export function legScaleOf(lengths: readonly number[], limits: [number, number]): Omit<LegScale, 'chain' | 'bone' | 'joints'> {
|
|
295
|
-
const straight = percentile(lengths, STRAIGHT_PERCENTILE);
|
|
324
|
+
/** One segment's scale per frame: picture length ÷ straight length (the larger of the two sides' 95th percentiles, #148), clamped; its asked scale without the clamp floor (#107); and the runs clamped. */
|
|
325
|
+
export function legScaleOf(lengths: readonly number[], limits: [number, number], straight = percentile(lengths, STRAIGHT_PERCENTILE)): Omit<LegScale, 'chain' | 'bone' | 'joints'> {
|
|
296
326
|
const ratio = lengths.map((l) => l / straight);
|
|
297
327
|
const scale = ratio.map((r) => Math.min(Math.max(r, limits[0]), limits[1]));
|
|
298
328
|
const asked = ratio.map((r) => Math.min(r, limits[1]));
|
|
@@ -444,6 +474,9 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
444
474
|
// #101: at a hand-over the front foot has not landed yet while the back ankle lifts its heel; when
|
|
445
475
|
// the front foot lands (the first frame after its lifted run) while the back ankle stays up, a back
|
|
446
476
|
// ankle that holds its height in y (< STILL_SPEED a frame for STILL_FRAMES frames) is a raised heel.
|
|
477
|
+
// #142: a run's push-off lifts the ankles off the floor while mid_hip is already rising from its
|
|
478
|
+
// crouch bottom; a jump starts at the first immediately preceding frame where both ankles are above
|
|
479
|
+
// their rolling floors and mid_hip is rising, so the support span ends at its ankle's push-off.
|
|
447
480
|
const midY = hip.r.map((h, i) => (h[1] + hip.l[i]![1]) / 2);
|
|
448
481
|
const rises: Array<{ ys: number[]; unit: number }> = [
|
|
449
482
|
{ ys: ankle.r.map((a) => a[1]), unit: legLength.r },
|
|
@@ -471,6 +504,7 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
471
504
|
};
|
|
472
505
|
const lifted = Array.from({ length: n }, (_, i) => up.every((u) => u[i]));
|
|
473
506
|
const jumps = runs(lifted).filter(([a, b]) => b - a + 1 >= JUMP_FRAMES && !raisedHeel(a, b));
|
|
507
|
+
for (const j of jumps) while (j[0] > 1 && up[0]![j[0] - 1] && up[1]![j[0] - 1] && midY[j[0] - 1]! < midY[j[0] - 2]!) j[0]--;
|
|
474
508
|
const jump = new Array<boolean>(n).fill(false);
|
|
475
509
|
for (const [a, b] of jumps) for (let i = a; i <= b; i++) jump[i] = true;
|
|
476
510
|
// Ankle speeds, leg lengths a frame (0 on the first frame).
|
|
@@ -705,8 +739,8 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
705
739
|
const b = travel(mode.from);
|
|
706
740
|
let y = mode.at[1] + a[1] - b[1];
|
|
707
741
|
if (mode.carry !== undefined && mode.land !== undefined && mode.end !== undefined && floorY !== null) {
|
|
708
|
-
if (mode.land < n &&
|
|
709
|
-
y += mode.carry * ((i - mode.from) / (mode.land - mode.from));
|
|
742
|
+
if (mode.land < n && mode.land > mode.from) {
|
|
743
|
+
y += mode.carry * ((Math.min(i, mode.land) - mode.from) / (mode.land - mode.from));
|
|
710
744
|
}
|
|
711
745
|
if (i <= mode.end) {
|
|
712
746
|
y = Math.max(y, floorY - Math.min(offset(i, 'r')[1], offset(i, 'l')[1]));
|
|
@@ -808,12 +842,16 @@ export type StanceLower = Record<Side, readonly number[]>;
|
|
|
808
842
|
* fold as far as the picture's leg is folded on that frame, never less far
|
|
809
843
|
* than the default. A leg with no straight length keeps the default.
|
|
810
844
|
*/
|
|
811
|
-
export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]
|
|
845
|
+
export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, inPlace = false): StanceLower {
|
|
812
846
|
const out = {} as Record<Side, number[]>;
|
|
813
847
|
for (const s of SIDES) {
|
|
814
848
|
const len = ankle[s].map((a, i) => Math.hypot(a[0] - hip[s][i]![0], a[1] - hip[s][i]![1]));
|
|
815
|
-
const
|
|
816
|
-
|
|
849
|
+
const n = len.length;
|
|
850
|
+
const globalStraight = percentile(len, STRAIGHT_PERCENTILE);
|
|
851
|
+
out[s] = len.map((l, i) => {
|
|
852
|
+
const straight = inPlace || n <= 25 ? globalStraight : percentile(len.slice(Math.max(0, i - 24), Math.min(n, i + 25)), STRAIGHT_PERCENTILE);
|
|
853
|
+
return straight > 1e-9 && Number.isFinite(l) ? Math.min(STANCE_BOUNDS[0], l / straight) : STANCE_BOUNDS[0];
|
|
854
|
+
});
|
|
817
855
|
}
|
|
818
856
|
return out;
|
|
819
857
|
}
|
package/src/map.ts
CHANGED
|
@@ -101,10 +101,19 @@ export interface AlternateSet {
|
|
|
101
101
|
/** `place: "by-hip"` (rig-c/pose#93): the chains whose wrist `pose` puts within one hip width of the mid-hip, their wrists placed by the offset from the hip while the set shows. Absent or null: the pose's angles. */
|
|
102
102
|
place?: { by: 'hip'; chains: string[] } | null;
|
|
103
103
|
}
|
|
104
|
+
export interface FaceAxis {
|
|
105
|
+
bone: string;
|
|
106
|
+
range: [number, number];
|
|
107
|
+
}
|
|
108
|
+
export interface FaceMap {
|
|
109
|
+
yaw?: FaceAxis;
|
|
110
|
+
pitch?: FaceAxis;
|
|
111
|
+
}
|
|
104
112
|
export interface JointMap {
|
|
105
113
|
chains: Chain[];
|
|
106
114
|
translate: Translate | null;
|
|
107
115
|
alternates: AlternateSet[];
|
|
116
|
+
face?: FaceMap;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/** What a slot of the built rig shows at setup, and every attachment any skin gives it. */
|
|
@@ -116,10 +125,14 @@ export interface RigSlot {
|
|
|
116
125
|
}
|
|
117
126
|
export type RigSlots = ReadonlyMap<string, RigSlot>;
|
|
118
127
|
|
|
128
|
+
export type InheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
|
|
129
|
+
|
|
119
130
|
/** The rig as the map is resolved against it: each bone's setup pose, by rig-c's poser. */
|
|
120
131
|
export interface RestBone extends RawBone {
|
|
121
132
|
/** The bone's `length` in the compiled model (0 when it has none). */
|
|
122
133
|
length: number;
|
|
134
|
+
/** The bone's `inheritMode` in the compiled model, folded to its canonical lowercase-first name (`normal` when absent). */
|
|
135
|
+
inherit?: InheritMode;
|
|
123
136
|
}
|
|
124
137
|
export type Rest = ReadonlyMap<string, RestBone>;
|
|
125
138
|
|
|
@@ -131,7 +144,7 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
131
144
|
p.throwIfAny();
|
|
132
145
|
throw new Error('unreachable');
|
|
133
146
|
}
|
|
134
|
-
refuseUnknownKeys(p, 'MAP_KEYS', where, doc, ['spec', 'note', 'chains', 'translate', 'alternates']);
|
|
147
|
+
refuseUnknownKeys(p, 'MAP_KEYS', where, doc, ['spec', 'note', 'chains', 'translate', 'alternates', 'face']);
|
|
135
148
|
if (doc.spec !== MAP_SPEC) p.add('MAP_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${MAP_SPEC}"`);
|
|
136
149
|
const chains: Chain[] = [];
|
|
137
150
|
if (!Array.isArray(doc.chains) || doc.chains.length === 0) {
|
|
@@ -231,8 +244,42 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
246
|
const alternates = 'alternates' in doc ? readAlternates(p, where, doc.alternates, chains, translate) : [];
|
|
247
|
+
const face = 'face' in doc ? readFaceMap(p, where, doc.face) : undefined;
|
|
234
248
|
p.throwIfAny();
|
|
235
|
-
return { chains, translate, alternates };
|
|
249
|
+
return { chains, translate, alternates, ...(face === undefined ? {} : { face }) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readFaceMap(p: Problems, where: string, raw: unknown): FaceMap | undefined {
|
|
253
|
+
const fw = `${where} "face"`;
|
|
254
|
+
if (!isObject(raw)) {
|
|
255
|
+
p.add('MAP_FACE_SHAPE', `${fw} is ${typeName(raw)}; it is { yaw?: { bone, range: [min, max] }, pitch?: { bone, range: [min, max] } }`);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
refuseUnknownKeys(p, 'MAP_KEYS', fw, raw, ['yaw', 'pitch']);
|
|
259
|
+
if (!('yaw' in raw) && !('pitch' in raw)) {
|
|
260
|
+
p.add('MAP_FACE_SHAPE', `${fw} names neither "yaw" nor "pitch"; it must name at least one axis`);
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const out: FaceMap = {};
|
|
264
|
+
for (const axis of ['yaw', 'pitch'] as const) {
|
|
265
|
+
if (!(axis in raw)) continue;
|
|
266
|
+
const aw = `${fw} "${axis}"`;
|
|
267
|
+
const ax = raw[axis];
|
|
268
|
+
if (!isObject(ax)) {
|
|
269
|
+
p.add('MAP_FACE_SHAPE', `${aw} is ${typeName(ax)}; it is { bone, range: [min, max] }`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, ax, ['bone', 'range']);
|
|
273
|
+
const bone = typeof ax.bone === 'string' && ax.bone !== '' ? ax.bone : null;
|
|
274
|
+
if (bone === null) p.add('MAP_FACE_BONE', `${aw} "bone" is ${typeName(ax.bone)}; it must name a bone`);
|
|
275
|
+
const v = ax.range;
|
|
276
|
+
if (!Array.isArray(v) || v.length !== 2 || !v.every((x) => typeof x === 'number' && Number.isFinite(x)) || (v[0] as number) > 0 || (v[1] as number) < 0 || (v[0] as number) >= (v[1] as number)) {
|
|
277
|
+
p.add('MAP_FACE_RANGE', `${aw} "range" is ${JSON.stringify(v)}; it must be [min, max], two finite numbers of degrees with min ≤ 0 ≤ max and min < max`);
|
|
278
|
+
} else if (bone !== null) {
|
|
279
|
+
out[axis] = { bone, range: [v[0] as number, v[1] as number] };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
236
283
|
}
|
|
237
284
|
|
|
238
285
|
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip" }';
|
|
@@ -556,6 +603,22 @@ export function resolveMap(map: JointMap, rest: Rest, where: string, slots: RigS
|
|
|
556
603
|
p.add('MAP_REACH', `chain "${c.name}" — bone "${b}" reaches from its posed origin, but it does not descend from the translated bone "${t.bone}"; its origin would not travel with the anchor`);
|
|
557
604
|
}
|
|
558
605
|
}
|
|
606
|
+
if (map.face !== undefined) {
|
|
607
|
+
for (const axis of ['yaw', 'pitch'] as const) {
|
|
608
|
+
const ax = map.face[axis];
|
|
609
|
+
if (ax === undefined) continue;
|
|
610
|
+
const label = `"face.${axis}"`;
|
|
611
|
+
if (!rest.has(ax.bone)) {
|
|
612
|
+
p.add('MAP_BONE_EXISTS', `${label} — bone "${ax.bone}" is not in the rig; its bones are ${listed}`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const prev = owner.get(ax.bone);
|
|
616
|
+
if (prev !== undefined) p.add('MAP_BONE_ONCE', `bone "${ax.bone}" is in ${prev.startsWith('"') ? prev : `chain "${prev}"`} and in ${label}; a bone carries one rotate track`);
|
|
617
|
+
owner.set(ax.bone, label);
|
|
618
|
+
const o = rest.get(ax.bone)!;
|
|
619
|
+
if (Math.hypot(o.a, o.c) < 1e-9) p.add('MAP_AIM_LENGTH', `${label} — bone "${ax.bone}" has a collapsed x axis at setup (scale 0)`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
559
622
|
resolveAlternates(p, map.alternates, slots);
|
|
560
623
|
if (p.any) p.list.unshift({ code: 'MAP_RESOLVES', text: `${where} does not resolve against the rig` });
|
|
561
624
|
p.throwIfAny();
|
package/src/reduce.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* of the gated build against the dense targets with the tolerance added.
|
|
32
32
|
*/
|
|
33
33
|
import { f32 } from 'rig-c/compile';
|
|
34
|
+
import { poseNoScale } from './legs.ts';
|
|
34
35
|
|
|
35
36
|
/** A key as the reducer writes it: a time, its values, and a raw curve on the segment to the next key. */
|
|
36
37
|
export interface ReducedKey {
|
|
@@ -444,11 +445,21 @@ const wrap180 = (d: number): number => {
|
|
|
444
445
|
* the reduced ones), or another bone of the same call given before it — times
|
|
445
446
|
* the setup locals of the unmapped bones between (`via`).
|
|
446
447
|
*/
|
|
448
|
+
export interface UnderStep {
|
|
449
|
+
wp0: M2;
|
|
450
|
+
w0: M2;
|
|
451
|
+
inherit?: 'noScale' | 'noScaleOrReflection';
|
|
452
|
+
}
|
|
453
|
+
|
|
447
454
|
export interface UnderBone {
|
|
448
455
|
bone: string;
|
|
449
|
-
parent: { rigid: { turn: readonly number[]; world0: M2; scale: readonly number[] }; via: M2 } | { under: string; via: M2 };
|
|
456
|
+
parent: { rigid: { turn: readonly number[]; world0: M2; scale: readonly number[] }; via: M2; steps?: readonly UnderStep[] } | { under: string; via: M2; steps?: readonly UnderStep[] };
|
|
450
457
|
/** Its setup local 2×2 in its parent's axes: inv(parent's setup world) · its setup world, by rig-c's setup. */
|
|
451
458
|
local: M2;
|
|
459
|
+
/** Its parent's and its own setup world 2×2 when `inherit` is `noScale` or `noScaleOrReflection` (#153). */
|
|
460
|
+
wp0?: M2;
|
|
461
|
+
w0?: M2;
|
|
462
|
+
inherit?: 'noScale' | 'noScaleOrReflection';
|
|
452
463
|
/** Its setup world x-axis length. */
|
|
453
464
|
len0: number;
|
|
454
465
|
/** The world angle its x axis is asked on every frame (degrees), and the scale of it asked (1: none). */
|
|
@@ -475,6 +486,12 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
|
|
|
475
486
|
const rotate = new Map<string, ReducedKey[]>();
|
|
476
487
|
const scale = new Map<string, ReducedKey[]>();
|
|
477
488
|
const rows: ReduceRow[] = [];
|
|
489
|
+
const stepVia = (m: M2, via: M2, steps: readonly UnderStep[] | undefined): M2 => {
|
|
490
|
+
if (steps === undefined || !steps.some((st) => st.inherit !== undefined)) return mul(m, via);
|
|
491
|
+
let cur = m;
|
|
492
|
+
for (const st of steps) cur = st.inherit !== undefined ? poseNoScale(cur, st.wp0, st.w0, st.inherit, 0, 1) : mul(cur, mul(inv(st.wp0), st.w0));
|
|
493
|
+
return cur;
|
|
494
|
+
};
|
|
478
495
|
for (const b of bones) {
|
|
479
496
|
for (const [what, xs] of [['target', b.target], ['asked', b.asked], ['dense', b.dense]] as const) {
|
|
480
497
|
if (xs.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${xs.length} ${what} value(s) for ${n} frame(s)`);
|
|
@@ -484,9 +501,9 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
|
|
|
484
501
|
if ('under' in p) {
|
|
485
502
|
const above = world.get(p.under);
|
|
486
503
|
if (above === undefined) throw new Error(`reduce: bone "${b.bone}" hangs from bone "${p.under}", which is not given before it`);
|
|
487
|
-
parent = above.map((m) =>
|
|
504
|
+
parent = above.map((m) => stepVia(m, p.via, p.steps));
|
|
488
505
|
} else {
|
|
489
|
-
parent = Array.from({ length: n }, (_, i) =>
|
|
506
|
+
parent = Array.from({ length: n }, (_, i) => stepVia(mul(mul(rot(p.rigid.turn[i]!), p.rigid.world0), scaleX(p.rigid.scale[i]!)), p.via, p.steps));
|
|
490
507
|
}
|
|
491
508
|
const l0: [number, number] = [b.local[0], b.local[2]];
|
|
492
509
|
const a0 = Math.atan2(l0[1], l0[0]) * DEG;
|
|
@@ -507,13 +524,14 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
|
|
|
507
524
|
const kAt = trackAt(rk, samples, 1).map((v) => v[0]!);
|
|
508
525
|
const ae = worstOf(kAt.map((_, i) => i), (i) => aimError(i, kAt[i]!))!;
|
|
509
526
|
rows.push({ bone: b.bone, kind: 'angle', before: n, after: rk.length, worst: ae.worst, frame: ae.frame });
|
|
510
|
-
// The scale that gives it the length asked on the rotate as reduced.
|
|
511
|
-
const
|
|
527
|
+
// The scale that gives it the length asked on the rotate as reduced (#153: under noScale, parent scale is normalised to 1).
|
|
528
|
+
const noScale = b.inherit;
|
|
529
|
+
const reach = noScale !== undefined ? kAt.map(() => 1) : kAt.map((k, i) => Math.hypot(...axis(i, k)) / b.len0);
|
|
512
530
|
const sk = fitTrack(times, samples, { targets: reach.map((r, i) => [b.asked[i]! / r]), keep, tolerance: scaleTolerance, error: (i, v) => Math.abs(v[0]! * reach[i]! - b.asked[i]!), round: r6 });
|
|
513
531
|
const sAt = trackAt(sk, samples, 1).map((v) => v[0]!);
|
|
514
532
|
const se = worstOf(sAt.map((_, i) => i), (i) => Math.abs(sAt[i]! * reach[i]! - b.asked[i]!))!;
|
|
515
533
|
rows.push({ bone: b.bone, kind: 'scale', before: n, after: sk.length, worst: se.worst, frame: se.frame });
|
|
516
|
-
world.set(b.bone, parent.map((m, i) => mul(mul(mul(m, rot(kAt[i]!)), b.local), scaleX(sAt[i]!))));
|
|
534
|
+
world.set(b.bone, parent.map((m, i) => (noScale !== undefined ? poseNoScale(m, b.wp0!, b.w0!, noScale, kAt[i]!, sAt[i]!) : mul(mul(mul(m, rot(kAt[i]!)), b.local), scaleX(sAt[i]!)))));
|
|
517
535
|
rotate.set(b.bone, rk);
|
|
518
536
|
scale.set(b.bone, sk);
|
|
519
537
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -82,7 +82,9 @@ import { Problems, Refusal } from './problems.ts';
|
|
|
82
82
|
import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
83
|
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
-
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
85
|
+
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
|
+
import type { FaceReading } from './face.ts';
|
|
87
|
+
import { readFace } from './face.ts';
|
|
86
88
|
|
|
87
89
|
const DEG = 180 / Math.PI;
|
|
88
90
|
|
|
@@ -322,6 +324,8 @@ export interface Retarget {
|
|
|
322
324
|
raised: Raised[];
|
|
323
325
|
/** Each span of a `place: "by-hip"` set on each chain it places, its wrist placed against the hip (#93). */
|
|
324
326
|
byHip: ByHip[];
|
|
327
|
+
/** With "face" in the map (#138): per-frame yaw/pitch reading and range-compressed mapping. */
|
|
328
|
+
face: FaceReading | null;
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
/** The hip's pivot against the thigh roots, at setup (rig-c/pose#47). */
|
|
@@ -433,6 +437,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
433
437
|
needed.add(map.translate.joint);
|
|
434
438
|
map.translate.scale.pose.forEach((j) => needed.add(j));
|
|
435
439
|
}
|
|
440
|
+
if (map.face !== undefined) {
|
|
441
|
+
needed.add('nose');
|
|
442
|
+
needed.add('r_eye');
|
|
443
|
+
needed.add('l_eye');
|
|
444
|
+
}
|
|
436
445
|
const series = new Map<string, Pt[]>();
|
|
437
446
|
const good = new Map<string, boolean[]>();
|
|
438
447
|
const holds: Hold[] = [];
|
|
@@ -678,8 +687,23 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
678
687
|
else if (plantOn) p.add('LEGS_MAP', `--plant-feet reads both hips and both ankles from the pose; the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`);
|
|
679
688
|
}
|
|
680
689
|
}
|
|
690
|
+
const poseScale: number[] | null =
|
|
691
|
+
t !== null && scale !== null && !inPlace
|
|
692
|
+
? (() => {
|
|
693
|
+
const [pa, pb] = t.scale.pose;
|
|
694
|
+
const A = series.get(pa)!;
|
|
695
|
+
const B = series.get(pb)!;
|
|
696
|
+
const raw = A.map((a, i) => Math.hypot(B[i]![0] - a[0], B[i]![1] - a[1]));
|
|
697
|
+
return raw.map((_, i) => {
|
|
698
|
+
const win = raw.slice(Math.max(0, i - 12), Math.min(n, i + 13)).filter((v) => v > 1e-6);
|
|
699
|
+
const m = win.length > 0 ? median(win) : scale!.pose;
|
|
700
|
+
return m > 1e-6 ? m / scale!.pose : 1;
|
|
701
|
+
});
|
|
702
|
+
})()
|
|
703
|
+
: null;
|
|
681
704
|
if (legScaleOn) {
|
|
682
705
|
const limits = opts.legScale as [number, number];
|
|
706
|
+
const rawSegments: Array<{ chain: string; bone: string; joints: [string, string]; kind: string; lengths: number[]; ownStraight: number }> = [];
|
|
683
707
|
for (const c of map.chains) {
|
|
684
708
|
if (c.reach) continue;
|
|
685
709
|
c.bones.forEach((b, k) => {
|
|
@@ -691,15 +715,56 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
691
715
|
}
|
|
692
716
|
const from = series.get(joints[0])!;
|
|
693
717
|
const to = series.get(joints[1])!;
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
718
|
+
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
719
|
+
const ownStraight = percentile(lengths, STRAIGHT_PERCENTILE);
|
|
720
|
+
if (!(ownStraight > 1e-6)) {
|
|
721
|
+
p.add('LEGS_MAP', `--leg-scale: chain "${c.name}" bone "${b}"'s segment ${joints[0]} → ${joints[1]} has a straight length (95th percentile) of ${ownStraight}px; there is nothing to scale against`);
|
|
697
722
|
return;
|
|
698
723
|
}
|
|
699
|
-
|
|
700
|
-
scales.set(b, ls.scale);
|
|
724
|
+
rawSegments.push({ chain: c.name, bone: b, joints, kind: `${joints[0].slice(2)}:${joints[1].slice(2)}`, lengths, ownStraight });
|
|
701
725
|
});
|
|
702
726
|
}
|
|
727
|
+
const straightByKind = new Map<string, number>();
|
|
728
|
+
for (const seg of rawSegments) straightByKind.set(seg.kind, Math.max(straightByKind.get(seg.kind) ?? 0, seg.ownStraight));
|
|
729
|
+
if (!inPlace && n > 25) {
|
|
730
|
+
const byChain = new Map<string, typeof rawSegments>();
|
|
731
|
+
for (const seg of rawSegments) {
|
|
732
|
+
const list = byChain.get(seg.chain) ?? [];
|
|
733
|
+
list.push(seg);
|
|
734
|
+
byChain.set(seg.chain, list);
|
|
735
|
+
}
|
|
736
|
+
const chainTotals = [...byChain.values()].filter((list) => list.length >= 2).map((list) => list[0]!.lengths.map((_, i) => list.reduce((s, seg) => s + seg.lengths[i]!, 0)));
|
|
737
|
+
const rawBySeg = new Map(rawSegments.map((seg) => [seg, [...seg.lengths]]));
|
|
738
|
+
for (const seg of rawSegments) {
|
|
739
|
+
const globalStraight = straightByKind.get(seg.kind)!;
|
|
740
|
+
const peers = rawSegments.filter((s) => s.kind === seg.kind);
|
|
741
|
+
seg.lengths = rawBySeg.get(seg)!.map((l, i) => {
|
|
742
|
+
const lo = Math.max(0, i - 24);
|
|
743
|
+
const hi = Math.min(n, i + 25);
|
|
744
|
+
let localStraight = 0;
|
|
745
|
+
for (const peer of peers) localStraight = Math.max(localStraight, percentile(rawBySeg.get(peer)!.slice(lo, hi), STRAIGHT_PERCENTILE));
|
|
746
|
+
if (chainTotals.length > 0 && straightByKind.size >= 2) {
|
|
747
|
+
let sumKindStraight = 0;
|
|
748
|
+
for (const kind of straightByKind.keys()) {
|
|
749
|
+
let ks = 0;
|
|
750
|
+
for (const s of rawSegments) if (s.kind === kind) ks = Math.max(ks, percentile(rawBySeg.get(s)!.slice(lo, hi), STRAIGHT_PERCENTILE));
|
|
751
|
+
sumKindStraight += ks;
|
|
752
|
+
}
|
|
753
|
+
let maxChainTotal = 0;
|
|
754
|
+
for (const tot of chainTotals) maxChainTotal = Math.max(maxChainTotal, percentile(tot.slice(lo, hi), STRAIGHT_PERCENTILE));
|
|
755
|
+
if (sumKindStraight > maxChainTotal && maxChainTotal > 1e-6) {
|
|
756
|
+
localStraight *= maxChainTotal / sumKindStraight;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return localStraight > 1e-6 ? l * (globalStraight / localStraight) : l;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (const seg of rawSegments) {
|
|
764
|
+
const ls = legScaleOf(seg.lengths, limits, straightByKind.get(seg.kind)!);
|
|
765
|
+
legScale.push({ chain: seg.chain, bone: seg.bone, joints: seg.joints, ...ls });
|
|
766
|
+
scales.set(seg.bone, ls.scale);
|
|
767
|
+
}
|
|
703
768
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
704
769
|
}
|
|
705
770
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
@@ -773,9 +838,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
773
838
|
// (stanceOf's mechanism, its bounds) so its ankle stays on the floor. Before --stance, whose
|
|
774
839
|
// floor is that supporting ankle. #95: the lower bound on a frame is the leg's own range there.
|
|
775
840
|
const ankleSeries = { r: series.get('r_ankle')!, l: series.get('l_ankle')! };
|
|
776
|
-
const lower = stanceRangeOf(ankleSeries, { r: series.get('r_hip')!, l: series.get('l_hip')! });
|
|
841
|
+
const lower = stanceRangeOf(ankleSeries, { r: series.get('r_hip')!, l: series.get('l_hip')! }, inPlace);
|
|
777
842
|
blendHold = holdBlend(planted.blends, n, posedFeet, lower);
|
|
843
|
+
const beforeBlendX = blendHold.stretch.map((b, i) => (b === null ? 0 : posedFeet(i).ankle[b.side][0]));
|
|
778
844
|
stretchChain(blendHold.stretch);
|
|
845
|
+
blendHold.stretch.forEach((b, i) => {
|
|
846
|
+
if (b === null) return;
|
|
847
|
+
const dx = beforeBlendX[i]! - posedFeet(i).ankle[b.side][0];
|
|
848
|
+
hip[i]![0] += dx;
|
|
849
|
+
travel[i]![0] += dx;
|
|
850
|
+
});
|
|
779
851
|
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
780
852
|
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
781
853
|
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
@@ -962,11 +1034,20 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
962
1034
|
if (refEye > 1e-6 && rigEye > 1e-6) nodScale = rigEye / refEye;
|
|
963
1035
|
}
|
|
964
1036
|
if (neckSeries !== undefined && nodScale !== null) {
|
|
965
|
-
const
|
|
966
|
-
const
|
|
1037
|
+
const bone = rest.get(hb)!;
|
|
1038
|
+
const parent = bone.parent === null ? null : rest.get(bone.parent)!;
|
|
1039
|
+
let neckRestLen = 0;
|
|
1040
|
+
for (let a = parent; a !== null; a = a.parent === null ? null : rest.get(a.parent)!) {
|
|
1041
|
+
const d = Math.hypot(bone.worldX - a.worldX, bone.worldY - a.worldY);
|
|
1042
|
+
if (d > 1e-6) {
|
|
1043
|
+
neckRestLen = d;
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const maxNod = !inPlace && neckRestLen > 1e-6 ? 0.02 * neckRestLen : Infinity;
|
|
1048
|
+
const refEN = rEye.map((re, i) => -((re[1] + lEye[i]![1]) / 2 - neckSeries[i]![1]) / (poseScale?.[i] ?? 1));
|
|
1049
|
+
const dy = refEN.map((v) => Math.max(-maxNod, Math.min(maxNod, (v - refEN[0]!) * nodScale!)));
|
|
967
1050
|
if (dy.some((v) => Math.abs(r3(v)) > 0)) {
|
|
968
|
-
const bone = rest.get(hb)!;
|
|
969
|
-
const parent = bone.parent === null ? null : rest.get(bone.parent)!;
|
|
970
1051
|
const keys: MotionKey[] = [];
|
|
971
1052
|
const nod: Pt[] = [];
|
|
972
1053
|
for (let i = 0; i < n; i++) {
|
|
@@ -1273,6 +1354,26 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1273
1354
|
}
|
|
1274
1355
|
}
|
|
1275
1356
|
}
|
|
1357
|
+
let face: FaceReading | null = null;
|
|
1358
|
+
if (map.face !== undefined) {
|
|
1359
|
+
const rEar = series.get('r_ear') ?? (() => {
|
|
1360
|
+
const h = heldOf('r_ear');
|
|
1361
|
+
return 'never' in h ? null : h.at;
|
|
1362
|
+
})();
|
|
1363
|
+
const lEar = series.get('l_ear') ?? (() => {
|
|
1364
|
+
const h = heldOf('l_ear');
|
|
1365
|
+
return 'never' in h ? null : h.at;
|
|
1366
|
+
})();
|
|
1367
|
+
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar);
|
|
1368
|
+
for (const axis of ['yaw', 'pitch'] as const) {
|
|
1369
|
+
const ax = face[axis];
|
|
1370
|
+
if (ax === undefined) continue;
|
|
1371
|
+
const rest0 = restAngle(rest, ax.bone, 'x');
|
|
1372
|
+
const above = mappedAbove(ax.bone);
|
|
1373
|
+
targets.set(ax.bone, ax.keys.map((v, i) => wrap(rest0 + (above === null ? 0 : turn(above, i)) + v)));
|
|
1374
|
+
tracks.push({ bone: ax.bone, property: 'rotate', keys: ax.keys.map((v, i) => ({ t: keyTimes[i]!, v: [v] })) });
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1276
1377
|
// Scale keys: a scaled leg segment's own scale, or the scale that lands a bone under one at its asked length.
|
|
1277
1378
|
for (const c of map.chains) {
|
|
1278
1379
|
for (const b of c.bones) {
|
|
@@ -1288,7 +1389,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1288
1389
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
1289
1390
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
1290
1391
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
1291
|
-
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip,
|
|
1392
|
+
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip, face,
|
|
1292
1393
|
};
|
|
1293
1394
|
}
|
|
1294
1395
|
|
package/src/rigc.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
|
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { poseRawSetup, readModel, type CompiledDocument } from 'rig-c/core';
|
|
16
|
-
import type { Rest, RestBone, RigSlot, RigSlots } from './map.ts';
|
|
16
|
+
import type { InheritMode, Rest, RestBone, RigSlot, RigSlots } from './map.ts';
|
|
17
17
|
import { isObject, parseJson, Problems, Refusal, typeName } from './problems.ts';
|
|
18
18
|
|
|
19
19
|
export interface RigInput {
|
|
@@ -129,11 +129,18 @@ export function readBuild(build: string): CompiledDocument {
|
|
|
129
129
|
return readModel(readFileSync(path, 'utf8'), path);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/** Every bone's setup pose, by rig-c's poser, with its model `length`. */
|
|
132
|
+
/** Every bone's setup pose, by rig-c's poser, with its model `length` and `inheritMode`. */
|
|
133
133
|
export function restOf(doc: CompiledDocument): Rest {
|
|
134
134
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
135
|
+
const inherits = new Map<string, InheritMode>(
|
|
136
|
+
doc.bones.map((b) => {
|
|
137
|
+
const s = b.inheritMode;
|
|
138
|
+
const folded = s === undefined || s.length === 0 ? 'normal' : `${s[0]!.toLowerCase()}${s.slice(1)}`;
|
|
139
|
+
return [b.name, folded === 'onlyTranslation' || folded === 'noRotationOrReflection' || folded === 'noScale' || folded === 'noScaleOrReflection' ? folded : 'normal'];
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
135
142
|
const out = new Map<string, RestBone>();
|
|
136
|
-
for (const b of poseRawSetup(doc).bones) out.set(b.name, { ...b, length: lengths.get(b.name) ?? 0 });
|
|
143
|
+
for (const b of poseRawSetup(doc).bones) out.set(b.name, { ...b, length: lengths.get(b.name) ?? 0, inherit: inherits.get(b.name) ?? 'normal' });
|
|
137
144
|
return out;
|
|
138
145
|
}
|
|
139
146
|
|
package/src/run.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { landed, measureFeet, measureLanding, measurePosture, measureShear, pose
|
|
|
19
19
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
20
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
21
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
22
|
+
import { faceLine } from './face.ts';
|
|
22
23
|
|
|
23
24
|
export interface RetargetArgs extends RetargetOptions {
|
|
24
25
|
rig: string;
|
|
@@ -157,6 +158,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
157
158
|
if (r.blendHold !== null) for (const l of blendLines(r.blendHold, r.times)) say(` FEET ${l}`);
|
|
158
159
|
}
|
|
159
160
|
if (r.stance !== null) for (const l of stanceLines(r.stance, r.times)) say(` STANCE ${l}`);
|
|
161
|
+
if (r.face !== null) say(` FACE ${faceLine(r.face)}`);
|
|
160
162
|
|
|
161
163
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
162
164
|
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip);
|
|
@@ -183,7 +185,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
183
185
|
if ((t.kind === 'bone' || t.kind === 'group') && Array.isArray(sample)) return boneIdentity(property, sample.length);
|
|
184
186
|
return undefined;
|
|
185
187
|
};
|
|
186
|
-
const mapped = [
|
|
188
|
+
const mapped = [
|
|
189
|
+
...map.chains.flatMap((c) => c.bones),
|
|
190
|
+
...(map.translate === null ? [] : [map.translate.bone]),
|
|
191
|
+
...(map.face?.yaw === undefined ? [] : [map.face.yaw.bone]),
|
|
192
|
+
...(map.face?.pitch === undefined ? [] : [map.face.pitch.bone]),
|
|
193
|
+
];
|
|
187
194
|
const below = new Map<string, string[]>();
|
|
188
195
|
for (const m of new Set(mapped)) {
|
|
189
196
|
for (let b = rest.get(m)?.parent ?? null; b !== null; b = rest.get(b)?.parent ?? null) {
|
|
@@ -317,6 +324,16 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
317
324
|
raised: r.raised,
|
|
318
325
|
// Only when a set declares `place` (#93): a map without one writes the report it always has.
|
|
319
326
|
...(map.alternates.some((x) => x.place !== undefined && x.place !== null) ? { byHip: r.byHip } : {}),
|
|
327
|
+
...(r.face === null
|
|
328
|
+
? {}
|
|
329
|
+
: {
|
|
330
|
+
face: {
|
|
331
|
+
ears: r.face.ears,
|
|
332
|
+
read: { yaw: r.face.read.yaw, pitch: r.face.read.pitch },
|
|
333
|
+
...(r.face.yaw === undefined ? {} : { yaw: { bone: r.face.yaw.bone, limits: r.face.yaw.limits, read: r.face.yaw.read, mapped: r.face.yaw.mapped, scale: r.face.yaw.scale, corr: r.face.yaw.corr } }),
|
|
334
|
+
...(r.face.pitch === undefined ? {} : { pitch: { bone: r.face.pitch.bone, limits: r.face.pitch.limits, read: r.face.pitch.read, mapped: r.face.pitch.mapped, scale: r.face.pitch.scale, corr: r.face.pitch.corr } }),
|
|
335
|
+
},
|
|
336
|
+
}),
|
|
320
337
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, ...rest }) => rest),
|
|
321
338
|
pivot: r.pivot,
|
|
322
339
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
@@ -431,21 +448,34 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
431
448
|
return d;
|
|
432
449
|
};
|
|
433
450
|
const underBones = [...rotate.keys()].filter(under).sort((x, y) => depth(x) - depth(y));
|
|
451
|
+
const noScaleOf = (b: string): 'noScale' | 'noScaleOrReflection' | undefined => {
|
|
452
|
+
const inh = rest.get(b)!.inherit;
|
|
453
|
+
return inh === 'noScale' || inh === 'noScaleOrReflection' ? inh : undefined;
|
|
454
|
+
};
|
|
434
455
|
const given: UnderBone[] = underBones.map((b) => {
|
|
435
456
|
if (!scalex.has(b)) throw new Refusal([{ code: 'POSE_REDUCE', text: `bone "${b}" hangs under a scaled bone and has no scalex track, so it was not keyed through its parent's posed matrix; --reduce re-solves only a bone the legs keyed that way` }]);
|
|
436
|
-
// Up through the unmapped bones between (their setup locals), to a bone given before it or one the key formula holds for.
|
|
457
|
+
// Up through the unmapped bones between (their setup locals, or their noScale unit frames, #153), to a bone given before it or one the key formula holds for.
|
|
437
458
|
let via: M2 = M2_IDENTITY;
|
|
459
|
+
const steps: Array<{ wp0: M2; w0: M2; inherit?: 'noScale' | 'noScaleOrReflection' }> = [];
|
|
438
460
|
let p = rest.get(b)!.parent!;
|
|
439
461
|
while (under(p) && !mapped.has(p)) {
|
|
440
|
-
|
|
462
|
+
const wp0 = world0(rest.get(p)!.parent!);
|
|
463
|
+
const w0 = world0(p);
|
|
464
|
+
via = mulM2(setupLocal(wp0, w0), via);
|
|
465
|
+
const inherit = noScaleOf(p);
|
|
466
|
+
steps.unshift({ wp0, w0, ...(inherit !== undefined ? { inherit } : {}) });
|
|
441
467
|
p = rest.get(p)!.parent!;
|
|
442
468
|
}
|
|
443
|
-
const
|
|
469
|
+
const viaSteps = steps.some((st) => st.inherit !== undefined) ? { steps } : {};
|
|
470
|
+
const parent: UnderBone['parent'] = under(p) ? { under: p, via, ...viaSteps } : { rigid: { turn: turnOf(p), world0: world0(p), scale: scaleAt.get(p) ?? new Array<number>(n).fill(1) }, via, ...viaSteps };
|
|
471
|
+
const wp0 = world0(rest.get(b)!.parent!);
|
|
444
472
|
const w0 = world0(b);
|
|
473
|
+
const inherit = noScaleOf(b);
|
|
445
474
|
return {
|
|
446
475
|
bone: b,
|
|
447
476
|
parent,
|
|
448
|
-
local: setupLocal(
|
|
477
|
+
local: setupLocal(wp0, w0),
|
|
478
|
+
...(inherit !== undefined ? { wp0, w0, inherit } : {}),
|
|
449
479
|
len0: Math.hypot(w0[0], w0[2]),
|
|
450
480
|
target: r.targets.get(b)!,
|
|
451
481
|
asked: r.scales.get(b) ?? new Array<number>(n).fill(1),
|
|
@@ -671,7 +701,7 @@ function legScaleLine(l: LegScale, times: readonly number[]): string {
|
|
|
671
701
|
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
672
702
|
const count = (rs: Array<[number, number]>) => rs.reduce((k, x) => k + x[1] - x[0] + 1, 0);
|
|
673
703
|
const clamp = (rs: Array<[number, number]>, at: number, how: string) => (rs.length === 0 ? `none ${how} ${at}` : `${count(rs)} frame(s) ${how} ${at}, clamped (frames ${rs.map(span).join(', ')})`);
|
|
674
|
-
return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (95th percentile of ${times.length} frames); picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}]: ${clamp(l.low, l.limits[0], 'under')}; ${clamp(l.high, l.limits[1], 'over')}`;
|
|
704
|
+
return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (larger 95th percentile of ${times.length} frames, both legs); picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}]: ${clamp(l.low, l.limits[0], 'under')}; ${clamp(l.high, l.limits[1], 'over')}`;
|
|
675
705
|
}
|
|
676
706
|
|
|
677
707
|
/** A LEG-SCALE past the floor (support) line (rig-c/pose#133): the supporting leg segment scaled below `--leg-scale`'s floor down to #95's bound. */
|
package/src/verify.ts
CHANGED
|
@@ -61,13 +61,19 @@ export function poseFrames(doc: CompiledDocument, animation: string, times: read
|
|
|
61
61
|
export function measureLanding(doc: CompiledDocument, animation: string, map: JointMap, r: Retarget, posed = poseFrames(doc, animation, r.times)): Landing[] {
|
|
62
62
|
const out: Landing[] = [];
|
|
63
63
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
64
|
+
const faceBones = new Set([r.face?.yaw?.bone, r.face?.pitch?.bone].filter((b): b is string => b !== undefined));
|
|
65
|
+
const isFaceSlider = (c: CompiledDocument['constraints'][number]): boolean => c.record?.kind === 'slider' && c.record.bone !== null && faceBones.has(c.record.bone);
|
|
66
|
+
const hasFaceSliders = faceBones.size > 0 && doc.constraints.some(isFaceSlider);
|
|
67
|
+
const chainPosed = hasFaceSliders
|
|
68
|
+
? poseFrames({ ...doc, constraints: doc.constraints.filter((c) => !isFaceSlider(c)) }, animation, r.times)
|
|
69
|
+
: posed;
|
|
64
70
|
for (const c of map.chains) {
|
|
65
71
|
c.bones.forEach((b, k) => {
|
|
66
72
|
const aim = c.aim[k]!;
|
|
67
73
|
const asked = r.targets.get(b)!;
|
|
68
74
|
let worst = -1;
|
|
69
75
|
let frame = 0;
|
|
70
|
-
|
|
76
|
+
chainPosed.forEach((bones, i) => {
|
|
71
77
|
const reached = aimAngle(bones, lengths, b, aim);
|
|
72
78
|
const e = Math.abs(wrap(reached - asked[i]!));
|
|
73
79
|
if (e > worst) {
|
|
@@ -78,6 +84,24 @@ export function measureLanding(doc: CompiledDocument, animation: string, map: Jo
|
|
|
78
84
|
out.push({ bone: b, worst, frame, kind: 'angle', tolerance: ANGLE_TOLERANCE });
|
|
79
85
|
});
|
|
80
86
|
}
|
|
87
|
+
if (r.face !== null) {
|
|
88
|
+
for (const axis of ['yaw', 'pitch'] as const) {
|
|
89
|
+
const ax = r.face[axis];
|
|
90
|
+
if (ax === undefined) continue;
|
|
91
|
+
const asked = r.targets.get(ax.bone)!;
|
|
92
|
+
let worst = -1;
|
|
93
|
+
let frame = 0;
|
|
94
|
+
posed.forEach((bones, i) => {
|
|
95
|
+
const reached = aimAngle(bones, lengths, ax.bone, 'x');
|
|
96
|
+
const e = Math.abs(wrap(reached - asked[i]!));
|
|
97
|
+
if (e > worst) {
|
|
98
|
+
worst = e;
|
|
99
|
+
frame = i;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
out.push({ bone: ax.bone, worst, frame, kind: 'angle', tolerance: ANGLE_TOLERANCE });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
81
105
|
const setup = new Map(poseRawSetup(doc).bones.map((b) => [b.name, b]));
|
|
82
106
|
for (const [b, asked] of r.scales) {
|
|
83
107
|
const s0 = setup.get(b)!;
|