@rig-c/pose 0.25.0 → 0.27.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 +5 -3
- package/package.json +1 -1
- package/src/face.ts +51 -2
- package/src/map.ts +51 -2
- package/src/retarget.ts +409 -8
- package/src/switch.ts +12 -2
package/README.md
CHANGED
|
@@ -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)): `{ "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), 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`).
|
|
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)): `{ "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), 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,10 +349,12 @@ 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: 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).
|
|
352
|
+
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key unless the set declares `symmetric` or `support` ([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
|
-
-
|
|
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 shows, each support chain's bones anchor to `drawn` within `±2.5°` (`≤ ±3°`) of the pose's delta relative to the set's reference pose, 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
|
+
- 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` and `MAP_ALT_SUPPORT`.
|
|
356
358
|
|
|
357
359
|
**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.
|
|
358
360
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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
CHANGED
|
@@ -123,6 +123,37 @@ function corrOf(a: readonly number[], b: readonly number[]): number {
|
|
|
123
123
|
return den > 1e-9 ? r3(num / den) : 1;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Eye-span foreshortening entry threshold `de < EAR_CHORD_K_DE * median(de)` (`rig-c/pose#189`):
|
|
128
|
+
* derived from $\cos(31.79^\circ) = 0.85$ ($\approx 32^\circ$ head turn), the foreshortening angle
|
|
129
|
+
* beyond which the near-planar 2D nose point begins to sit trapped between the foreshortened eyes.
|
|
130
|
+
*/
|
|
131
|
+
export const EAR_CHORD_K_DE = 0.85;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Eye-span foreshortening full-weight threshold `de <= EAR_CHORD_K_LO * median(de)` (`rig-c/pose#189`):
|
|
135
|
+
* derived from $\cos(36.87^\circ) = 0.80$ ($4/5$, $\approx 37^\circ$ head turn), where the foreshortening
|
|
136
|
+
* blend weight `w = clamp((EAR_CHORD_K_DE * de0 - de) / ((EAR_CHORD_K_DE - EAR_CHORD_K_LO) * de0), 0, 1)`
|
|
137
|
+
* reaches `1.0` so the exit from a foreshortened side gaze ramps smoothly instead of snapping at `EAR_CHORD_K_DE`.
|
|
138
|
+
*/
|
|
139
|
+
export const EAR_CHORD_K_LO = 0.8;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ear-chord to nose-cue disagreement ratio `|xea| > EAR_CHORD_K_RATIO * |xne|` (`rig-c/pose#189`):
|
|
143
|
+
* derived from the 95th-percentile of `|xea| / |xne|` on confident frames (`|de - median(de)| <= 15%`)
|
|
144
|
+
* above `EAR_CHORD_K_MIN`, measured at `p95 = 1.7352` on Dancer (`37` frames) and `p95 = 1.1937`
|
|
145
|
+
* (`p100 = 1.2867`) on Toca (`58` frames) -> `1.8`.
|
|
146
|
+
*/
|
|
147
|
+
export const EAR_CHORD_K_RATIO = 1.8;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Minimum ear-to-eye-midpoint offset `|xea| > EAR_CHORD_K_MIN` (`rig-c/pose#189`):
|
|
151
|
+
* derived from the 100th-percentile (maximum) detector ear-placement noise `|xea - xne|` on
|
|
152
|
+
* confident frames (`|de - median(de)| <= 15%`), measured at `p99 = 0.0949, p100 = 0.1094` on Dancer
|
|
153
|
+
* (`231` frames) and `p99 = 0.1178, p100 = 0.1450` on Toca (`373` frames) -> `0.15` (`16.70°`).
|
|
154
|
+
*/
|
|
155
|
+
export const EAR_CHORD_K_MIN = 0.15;
|
|
156
|
+
|
|
126
157
|
/**
|
|
127
158
|
* Read per-frame yaw and pitch (degrees) from `nose`, `r_eye`, `l_eye`, and
|
|
128
159
|
* (when both are present) `r_ear` and `l_ear`.
|
|
@@ -140,8 +171,10 @@ export function readFaceAngles(
|
|
|
140
171
|
): { ears: boolean; yaw: number[]; pitch: number[] } {
|
|
141
172
|
const n = nose.length;
|
|
142
173
|
const ears = rEar !== null && lEar !== null && rEar.length === n && lEar.length === n;
|
|
174
|
+
const des: number[] = [];
|
|
143
175
|
const xne: number[] = [];
|
|
144
176
|
const xna: number[] = [];
|
|
177
|
+
const xea: number[] = [];
|
|
145
178
|
const yne: number[] = [];
|
|
146
179
|
const yna: number[] = [];
|
|
147
180
|
|
|
@@ -153,14 +186,17 @@ export function readFaceAngles(
|
|
|
153
186
|
const ey = El[1] - Er[1];
|
|
154
187
|
const de = Math.hypot(ex, ey);
|
|
155
188
|
if (!(de > 1e-9)) {
|
|
189
|
+
des.push(des[i - 1] ?? 0);
|
|
156
190
|
xne.push(xne[i - 1] ?? 0);
|
|
157
191
|
yne.push(yne[i - 1] ?? 0);
|
|
158
192
|
if (ears) {
|
|
159
193
|
xna.push(xna[i - 1] ?? 0);
|
|
194
|
+
xea.push(xea[i - 1] ?? 0);
|
|
160
195
|
yna.push(yna[i - 1] ?? 0);
|
|
161
196
|
}
|
|
162
197
|
continue;
|
|
163
198
|
}
|
|
199
|
+
des.push(de);
|
|
164
200
|
const ux = ex / de;
|
|
165
201
|
const uy = ey / de;
|
|
166
202
|
const vx = -uy;
|
|
@@ -176,18 +212,31 @@ export function readFaceAngles(
|
|
|
176
212
|
const may = 0.5 * (Ar[1] + Al[1]);
|
|
177
213
|
const da = Math.max(de, (Al[0] - Ar[0]) * ux + (Al[1] - Ar[1]) * uy);
|
|
178
214
|
xna.push(((max - N[0]) * ux + (may - N[1]) * uy) / da);
|
|
215
|
+
xea.push(((max - mex) * ux + (may - mey) * uy) / da);
|
|
179
216
|
yna.push(((N[0] - max) * vx + (N[1] - may) * vy) / de);
|
|
180
217
|
}
|
|
181
218
|
}
|
|
182
219
|
|
|
220
|
+
const de0 = n > 0 ? median(des) : 0;
|
|
183
221
|
const yne0 = median(yne);
|
|
184
222
|
const yna0 = ears ? median(yna) : 0;
|
|
185
223
|
const yaw: number[] = [];
|
|
186
224
|
const pitch: number[] = [];
|
|
187
225
|
for (let i = 0; i < n; i++) {
|
|
188
|
-
const
|
|
226
|
+
const earChord =
|
|
227
|
+
ears &&
|
|
228
|
+
de0 > 1e-9 &&
|
|
229
|
+
des[i]! < EAR_CHORD_K_DE * de0 &&
|
|
230
|
+
Math.abs(xea[i]!) > EAR_CHORD_K_RATIO * Math.abs(xne[i]!) &&
|
|
231
|
+
Math.abs(xea[i]!) > EAR_CHORD_K_MIN;
|
|
232
|
+
const w = earChord
|
|
233
|
+
? EAR_CHORD_K_DE > EAR_CHORD_K_LO
|
|
234
|
+
? Math.max(0, Math.min(1, (EAR_CHORD_K_DE * de0 - des[i]!) / ((EAR_CHORD_K_DE - EAR_CHORD_K_LO) * de0)))
|
|
235
|
+
: 1
|
|
236
|
+
: 0;
|
|
237
|
+
const rawYaw = Math.atan(ears ? (2 * xne[i]! + xna[i]!) / 2 : 2 * xne[i]!) * DEG;
|
|
189
238
|
const tp = ears ? (yne[i]! - yne0 + (yna[i]! - yna0)) / 2 : yne[i]! - yne0;
|
|
190
|
-
yaw.push(Math.atan(
|
|
239
|
+
yaw.push(w > 0 ? w * (Math.atan(2 * xea[i]!) * DEG) + (1 - w) * rawYaw : rawYaw);
|
|
191
240
|
pitch.push(Math.atan(tp) * DEG);
|
|
192
241
|
}
|
|
193
242
|
return { ears, yaw, pitch };
|
package/src/map.ts
CHANGED
|
@@ -100,6 +100,10 @@ export interface AlternateSet {
|
|
|
100
100
|
frames: Array<[number, number]> | null;
|
|
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
|
+
/** `symmetric: true` (rig-c/pose#188): normalise the set's two two-bone arm chains to the drawn symmetry centre and keep their wrists inside the head silhouette while the set shows. */
|
|
104
|
+
symmetric?: boolean;
|
|
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
|
+
support?: string[];
|
|
103
107
|
}
|
|
104
108
|
export interface YawSet {
|
|
105
109
|
set: string;
|
|
@@ -435,7 +439,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
435
439
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
436
440
|
return;
|
|
437
441
|
}
|
|
438
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place']);
|
|
442
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support']);
|
|
439
443
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
440
444
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
441
445
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -553,7 +557,52 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
553
557
|
else place = { by: 'hip', chains: by.chains };
|
|
554
558
|
}
|
|
555
559
|
}
|
|
556
|
-
|
|
560
|
+
let symmetric: boolean | undefined;
|
|
561
|
+
if ('symmetric' in a) {
|
|
562
|
+
if (a.symmetric !== true) {
|
|
563
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is ${JSON.stringify(a.symmetric)}; it must be true`);
|
|
564
|
+
ok = false;
|
|
565
|
+
} else if (own.length !== 2 || !own.every((c) => c.bones.length === 2)) {
|
|
566
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is true, and its chains are ${ownList}; a symmetric set normalises a pair of two-bone arm chains`);
|
|
567
|
+
ok = false;
|
|
568
|
+
} else {
|
|
569
|
+
const missing = ownBones.filter((b) => !(b in drawn));
|
|
570
|
+
if (missing.length > 0) {
|
|
571
|
+
p.add('MAP_ALT_SYMMETRIC', `${label} "symmetric" is true, and "drawn" has no ${missing.map((b) => `"${b}"`).join(', ')}; a symmetric set normalises both chains around their drawn angles`);
|
|
572
|
+
ok = false;
|
|
573
|
+
} else symmetric = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
let support: string[] | undefined;
|
|
577
|
+
if ('support' in a) {
|
|
578
|
+
const sup = typeof a.support === 'string' && a.support !== '' ? [a.support] : strings(a.support);
|
|
579
|
+
if (sup === null || sup.length === 0 || sup.some((s) => s === '')) {
|
|
580
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" is ${JSON.stringify(a.support)}; it must name one or more of the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
581
|
+
ok = false;
|
|
582
|
+
} else {
|
|
583
|
+
if (new Set(sup).size !== sup.length) {
|
|
584
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" names a chain twice: ${sup.join(', ')}`);
|
|
585
|
+
ok = false;
|
|
586
|
+
}
|
|
587
|
+
const outside = sup.filter((c) => !(cs ?? []).includes(c));
|
|
588
|
+
if (outside.length > 0) {
|
|
589
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" names chain ${outside.map((c) => `"${c}"`).join(', ')}, which is not in the set's chains (${(cs ?? []).join(', ') || 'none'})`);
|
|
590
|
+
ok = false;
|
|
591
|
+
}
|
|
592
|
+
if (a.symmetric === true) {
|
|
593
|
+
p.add('MAP_ALT_SUPPORT', `${label} names both "symmetric" and "support"; a set either normalises both arms symmetrically or anchors a support arm`);
|
|
594
|
+
ok = false;
|
|
595
|
+
}
|
|
596
|
+
const supBones = own.filter((c) => sup.includes(c.name)).flatMap((c) => c.bones);
|
|
597
|
+
const missing = supBones.filter((b) => !(b in drawn));
|
|
598
|
+
if (missing.length > 0) {
|
|
599
|
+
p.add('MAP_ALT_SUPPORT', `${label} "support" chain has no "drawn" angle for ${missing.map((b) => `"${b}"`).join(', ')}; a support chain anchors to its drawn angles`);
|
|
600
|
+
ok = false;
|
|
601
|
+
}
|
|
602
|
+
if (ok) support = sup;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
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 }) });
|
|
557
606
|
});
|
|
558
607
|
return out;
|
|
559
608
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -1157,6 +1157,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1157
1157
|
handOffsets.set(c.name, arr);
|
|
1158
1158
|
}
|
|
1159
1159
|
}
|
|
1160
|
+
const rawTargets = new Map<string, number[]>([...targets].map(([b, arr]) => [b, [...arr]]));
|
|
1160
1161
|
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets });
|
|
1161
1162
|
|
|
1162
1163
|
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
@@ -1326,6 +1327,236 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1326
1327
|
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 }> = [];
|
|
1327
1328
|
const headOk: Anchor | null = head !== null && !('problem' in head) ? { kind: 'head', joints: head.joints, points: head.points } : null;
|
|
1328
1329
|
const hipsOk: Anchor | null = hips !== null && !('problem' in hips) ? { kind: 'hip', joints: hips.joints, points: hips.points } : null;
|
|
1330
|
+
const fallbackHead = headOf(map);
|
|
1331
|
+
const gestureHead: Anchor | null = headOk ?? (!('problem' in fallbackHead) ? { kind: 'head', joints: fallbackHead.joints, points: fallbackHead.points } : null);
|
|
1332
|
+
/**
|
|
1333
|
+
* #188: For an active raised chain in a set that declares a `support` arm, compute the
|
|
1334
|
+
* outward-cleared world angles `[a0, a1]` on frame `i` from the scan's 2D shoulder→wrist
|
|
1335
|
+
* direction and the head/bun clearance.
|
|
1336
|
+
*/
|
|
1337
|
+
const solveSupportActiveFrame = (set: (typeof map.alternates)[number], c: Chain, i: number): [number, number] | null => {
|
|
1338
|
+
if (c.bones.length < 2 || c.joints.length < 3) return null;
|
|
1339
|
+
const [b0, b1] = c.bones as [string, string];
|
|
1340
|
+
const drawn0 = set.drawn[b0];
|
|
1341
|
+
const drawn1 = set.drawn[b1];
|
|
1342
|
+
if (drawn0 === undefined || drawn1 === undefined || !isRaised(drawn0)) return null;
|
|
1343
|
+
const S0 = rigPoint(rest, b0);
|
|
1344
|
+
const E0 = rigPoint(rest, b1);
|
|
1345
|
+
const wSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
1346
|
+
const W0 = rigPoint(rest, wSpec);
|
|
1347
|
+
if (S0 === null || E0 === null || W0 === null) return null;
|
|
1348
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
1349
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
1350
|
+
let rigEye0 = 0.32 * l0;
|
|
1351
|
+
let headX0 = 0;
|
|
1352
|
+
if (gestureHead !== null) {
|
|
1353
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
1354
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
1355
|
+
if (ha0 !== null && hb0 !== null) {
|
|
1356
|
+
const d = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1357
|
+
if (d > 1e-6) {
|
|
1358
|
+
rigEye0 = d;
|
|
1359
|
+
headX0 = (ha0[0] + hb0[0]) / 2;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const Ex = S0[0] + l0 * Math.cos(drawn0 / DEG);
|
|
1364
|
+
const outSign: 1 | -1 = Ex <= headX0 ? 1 : -1;
|
|
1365
|
+
const clear0 = Math.asin(Math.min(1, (0.55 * rigEye0) / Math.max(1e-6, l0))) * DEG;
|
|
1366
|
+
const clear1 = Math.asin(Math.min(1, (0.90 * rigEye0) / Math.max(1e-6, l1))) * DEG;
|
|
1367
|
+
let swMod = 0;
|
|
1368
|
+
const j0 = c.joints[0]!;
|
|
1369
|
+
const jLast = c.joints[c.joints.length - 1]!;
|
|
1370
|
+
const kp0 = set.pose[j0];
|
|
1371
|
+
const kpLast = set.pose[jLast];
|
|
1372
|
+
const shS = series.get(j0);
|
|
1373
|
+
const wrS = series.get(jLast);
|
|
1374
|
+
if (kp0 !== undefined && kpLast !== undefined && shS !== undefined && wrS !== undefined) {
|
|
1375
|
+
const keySW = pictureAngle(kp0, kpLast);
|
|
1376
|
+
const scanSW = pictureAngle(shS[i]!, wrS[i]!);
|
|
1377
|
+
swMod = Math.max(0, Math.min(6, outSign * wrap(scanSW - keySW)));
|
|
1378
|
+
}
|
|
1379
|
+
const lim0 = set.turn[b0];
|
|
1380
|
+
const lim1 = set.turn[b1];
|
|
1381
|
+
const off0 = outSign * (clear0 + 0.5 * swMod);
|
|
1382
|
+
const off1 = outSign * (clear1 + 0.5 * swMod);
|
|
1383
|
+
const cl0 = lim0 !== undefined ? Math.min(Math.max(off0, lim0[0]), lim0[1]) : off0;
|
|
1384
|
+
const cl1 = lim1 !== undefined ? Math.min(Math.max(off1, lim1[0]), lim1[1]) : off1;
|
|
1385
|
+
return [wrap(drawn0 + cl0), wrap(drawn1 + cl1)];
|
|
1386
|
+
};
|
|
1387
|
+
/**
|
|
1388
|
+
* #188: For a support chain in `set.support`, anchor its bones on frame `i` to `set.drawn`
|
|
1389
|
+
* within a ±2.5° (≤ ±3°) breathing tolerance from the pose's delta relative to the set's
|
|
1390
|
+
* reference pose.
|
|
1391
|
+
*/
|
|
1392
|
+
const solveSupportChainFrame = (set: (typeof map.alternates)[number], c: Chain, i: number): void => {
|
|
1393
|
+
for (let k = 0; k < c.bones.length; k++) {
|
|
1394
|
+
const b = c.bones[k]!;
|
|
1395
|
+
const dr = set.drawn[b];
|
|
1396
|
+
if (dr === undefined) continue;
|
|
1397
|
+
const kp0 = c.joints[k] !== undefined ? set.pose[c.joints[k]!] : undefined;
|
|
1398
|
+
const kp1 = c.joints[k + 1] !== undefined ? set.pose[c.joints[k + 1]!] : undefined;
|
|
1399
|
+
const keyAng = kp0 !== undefined && kp1 !== undefined ? pictureAngle(kp0, kp1) : dr;
|
|
1400
|
+
const raw = rawTargets.get(b)?.[i] ?? keyAng;
|
|
1401
|
+
const diff = Math.max(-2.5, Math.min(2.5, 0.1 * wrap(raw - keyAng)));
|
|
1402
|
+
targets.get(b)![i] = wrap(dr + diff);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
/**
|
|
1406
|
+
* #188: For a `set.symmetric === true` gesture set, normalise its two two-bone arm chains
|
|
1407
|
+
* on frame `i` to the set's `drawn` symmetry centre so `|elbow_l.y - elbow_r.y| == 0` and
|
|
1408
|
+
* clamp both forearms so the wrists stay inside the head silhouette.
|
|
1409
|
+
*/
|
|
1410
|
+
const solveSymmetricFrame = (set: (typeof map.alternates)[number], i: number): { cR: Chain; cL: Chain; wR: Pt; wL: Pt; hc: Pt; ed: number } | null => {
|
|
1411
|
+
if (set.chains.length !== 2) return null;
|
|
1412
|
+
const c0 = map.chains.find((x) => x.name === set.chains[0]!);
|
|
1413
|
+
const c1 = map.chains.find((x) => x.name === set.chains[1]!);
|
|
1414
|
+
if (c0 === undefined || c1 === undefined || c0.bones.length !== 2 || c1.bones.length !== 2) return null;
|
|
1415
|
+
const S0_0 = rigPoint(rest, c0.bones[0]!);
|
|
1416
|
+
const S0_1 = rigPoint(rest, c1.bones[0]!);
|
|
1417
|
+
if (S0_0 === null || S0_1 === null) return null;
|
|
1418
|
+
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
1419
|
+
const [bR0, bR1] = cR.bones as [string, string];
|
|
1420
|
+
const [bL0, bL1] = cL.bones as [string, string];
|
|
1421
|
+
const wSpecR = cR.aim[1] === 'x' ? `${bR1}:tip` : cR.aim[1]!;
|
|
1422
|
+
const wSpecL = cL.aim[1] === 'x' ? `${bL1}:tip` : cL.aim[1]!;
|
|
1423
|
+
const SR0 = rigPoint(rest, bR0)!;
|
|
1424
|
+
const ER0 = rigPoint(rest, bR1)!;
|
|
1425
|
+
const WR0 = rigPoint(rest, wSpecR)!;
|
|
1426
|
+
const SL0 = rigPoint(rest, bL0)!;
|
|
1427
|
+
const EL0 = rigPoint(rest, bL1)!;
|
|
1428
|
+
const WL0 = rigPoint(rest, wSpecL)!;
|
|
1429
|
+
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
1430
|
+
const lR1 = Math.hypot(WR0[0] - ER0[0], WR0[1] - ER0[1]);
|
|
1431
|
+
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
1432
|
+
const lL1 = Math.hypot(WL0[0] - EL0[0], WL0[1] - EL0[1]);
|
|
1433
|
+
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
1434
|
+
const dR1 = wrap(Math.atan2(WR0[1] - ER0[1], WR0[0] - ER0[0]) * DEG - restOf.get(bR1)!);
|
|
1435
|
+
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
1436
|
+
const dL1 = wrap(Math.atan2(WL0[1] - EL0[1], WL0[0] - EL0[0]) * DEG - restOf.get(bL1)!);
|
|
1437
|
+
const sr = posedOrigin(bR0, i);
|
|
1438
|
+
const sl = posedOrigin(bL0, i);
|
|
1439
|
+
let hc: Pt = [(sr[0] + sl[0]) / 2, (sr[1] + sl[1]) / 2 + 0.85 * Math.hypot(sl[0] - sr[0], sl[1] - sr[1])];
|
|
1440
|
+
let ed = Math.max(1e-3, 0.30 * Math.hypot(sl[0] - sr[0], sl[1] - sr[1]));
|
|
1441
|
+
if (gestureHead !== null) {
|
|
1442
|
+
const ha = posedPoint(gestureHead.points[0], i);
|
|
1443
|
+
const hb = posedPoint(gestureHead.points[1], i);
|
|
1444
|
+
const d = Math.hypot(hb[0] - ha[0], hb[1] - ha[1]);
|
|
1445
|
+
if (d > 1e-6) {
|
|
1446
|
+
hc = [(ha[0] + hb[0]) / 2, (ha[1] + hb[1]) / 2];
|
|
1447
|
+
ed = d;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const drawnR0 = set.drawn[bR0]!;
|
|
1451
|
+
const drawnR1 = set.drawn[bR1]!;
|
|
1452
|
+
const drawnL0 = set.drawn[bL0]!;
|
|
1453
|
+
const drawnL1 = set.drawn[bL1]!;
|
|
1454
|
+
const turnR0 = set.turn[bR0] ?? [-20, 20];
|
|
1455
|
+
const turnR1 = set.turn[bR1] ?? [-20, 20];
|
|
1456
|
+
const turnL0 = set.turn[bL0] ?? [-20, 20];
|
|
1457
|
+
const turnL1 = set.turn[bL1] ?? [-20, 20];
|
|
1458
|
+
const rawR0 = rawTargets.get(bR0)?.[i] ?? drawnR0;
|
|
1459
|
+
const rawL0 = rawTargets.get(bL0)?.[i] ?? drawnL0;
|
|
1460
|
+
const offR0 = wrap(rawR0 - drawnR0);
|
|
1461
|
+
const offL0 = wrap(rawL0 - drawnL0);
|
|
1462
|
+
let cOpen = Math.max(-4, Math.min(2, 0.15 * 0.5 * (offR0 - offL0)));
|
|
1463
|
+
const solveTilt = (open: number): { aR0: number; aL0: number; exR: number; eyR: number; exL: number; eyL: number } => {
|
|
1464
|
+
const evalDiff = (deg: number) => {
|
|
1465
|
+
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1466
|
+
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1467
|
+
const ar = drawnR0 + Math.min(Math.max(open + dR, turnR0[0]), turnR0[1]);
|
|
1468
|
+
const al = drawnL0 + Math.min(Math.max(-open + dL, turnL0[0]), turnL0[1]);
|
|
1469
|
+
const eyr = sr[1] + lR0 * Math.sin((ar + dR0) / DEG);
|
|
1470
|
+
const eyl = sl[1] + lL0 * Math.sin((al + dL0) / DEG);
|
|
1471
|
+
return eyl - eyr;
|
|
1472
|
+
};
|
|
1473
|
+
let lo = -45;
|
|
1474
|
+
let hi = 45;
|
|
1475
|
+
let fLo = evalDiff(lo);
|
|
1476
|
+
const fHi = evalDiff(hi);
|
|
1477
|
+
let deg: number;
|
|
1478
|
+
if ((fLo <= 0 && fHi <= 0) || (fLo > 0 && fHi > 0)) {
|
|
1479
|
+
deg = Math.abs(fLo) <= Math.abs(fHi) ? lo : hi;
|
|
1480
|
+
} else {
|
|
1481
|
+
for (let it = 0; it < 36; it++) {
|
|
1482
|
+
const mid = 0.5 * (lo + hi);
|
|
1483
|
+
const fMid = evalDiff(mid);
|
|
1484
|
+
if ((fLo <= 0 && fMid <= 0) || (fLo > 0 && fMid > 0)) {
|
|
1485
|
+
lo = mid;
|
|
1486
|
+
fLo = fMid;
|
|
1487
|
+
} else {
|
|
1488
|
+
hi = mid;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
deg = 0.5 * (lo + hi);
|
|
1492
|
+
}
|
|
1493
|
+
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1494
|
+
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1495
|
+
let aR0 = drawnR0 + Math.min(Math.max(open + dR, turnR0[0]), turnR0[1]);
|
|
1496
|
+
let aL0 = drawnL0 + Math.min(Math.max(-open + dL, turnL0[0]), turnL0[1]);
|
|
1497
|
+
const eyR0 = sr[1] + lR0 * Math.sin((aR0 + dR0) / DEG);
|
|
1498
|
+
const eyL0 = sl[1] + lL0 * Math.sin((aL0 + dL0) / DEG);
|
|
1499
|
+
if (Math.abs(eyL0 - eyR0) > 1e-4) {
|
|
1500
|
+
const sL = Math.max(-1, Math.min(1, (eyR0 - sl[1]) / Math.max(1e-6, lL0)));
|
|
1501
|
+
const candL = wrap(Math.asin(sL) * DEG - dL0);
|
|
1502
|
+
const tL = wrap(candL - drawnL0);
|
|
1503
|
+
if (tL >= turnL0[0] && tL <= turnL0[1]) aL0 = candL;
|
|
1504
|
+
}
|
|
1505
|
+
const rR0 = (aR0 + dR0) / DEG;
|
|
1506
|
+
const rL0 = (aL0 + dL0) / DEG;
|
|
1507
|
+
return {
|
|
1508
|
+
aR0: wrap(aR0),
|
|
1509
|
+
aL0: wrap(aL0),
|
|
1510
|
+
exR: sr[0] + lR0 * Math.cos(rR0),
|
|
1511
|
+
eyR: sr[1] + lR0 * Math.sin(rR0),
|
|
1512
|
+
exL: sl[0] + lL0 * Math.cos(rL0),
|
|
1513
|
+
eyL: sl[1] + lL0 * Math.sin(rL0),
|
|
1514
|
+
};
|
|
1515
|
+
};
|
|
1516
|
+
const upper = solveTilt(cOpen);
|
|
1517
|
+
const aimFore = (ex: number, ey: number, l1: number, d1: number, drawn1: number, turn1: readonly [number, number], tx: number, ty: number): [number, Pt] => {
|
|
1518
|
+
const want = wrap(Math.atan2(ty - ey, tx - ex) * DEG - d1);
|
|
1519
|
+
const off = Math.min(Math.max(wrap(want - drawn1), turn1[0]), turn1[1]);
|
|
1520
|
+
let a1 = wrap(drawn1 + off);
|
|
1521
|
+
let r1 = (a1 + d1) / DEG;
|
|
1522
|
+
let wx = ex + l1 * Math.cos(r1);
|
|
1523
|
+
let wy = ey + l1 * Math.sin(r1);
|
|
1524
|
+
const xMin = hc[0] - 0.30 * ed;
|
|
1525
|
+
const xMax = hc[0] + 0.30 * ed;
|
|
1526
|
+
const yMin = hc[1] + 0.15 * ed;
|
|
1527
|
+
const yMax = hc[1] + 0.95 * ed;
|
|
1528
|
+
if (wx < xMin || wx > xMax || wy < yMin || wy > yMax) {
|
|
1529
|
+
let bestA = a1;
|
|
1530
|
+
let bestErr = Infinity;
|
|
1531
|
+
for (let step = -200; step <= 200; step++) {
|
|
1532
|
+
const candOff = turn1[0] + ((turn1[1] - turn1[0]) * (step + 200)) / 400;
|
|
1533
|
+
const candA = wrap(drawn1 + candOff);
|
|
1534
|
+
const cr = (candA + d1) / DEG;
|
|
1535
|
+
const cwx = ex + l1 * Math.cos(cr);
|
|
1536
|
+
const cwy = ey + l1 * Math.sin(cr);
|
|
1537
|
+
const ox = cwx < xMin ? xMin - cwx : cwx > xMax ? cwx - xMax : 0;
|
|
1538
|
+
const oy = cwy < yMin ? yMin - cwy : cwy > yMax ? cwy - yMax : 0;
|
|
1539
|
+
const err = (ox + oy) * 1000 + Math.hypot(cwx - tx, cwy - ty);
|
|
1540
|
+
if (err < bestErr) {
|
|
1541
|
+
bestErr = err;
|
|
1542
|
+
bestA = candA;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
a1 = bestA;
|
|
1546
|
+
r1 = (a1 + d1) / DEG;
|
|
1547
|
+
wx = ex + l1 * Math.cos(r1);
|
|
1548
|
+
wy = ey + l1 * Math.sin(r1);
|
|
1549
|
+
}
|
|
1550
|
+
return [a1, [wx, wy]];
|
|
1551
|
+
};
|
|
1552
|
+
const [aR1, wR] = aimFore(upper.exR, upper.eyR, lR1, dR1, drawnR1, turnR1, hc[0] - 0.20 * ed, hc[1] + 0.65 * ed);
|
|
1553
|
+
const [aL1, wL] = aimFore(upper.exL, upper.eyL, lL1, dL1, drawnL1, turnL1, hc[0] + 0.20 * ed, hc[1] + 0.65 * ed);
|
|
1554
|
+
targets.get(bR0)![i] = upper.aR0;
|
|
1555
|
+
targets.get(bR1)![i] = aR1;
|
|
1556
|
+
targets.get(bL0)![i] = upper.aL0;
|
|
1557
|
+
targets.get(bL1)![i] = aL1;
|
|
1558
|
+
return { cR, cL, wR, wL, hc, ed };
|
|
1559
|
+
};
|
|
1329
1560
|
if (headOk !== null || hipsOk !== null) {
|
|
1330
1561
|
/** The anchor's two rig points posed on frame i: their midpoint and their distance apart. */
|
|
1331
1562
|
const pairOn = (an: Anchor, i: number): { centre: Pt; dist: number } => {
|
|
@@ -1382,18 +1613,34 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1382
1613
|
for (const cn of s.chains) {
|
|
1383
1614
|
if (sw.chainHolds?.some((h) => h.chain === cn && h.set !== s.set && h.frames[0] <= s.frames[0] && h.frames[1] >= s.frames[1]) === true) continue;
|
|
1384
1615
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
1616
|
+
if (set.support?.includes(c.name) === true) {
|
|
1617
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSupportChainFrame(set, c, i);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1385
1620
|
const drawn = set.drawn[c.bones[0]!];
|
|
1386
1621
|
const up = isChainHeadPlaced(set, c, drawn);
|
|
1387
1622
|
const low = hipsOk !== null && set.place?.chains.includes(c.name) === true;
|
|
1388
1623
|
if (!up && !low && headOk !== null && drawn !== undefined && isRaised(drawn) && c.bones.length >= 1 && c.joints.length >= 2) {
|
|
1389
|
-
|
|
1390
|
-
const p1 = set.pose[c.joints[1]!];
|
|
1391
|
-
if (p0 !== undefined && p1 !== undefined) {
|
|
1392
|
-
const key0 = pictureAngle(p0, p1);
|
|
1624
|
+
if (set.support !== undefined && c.bones.length >= 2) {
|
|
1393
1625
|
const t0 = targets.get(c.bones[0]!)!;
|
|
1626
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
1394
1627
|
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1628
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
1629
|
+
if (pair !== null) {
|
|
1630
|
+
t0[i] = pair[0];
|
|
1631
|
+
t1[i] = pair[1];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
const p0 = set.pose[c.joints[0]!];
|
|
1636
|
+
const p1 = set.pose[c.joints[1]!];
|
|
1637
|
+
if (p0 !== undefined && p1 !== undefined) {
|
|
1638
|
+
const key0 = pictureAngle(p0, p1);
|
|
1639
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
1640
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1641
|
+
const diff = Math.max(-3, Math.min(3, wrap(t0[i]! - key0)));
|
|
1642
|
+
t0[i] = wrap(drawn + diff);
|
|
1643
|
+
}
|
|
1397
1644
|
}
|
|
1398
1645
|
}
|
|
1399
1646
|
}
|
|
@@ -1561,7 +1808,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1561
1808
|
t1[i] = a1;
|
|
1562
1809
|
}
|
|
1563
1810
|
// A held upper arm whose re-aimed forearm leaves the wrist more than HELD_MISS short of its placed point: ART-RANGE on the upper arm, with the shortfall.
|
|
1564
|
-
if (turn0 !== undefined) {
|
|
1811
|
+
if (turn0 !== undefined && set.symmetric !== true) {
|
|
1565
1812
|
for (const [a, b] of runsOf(heldMiss.map((m) => m > HELD_MISS), RANGE_GAP)) {
|
|
1566
1813
|
let worst = a;
|
|
1567
1814
|
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
@@ -1570,7 +1817,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1570
1817
|
art.push({ set: set.name, chain: c.name, bone: b0, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(upperOver[worst]!), turn: r3(upperAsked[worst]!), limits: turn0, clamped: true, shortfall: r3(heldMiss[worst]!), ...(up ? {} : { anchor: 'hip' as const }) });
|
|
1571
1818
|
}
|
|
1572
1819
|
}
|
|
1573
|
-
if (turn1 !== undefined) {
|
|
1820
|
+
if (turn1 !== undefined && set.symmetric !== true) {
|
|
1574
1821
|
for (const [a, b] of runsOf(foreOver.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
1575
1822
|
let worst = a;
|
|
1576
1823
|
for (let k = a; k <= b; k++) if (foreOver[k]! > foreOver[worst]!) worst = k;
|
|
@@ -1581,10 +1828,51 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1581
1828
|
}
|
|
1582
1829
|
raisedJobs.push(job);
|
|
1583
1830
|
}
|
|
1831
|
+
if (set.symmetric === true) {
|
|
1832
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1833
|
+
const sym = solveSymmetricFrame(set, i);
|
|
1834
|
+
if (sym === null) continue;
|
|
1835
|
+
const k = i - s.frames[0];
|
|
1836
|
+
for (const job of raisedJobs) {
|
|
1837
|
+
if (job.sw !== s) continue;
|
|
1838
|
+
const w = job.chain.name === sym.cR.name ? sym.wR : job.chain.name === sym.cL.name ? sym.wL : null;
|
|
1839
|
+
if (w !== null && k >= 0 && k < job.placed.length) {
|
|
1840
|
+
job.placed[k] = [(w[0] - sym.hc[0]) / job.rigEye, (w[1] - sym.hc[1]) / job.rigEye];
|
|
1841
|
+
job.upperHeld = [];
|
|
1842
|
+
job.short = [];
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1584
1847
|
art.sort((x, y) => x.frames[0] - y.frames[0]);
|
|
1585
1848
|
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1586
1849
|
}
|
|
1587
1850
|
}
|
|
1851
|
+
// #188: Even without --raised-by-head, apply symmetric normalisation and support-arm anchoring on each switch's span.
|
|
1852
|
+
for (const s of sw.switches) {
|
|
1853
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1854
|
+
if (set.symmetric === true && headOk === null && hipsOk === null) {
|
|
1855
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
1856
|
+
}
|
|
1857
|
+
if (set.support !== undefined) {
|
|
1858
|
+
for (const cn of s.chains) {
|
|
1859
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
1860
|
+
if (set.support.includes(c.name)) {
|
|
1861
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) solveSupportChainFrame(set, c, i);
|
|
1862
|
+
} else if (headOk === null && hipsOk === null && c.bones.length >= 2) {
|
|
1863
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
1864
|
+
const t1 = targets.get(c.bones[1]!)!;
|
|
1865
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1866
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
1867
|
+
if (pair !== null) {
|
|
1868
|
+
t0[i] = pair[0];
|
|
1869
|
+
t1[i] = pair[1];
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1588
1876
|
p.throwIfAny();
|
|
1589
1877
|
// The art turn of every span; a raised chain's bones were held by the solve, and only its re-aimed forearm is listed (above).
|
|
1590
1878
|
const measured = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
@@ -1673,6 +1961,119 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1673
1961
|
}
|
|
1674
1962
|
}
|
|
1675
1963
|
}
|
|
1964
|
+
// #188: Cover the full shown/bridged span of symmetric sets and support-arm sets after short interpolation.
|
|
1965
|
+
for (let sIdx = 0; sIdx < sw.switches.length; sIdx++) {
|
|
1966
|
+
const s = sw.switches[sIdx]!;
|
|
1967
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1968
|
+
const setSlots = Object.entries(set.slots);
|
|
1969
|
+
const isSetShownAt = (i: number): boolean =>
|
|
1970
|
+
setSlots.length > 0 && setSlots.every(([sl, att]) => sw.shown.get(sl)?.[i] === att);
|
|
1971
|
+
let loShow = s.frames[0];
|
|
1972
|
+
while (loShow - 1 >= 0 && isSetShownAt(loShow - 1)) loShow--;
|
|
1973
|
+
let hiShow = s.frames[1];
|
|
1974
|
+
while (hiShow + 1 < n && isSetShownAt(hiShow + 1)) hiShow++;
|
|
1975
|
+
if (set.symmetric === true) {
|
|
1976
|
+
for (let i = loShow; i <= s.frames[1]; i++) solveSymmetricFrame(set, i);
|
|
1977
|
+
if (loShow < s.frames[0]) {
|
|
1978
|
+
const loRamp = Math.max(0, loShow - 6);
|
|
1979
|
+
const steps = loShow - loRamp;
|
|
1980
|
+
if (steps >= 2) {
|
|
1981
|
+
for (const cn of set.chains) {
|
|
1982
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
1983
|
+
if (!c) continue;
|
|
1984
|
+
for (const b of c.bones) {
|
|
1985
|
+
const tg = targets.get(b)!;
|
|
1986
|
+
const startA = tg[loRamp]!;
|
|
1987
|
+
const total = wrap(tg[loShow]! - startA);
|
|
1988
|
+
for (let k = loRamp + 1; k < loShow; k++) {
|
|
1989
|
+
tg[k] = wrap(startA + ((k - loRamp) / steps) * total);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (hiShow > s.frames[1] && set.chains.length === 2) {
|
|
1996
|
+
const c0 = map.chains.find((x) => x.name === set.chains[0]!);
|
|
1997
|
+
const c1 = map.chains.find((x) => x.name === set.chains[1]!);
|
|
1998
|
+
if (c0 !== undefined && c1 !== undefined && c0.bones.length === 2 && c1.bones.length === 2) {
|
|
1999
|
+
const S0_0 = rigPoint(rest, c0.bones[0]!)!;
|
|
2000
|
+
const S0_1 = rigPoint(rest, c1.bones[0]!)!;
|
|
2001
|
+
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
2002
|
+
const bR0 = cR.bones[0]!;
|
|
2003
|
+
const bL0 = cL.bones[0]!;
|
|
2004
|
+
const SR0 = rigPoint(rest, bR0)!;
|
|
2005
|
+
const ER0 = rigPoint(rest, cR.bones[1]!)!;
|
|
2006
|
+
const SL0 = rigPoint(rest, bL0)!;
|
|
2007
|
+
const EL0 = rigPoint(rest, cL.bones[1]!)!;
|
|
2008
|
+
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
2009
|
+
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
2010
|
+
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
2011
|
+
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
2012
|
+
for (let i = s.frames[1] + 1; i <= hiShow; i++) {
|
|
2013
|
+
const sr = posedOrigin(bR0, i);
|
|
2014
|
+
const sl = posedOrigin(bL0, i);
|
|
2015
|
+
const baseR = targets.get(bR0)![i]!;
|
|
2016
|
+
const baseL = targets.get(bL0)![i]!;
|
|
2017
|
+
let lo = -20;
|
|
2018
|
+
let hi = 20;
|
|
2019
|
+
const evalD = (deg: number) =>
|
|
2020
|
+
sl[1] + lL0 * Math.sin((baseL + deg + dL0) / DEG) - (sr[1] + lR0 * Math.sin((baseR + deg + dR0) / DEG));
|
|
2021
|
+
let fLo = evalD(lo);
|
|
2022
|
+
for (let it = 0; it < 32; it++) {
|
|
2023
|
+
const mid = 0.5 * (lo + hi);
|
|
2024
|
+
const fMid = evalD(mid);
|
|
2025
|
+
if ((fLo <= 0 && fMid <= 0) || (fLo > 0 && fMid > 0)) {
|
|
2026
|
+
lo = mid;
|
|
2027
|
+
fLo = fMid;
|
|
2028
|
+
} else hi = mid;
|
|
2029
|
+
}
|
|
2030
|
+
const deg = 0.5 * (lo + hi);
|
|
2031
|
+
targets.get(bR0)![i] = wrap(baseR + deg);
|
|
2032
|
+
targets.get(bL0)![i] = wrap(baseL + deg);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (set.support !== undefined) {
|
|
2038
|
+
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2039
|
+
const loGap = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] + 1 : loShow;
|
|
2040
|
+
for (const cn of s.chains) {
|
|
2041
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
2042
|
+
if (set.support.includes(c.name)) {
|
|
2043
|
+
for (let i = Math.min(loGap, loShow); i <= hiShow; i++) solveSupportChainFrame(set, c, i);
|
|
2044
|
+
const anchorStart = Math.min(loGap, loShow);
|
|
2045
|
+
if (anchorStart >= 2) {
|
|
2046
|
+
for (const b of c.bones) {
|
|
2047
|
+
const tg = targets.get(b)!;
|
|
2048
|
+
tg[anchorStart - 1] = wrap(tg[anchorStart - 2]! + 0.5 * wrap(tg[anchorStart]! - tg[anchorStart - 2]!));
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
} else if (c.bones.length >= 2) {
|
|
2052
|
+
const [b0, b1] = c.bones as [string, string];
|
|
2053
|
+
const drawn0 = set.drawn[b0];
|
|
2054
|
+
if (drawn0 !== undefined && isRaised(drawn0)) {
|
|
2055
|
+
const S0 = rigPoint(rest, b0);
|
|
2056
|
+
let headX0 = 0;
|
|
2057
|
+
if (gestureHead !== null) {
|
|
2058
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
2059
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
2060
|
+
if (ha0 !== null && hb0 !== null) headX0 = (ha0[0] + hb0[0]) / 2;
|
|
2061
|
+
}
|
|
2062
|
+
const l0 = S0 !== null && rigPoint(rest, b1) !== null ? Math.hypot(rigPoint(rest, b1)![0] - S0[0], rigPoint(rest, b1)![1] - S0[1]) : 100;
|
|
2063
|
+
const outSign: 1 | -1 = (S0?.[0] ?? 0) + l0 * Math.cos(drawn0 / DEG) <= headX0 ? 1 : -1;
|
|
2064
|
+
const t0 = targets.get(b0)!;
|
|
2065
|
+
const t1 = targets.get(b1)!;
|
|
2066
|
+
for (let i = loShow; i <= hiShow; i++) {
|
|
2067
|
+
const pair = solveSupportActiveFrame(set, c, i);
|
|
2068
|
+
if (pair === null) continue;
|
|
2069
|
+
if (i >= s.frames[0] || outSign * wrap(t0[i]! - pair[0]) < 0) t0[i] = pair[0];
|
|
2070
|
+
if (i >= s.frames[0] || outSign * wrap(t1[i]! - pair[1]) < 0) t1[i] = pair[1];
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1676
2077
|
|
|
1677
2078
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
1678
2079
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
package/src/switch.ts
CHANGED
|
@@ -666,6 +666,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
666
666
|
});
|
|
667
667
|
if (!hasUpChain) continue;
|
|
668
668
|
for (const cn of set.chains) {
|
|
669
|
+
if (set.support?.includes(cn) === true) continue;
|
|
669
670
|
const c = map.chains.find((x) => x.name === cn);
|
|
670
671
|
if (c === undefined) continue;
|
|
671
672
|
const allDown = c.bones.every((b) => (set.drawn[b] ?? 0) < 0);
|
|
@@ -885,7 +886,13 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
885
886
|
}
|
|
886
887
|
return raw;
|
|
887
888
|
};
|
|
888
|
-
const
|
|
889
|
+
const activeSwChains = switchingChains.filter((cn) => !set1.support?.includes(cn) && !set0.support?.includes(cn));
|
|
890
|
+
const measureChains = activeSwChains.length > 0 ? activeSwChains : switchingChains;
|
|
891
|
+
const measureBones = measureChains.flatMap((cn) => map.chains.find((c) => c.name === cn)?.bones ?? []);
|
|
892
|
+
const rootBones = measureChains
|
|
893
|
+
.map((cn) => map.chains.find((c) => c.name === cn)?.bones[0])
|
|
894
|
+
.filter((b): b is string => b !== undefined && set0.drawn[b] !== undefined && set1.drawn[b] !== undefined);
|
|
895
|
+
const pairs = measureBones.filter((b) => set0.drawn[b] !== undefined && set1.drawn[b] !== undefined);
|
|
889
896
|
const span = pairs.length > 0
|
|
890
897
|
? pairs.reduce((s, b) => s + Math.abs(wrap(set1.drawn[b]! - set0.drawn[b]!)), 0) / pairs.length
|
|
891
898
|
: 0;
|
|
@@ -896,7 +903,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
896
903
|
const d1 = pairs.length > 0
|
|
897
904
|
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
898
905
|
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
899
|
-
|
|
906
|
+
const rootsCrossed =
|
|
907
|
+
activeSwChains.length === switchingChains.length ||
|
|
908
|
+
rootBones.every((b) => Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)) <= Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)));
|
|
909
|
+
if ((d1 <= d0 || (span > 1e-6 && d0 >= 0.5 * span)) && rootsCrossed) {
|
|
900
910
|
tSwitch = i;
|
|
901
911
|
break;
|
|
902
912
|
}
|