@rig-c/pose 0.28.0 → 0.29.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 +8 -7
- package/package.json +1 -1
- package/src/map.ts +45 -2
- package/src/retarget.ts +371 -50
- package/src/switch.ts +68 -77
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ A retarget of the cleaned dancer, with `--flip 90 --short 0.35 --bends clamp --l
|
|
|
107
107
|
rig-pose retarget --rig out/rig --map map.json --pose dance.pose.json --out motion.json --name dance
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while
|
|
110
|
+
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while on sequences where that clamp acts, the clamped vertical nod (`clampedY`) is coupled in phase into `face.pitch`'s dial (`+2.8°/unit`, mapped through `face.pitch.range`) and the head bone's `rotate` track (`+1.6°/unit`, [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205); gains calibrated against the hand-polished reference `p1`). The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip (and on the head when the reference nods), one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
|
|
111
111
|
|
|
112
112
|
Nothing is written until every check passes:
|
|
113
113
|
|
|
@@ -315,7 +315,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
315
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 (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.
|
|
316
316
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
317
317
|
- `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
|
-
- `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)): `{ "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
|
|
318
|
+
- `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`).
|
|
319
319
|
- `body` (optional, [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "body_yaw", "range": [-30, 30], "sets"?: [{ "set": "T8", "above": 45 }] }, "sets"?: [{ "set": "T8", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Maps the per-frame body yaw read from the camera-normalised shoulder and hip width (`BODY`) into `range` with the same `0.8 × range` knee mapping as `face`, keying the named dial bone's `rotate` track, and switches any declared `sets` (`body.sets` or `body.yaw.sets`) with the same `above` / `above − 10°` hysteresis (overriding `face` sets and arm `alternates` on slots they share while active). Without `body` in the map, `BODY` and `--report`'s `body` still report the reading while leaving every track in `motion.json` unchanged.
|
|
320
320
|
|
|
321
321
|
### A joint's range: `range`
|
|
@@ -349,12 +349,13 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
349
349
|
|
|
350
350
|
- `slots`: what each slot of the set shows while it shows; each attachment must be in that slot under some skin of the rig, as rig-c built it. `hides`: the slots that are empty while it shows. A slot's place in the draw order is the rig's own, so a drawing that must sit in front of the torso is an attachment in a slot that is: a switch is attachment keys only, and no draw-order key is written.
|
|
351
351
|
- `chains`: the map's chains whose pose the set depicts; it is selected by them. `pose`: the key skeleton the art was painted from, body-18 (or `mid_hip`/`mid_shoulder`) in picture px, y down, any scale and place — only its directions are read; every joint the listed chains use must be in it.
|
|
352
|
-
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key unless the set declares `symmetric` or `
|
|
352
|
+
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key unless the set declares `symmetric`, `support` or `anchor` ([below](#alternate-drawings-alternates)): otherwise, while a set shows, a bone is keyed to the pose as always, and the art turns with it. What it gives is a figure: the art turn, how far each bone is from the angle its art was painted at (`artTurn`, and `ART-RANGE` past the bone's range).
|
|
353
353
|
- `offsets` (optional): a translate per bone, in its parent's axes, held while the set shows and 0 elsewhere (stepped keys), so the joints as painted are the pivots. A bone `translate` already keys (the hip) cannot take one.
|
|
354
354
|
- `place` (optional): `"by-hip"` places a hand that meets the body by its offset from the hip ([below](#alternate-drawings-alternates)).
|
|
355
|
-
- `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)). Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
|
|
356
|
-
- `support` (optional, [#188](https://github.com/rig-c/pose/issues/188)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set
|
|
357
|
-
-
|
|
355
|
+
- `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)), ramping in and out in joint space across any adjacent bridge frames where the set's slots remain shown. Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
|
|
356
|
+
- `support` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set's slots show (`loShow..hiShow`), each support chain's bones anchor to `drawn` within `±2.5°` (`≤ ±3°`) of the pose's delta relative to the set's reference pose (bridging in joint space across any preceding gap frames where the leaving set's slots still show), and an unplaced active raised arm in the same set clears the head/bun outward along the 2D shoulder→wrist direction (`MAP_ALT_SUPPORT`).
|
|
357
|
+
- `anchor` (optional, [#191](https://github.com/rig-c/pose/issues/191)): a chain name or non-empty list of chain names of the set (`"anchor": "arm_l"` or `["arm_l"]`) whose hand is anchored to the set's drawn hand-tip-to-head offset in the `head` bone's local frame (`MAP_ALT_ANCHOR`, `ANCHOR_HEAD`); while the set's slots for that chain show, the target hand tip is `head_posed_origin + rotate(W_drawn − H_setup, head_posed_rot − head_drawn_rot)` and the 2-bone arm is solved by two-circle intersection (`twoBone` / `reachLimit`) inside the set's `turn`, listing any frame where `turn` saturates and leaves a residual hand-tip shortfall (`saturated: [{ frame, residual }]` on `raised`, printed as `; residual f46 9.68u, …` on `RAISED`).
|
|
358
|
+
- Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC`, `MAP_ALT_SUPPORT` and `MAP_ALT_ANCHOR`.
|
|
358
359
|
|
|
359
360
|
**When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton, with a foreshortened segment (`SHORT`) weighted at `0.5 ×` (`SHORT_SWITCH_WEIGHT`, [#180](https://github.com/rig-c/pose/issues/180)). On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; once entered, a set stays active until its distance exceeds `--switch-tolerance + 15°` (`SWITCH_LEAVE_HYSTERESIS`, [#180](https://github.com/rig-c/pose/issues/180)) for at least `--switch-min` (default 0.25 s) or a competing set with a lower distance enters within `--switch-tolerance`; a run of one set shorter than `--switch-min` is dropped and the base art shows there. Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
|
|
360
361
|
|
|
@@ -364,7 +365,7 @@ The bound exists because speed alone rewarded showing art far from its pose. Unb
|
|
|
364
365
|
|
|
365
366
|
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`.
|
|
366
367
|
|
|
367
|
-
**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)
|
|
368
|
+
**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.
|
|
368
369
|
|
|
369
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 the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence 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.
|
|
370
371
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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/map.ts
CHANGED
|
@@ -104,6 +104,8 @@ export interface AlternateSet {
|
|
|
104
104
|
symmetric?: boolean;
|
|
105
105
|
/** `support` (rig-c/pose#188): the set's chains whose bones anchor to the set's `drawn` local angles while the non-support chains track the active gesture. */
|
|
106
106
|
support?: string[];
|
|
107
|
+
/** `anchor` (rig-c/pose#191): the set's two-bone arm chains whose hand-tip is anchored to the set's `drawn` head-local offset while the set shows. */
|
|
108
|
+
anchor?: string[];
|
|
107
109
|
}
|
|
108
110
|
export interface YawSet {
|
|
109
111
|
set: string;
|
|
@@ -439,7 +441,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
439
441
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
440
442
|
return;
|
|
441
443
|
}
|
|
442
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support']);
|
|
444
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support', 'anchor']);
|
|
443
445
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
444
446
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
445
447
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -602,7 +604,48 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
602
604
|
if (ok) support = sup;
|
|
603
605
|
}
|
|
604
606
|
}
|
|
605
|
-
|
|
607
|
+
let anchor: string[] | undefined;
|
|
608
|
+
if ('anchor' in a) {
|
|
609
|
+
const anc = typeof a.anchor === 'string' && a.anchor !== '' ? [a.anchor] : strings(a.anchor);
|
|
610
|
+
if (anc === null || anc.length === 0 || anc.some((s) => s === '')) {
|
|
611
|
+
p.add('MAP_ALT_ANCHOR', `${label} "anchor" is ${JSON.stringify(a.anchor)}; it must name one or more of the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
612
|
+
ok = false;
|
|
613
|
+
} else {
|
|
614
|
+
if (new Set(anc).size !== anc.length) {
|
|
615
|
+
p.add('MAP_ALT_ANCHOR', `${label} "anchor" names a chain twice: ${anc.join(', ')}`);
|
|
616
|
+
ok = false;
|
|
617
|
+
}
|
|
618
|
+
const outside = anc.filter((c) => !(cs ?? []).includes(c));
|
|
619
|
+
if (outside.length > 0) {
|
|
620
|
+
p.add('MAP_ALT_ANCHOR', `${label} "anchor" names chain ${outside.map((c) => `"${c}"`).join(', ')}, which is not in the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
621
|
+
ok = false;
|
|
622
|
+
}
|
|
623
|
+
const ancChains = own.filter((c) => anc.includes(c.name));
|
|
624
|
+
const nonTwo = ancChains.filter((c) => c.bones.length !== 2);
|
|
625
|
+
if (nonTwo.length > 0) {
|
|
626
|
+
p.add('MAP_ALT_ANCHOR', `${label} "anchor" chain ${nonTwo.map((c) => `"${c.name}" (${c.bones.length} bone(s))`).join(', ')} is not a two-bone chain; a head-anchored chain solves a two-bone arm`);
|
|
627
|
+
ok = false;
|
|
628
|
+
}
|
|
629
|
+
if (a.symmetric === true) {
|
|
630
|
+
p.add('MAP_ALT_ANCHOR', `${label} names both "symmetric" and "anchor"; a set either normalises both arms symmetrically or anchors an arm to the head`);
|
|
631
|
+
ok = false;
|
|
632
|
+
}
|
|
633
|
+
const supRaw = typeof a.support === 'string' ? [a.support] : strings(a.support) ?? [];
|
|
634
|
+
const bothSup = anc.filter((c) => supRaw.includes(c));
|
|
635
|
+
if (bothSup.length > 0) {
|
|
636
|
+
p.add('MAP_ALT_ANCHOR', `${label} names chain ${bothSup.map((c) => `"${c}"`).join(', ')} in both "support" and "anchor"; a chain is anchored one way`);
|
|
637
|
+
ok = false;
|
|
638
|
+
}
|
|
639
|
+
const ancBones = ancChains.flatMap((c) => c.bones);
|
|
640
|
+
const missing = ancBones.filter((b) => !(b in drawn));
|
|
641
|
+
if (missing.length > 0) {
|
|
642
|
+
p.add('MAP_ALT_ANCHOR', `${label} "anchor" chain has no "drawn" angle for ${missing.map((b) => `"${b}"`).join(', ')}; a head-anchored chain derives its head-local offset from its drawn angles`);
|
|
643
|
+
ok = false;
|
|
644
|
+
}
|
|
645
|
+
if (ok) anchor = anc;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn, frames, ...(place === null ? {} : { place }), ...(symmetric === undefined ? {} : { symmetric }), ...(support === undefined ? {} : { support }), ...(anchor === undefined ? {} : { anchor }) });
|
|
606
649
|
});
|
|
607
650
|
return out;
|
|
608
651
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -99,21 +99,31 @@ const DEG = 180 / Math.PI;
|
|
|
99
99
|
export const HELD_MISS = 0.01;
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Beat-nod phase-coupling gain (`°/unit`) from the
|
|
103
|
-
* `±2%` neck
|
|
104
|
-
*
|
|
105
|
-
* dancer reference hand pass (`hand/p1`
|
|
102
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the clamped `head.translate.y`
|
|
103
|
+
* offset (`clampedY`, bounded by `±2%` of neck length) onto `map.face.pitch`
|
|
104
|
+
* (`pitch_dial.rotate`, `rig-c/pose#190`, `#205`) when the neck-length clamp
|
|
105
|
+
* acts on the sequence. Measured on the dancer reference hand pass (`hand/p1`
|
|
106
|
+
* HP2: `+2.8°/unit`).
|
|
106
107
|
*/
|
|
107
108
|
export const NOD_RESIDUAL_PITCH_GAIN = 2.8;
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
|
-
* Beat-nod phase-coupling gain (`°/unit`) from the
|
|
111
|
-
* `±2%` neck
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the clamped `head.translate.y`
|
|
112
|
+
* offset (`clampedY`, bounded by `±2%` of neck length) onto the head chain
|
|
113
|
+
* bone's `rotate` track (`head.rotate`, `rig-c/pose#190`, `#205`) when the
|
|
114
|
+
* neck-length clamp acts on the sequence. Measured on the dancer reference
|
|
115
|
+
* hand pass (`hand/p1` HP2: `+1.6°/unit`).
|
|
114
116
|
*/
|
|
115
117
|
export const NOD_RESIDUAL_HEAD_GAIN = 1.6;
|
|
116
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Reference frame for sets declaring `anchor` (`rig-c/pose#191`): `'head-local'`
|
|
121
|
+
* rotates the set's drawn hand-tip-to-head offset by the head bone's posed world
|
|
122
|
+
* turn on each frame; `'world-fixed'` holds the unrotated setup world offset;
|
|
123
|
+
* `'off'` disables head-anchored end-effector solving.
|
|
124
|
+
*/
|
|
125
|
+
export const HEAD_ANCHOR_FRAME: 'head-local' | 'world-fixed' | 'off' = 'head-local';
|
|
126
|
+
|
|
117
127
|
export interface HeadNodReading {
|
|
118
128
|
bone: string;
|
|
119
129
|
maxNod: number;
|
|
@@ -481,6 +491,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
481
491
|
// `place: "by-hip"` (#93) measures against the hip in hip widths: the map must name the rig's two hip points.
|
|
482
492
|
const hips = map.alternates.some((a) => a.place !== undefined && a.place !== null) ? hipOf(map) : null;
|
|
483
493
|
if (hips !== null && 'problem' in hips) p.add('PLACE_HIP', hips.problem);
|
|
494
|
+
const headAnchorBone = map.alternates.some((a) => a.anchor !== undefined) ? headBoneOf(map) : null;
|
|
495
|
+
if (map.alternates.some((a) => a.anchor !== undefined) && headAnchorBone === null) {
|
|
496
|
+
p.add('ANCHOR_HEAD', '"anchor" anchors a set\'s hand to the head bone\'s local frame, and no chain of the map carries the eye line (joints r_eye → l_eye)');
|
|
497
|
+
}
|
|
484
498
|
|
|
485
499
|
// Every point the map reads, held where it is not good.
|
|
486
500
|
const needed = new Set<string>();
|
|
@@ -1353,11 +1367,147 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1353
1367
|
const raised: Raised[] = [];
|
|
1354
1368
|
const byHip: ByHip[] = [];
|
|
1355
1369
|
const raisedArt: Array<{ sw: Switch; entries: ArtRange[] }> = [];
|
|
1356
|
-
const raisedJobs: Array<{ anchor: Anchor; sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; l0: number; l1: number; farthest: number }> = [];
|
|
1370
|
+
const raisedJobs: Array<{ anchor: Anchor; sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; saturated?: Array<{ frame: number; residual: number }>; l0: number; l1: number; farthest: number }> = [];
|
|
1357
1371
|
const headOk: Anchor | null = head !== null && !('problem' in head) ? { kind: 'head', joints: head.joints, points: head.points } : null;
|
|
1358
1372
|
const hipsOk: Anchor | null = hips !== null && !('problem' in hips) ? { kind: 'hip', joints: hips.joints, points: hips.points } : null;
|
|
1359
1373
|
const fallbackHead = headOf(map);
|
|
1360
1374
|
const gestureHead: Anchor | null = headOk ?? (!('problem' in fallbackHead) ? { kind: 'head', joints: fallbackHead.joints, points: fallbackHead.points } : null);
|
|
1375
|
+
/**
|
|
1376
|
+
* #191: Posed origin and final world rotation (degrees, including #190's nod-residual coupling
|
|
1377
|
+
* and `headNodTrack` translation) of the head bone `hb` on frame `i`.
|
|
1378
|
+
*/
|
|
1379
|
+
const finalHeadPose = (hb: string, i: number): { origin: Pt; rotDeg: number } => {
|
|
1380
|
+
let origin = posedOrigin(hb, i);
|
|
1381
|
+
if (headNodTrack !== null && headNodTrack.bone === hb) {
|
|
1382
|
+
const k = headNodTrack.keys.find((x) => Math.abs(x.t - keyTimes[i]!) < 1e-5);
|
|
1383
|
+
if (k !== undefined && Array.isArray(k.v)) {
|
|
1384
|
+
const pName = rest.get(hb)?.parent ?? null;
|
|
1385
|
+
const pRest = pName !== null ? rest.get(pName) : undefined;
|
|
1386
|
+
if (pName !== null && pRest !== undefined) {
|
|
1387
|
+
const pTurn = turnAbove(pName, i);
|
|
1388
|
+
const rad = pTurn / DEG;
|
|
1389
|
+
const c = Math.cos(rad);
|
|
1390
|
+
const s = Math.sin(rad);
|
|
1391
|
+
const pa = c * pRest.a - s * pRest.c;
|
|
1392
|
+
const pb = c * pRest.b - s * pRest.d;
|
|
1393
|
+
const pc = s * pRest.a + c * pRest.c;
|
|
1394
|
+
const pd = s * pRest.b + c * pRest.d;
|
|
1395
|
+
const [lx, ly] = k.v;
|
|
1396
|
+
origin = [origin[0] + pa * lx + pb * ly, origin[1] + pc * lx + pd * ly];
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
let rotDeg = targets.get(hb)?.[i] ?? restOf.get(hb) ?? 0;
|
|
1401
|
+
if (headNodReading !== null && headNodReading.bone === hb && headNodReading.residual.some((r) => r !== 0)) {
|
|
1402
|
+
const nodY = headNodReading.clampedY[i] ?? 0;
|
|
1403
|
+
if (nodY !== 0) {
|
|
1404
|
+
const above = mappedAbove(hb);
|
|
1405
|
+
const hc = map.chains.find((c) => c.bones.includes(hb));
|
|
1406
|
+
const limits = hc?.range[hb];
|
|
1407
|
+
const cov = hc !== undefined ? covered.get(hc.name) : undefined;
|
|
1408
|
+
let next = wrap(rotDeg + NOD_RESIDUAL_HEAD_GAIN * nodY);
|
|
1409
|
+
if (limits !== undefined && cov?.[i] !== true) {
|
|
1410
|
+
const key = wrap((next - restOf.get(hb)!) - (above === null ? 0 : turn(above, i)));
|
|
1411
|
+
if (overflow(key, limits) > RANGE_SLACK) {
|
|
1412
|
+
next = wrap(next + Math.min(Math.max(key, limits[0]), limits[1]) - key);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
rotDeg = next;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return { origin, rotDeg };
|
|
1419
|
+
};
|
|
1420
|
+
/**
|
|
1421
|
+
* #191: For a chain listed in `set.anchor`, compute the target hand-tip position from the set's
|
|
1422
|
+
* `drawn` hand-tip-to-head offset in the `head` bone's local frame (or fixed world offset when
|
|
1423
|
+
* `HEAD_ANCHOR_FRAME === 'world-fixed'`), and solve the two-bone arm inside `set.turn`.
|
|
1424
|
+
*/
|
|
1425
|
+
const solveHeadAnchorFrame = (
|
|
1426
|
+
set: (typeof map.alternates)[number],
|
|
1427
|
+
c: Chain,
|
|
1428
|
+
i: number,
|
|
1429
|
+
): {
|
|
1430
|
+
a0: number;
|
|
1431
|
+
a1: number;
|
|
1432
|
+
targetTip: Pt;
|
|
1433
|
+
placedTip: Pt;
|
|
1434
|
+
achievedTip: Pt;
|
|
1435
|
+
residual: number;
|
|
1436
|
+
saturated: boolean;
|
|
1437
|
+
upperHeld: boolean;
|
|
1438
|
+
} | null => {
|
|
1439
|
+
if (HEAD_ANCHOR_FRAME === 'off' || headAnchorBone === null || c.bones.length < 2) return null;
|
|
1440
|
+
const [b0, b1] = c.bones as [string, string];
|
|
1441
|
+
const drawn0 = set.drawn[b0];
|
|
1442
|
+
const drawn1 = set.drawn[b1];
|
|
1443
|
+
if (drawn0 === undefined || drawn1 === undefined) return null;
|
|
1444
|
+
const S0 = rigPoint(rest, b0);
|
|
1445
|
+
const E0 = rigPoint(rest, b1);
|
|
1446
|
+
const wSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
1447
|
+
const W0 = rigPoint(rest, wSpec);
|
|
1448
|
+
const H0 = rigPoint(rest, headAnchorBone);
|
|
1449
|
+
const headRestRot = restOf.get(headAnchorBone);
|
|
1450
|
+
if (S0 === null || E0 === null || W0 === null || H0 === null || headRestRot === undefined) return null;
|
|
1451
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
1452
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
1453
|
+
const d0 = wrap(Math.atan2(E0[1] - S0[1], E0[0] - S0[0]) * DEG - restOf.get(b0)!);
|
|
1454
|
+
const d1 = wrap(Math.atan2(W0[1] - E0[1], W0[0] - E0[0]) * DEG - restOf.get(b1)!);
|
|
1455
|
+
const r0Drawn = (drawn0 + d0) / DEG;
|
|
1456
|
+
const r1Drawn = (drawn1 + d1) / DEG;
|
|
1457
|
+
const WDrawn: Pt = [
|
|
1458
|
+
S0[0] + l0 * Math.cos(r0Drawn) + l1 * Math.cos(r1Drawn),
|
|
1459
|
+
S0[1] + l0 * Math.sin(r0Drawn) + l1 * Math.sin(r1Drawn),
|
|
1460
|
+
];
|
|
1461
|
+
const dWorld0: Pt = [WDrawn[0] - H0[0], WDrawn[1] - H0[1]];
|
|
1462
|
+
const drawnHeadRot = set.drawn[headAnchorBone] ?? headRestRot;
|
|
1463
|
+
const { origin: H, rotDeg: headRot } = finalHeadPose(headAnchorBone, i);
|
|
1464
|
+
const dRotRad = HEAD_ANCHOR_FRAME === 'world-fixed' ? 0 : wrap(headRot - drawnHeadRot) / DEG;
|
|
1465
|
+
const cosH = Math.cos(dRotRad);
|
|
1466
|
+
const sinH = Math.sin(dRotRad);
|
|
1467
|
+
const targetTip: Pt = [
|
|
1468
|
+
H[0] + dWorld0[0] * cosH - dWorld0[1] * sinH,
|
|
1469
|
+
H[1] + dWorld0[0] * sinH + dWorld0[1] * cosH,
|
|
1470
|
+
];
|
|
1471
|
+
const shoulder = posedOrigin(b0, i);
|
|
1472
|
+
const place = reachLimit(H, targetTip, shoulder, l0, l1);
|
|
1473
|
+
const P = place.point;
|
|
1474
|
+
const side: 1 | -1 = wrap(drawn1 + d1 - (drawn0 + d0)) >= 0 ? 1 : -1;
|
|
1475
|
+
const sol = twoBone(shoulder, P, l0, l1, side);
|
|
1476
|
+
const turn0 = set.turn[b0];
|
|
1477
|
+
const turn1 = set.turn[b1];
|
|
1478
|
+
let a0 = sol.upper !== null ? wrap(sol.upper - d0) : drawn0;
|
|
1479
|
+
let a1 = sol.upper !== null ? wrap(sol.fore - d1) : drawn1;
|
|
1480
|
+
let upperHeld = false;
|
|
1481
|
+
let foreHeld = false;
|
|
1482
|
+
if (turn0 !== undefined) {
|
|
1483
|
+
const tt0 = wrap(a0 - drawn0);
|
|
1484
|
+
const cl0 = Math.min(Math.max(tt0, turn0[0]), turn0[1]);
|
|
1485
|
+
if (Math.abs(cl0 - tt0) > RANGE_SLACK) {
|
|
1486
|
+
upperHeld = true;
|
|
1487
|
+
a0 = wrap(drawn0 + cl0);
|
|
1488
|
+
const r0 = (a0 + d0) / DEG;
|
|
1489
|
+
const E: Pt = [shoulder[0] + l0 * Math.cos(r0), shoulder[1] + l0 * Math.sin(r0)];
|
|
1490
|
+
a1 = wrap(Math.atan2(P[1] - E[1], P[0] - E[0]) * DEG - d1);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (turn1 !== undefined) {
|
|
1494
|
+
const tt1 = wrap(a1 - drawn1);
|
|
1495
|
+
const cl1 = Math.min(Math.max(tt1, turn1[0]), turn1[1]);
|
|
1496
|
+
if (Math.abs(cl1 - tt1) > RANGE_SLACK) {
|
|
1497
|
+
foreHeld = true;
|
|
1498
|
+
a1 = wrap(drawn1 + cl1);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const r0 = (a0 + d0) / DEG;
|
|
1502
|
+
const r1 = (a1 + d1) / DEG;
|
|
1503
|
+
const achievedTip: Pt = [
|
|
1504
|
+
shoulder[0] + l0 * Math.cos(r0) + l1 * Math.cos(r1),
|
|
1505
|
+
shoulder[1] + l0 * Math.sin(r0) + l1 * Math.sin(r1),
|
|
1506
|
+
];
|
|
1507
|
+
const residual = Math.hypot(achievedTip[0] - targetTip[0], achievedTip[1] - targetTip[1]);
|
|
1508
|
+
const saturated = (place.how !== 'in' || upperHeld || foreHeld) && residual > 1e-3;
|
|
1509
|
+
return { a0, a1, targetTip, placedTip: P, achievedTip, residual, saturated, upperHeld };
|
|
1510
|
+
};
|
|
1361
1511
|
/**
|
|
1362
1512
|
* #188: For an active raised chain in a set that declares a `support` arm, compute the
|
|
1363
1513
|
* outward-cleared world angles `[a0, a1]` on frame `i` from the scan's 2D shoulder→wrist
|
|
@@ -1595,6 +1745,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1595
1745
|
};
|
|
1596
1746
|
const isChainHeadPlaced = (set: (typeof map.alternates)[number], c: Chain, drawn: number | undefined): boolean => {
|
|
1597
1747
|
if (headOk === null) return false;
|
|
1748
|
+
if (set.anchor?.includes(c.name) === true) return true;
|
|
1598
1749
|
const wJoint = c.joints[c.joints.length - 1];
|
|
1599
1750
|
const ea = set.pose[headOk.joints[0]];
|
|
1600
1751
|
const eb = set.pose[headOk.joints[1]];
|
|
@@ -1736,6 +1887,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1736
1887
|
const upperAsked: number[] = [];
|
|
1737
1888
|
const upperOver: number[] = [];
|
|
1738
1889
|
const heldMiss: number[] = [];
|
|
1890
|
+
const isHeadAnchored = HEAD_ANCHOR_FRAME !== 'off' && set.anchor?.includes(c.name) === true;
|
|
1891
|
+
if (isHeadAnchored) job.saturated = [];
|
|
1739
1892
|
for (const i of span) {
|
|
1740
1893
|
const h = pairOn(anchor, i);
|
|
1741
1894
|
const rc = refCentre(i);
|
|
@@ -1745,6 +1898,25 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1745
1898
|
job.reference.push(ref);
|
|
1746
1899
|
job.before.push([(w[0] - h.centre[0]) / rigEye, (w[1] - h.centre[1]) / rigEye]);
|
|
1747
1900
|
const shoulder = posedOrigin(b0, i);
|
|
1901
|
+
if (isHeadAnchored) {
|
|
1902
|
+
const ha = solveHeadAnchorFrame(set, c, i);
|
|
1903
|
+
if (ha !== null) {
|
|
1904
|
+
job.farthest = Math.max(job.farthest, Math.hypot(ha.targetTip[0] - shoulder[0], ha.targetTip[1] - shoulder[1]));
|
|
1905
|
+
job.placed.push([(ha.placedTip[0] - h.centre[0]) / rigEye, (ha.placedTip[1] - h.centre[1]) / rigEye]);
|
|
1906
|
+
if (ha.upperHeld) job.upperHeld.push(i);
|
|
1907
|
+
if (ha.saturated) job.saturated!.push({ frame: i, residual: r3(ha.residual) });
|
|
1908
|
+
const tt0 = turn0 === undefined ? 0 : wrap(ha.a0 - set.drawn[b0]!);
|
|
1909
|
+
const tt1 = turn1 === undefined ? 0 : wrap(ha.a1 - set.drawn[b1]!);
|
|
1910
|
+
upperAsked.push(tt0);
|
|
1911
|
+
upperOver.push(0);
|
|
1912
|
+
foreAsked.push(tt1);
|
|
1913
|
+
foreOver.push(0);
|
|
1914
|
+
heldMiss.push(ha.saturated ? ha.residual / rigEye : 0);
|
|
1915
|
+
t0[i] = ha.a0;
|
|
1916
|
+
t1[i] = ha.a1;
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1748
1920
|
const target: Pt = [h.centre[0] + ref[0] * rigEye, h.centre[1] + ref[1] * rigEye];
|
|
1749
1921
|
let solveTarget: Pt = target;
|
|
1750
1922
|
if (up && l0 / rigEye <= 3.5 && (turn0 !== undefined || turn1 !== undefined)) {
|
|
@@ -1877,12 +2049,28 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1877
2049
|
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1878
2050
|
}
|
|
1879
2051
|
}
|
|
1880
|
-
// #188: Even without --raised-by-head, apply symmetric normalisation
|
|
2052
|
+
// #188 / #191: Even without --raised-by-head, apply symmetric normalisation, support-arm anchoring, and head-anchored IK on each switch's span.
|
|
1881
2053
|
for (const s of sw.switches) {
|
|
1882
2054
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1883
2055
|
if (set.symmetric === true && headOk === null && hipsOk === null) {
|
|
1884
2056
|
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
1885
2057
|
}
|
|
2058
|
+
if (set.anchor !== undefined && headOk === null && hipsOk === null && HEAD_ANCHOR_FRAME !== 'off') {
|
|
2059
|
+
for (const cn of set.anchor) {
|
|
2060
|
+
if (!s.chains.includes(cn)) continue;
|
|
2061
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
2062
|
+
if (!c || c.bones.length < 2) continue;
|
|
2063
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
2064
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
2065
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
2066
|
+
const ha = solveHeadAnchorFrame(set, c, i);
|
|
2067
|
+
if (ha !== null) {
|
|
2068
|
+
t0[i] = ha.a0;
|
|
2069
|
+
t1[i] = ha.a1;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1886
2074
|
if (set.support !== undefined) {
|
|
1887
2075
|
for (const cn of s.chains) {
|
|
1888
2076
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
@@ -1944,6 +2132,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1944
2132
|
worst: { before: worst(job.before), after: worst(after), placed: r6(Math.max(...after.map((v, k) => Math.hypot(v[0] - job.placed[k]![0], v[1] - job.placed[k]![1])))) },
|
|
1945
2133
|
short,
|
|
1946
2134
|
upperHeld: job.upperHeld,
|
|
2135
|
+
...(job.saturated !== undefined ? { saturated: job.saturated } : {}),
|
|
1947
2136
|
reach: { min: r6(Math.abs(job.l0 - job.l1)), max: r6(job.l0 + job.l1), farthest: r6(job.farthest) },
|
|
1948
2137
|
};
|
|
1949
2138
|
// The unit sits after `seconds`, as the report has always written it.
|
|
@@ -1958,15 +2147,38 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1958
2147
|
const [lo, hi] = s.frames;
|
|
1959
2148
|
if (lo <= 0 || hi + 1 >= n) continue;
|
|
1960
2149
|
if (cov?.[lo - 1] !== true && cov?.[hi + 1] !== true) continue;
|
|
2150
|
+
if (cov?.[lo - 1] === true && cov?.[hi + 1] === true && hi - lo + 1 <= 10) continue;
|
|
2151
|
+
const ch = map.chains.find((c) => c.name === s.chain);
|
|
2152
|
+
const bIdx = ch !== undefined ? ch.bones.indexOf(s.bone) : -1;
|
|
2153
|
+
const parentTg = ch !== undefined && bIdx > 0 ? targets.get(ch.bones[bIdx - 1]!) : undefined;
|
|
1961
2154
|
const tg = targets.get(s.bone)!;
|
|
1962
|
-
const startA = tg[lo - 1]!;
|
|
1963
|
-
const
|
|
2155
|
+
const startA = parentTg !== undefined ? wrap(tg[lo - 1]! - parentTg[lo - 1]!) : tg[lo - 1]!;
|
|
2156
|
+
const endA = parentTg !== undefined ? wrap(tg[hi + 1]! - parentTg[hi + 1]!) : tg[hi + 1]!;
|
|
2157
|
+
const total = wrap(endA - startA);
|
|
1964
2158
|
const steps = hi - lo + 2;
|
|
1965
2159
|
for (let k = lo; k <= hi; k++) {
|
|
1966
|
-
|
|
2160
|
+
const localA = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
2161
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
1967
2162
|
}
|
|
1968
2163
|
s.step = r3(total / steps);
|
|
1969
2164
|
}
|
|
2165
|
+
const interpolateChainInJointSpace = (ch: Chain, i0: number, i1: number) => {
|
|
2166
|
+
const steps = i1 - i0;
|
|
2167
|
+
if (steps < 2 || i0 < 0 || i1 >= n) return;
|
|
2168
|
+
for (let bIdx = 0; bIdx < ch.bones.length; bIdx++) {
|
|
2169
|
+
const b = ch.bones[bIdx]!;
|
|
2170
|
+
const tg = targets.get(b);
|
|
2171
|
+
if (!tg) continue;
|
|
2172
|
+
const parentTg = bIdx > 0 ? targets.get(ch.bones[bIdx - 1]!) : undefined;
|
|
2173
|
+
const startA = parentTg !== undefined ? wrap(tg[i0]! - parentTg[i0]!) : tg[i0]!;
|
|
2174
|
+
const endA = parentTg !== undefined ? wrap(tg[i1]! - parentTg[i1]!) : tg[i1]!;
|
|
2175
|
+
const total = wrap(endA - startA);
|
|
2176
|
+
for (let k = i0 + 1; k < i1; k++) {
|
|
2177
|
+
const localA = wrap(startA + ((k - i0) / steps) * total);
|
|
2178
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
1970
2182
|
const holds = sw.chainHolds ?? [];
|
|
1971
2183
|
for (const h of holds) {
|
|
1972
2184
|
const swParent = sw.switches.find((s) => s.set === h.set && s.frames[0] === h.frames[0] && s.frames[1] > h.frames[1]);
|
|
@@ -1978,19 +2190,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1978
2190
|
if (lo > hi || lo <= 0 || hi + 1 >= n) continue;
|
|
1979
2191
|
const ch = map.chains.find((c) => c.name === h.chain);
|
|
1980
2192
|
if (!ch) continue;
|
|
1981
|
-
|
|
1982
|
-
for (const b of ch.bones) {
|
|
1983
|
-
const tg = targets.get(b);
|
|
1984
|
-
if (!tg) continue;
|
|
1985
|
-
const startA = tg[lo - 1]!;
|
|
1986
|
-
const total = wrap(tg[hi + 1]! - startA);
|
|
1987
|
-
for (let k = lo; k <= hi; k++) {
|
|
1988
|
-
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
2193
|
+
interpolateChainInJointSpace(ch, lo - 1, hi + 1);
|
|
1991
2194
|
}
|
|
1992
2195
|
}
|
|
1993
|
-
// #188: Cover the full shown/bridged span of symmetric sets
|
|
2196
|
+
// #188 / #191 / #206: Cover the full shown/bridged span of symmetric sets, support-arm sets, and head-anchored chains after short interpolation.
|
|
1994
2197
|
for (let sIdx = 0; sIdx < sw.switches.length; sIdx++) {
|
|
1995
2198
|
const s = sw.switches[sIdx]!;
|
|
1996
2199
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
@@ -2001,6 +2204,46 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2001
2204
|
while (loShow - 1 >= 0 && isSetShownAt(loShow - 1)) loShow--;
|
|
2002
2205
|
let hiShow = s.frames[1];
|
|
2003
2206
|
while (hiShow + 1 < n && isSetShownAt(hiShow + 1)) hiShow++;
|
|
2207
|
+
if (set.anchor !== undefined && HEAD_ANCHOR_FRAME !== 'off') {
|
|
2208
|
+
for (const cn of set.anchor) {
|
|
2209
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
2210
|
+
if (!c || c.bones.length < 2) continue;
|
|
2211
|
+
const chainSlots = setSlots.filter(([sl]) => {
|
|
2212
|
+
const sb = slots.get(sl)?.bone;
|
|
2213
|
+
return sb !== undefined && c.bones.includes(sb);
|
|
2214
|
+
});
|
|
2215
|
+
const isChainShownAt = (i: number): boolean =>
|
|
2216
|
+
chainSlots.length > 0
|
|
2217
|
+
? chainSlots.every(([sl, att]) => sw.shown.get(sl)?.[i] === att)
|
|
2218
|
+
: isSetShownAt(i);
|
|
2219
|
+
let loC = s.frames[0];
|
|
2220
|
+
while (loC - 1 >= 0 && isChainShownAt(loC - 1)) loC--;
|
|
2221
|
+
let hiC = s.frames[1];
|
|
2222
|
+
while (hiC + 1 < n && isChainShownAt(hiC + 1)) hiC++;
|
|
2223
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
2224
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
2225
|
+
for (let i = loC; i <= hiC; i++) {
|
|
2226
|
+
const ha = solveHeadAnchorFrame(set, c, i);
|
|
2227
|
+
if (ha !== null) {
|
|
2228
|
+
t0[i] = ha.a0;
|
|
2229
|
+
t1[i] = ha.a1;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
// If `short` or `chainHolds` ramp-interpolated the boundary frame immediately outside the
|
|
2233
|
+
// anchored chain's shown window (`loC - 1` or `hiC + 1`) while a neighbouring set's
|
|
2234
|
+
// attachment is still displayed on that bone, restore that boundary frame from the
|
|
2235
|
+
// neighbouring set's clamped key (`loC - 2` / `hiC + 2`) so the neighbouring set's art
|
|
2236
|
+
// is not bent toward the anchored pose before its slot switches.
|
|
2237
|
+
if (loC >= 2 && covered.get(cn)?.[loC - 1] === true) {
|
|
2238
|
+
t0[loC - 1] = t0[loC - 2]!;
|
|
2239
|
+
t1[loC - 1] = t1[loC - 2]!;
|
|
2240
|
+
}
|
|
2241
|
+
if (hiC + 2 < n && covered.get(cn)?.[hiC + 1] === true) {
|
|
2242
|
+
t0[hiC + 1] = t0[hiC + 2]!;
|
|
2243
|
+
t1[hiC + 1] = t1[hiC + 2]!;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2004
2247
|
if (set.symmetric === true) {
|
|
2005
2248
|
for (let i = loShow; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
2006
2249
|
if (loShow < s.frames[0]) {
|
|
@@ -2010,12 +2253,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2010
2253
|
for (const cn of set.chains) {
|
|
2011
2254
|
const c = map.chains.find((x) => x.name === cn);
|
|
2012
2255
|
if (!c) continue;
|
|
2013
|
-
for (
|
|
2256
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2257
|
+
const b = c.bones[bIdx]!;
|
|
2014
2258
|
const tg = targets.get(b)!;
|
|
2015
|
-
const
|
|
2016
|
-
const
|
|
2259
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2260
|
+
const startA = parentTg !== undefined ? wrap(tg[loRamp]! - parentTg[loRamp]!) : tg[loRamp]!;
|
|
2261
|
+
const endA = parentTg !== undefined ? wrap(tg[loShow]! - parentTg[loShow]!) : tg[loShow]!;
|
|
2262
|
+
const total = wrap(endA - startA);
|
|
2017
2263
|
for (let k = loRamp + 1; k < loShow; k++) {
|
|
2018
|
-
|
|
2264
|
+
const localA = wrap(startA + ((k - loRamp) / steps) * total);
|
|
2265
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2019
2266
|
}
|
|
2020
2267
|
}
|
|
2021
2268
|
}
|
|
@@ -2030,23 +2277,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2030
2277
|
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
2031
2278
|
const bR0 = cR.bones[0]!;
|
|
2032
2279
|
const bL0 = cL.bones[0]!;
|
|
2280
|
+
const bR1 = cR.bones[1]!;
|
|
2281
|
+
const bL1 = cL.bones[1]!;
|
|
2033
2282
|
const SR0 = rigPoint(rest, bR0)!;
|
|
2034
|
-
const ER0 = rigPoint(rest,
|
|
2283
|
+
const ER0 = rigPoint(rest, bR1)!;
|
|
2035
2284
|
const SL0 = rigPoint(rest, bL0)!;
|
|
2036
|
-
const EL0 = rigPoint(rest,
|
|
2285
|
+
const EL0 = rigPoint(rest, bL1)!;
|
|
2037
2286
|
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
2038
2287
|
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
2039
2288
|
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
2040
2289
|
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
2041
|
-
|
|
2290
|
+
const iEnd = s.frames[1];
|
|
2291
|
+
const iNext = Math.min(n - 1, hiShow + 1);
|
|
2292
|
+
const spanOut = Math.max(1, iNext - iEnd);
|
|
2293
|
+
const startR0 = targets.get(bR0)![iEnd]!;
|
|
2294
|
+
const startL0 = targets.get(bL0)![iEnd]!;
|
|
2295
|
+
const drawnR0 = set.drawn[bR0] ?? startR0;
|
|
2296
|
+
const drawnL0 = set.drawn[bL0] ?? startL0;
|
|
2297
|
+
const drawnR1 = set.drawn[bR1] ?? targets.get(bR1)![iEnd]!;
|
|
2298
|
+
const drawnL1 = set.drawn[bL1] ?? targets.get(bL1)![iEnd]!;
|
|
2299
|
+
const inR1 = wrap(drawnR1 + (set.turn[bR1]?.[0] ?? 0));
|
|
2300
|
+
const inL1 = wrap(drawnL1 + 0.6 * (set.turn[bL1]?.[1] ?? 0));
|
|
2301
|
+
const endR0 = targets.get(bR0)![iNext]!;
|
|
2302
|
+
const endL0 = targets.get(bL0)![iNext]!;
|
|
2303
|
+
for (let i = iEnd + 1; i <= hiShow; i++) {
|
|
2304
|
+
const u = (i - iEnd) / spanOut;
|
|
2305
|
+
const w = 1 - u;
|
|
2306
|
+
const alpha = 0.25 * u + 0.75 * u * u;
|
|
2042
2307
|
const sr = posedOrigin(bR0, i);
|
|
2043
2308
|
const sl = posedOrigin(bL0, i);
|
|
2044
|
-
const baseR =
|
|
2045
|
-
const baseL =
|
|
2046
|
-
let lo = -
|
|
2047
|
-
let hi =
|
|
2309
|
+
const baseR = wrap(startR0 + alpha * wrap(endR0 - startR0));
|
|
2310
|
+
const baseL = wrap(startL0 + alpha * wrap(endL0 - startL0));
|
|
2311
|
+
let lo = -30;
|
|
2312
|
+
let hi = 30;
|
|
2313
|
+
const dR = (deg: number) => (deg < 0 ? 0.25 * deg : deg);
|
|
2314
|
+
const dL = (deg: number) => (deg > 0 ? 0.25 * deg : deg);
|
|
2048
2315
|
const evalD = (deg: number) =>
|
|
2049
|
-
sl[1] + lL0 * Math.sin((baseL + deg + dL0) / DEG) -
|
|
2316
|
+
0.4 * w * (sl[1] - sr[1]) + lL0 * Math.sin((baseL + dL(deg) + dL0) / DEG) - lR0 * Math.sin((baseR + dR(deg) + dR0) / DEG);
|
|
2050
2317
|
let fLo = evalD(lo);
|
|
2051
2318
|
for (let it = 0; it < 32; it++) {
|
|
2052
2319
|
const mid = 0.5 * (lo + hi);
|
|
@@ -2057,24 +2324,50 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2057
2324
|
} else hi = mid;
|
|
2058
2325
|
}
|
|
2059
2326
|
const deg = 0.5 * (lo + hi);
|
|
2060
|
-
|
|
2061
|
-
|
|
2327
|
+
const aR0 = wrap(baseR + dR(deg));
|
|
2328
|
+
const aL0 = wrap(baseL + dL(deg));
|
|
2329
|
+
targets.get(bR0)![i] = aR0;
|
|
2330
|
+
targets.get(bL0)![i] = aL0;
|
|
2331
|
+
targets.get(bR1)![i] = wrap(inR1 + 0.45 * wrap(aR0 - drawnR0));
|
|
2332
|
+
targets.get(bL1)![i] = wrap(inL1 + 0.45 * wrap(aL0 - drawnL0));
|
|
2062
2333
|
}
|
|
2063
2334
|
}
|
|
2064
2335
|
}
|
|
2065
2336
|
}
|
|
2337
|
+
if (set.anchor === undefined && set.symmetric !== true && set.support === undefined && loShow < s.frames[0]) {
|
|
2338
|
+
for (const cn of s.chains) {
|
|
2339
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
2340
|
+
if (!c || c.bones.length < 2) continue;
|
|
2341
|
+
const b1 = c.bones[1]!;
|
|
2342
|
+
const t1 = targets.get(b1)!;
|
|
2343
|
+
const entryWorld1 = t1[s.frames[0]]!;
|
|
2344
|
+
for (let i = loShow; i < s.frames[0]; i++) {
|
|
2345
|
+
t1[i] = entryWorld1;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2066
2349
|
if (set.support !== undefined) {
|
|
2067
2350
|
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2068
2351
|
const loGap = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] + 1 : loShow;
|
|
2069
2352
|
for (const cn of s.chains) {
|
|
2070
2353
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
2071
2354
|
if (set.support.includes(c.name)) {
|
|
2072
|
-
for (let i =
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2355
|
+
for (let i = loShow; i <= hiShow; i++) solveSupportChainFrame(set, c, i);
|
|
2356
|
+
if (loGap < loShow && loGap >= 1) {
|
|
2357
|
+
const i0 = loGap - 1;
|
|
2358
|
+
const i1 = loShow;
|
|
2359
|
+
const steps = i1 - i0;
|
|
2360
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2361
|
+
const b = c.bones[bIdx]!;
|
|
2076
2362
|
const tg = targets.get(b)!;
|
|
2077
|
-
|
|
2363
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2364
|
+
const startA = parentTg !== undefined ? wrap(tg[i0]! - parentTg[i0]!) : tg[i0]!;
|
|
2365
|
+
const endA = parentTg !== undefined ? wrap(tg[i1]! - parentTg[i1]!) : tg[i1]!;
|
|
2366
|
+
const total = wrap(endA - startA);
|
|
2367
|
+
for (let k = loGap; k < loShow; k++) {
|
|
2368
|
+
const localA = wrap(startA + ((k - i0) / steps) * total);
|
|
2369
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2370
|
+
}
|
|
2078
2371
|
}
|
|
2079
2372
|
}
|
|
2080
2373
|
} else if (c.bones.length >= 2) {
|
|
@@ -2098,6 +2391,18 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2098
2391
|
if (i >= s.frames[0] || outSign * wrap(t0[i]! - pair[0]) < 0) t0[i] = pair[0];
|
|
2099
2392
|
if (i >= s.frames[0] || outSign * wrap(t1[i]! - pair[1]) < 0) t1[i] = pair[1];
|
|
2100
2393
|
}
|
|
2394
|
+
if (loGap <= loShow && loGap >= 1 && loShow + 1 <= hiShow) {
|
|
2395
|
+
const i0 = loGap - 1;
|
|
2396
|
+
const i1 = loShow + 1;
|
|
2397
|
+
const steps = i1 - i0;
|
|
2398
|
+
const startLocal = wrap(t1[i0]! - t0[i0]!);
|
|
2399
|
+
const endLocal = wrap(t1[i1]! - t0[i1]!);
|
|
2400
|
+
const totalLocal = wrap(endLocal - startLocal);
|
|
2401
|
+
for (let k = loGap; k <= loShow; k++) {
|
|
2402
|
+
const localA = wrap(startLocal + ((k - i0) / steps) * totalLocal);
|
|
2403
|
+
t1[k] = wrap(t0[k]! + localA);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2101
2406
|
}
|
|
2102
2407
|
}
|
|
2103
2408
|
}
|
|
@@ -2123,9 +2428,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2123
2428
|
}
|
|
2124
2429
|
const solvedKey = (b: string, i: number) => legPoses?.[i]!.solved.get(b);
|
|
2125
2430
|
|
|
2126
|
-
// #190:
|
|
2127
|
-
//
|
|
2128
|
-
|
|
2431
|
+
// #190 / #205: when the neck-length clamp acts on the sequence, couple the clamped
|
|
2432
|
+
// head.translate.y (`clampedY`, the signal the gains were calibrated against) into
|
|
2433
|
+
// head.rotate (+NOD_RESIDUAL_HEAD_GAIN °/unit) and pitch_dial.rotate (+NOD_RESIDUAL_PITCH_GAIN °/unit).
|
|
2434
|
+
const nodCoupled = headNodReading !== null && headNodReading.residual.some((r) => r !== 0) ? headNodReading.clampedY : null;
|
|
2435
|
+
if (headNodReading !== null && nodCoupled !== null) {
|
|
2129
2436
|
const hb = headNodReading.bone;
|
|
2130
2437
|
const tg = targets.get(hb);
|
|
2131
2438
|
if (tg !== undefined) {
|
|
@@ -2134,9 +2441,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2134
2441
|
const limits = hc?.range[hb];
|
|
2135
2442
|
const cov = hc !== undefined ? covered.get(hc.name) : undefined;
|
|
2136
2443
|
for (let i = 0; i < n; i++) {
|
|
2137
|
-
const
|
|
2138
|
-
if (
|
|
2139
|
-
let next = wrap(tg[i]! + NOD_RESIDUAL_HEAD_GAIN *
|
|
2444
|
+
const nodY = nodCoupled[i]!;
|
|
2445
|
+
if (nodY === 0) continue;
|
|
2446
|
+
let next = wrap(tg[i]! + NOD_RESIDUAL_HEAD_GAIN * nodY);
|
|
2140
2447
|
if (limits !== undefined && cov?.[i] !== true) {
|
|
2141
2448
|
const key = wrap((next - restOf.get(hb)!) - (above === null ? 0 : turn(above, i)));
|
|
2142
2449
|
if (overflow(key, limits) > RANGE_SLACK) {
|
|
@@ -2183,7 +2490,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2183
2490
|
const h = heldOf('l_ear');
|
|
2184
2491
|
return 'never' in h ? null : h.at;
|
|
2185
2492
|
})();
|
|
2186
|
-
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar,
|
|
2493
|
+
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar, nodCoupled, NOD_RESIDUAL_PITCH_GAIN);
|
|
2187
2494
|
for (const axis of ['yaw', 'pitch'] as const) {
|
|
2188
2495
|
const ax = face[axis];
|
|
2189
2496
|
if (ax === undefined) continue;
|
|
@@ -2381,6 +2688,20 @@ function headOf(map: JointMap): Head | { problem: string } {
|
|
|
2381
2688
|
return { problem: `${lead}; no chain of the map carries the eye line (joints r_eye → l_eye), so the rig has no eye points to measure` };
|
|
2382
2689
|
}
|
|
2383
2690
|
|
|
2691
|
+
/** The rig's head bone (the bone carrying `r_eye → l_eye` or `l_eye → r_eye` in the map), or null if no chain carries the eye line. */
|
|
2692
|
+
function headBoneOf(map: JointMap): string | null {
|
|
2693
|
+
for (const c of map.chains) {
|
|
2694
|
+
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
2695
|
+
const a = c.joints[k]!;
|
|
2696
|
+
const b = c.joints[k + 1]!;
|
|
2697
|
+
if ((a === 'r_eye' && b === 'l_eye') || (a === 'l_eye' && b === 'r_eye')) {
|
|
2698
|
+
return c.bones[k] ?? null;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2384
2705
|
type ShortSpan = Omit<Short, 'chain' | 'bone' | 'joints'>;
|
|
2385
2706
|
|
|
2386
2707
|
/**
|
package/src/switch.ts
CHANGED
|
@@ -787,14 +787,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
787
787
|
}
|
|
788
788
|
const tight0 = isTightSet(set0, cn);
|
|
789
789
|
const tight1 = isTightSet(set1, cn);
|
|
790
|
-
if (tight0 && set0 !== set1) {
|
|
790
|
+
if (tight0 && set0 !== set1 && g1 - g0 + 1 <= 10) {
|
|
791
791
|
let i = g0;
|
|
792
792
|
while (i <= g1 && rawChainMax(set0, cn, i) <= opts.switchTolerance + hyst) {
|
|
793
793
|
cClamp.get(cn)![i] = set0;
|
|
794
794
|
i++;
|
|
795
795
|
}
|
|
796
796
|
}
|
|
797
|
-
if (tight1 && set0 !== set1) {
|
|
797
|
+
if (tight1 && set0 !== set1 && g1 - g0 + 1 <= 10) {
|
|
798
798
|
let enter1 = g1 + 1;
|
|
799
799
|
for (let i = g0; i <= g1; i++) {
|
|
800
800
|
if (rawChainMax(set1, cn, i) <= opts.switchTolerance) {
|
|
@@ -809,63 +809,53 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
809
809
|
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
-
if (!tight0 && set0 !== set1) {
|
|
813
|
-
const c = map.chains.find((x) => x.name === cn);
|
|
814
|
-
if (c !== undefined) {
|
|
815
|
-
let k = sw0.frames[1];
|
|
816
|
-
while (
|
|
817
|
-
k >= sw0.frames[0] &&
|
|
818
|
-
c.bones.some((b) => {
|
|
819
|
-
const lim = set0.turn[b];
|
|
820
|
-
const dr = set0.drawn[b];
|
|
821
|
-
const ndr = set1.drawn[b];
|
|
822
|
-
if (lim === undefined || dr === undefined || ndr === undefined) return false;
|
|
823
|
-
const t = wrap(targets.get(b)![k]! - dr);
|
|
824
|
-
return overflow(t, lim) > RANGE_SLACK && Math.abs(wrap(targets.get(b)![k + 1]! - ndr)) < Math.abs(wrap(targets.get(b)![k]! - ndr));
|
|
825
|
-
})
|
|
826
|
-
) {
|
|
827
|
-
cClamp.get(cn)![k] = null;
|
|
828
|
-
k--;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
812
|
cPick.set(cn, arr);
|
|
833
813
|
}
|
|
834
|
-
// #187 Rule 1 & Rule 2: when two gesture sets are adjacent (gap <= 10 frames,
|
|
835
|
-
//
|
|
836
|
-
//
|
|
814
|
+
// #187 Rule 1 & Rule 2 / #206: when two gesture sets are adjacent (gap <= 10 frames),
|
|
815
|
+
// unwrap any bone that turned the long way (> 180° through inverted angles in its own joint
|
|
816
|
+
// space — world angle for a chain's root bone, parent-local angle for a child bone) onto the
|
|
817
|
+
// short arc and pick one shared switch frame t_switch where the measured path crosses halfway
|
|
837
818
|
// between set0.drawn and set1.drawn.
|
|
838
|
-
const
|
|
839
|
-
set0 !== set1 &&
|
|
840
|
-
distances.get(set1.name)![g1]! < distances.get(set1.name)![g0]! &&
|
|
841
|
-
distances.get(set0.name)![g1]! > distances.get(set0.name)![g0]!;
|
|
842
|
-
const bridgeDirect = set0 !== set1 && (g1 - g0 + 1 <= 10 || movingTowardNext);
|
|
819
|
+
const bridgeDirect = set0 !== set1 && g1 - g0 + 1 <= 10;
|
|
843
820
|
let tSwitch = g1 + 1;
|
|
844
821
|
if (bridgeDirect && switchingChains.length > 0) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
822
|
+
for (const cn of switchingChains) {
|
|
823
|
+
const ch = map.chains.find((c) => c.name === cn);
|
|
824
|
+
if (!ch) continue;
|
|
825
|
+
for (let bIdx = 0; bIdx < ch.bones.length; bIdx++) {
|
|
826
|
+
const b = ch.bones[bIdx]!;
|
|
827
|
+
const parentBone = bIdx > 0 ? ch.bones[bIdx - 1] : undefined;
|
|
828
|
+
const tg = targets.get(b) as number[] | undefined;
|
|
829
|
+
const parentTg = parentBone !== undefined ? targets.get(parentBone) : undefined;
|
|
830
|
+
const d0World = set0.drawn[b];
|
|
831
|
+
const d1World = set1.drawn[b];
|
|
832
|
+
if (tg === undefined || d0World === undefined || d1World === undefined) continue;
|
|
833
|
+
const pd0 = parentBone !== undefined ? set0.drawn[parentBone] : undefined;
|
|
834
|
+
const pd1 = parentBone !== undefined ? set1.drawn[parentBone] : undefined;
|
|
835
|
+
const jointAngle = (i: number): number =>
|
|
836
|
+
parentTg !== undefined ? wrap(tg[i]! - parentTg[i]!) : tg[i]!;
|
|
837
|
+
const d0 = pd0 !== undefined ? wrap(d0World - pd0) : d0World;
|
|
838
|
+
const d1 = pd1 !== undefined ? wrap(d1World - pd1) : d1World;
|
|
839
|
+
const i0 = sw0.frames[1];
|
|
840
|
+
const i1 = sw1.frames[0];
|
|
841
|
+
const startA = jointAngle(i0);
|
|
842
|
+
const endA = jointAngle(i1);
|
|
843
|
+
const unwrapped: number[] = [startA];
|
|
844
|
+
for (let i = i0 + 1; i <= i1; i++) {
|
|
845
|
+
unwrapped.push(unwrapped[unwrapped.length - 1]! + wrap(jointAngle(i) - jointAngle(i - 1)));
|
|
846
|
+
}
|
|
847
|
+
const totalTurn = unwrapped[unwrapped.length - 1]! - startA;
|
|
848
|
+
const shortTurn = wrap(endA - startA);
|
|
849
|
+
const invertsBetween = Array.from({ length: g1 - g0 + 1 }, (_, k) => {
|
|
850
|
+
const a = jointAngle(g0 + k);
|
|
851
|
+
return Math.abs(wrap(a - d0)) > 90 && Math.abs(wrap(a - d1)) > 90;
|
|
852
|
+
}).some(Boolean);
|
|
853
|
+
if (Math.abs(totalTurn) > 180 && Math.abs(totalTurn - shortTurn) > 1e-6 && invertsBetween && Math.abs(shortTurn) <= 150) {
|
|
854
|
+
for (let i = g0; i <= g1; i++) {
|
|
855
|
+
const p = (unwrapped[i - i0]! - startA) / totalTurn;
|
|
856
|
+
const localA = wrap(startA + p * shortTurn);
|
|
857
|
+
tg[i] = parentTg !== undefined ? wrap(parentTg[i]! + localA) : localA;
|
|
858
|
+
}
|
|
869
859
|
}
|
|
870
860
|
}
|
|
871
861
|
}
|
|
@@ -904,7 +894,8 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
904
894
|
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
905
895
|
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
906
896
|
const rootsCrossed =
|
|
907
|
-
|
|
897
|
+
switchingChains.length === 1 ||
|
|
898
|
+
rootBones.length === 0 ||
|
|
908
899
|
rootBones.every((b) => Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)) <= Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)));
|
|
909
900
|
if ((d1 <= d0 || (span > 1e-6 && d0 >= 0.5 * span)) && rootsCrossed) {
|
|
910
901
|
tSwitch = i;
|
|
@@ -912,6 +903,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
912
903
|
}
|
|
913
904
|
}
|
|
914
905
|
}
|
|
906
|
+
const syncBridgeClamp = (cn: string, i: number, picked: AlternateSet) => {
|
|
907
|
+
if (isTightSet(picked, cn)) cClamp.get(cn)![i] = picked;
|
|
908
|
+
};
|
|
915
909
|
const anyHeldBehind = switchingChains.some((cn) => {
|
|
916
910
|
const arr = cPick.get(cn)!;
|
|
917
911
|
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
@@ -924,7 +918,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
924
918
|
});
|
|
925
919
|
if (bridgeDirect) {
|
|
926
920
|
for (const cn of switchingChains) {
|
|
927
|
-
for (let i = g0; i <= g1; i++)
|
|
921
|
+
for (let i = g0; i <= g1; i++) {
|
|
922
|
+
const picked = i < tSwitch ? set0 : set1;
|
|
923
|
+
cPick.get(cn)![i] = picked;
|
|
924
|
+
syncBridgeClamp(cn, i, picked);
|
|
925
|
+
}
|
|
928
926
|
}
|
|
929
927
|
} else if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
930
928
|
} else {
|
|
@@ -980,7 +978,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
980
978
|
}
|
|
981
979
|
if (bridgeDirect) {
|
|
982
980
|
for (const cn of switchingChains) {
|
|
983
|
-
for (let i = g0; i <= g1; i++)
|
|
981
|
+
for (let i = g0; i <= g1; i++) {
|
|
982
|
+
const picked = i < tSwitch ? set0 : set1;
|
|
983
|
+
cPick.get(cn)![i] = picked;
|
|
984
|
+
syncBridgeClamp(cn, i, picked);
|
|
985
|
+
}
|
|
984
986
|
}
|
|
985
987
|
}
|
|
986
988
|
}
|
|
@@ -1224,7 +1226,7 @@ export const ART_EASE_STEP = 3;
|
|
|
1224
1226
|
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
1225
1227
|
const boneChain = new Map<string, string>();
|
|
1226
1228
|
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
1227
|
-
const clampSpan = (set: AlternateSet, bone: string, [a,
|
|
1229
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b]: [number, number]) => {
|
|
1228
1230
|
const limits = set.turn[bone];
|
|
1229
1231
|
if (limits === undefined) return;
|
|
1230
1232
|
const drawn = set.drawn[bone]!;
|
|
@@ -1236,28 +1238,14 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
1236
1238
|
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
1237
1239
|
};
|
|
1238
1240
|
const tightHold = limits[1] - limits[0] <= 10;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (!tightHold && b + 1 < n && delta(b) !== 0) {
|
|
1242
|
-
const nextSw = switches.find((s) => s.frames[0] > b && s.frames[0] - b <= 10 && s.set !== set.name);
|
|
1243
|
-
const nextDrawn = nextSw !== undefined ? map.alternates.find((x) => x.name === nextSw.set)?.drawn[bone] : undefined;
|
|
1244
|
-
if (nextDrawn !== undefined) {
|
|
1245
|
-
const distNext = (i: number) => Math.abs(wrap(target[i]! - nextDrawn));
|
|
1246
|
-
if (distNext(b + 1) < distNext(b)) {
|
|
1247
|
-
while (b >= a && delta(b) !== 0 && (b === a || distNext(b) < distNext(b - 1))) {
|
|
1248
|
-
b--;
|
|
1249
|
-
trimmedExit = true;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (a > b) return;
|
|
1241
|
+
const prevNearSw = switches.some((s) => s.frames[1] < a && a - s.frames[1] <= 11);
|
|
1242
|
+
const nextNearSw = switches.some((s) => s.frames[0] > b && s.frames[0] - b <= 11);
|
|
1255
1243
|
const da = delta(a);
|
|
1256
1244
|
const db = delta(b);
|
|
1257
|
-
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !tightHold
|
|
1245
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !(tightHold && prevNearSw)
|
|
1258
1246
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
1259
1247
|
: 0;
|
|
1260
|
-
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !tightHold &&
|
|
1248
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !(tightHold && nextNearSw)
|
|
1261
1249
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
1262
1250
|
: 0;
|
|
1263
1251
|
for (let i = a; i <= b; i++) {
|
|
@@ -1426,6 +1414,8 @@ export interface Raised {
|
|
|
1426
1414
|
short: ReachShort[];
|
|
1427
1415
|
/** The frames whose upper arm the set's `turn` held: the forearm re-aimed at the placed wrist. */
|
|
1428
1416
|
upperHeld: number[];
|
|
1417
|
+
/** For a head-anchored chain (`anchor`, rig-c/pose#191): each frame where the set's `turn` clamp saturated and left a non-zero wrist residual, in rig world units. */
|
|
1418
|
+
saturated?: Array<{ frame: number; residual: number }>;
|
|
1429
1419
|
/** The arm's reach (from |l0 − l1| to l0 + l1, world units from the shoulder) and the farthest target from the shoulder over the span. */
|
|
1430
1420
|
reach: { min: number; max: number; farthest: number };
|
|
1431
1421
|
}
|
|
@@ -1459,7 +1449,8 @@ export function raisedLine(e: Raised | ByHip, times: readonly number[]): string
|
|
|
1459
1449
|
const w = anchorOf(e);
|
|
1460
1450
|
const units = 'hips' in e ? e.hips : e.eyes;
|
|
1461
1451
|
const med = (vs: readonly P2[]): P2 => [median(vs.map((v) => v[0])), median(vs.map((v) => v[1]))];
|
|
1462
|
-
const
|
|
1452
|
+
const satText = e.saturated !== undefined && e.saturated.length > 0 ? `; residual ${e.saturated.map((s) => `f${s.frame} ${s.residual.toFixed(2)}u`).join(', ')}` : '';
|
|
1453
|
+
const held = e.upperHeld.length === 0 ? 'the upper arm within its turn on every frame' : `the upper arm held by the set's turn on ${e.upperHeld.length} frame(s) (${framesText(e.upperHeld)}${satText}), the forearm re-aimed`;
|
|
1463
1454
|
const out = e.short.length === 0 ? 'every target in reach' : `out of reach on ${e.short.reduce((k, s) => k + s.frames[1] - s.frames[0] + 1, 0)} frame(s) (REACH)`;
|
|
1464
1455
|
return `set "${e.set}" chain "${e.chain}" ${spanOf(e.frames, times)}: the wrist placed against ${w.against}; ${w.unitOne} (median) rig ${units.rig.toFixed(2)} units, reference ${units.reference.toFixed(2)} px; wrist − ${w.centre} in ${w.unit} (x right, y up), median reference ${xy(med(e.reference))}, placed ${xy(med(e.placed))}, before ${xy(med(e.before))}, after ${xy(med(e.after))}; worst |rig − reference| before ${xy(e.worst.before)}, after ${xy(e.worst.after)}; worst |after − placed| ${e.worst.placed.toFixed(2)}; ${held}; ${out}`;
|
|
1465
1456
|
}
|