@rig-c/pose 0.27.0 → 0.28.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 +2 -2
- package/package.json +1 -1
- package/src/face.ts +20 -3
- package/src/retarget.ts +56 -2
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)),
|
|
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 any vertical residual removed by that clamp (`rawNod − dy`) 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); gains calibrated against the hand-polished reference `p1`), adding `0°` on frames where the clamp does not act. 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)): `{ "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`).
|
|
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 any vertical residual removed by the head nod channel's `±2%` neck-length clamp (`+2.8°/unit`, `0°` where un-clamped) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190)), 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`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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
|
@@ -293,18 +293,35 @@ export function readFace(
|
|
|
293
293
|
lEye: readonly Pt[],
|
|
294
294
|
rEar: readonly Pt[] | null,
|
|
295
295
|
lEar: readonly Pt[] | null,
|
|
296
|
+
nodResidual?: readonly number[] | null,
|
|
297
|
+
pitchGain = 0,
|
|
296
298
|
): FaceReading {
|
|
297
299
|
const { ears, yaw, pitch } = readFaceAngles(nose, rEye, lEye, rEar, lEar);
|
|
300
|
+
const coupledPitch =
|
|
301
|
+
nodResidual !== undefined && nodResidual !== null && nodResidual.length === pitch.length && pitchGain !== 0
|
|
302
|
+
? pitch.map((p, i) => (nodResidual[i]! === 0 ? p : p + pitchGain * nodResidual[i]!))
|
|
303
|
+
: pitch;
|
|
304
|
+
const basePitch = face.pitch === undefined ? undefined : mapFaceAxis(pitch, face.pitch);
|
|
305
|
+
const mappedPitch =
|
|
306
|
+
face.pitch === undefined
|
|
307
|
+
? undefined
|
|
308
|
+
: coupledPitch === pitch
|
|
309
|
+
? basePitch!
|
|
310
|
+
: (() => {
|
|
311
|
+
const m = mapFaceAxis(coupledPitch, face.pitch);
|
|
312
|
+
const keys = m.keys.map((v, i) => (nodResidual![i]! === 0 ? basePitch!.keys[i]! : v));
|
|
313
|
+
return { ...m, mapped: spanOf(keys), corr: corrOf(m.raw, keys), keys };
|
|
314
|
+
})();
|
|
298
315
|
return {
|
|
299
316
|
ears,
|
|
300
317
|
read: {
|
|
301
318
|
yaw: spanOf(yaw),
|
|
302
|
-
pitch: spanOf(
|
|
319
|
+
pitch: spanOf(coupledPitch),
|
|
303
320
|
rawYaw: yaw,
|
|
304
|
-
rawPitch:
|
|
321
|
+
rawPitch: coupledPitch,
|
|
305
322
|
},
|
|
306
323
|
...(face.yaw === undefined ? {} : { yaw: mapFaceAxis(yaw, face.yaw) }),
|
|
307
|
-
...(
|
|
324
|
+
...(mappedPitch === undefined ? {} : { pitch: mappedPitch }),
|
|
308
325
|
};
|
|
309
326
|
}
|
|
310
327
|
|
package/src/retarget.ts
CHANGED
|
@@ -98,6 +98,30 @@ const DEG = 180 / Math.PI;
|
|
|
98
98
|
*/
|
|
99
99
|
export const HELD_MISS = 0.01;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the vertical residual that the
|
|
103
|
+
* `±2%` neck-length clamp removes from `head.translate.y` (`rawNod - dy`) onto
|
|
104
|
+
* `map.face.pitch` (`pitch_dial.rotate`, `rig-c/pose#190`). Measured on the
|
|
105
|
+
* dancer reference hand pass (`hand/p1` HP2: `+2.8°/unit`).
|
|
106
|
+
*/
|
|
107
|
+
export const NOD_RESIDUAL_PITCH_GAIN = 2.8;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the vertical residual that the
|
|
111
|
+
* `±2%` neck-length clamp removes from `head.translate.y` (`rawNod - dy`) onto
|
|
112
|
+
* the head chain bone's `rotate` track (`head.rotate`, `rig-c/pose#190`).
|
|
113
|
+
* Measured on the dancer reference hand pass (`hand/p1` HP2: `+1.6°/unit`).
|
|
114
|
+
*/
|
|
115
|
+
export const NOD_RESIDUAL_HEAD_GAIN = 1.6;
|
|
116
|
+
|
|
117
|
+
export interface HeadNodReading {
|
|
118
|
+
bone: string;
|
|
119
|
+
maxNod: number;
|
|
120
|
+
rawY: number[];
|
|
121
|
+
clampedY: number[];
|
|
122
|
+
residual: number[];
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
export interface RetargetOptions extends SwitchOptions {
|
|
102
126
|
/** A joint with a score below this is not good on that frame (a joint with no score is). */
|
|
103
127
|
minScore: number;
|
|
@@ -334,6 +358,8 @@ export interface Retarget {
|
|
|
334
358
|
body: BodyReading | null;
|
|
335
359
|
/** Continuous per-frame figure scale relative to frame 0 (`#163`, `#161`). */
|
|
336
360
|
figureScale: FigureScaleReading;
|
|
361
|
+
/** Per-frame vertical head nod reading (`rawY`, `clampedY`, and clamped `residual`, `#111`, `#181`, `#190`). */
|
|
362
|
+
headNod: HeadNodReading | null;
|
|
337
363
|
}
|
|
338
364
|
|
|
339
365
|
export interface FigureScaleFrame {
|
|
@@ -1080,6 +1106,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1080
1106
|
let headNodBone: string | null = null;
|
|
1081
1107
|
let headNod: Pt[] | null = null;
|
|
1082
1108
|
let headNodTrack: MotionTrack | null = null;
|
|
1109
|
+
let headNodReading: HeadNodReading | null = null;
|
|
1083
1110
|
const posedOrigin = (b: string, i: number): Pt => {
|
|
1084
1111
|
const own = rest.get(b)!;
|
|
1085
1112
|
if (t !== null && b === t.bone) return [own.worldX + (travel[i]?.[0] ?? 0), own.worldY + (travel[i]?.[1] ?? 0)];
|
|
@@ -1271,6 +1298,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1271
1298
|
}
|
|
1272
1299
|
return Math.max(-maxNod, Math.min(maxNod, v));
|
|
1273
1300
|
});
|
|
1301
|
+
const residual = rawNod.map((v, i) => v - dy[i]!);
|
|
1302
|
+
headNodReading = { bone: hb, maxNod, rawY: rawNod, clampedY: dy, residual };
|
|
1274
1303
|
if (dy.some((v) => Math.abs(r3(v)) > 0)) {
|
|
1275
1304
|
const keys: MotionKey[] = [];
|
|
1276
1305
|
const nod: Pt[] = [];
|
|
@@ -2094,6 +2123,31 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2094
2123
|
}
|
|
2095
2124
|
const solvedKey = (b: string, i: number) => legPoses?.[i]!.solved.get(b);
|
|
2096
2125
|
|
|
2126
|
+
// #190: couple the vertical residual removed by the neck-length clamp into head.rotate
|
|
2127
|
+
// in phase (+NOD_RESIDUAL_HEAD_GAIN °/unit), adding 0 on frames where the clamp did not act.
|
|
2128
|
+
if (headNodReading !== null) {
|
|
2129
|
+
const hb = headNodReading.bone;
|
|
2130
|
+
const tg = targets.get(hb);
|
|
2131
|
+
if (tg !== undefined) {
|
|
2132
|
+
const above = mappedAbove(hb);
|
|
2133
|
+
const hc = map.chains.find((c) => c.bones.includes(hb));
|
|
2134
|
+
const limits = hc?.range[hb];
|
|
2135
|
+
const cov = hc !== undefined ? covered.get(hc.name) : undefined;
|
|
2136
|
+
for (let i = 0; i < n; i++) {
|
|
2137
|
+
const res = headNodReading.residual[i]!;
|
|
2138
|
+
if (res === 0) continue;
|
|
2139
|
+
let next = wrap(tg[i]! + NOD_RESIDUAL_HEAD_GAIN * res);
|
|
2140
|
+
if (limits !== undefined && cov?.[i] !== true) {
|
|
2141
|
+
const key = wrap((next - restOf.get(hb)!) - (above === null ? 0 : turn(above, i)));
|
|
2142
|
+
if (overflow(key, limits) > RANGE_SLACK) {
|
|
2143
|
+
next = wrap(next + Math.min(Math.max(key, limits[0]), limits[1]) - key);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
tg[i] = next;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2097
2151
|
// Keys: the bone's turn less the turn of the nearest mapped bone above it.
|
|
2098
2152
|
const tracks: Array<MotionTrack | SlotTrack> = [];
|
|
2099
2153
|
for (const c of map.chains) {
|
|
@@ -2129,7 +2183,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2129
2183
|
const h = heldOf('l_ear');
|
|
2130
2184
|
return 'never' in h ? null : h.at;
|
|
2131
2185
|
})();
|
|
2132
|
-
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar);
|
|
2186
|
+
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar, headNodReading?.residual ?? null, NOD_RESIDUAL_PITCH_GAIN);
|
|
2133
2187
|
for (const axis of ['yaw', 'pitch'] as const) {
|
|
2134
2188
|
const ax = face[axis];
|
|
2135
2189
|
if (ax === undefined) continue;
|
|
@@ -2260,7 +2314,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2260
2314
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
2261
2315
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
2262
2316
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
2263
|
-
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip, face, body, figureScale,
|
|
2317
|
+
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip, face, body, figureScale, headNod: headNodReading,
|
|
2264
2318
|
};
|
|
2265
2319
|
}
|
|
2266
2320
|
|