@rig-c/pose 0.22.1 → 0.22.2
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/retarget.ts +107 -8
- package/src/verify.ts +23 -2
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ Nothing is written until every check passes:
|
|
|
114
114
|
| Check | Fails with |
|
|
115
115
|
| --- | --- |
|
|
116
116
|
| rig-c's gate is green with the rig as it is, and again with the animation in (`rigc build`) | `RIG_GATE_BASE`, `RIG_GATE`, then rigc's own `FAIL` lines |
|
|
117
|
-
| every mapped bone **lands** on the angle the pose asks for, on every frame — the gated build is posed by rig-c's poser and measured (0.01° / 0.01 units); with `--leg-scale`, every scaled bone's x axis on its asked scale (0.0001 of its setup length), and with either leg flag each ankle where the keys put it (0.01 units + 0.0001 × its setup distance from the hip) | `POSE_LANDS`: the bone, the worst error, the frame |
|
|
117
|
+
| every mapped bone **lands** on the angle the pose asks for, on every frame — the gated build is posed by rig-c's poser and measured (0.01° / 0.01 units); with `--leg-scale`, every scaled bone's x axis (and the trunk's y axis when trunk foreshortening is keyed, [#172](https://github.com/rig-c/pose/issues/172)) on its asked scale (0.0001 of its setup length), and with either leg flag each ankle where the keys put it (0.01 units + 0.0001 × its setup distance from the hip) | `POSE_LANDS`: the bone, the worst error, the frame |
|
|
118
118
|
| elbows and knees bend only the way they can: at every joint inside a chain, the inner angle stays on the side of the rest pose's | `POSE_BEND_ONE_WAY`: the chain, the joint, the frame, the angle |
|
|
119
119
|
| every point the map needs is good on at least one frame | `POSE_POINT_NEVER_GOOD` |
|
|
120
120
|
| the map resolves against the rig: bones exist, chains descend, aims descend | `MAP_*`, naming both sides |
|
|
@@ -196,7 +196,7 @@ Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. O
|
|
|
196
196
|
|
|
197
197
|
A knee bending toward the camera is seen from the front only as a shorter leg, and a rig with rigid legs cannot lower its hip without lifting its feet ([#47](https://github.com/rig-c/pose/issues/47)). Two flags, both off by default:
|
|
198
198
|
|
|
199
|
-
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf`
|
|
199
|
+
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf` derived from posture-invariant measures — `max(r_thigh + r_shin, l_thigh + l_shin)`, shoulder width and eye width, never the trunk — with frame-to-frame step ≤ 1 % and frozen spans ≥ 5 frames held at their median, so neither camera zoom nor a forward trunk bend reads as the camera moving, and `--camera <file>` writes that track as `rig-pose-camera/1` without changing `motion.json`, [#159](https://github.com/rig-c/pose/issues/159), [#161](https://github.com/rig-c/pose/issues/161), [#163](https://github.com/rig-c/pose/issues/163), [#172](https://github.com/rig-c/pose/issues/172)), clamped to [min, max]. When the camera-normalised trunk (`neck → mid_hip` or `mid_shoulder → mid_hip`) foreshortens or extends by more than 4 % from its standing length, the trunk bone is keyed in `scaley` (for a `+y`-aimed trunk, `scalex` for `+x`) clamped to the same `[min, max]` bounds, carrying the trunk and head's depth bend while keeping the aimed rotation ([#172](https://github.com/rig-c/pose/issues/172)). The card's range is `0.8,1.05`. A segment the map aims other than `"x"` is refused (`LEGS_MAP`). `LEG-SCALE` lists each segment's frames clamped at either limit.
|
|
200
200
|
|
|
201
201
|
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. A child (or its intermediate control bone) with `noScale` or `noScaleOrReflection` ([#153](https://github.com/rig-c/pose/issues/153)) still has its direction carried through the parent's scaled matrix, so its angle is skewed the same way, while rig-c normalises that direction to unit length so the parent's scale is not inherited. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' put its world x axis on its target at the scale asked (under `noScale`, s' is the bone's own asked scale s directly). On HQ's dancer the unit-scale formula missed the shins by up to 2.4°, their length by 0.16 of setup and the ankles by 38 units; solved, every bone lands. A `normal` child's art is still sheared by its parent's scale, and a bone under it (a foot) is squashed along the shin: `SHEAR` reads both.
|
|
202
202
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
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/retarget.ts
CHANGED
|
@@ -219,7 +219,7 @@ export interface MotionKey {
|
|
|
219
219
|
}
|
|
220
220
|
export interface MotionTrack {
|
|
221
221
|
bone: string;
|
|
222
|
-
property: 'rotate' | 'translate' | 'scalex';
|
|
222
|
+
property: 'rotate' | 'translate' | 'scalex' | 'scaley';
|
|
223
223
|
keys: MotionKey[];
|
|
224
224
|
}
|
|
225
225
|
export interface Animation {
|
|
@@ -713,19 +713,76 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
713
713
|
const h = heldOf(name);
|
|
714
714
|
return 'never' in h ? null : h.at;
|
|
715
715
|
};
|
|
716
|
+
const winPercentile = (xs: readonly number[], w: number, q: number): number[] => {
|
|
717
|
+
const effW = Math.min(w, Math.floor((n - 1) / 2));
|
|
718
|
+
if (effW <= 0) return [...xs];
|
|
719
|
+
return xs.map((_, i) => {
|
|
720
|
+
const c = Math.max(effW, Math.min(n - 1 - effW, i));
|
|
721
|
+
return percentile(xs.slice(c - effW, c + effW + 1), q);
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
const dist2 = (a: Pt, b: Pt): number => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
716
725
|
const rawFigureHeight: number[] = (() => {
|
|
717
726
|
const ar = heldPt('r_ankle');
|
|
718
727
|
const al = heldPt('l_ankle');
|
|
719
|
-
const top = heldPt('nose') ?? heldPt('neck');
|
|
720
|
-
if (ar !== null && al !== null && top !== null) {
|
|
721
|
-
const h = ar.map((r, i) => Math.max(r[1], al[i]![1]) - top[i]![1]);
|
|
722
|
-
if (h.every((v) => v > 1e-6)) return h;
|
|
723
|
-
}
|
|
724
728
|
const hr = heldPt('r_hip');
|
|
725
729
|
const hl = heldPt('l_hip');
|
|
726
730
|
if (ar !== null && al !== null && hr !== null && hl !== null) {
|
|
727
|
-
const
|
|
728
|
-
|
|
731
|
+
const kr = heldPt('r_knee');
|
|
732
|
+
const kl = heldPt('l_knee');
|
|
733
|
+
const legSum = ar.map((r, i) =>
|
|
734
|
+
kr !== null && kl !== null
|
|
735
|
+
? Math.max(dist2(hr[i]!, kr[i]!) + dist2(kr[i]!, r), dist2(hl[i]!, kl[i]!) + dist2(kl[i]!, al[i]!))
|
|
736
|
+
: Math.max(dist2(r, hr[i]!), dist2(al[i]!, hl[i]!)),
|
|
737
|
+
);
|
|
738
|
+
if (legSum.every((v) => v > 1e-6)) {
|
|
739
|
+
if (n > 160) {
|
|
740
|
+
const seriesList: number[][] = [winPercentile(legSum, 75, STRAIGHT_PERCENTILE)];
|
|
741
|
+
const rs = heldPt('r_shoulder');
|
|
742
|
+
const ls = heldPt('l_shoulder');
|
|
743
|
+
if (rs !== null && ls !== null) {
|
|
744
|
+
const shW = rs.map((r, i) => dist2(r, ls[i]!));
|
|
745
|
+
if (shW.every((v) => v > 1e-6)) seriesList.push(winPercentile(shW, 75, STRAIGHT_PERCENTILE));
|
|
746
|
+
}
|
|
747
|
+
const re = heldPt('r_eye');
|
|
748
|
+
const le = heldPt('l_eye');
|
|
749
|
+
if (re !== null && le !== null) {
|
|
750
|
+
const eyeW = re.map((r, i) => dist2(r, le[i]!));
|
|
751
|
+
if (eyeW.every((v) => v > 1e-6)) seriesList.push(winPercentile(eyeW, 75, STRAIGHT_PERCENTILE));
|
|
752
|
+
}
|
|
753
|
+
if (kr !== null && kl !== null) {
|
|
754
|
+
const thighMin = hr.map((r, i) => Math.min(dist2(r, kr[i]!), dist2(hl[i]!, kl[i]!)));
|
|
755
|
+
if (thighMin.every((v) => v > 1e-6)) seriesList.push(winPercentile(thighMin, 75, 0.5));
|
|
756
|
+
}
|
|
757
|
+
const rel = heldPt('r_elbow');
|
|
758
|
+
const lel = heldPt('l_elbow');
|
|
759
|
+
const rw = heldPt('r_wrist');
|
|
760
|
+
const lw = heldPt('l_wrist');
|
|
761
|
+
if (rs !== null && ls !== null && rel !== null && lel !== null && rw !== null && lw !== null) {
|
|
762
|
+
const armSum = rs.map((r, i) => Math.max(dist2(r, rel[i]!) + dist2(rel[i]!, rw[i]!), dist2(ls[i]!, lel[i]!) + dist2(lel[i]!, lw[i]!)));
|
|
763
|
+
if (armSum.every((v) => v > 1e-6)) seriesList.push(winPercentile(armSum, 75, 0.5));
|
|
764
|
+
}
|
|
765
|
+
if (seriesList.length >= 3) {
|
|
766
|
+
const norms = seriesList.map((s) => s.map((v) => v / s[0]!));
|
|
767
|
+
const cons = legSum.map((_, i) => {
|
|
768
|
+
let minR = Infinity;
|
|
769
|
+
let maxR = -Infinity;
|
|
770
|
+
for (const nr of norms) {
|
|
771
|
+
if (nr[i]! < minR) minR = nr[i]!;
|
|
772
|
+
if (nr[i]! > maxR) maxR = nr[i]!;
|
|
773
|
+
}
|
|
774
|
+
return minR > 1 ? minR : maxR < 1 ? maxR : 1;
|
|
775
|
+
});
|
|
776
|
+
if (cons.every((c) => Math.abs(c - 1) <= 0.02)) {
|
|
777
|
+
return cons.map((c) => c * legSum[0]!);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const out = winPercentile(legSum, 1, STRAIGHT_PERCENTILE);
|
|
781
|
+
out[0] = legSum[0]!;
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
return legSum;
|
|
785
|
+
}
|
|
729
786
|
}
|
|
730
787
|
if (t !== null) {
|
|
731
788
|
const [pa, pb] = t.scale.pose;
|
|
@@ -761,6 +818,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
761
818
|
};
|
|
762
819
|
})();
|
|
763
820
|
const poseScale: number[] | null = t !== null && scale !== null && n > 25 && !inPlace ? continuousScale : null;
|
|
821
|
+
let trunkScaleBone: string | null = null;
|
|
822
|
+
let trunkScale: number[] | null = null;
|
|
764
823
|
if (legScaleOn) {
|
|
765
824
|
const limits = opts.legScale as [number, number];
|
|
766
825
|
const rawSegments: Array<{ chain: string; bone: string; joints: [string, string]; kind: string; lengths: number[]; ownStraight: number }> = [];
|
|
@@ -817,6 +876,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
817
876
|
scales.set(seg.bone, ls.scale);
|
|
818
877
|
}
|
|
819
878
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
879
|
+
const isTrunkPair = (a: string, b: string): boolean => (a === 'mid_hip' && (b === 'neck' || b === 'mid_shoulder')) || (b === 'mid_hip' && (a === 'neck' || a === 'mid_shoulder'));
|
|
880
|
+
const trunkChain = map.chains.find((c) => c.bones.length === 1 && isTrunkPair(c.joints[0]!, c.joints[1]!));
|
|
881
|
+
if (!inPlace && trunkChain !== undefined && !p.any) {
|
|
882
|
+
const tb = trunkChain.bones[0]!;
|
|
883
|
+
const from = series.get(trunkChain.joints[0]!)!;
|
|
884
|
+
const to = series.get(trunkChain.joints[1]!)!;
|
|
885
|
+
const rawT = from.map((f, i) => dist2(f, to[i]!));
|
|
886
|
+
const frameScale = (i: number): number => (!inPlace && n > 25 && rawFigureHeight[0]! > 1e-6 && rawFigureHeight[i]! > 1e-6 ? rawFigureHeight[i]! / rawFigureHeight[0]! : 1);
|
|
887
|
+
const normT = rawT.map((l, i) => l / frameScale(i));
|
|
888
|
+
const standingT = median(normT);
|
|
889
|
+
if (standingT > 1e-6) {
|
|
890
|
+
const rawRatio = normT.map((l) => l / standingT);
|
|
891
|
+
const re = heldPt('r_eye');
|
|
892
|
+
const le = heldPt('l_eye');
|
|
893
|
+
let effRatio = rawRatio;
|
|
894
|
+
if (re !== null && le !== null) {
|
|
895
|
+
const mh = series.get('mid_hip') ?? from;
|
|
896
|
+
const rawEH = mh.map((h, i) => dist2(h, [(re[i]![0] + le[i]![0]) / 2, (re[i]![1] + le[i]![1]) / 2]));
|
|
897
|
+
const normEH = rawEH.map((l, i) => l / frameScale(i));
|
|
898
|
+
const standingEH = median(normEH);
|
|
899
|
+
if (standingEH > 1e-6) {
|
|
900
|
+
effRatio = rawRatio.map((r, i) => (r > 1 ? Math.min(r, Math.max(1, normEH[i]! / standingEH)) : r));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const minR = Math.max(0.5, Math.min(...effRatio));
|
|
904
|
+
const supFloor = Math.min(limits[0], minR);
|
|
905
|
+
const sy = effRatio.map((r, i) => {
|
|
906
|
+
const supported = plantOn && feet !== null && (feet.support[i] !== null || feet.tracked?.some((tr) => i >= tr.frames[0] && i <= tr.frames[1]) === true);
|
|
907
|
+
const lo = supported ? supFloor : limits[0];
|
|
908
|
+
return Math.min(Math.max(r, lo), limits[1]);
|
|
909
|
+
});
|
|
910
|
+
if (effRatio.some((v) => Math.abs(v - 1) > 0.04) && sy.some((v) => Math.abs(r6(v) - 1) > 0)) {
|
|
911
|
+
trunkScaleBone = tb;
|
|
912
|
+
trunkScale = sy;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
820
916
|
}
|
|
821
917
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
822
918
|
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
@@ -1432,6 +1528,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1432
1528
|
tracks.push({ bone: b, property: 'scalex', keys: Array.from({ length: n }, (_, i) => ({ t: keyTimes[i]!, v: [r6(solvedKey(b, i)?.scale ?? scales.get(b)?.[i] ?? 1)] })) });
|
|
1433
1529
|
}
|
|
1434
1530
|
}
|
|
1531
|
+
if (trunkScaleBone !== null && trunkScale !== null) {
|
|
1532
|
+
tracks.push({ bone: trunkScaleBone, property: 'scaley', keys: trunkScale.map((v, i) => ({ t: keyTimes[i]!, v: [r6(v)] })) });
|
|
1533
|
+
}
|
|
1435
1534
|
if (translateTrack !== null) tracks.push(translateTrack);
|
|
1436
1535
|
if (headNodTrack !== null) tracks.push(headNodTrack);
|
|
1437
1536
|
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
package/src/verify.ts
CHANGED
|
@@ -64,8 +64,29 @@ export function measureLanding(doc: CompiledDocument, animation: string, map: Jo
|
|
|
64
64
|
const faceBones = new Set([r.face?.yaw?.bone, r.face?.pitch?.bone].filter((b): b is string => b !== undefined));
|
|
65
65
|
const isFaceSlider = (c: CompiledDocument['constraints'][number]): boolean => c.record?.kind === 'slider' && c.record.bone !== null && faceBones.has(c.record.bone);
|
|
66
66
|
const hasFaceSliders = faceBones.size > 0 && doc.constraints.some(isFaceSlider);
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
const hasScaleY = doc.animations.some((a) => a.name === animation && a.timelines.bones.some((b) => b.timelines.some((t) => t.kind === 'scaley')));
|
|
68
|
+
const chainPosed = hasFaceSliders || hasScaleY
|
|
69
|
+
? poseFrames(
|
|
70
|
+
{
|
|
71
|
+
...doc,
|
|
72
|
+
constraints: doc.constraints.filter((c) => !isFaceSlider(c)),
|
|
73
|
+
animations: hasScaleY
|
|
74
|
+
? doc.animations.map((a) =>
|
|
75
|
+
a.name === animation
|
|
76
|
+
? {
|
|
77
|
+
...a,
|
|
78
|
+
timelines: {
|
|
79
|
+
...a.timelines,
|
|
80
|
+
bones: a.timelines.bones.map((b) => ({ ...b, timelines: b.timelines.filter((t) => t.kind !== 'scaley') })),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
: a,
|
|
84
|
+
)
|
|
85
|
+
: doc.animations,
|
|
86
|
+
},
|
|
87
|
+
animation,
|
|
88
|
+
r.times,
|
|
89
|
+
)
|
|
69
90
|
: posed;
|
|
70
91
|
for (const c of map.chains) {
|
|
71
92
|
c.bones.forEach((b, k) => {
|