@rig-c/pose 0.31.1 → 0.32.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 +42 -19
- package/cli.ts +84 -25
- package/package.json +1 -1
- package/src/clean.ts +23 -2
- package/src/legs.ts +93 -0
- package/src/map.ts +35 -4
- package/src/problems.ts +9 -2
- package/src/retarget.ts +461 -27
- package/src/run.ts +332 -46
- package/src/verify.ts +111 -2
package/src/verify.ts
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { poseRawAnimation, poseRawSetup, type CompiledDocument, type RawBone } from 'rig-c/core';
|
|
12
12
|
import type { Body18 } from './joints.ts';
|
|
13
|
-
import { aimAngle, aimEnds, pointAt, type JointMap } from './map.ts';
|
|
13
|
+
import { aimAngle, aimEnds, pointAt, type Chain, type JointMap } from './map.ts';
|
|
14
14
|
import type { HipsInferred, Joint, PelvisFit, Run } from './pose.ts';
|
|
15
15
|
import type { Retarget } from './retarget.ts';
|
|
16
|
-
import { pictureAngle, pointOn, wrap } from './retarget.ts';
|
|
16
|
+
import { meanSecondDiff, pictureAngle, pointOn, wrap } from './retarget.ts';
|
|
17
|
+
|
|
18
|
+
const DEG = 180 / Math.PI;
|
|
17
19
|
|
|
18
20
|
export interface Landing {
|
|
19
21
|
bone: string;
|
|
@@ -708,3 +710,110 @@ export function postureLines(q: Posture): string[] {
|
|
|
708
710
|
}
|
|
709
711
|
return out;
|
|
710
712
|
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* One keyed bone's jitter against its map pair's (rig-c/pose#236): the mean
|
|
716
|
+
* |second difference| of its local rotation as rig-c's poser posed the gated
|
|
717
|
+
* build — its world matrix in its parent's axes, P⁻¹C, so a rotate key's value
|
|
718
|
+
* up to a constant — against the same figure of its pair's picture angle, less
|
|
719
|
+
* the pair of the nearest keyed bone above it (as the rig's local rotation is
|
|
720
|
+
* against its parent). Both are read on the same triples: three consecutive
|
|
721
|
+
* frames on which every pose point the two pairs need is good.
|
|
722
|
+
*/
|
|
723
|
+
export interface JitterRow {
|
|
724
|
+
bone: string;
|
|
725
|
+
chain: string;
|
|
726
|
+
joints: [string, string];
|
|
727
|
+
/** The nearest keyed bone above it, whose pair the pose's local angle is read against; null at the top. */
|
|
728
|
+
against: string | null;
|
|
729
|
+
/** °/frame², rounded to 1e-4; null when no triple was read. */
|
|
730
|
+
rig: number | null;
|
|
731
|
+
pair: number | null;
|
|
732
|
+
/** rig ÷ pair; null when either is unread or the pair is still (its figure rounds to 0). */
|
|
733
|
+
ratio: number | null;
|
|
734
|
+
triples: number;
|
|
735
|
+
/** Whether the bone is aimed by a `reach` chain. */
|
|
736
|
+
reach: boolean;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** #236: a JITTER row for every bone a chain keys with a rotate track, in map order (a face or body dial bone excepted: its key drives a slider, not a pair). */
|
|
740
|
+
export function measureJitter(posed: ReadonlyArray<ReadonlyMap<string, RawBone>>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number): JitterRow[] {
|
|
741
|
+
const dials = new Set([r.face?.yaw?.bone, r.face?.pitch?.bone, r.body?.yaw?.bone].filter((b): b is string => b !== undefined));
|
|
742
|
+
const rotated = new Set(r.animation.tracks.filter((tr) => 'bone' in tr && tr.property === 'rotate').map((tr) => ('bone' in tr ? tr.bone : '')));
|
|
743
|
+
const pairs = new Map<string, { chain: Chain; joints: [string, string] }>();
|
|
744
|
+
for (const c of map.chains) {
|
|
745
|
+
c.bones.forEach((b, k) => {
|
|
746
|
+
if (k + 1 >= c.joints.length || dials.has(b) || !rotated.has(b) || pairs.has(b)) return;
|
|
747
|
+
pairs.set(b, { chain: c, joints: [c.joints[k]!, c.joints[k + 1]!] });
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const n = Math.min(posed.length, frames.length);
|
|
751
|
+
const at = (i: number, j: string): [number, number] | null => {
|
|
752
|
+
const v = pointOn(frames[i]!, j, minScore);
|
|
753
|
+
return typeof v === 'string' ? null : v;
|
|
754
|
+
};
|
|
755
|
+
const pairAngle = (b: string, i: number): number | null => {
|
|
756
|
+
const [j0, j1] = pairs.get(b)!.joints;
|
|
757
|
+
const p0 = at(i, j0);
|
|
758
|
+
const p1 = at(i, j1);
|
|
759
|
+
return p0 === null || p1 === null || (p0[0] === p1[0] && p0[1] === p1[1]) ? null : pictureAngle(p0, p1);
|
|
760
|
+
};
|
|
761
|
+
const out: JitterRow[] = [];
|
|
762
|
+
for (const [b, { chain, joints }] of pairs) {
|
|
763
|
+
let against: string | null = null;
|
|
764
|
+
const first = posed[0]?.get(b);
|
|
765
|
+
for (let a = first?.parent ?? null; a !== null; a = posed[0]!.get(a)?.parent ?? null) {
|
|
766
|
+
if (pairs.has(a)) {
|
|
767
|
+
against = a;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const rig: number[] = [];
|
|
772
|
+
const pose: number[] = [];
|
|
773
|
+
const good: boolean[] = [];
|
|
774
|
+
for (let i = 0; i < n; i++) {
|
|
775
|
+
const bone = posed[i]!.get(b)!;
|
|
776
|
+
const parent = bone.parent === null ? undefined : posed[i]!.get(bone.parent);
|
|
777
|
+
rig.push(parent === undefined ? Math.atan2(bone.c, bone.a) * DEG : localAngle(parent, bone));
|
|
778
|
+
const own = pairAngle(b, i);
|
|
779
|
+
const up = against === null ? 0 : pairAngle(against, i);
|
|
780
|
+
good.push(own !== null && up !== null);
|
|
781
|
+
pose.push(own === null || up === null ? 0 : wrap(own - up));
|
|
782
|
+
}
|
|
783
|
+
const ok = (i: number) => good[i] === true;
|
|
784
|
+
const jr = meanSecondDiff([rig], ok);
|
|
785
|
+
const jp = meanSecondDiff([pose], ok);
|
|
786
|
+
const r4 = (v: number | null) => (v === null ? null : Math.round(v * 10000) / 10000);
|
|
787
|
+
out.push({
|
|
788
|
+
bone: b,
|
|
789
|
+
chain: chain.name,
|
|
790
|
+
joints,
|
|
791
|
+
against,
|
|
792
|
+
rig: r4(jr.value),
|
|
793
|
+
pair: r4(jp.value),
|
|
794
|
+
// A pair whose figure rounds to 0 is still: a ratio over its float noise would name nothing.
|
|
795
|
+
ratio: jr.value === null || jp.value === null || r4(jp.value) === 0 ? null : Number((jr.value / jp.value).toFixed(3)),
|
|
796
|
+
triples: jr.triples,
|
|
797
|
+
reach: chain.reach,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** A bone's rotation in its parent's axes, degrees: the angle of P⁻¹C's x axis. */
|
|
804
|
+
function localAngle(p: RawBone, c: RawBone): number {
|
|
805
|
+
const det = p.a * p.d - p.b * p.c;
|
|
806
|
+
if (det === 0) return Math.atan2(c.c, c.a) * DEG;
|
|
807
|
+
const la = (p.d * c.a - p.b * c.c) / det;
|
|
808
|
+
const lc = (-p.c * c.a + p.a * c.c) / det;
|
|
809
|
+
return Math.atan2(lc, la) * DEG;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** The `JITTER` line of one keyed bone (#236). */
|
|
813
|
+
export function jitterLine(j: JitterRow): string {
|
|
814
|
+
const f = (v: number | null) => (v === null ? 'not measured' : `${v.toFixed(4)}°/f²`);
|
|
815
|
+
const vs = j.against === null ? `the pair's angle` : `the pair's angle against "${j.against}"'s pair`;
|
|
816
|
+
if (j.triples === 0) return `bone "${j.bone}" (chain "${j.chain}", ${j.joints[0]} → ${j.joints[1]}${j.reach ? ', reach' : ''}): not measured — no three consecutive frames on which the pose points of ${vs} are good`;
|
|
817
|
+
const ratio = j.ratio === null ? (j.pair === 0 ? ' — the pair is still' : '') : ` (${j.ratio.toFixed(2)}×)`;
|
|
818
|
+
return `bone "${j.bone}" (chain "${j.chain}", ${j.joints[0]} → ${j.joints[1]}${j.reach ? ', reach' : ''}): mean |Δ²| of the keyed local rotation as posed ${f(j.rig)} against ${vs} ${f(j.pair)}${ratio}, over ${j.triples} frame triple(s); a reading`;
|
|
819
|
+
}
|