@rig-c/pose 0.18.0 → 0.20.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 +16 -16
- package/cli.ts +1 -1
- package/package.json +1 -1
- package/src/clean.ts +115 -21
- package/src/legs.ts +174 -46
- package/src/pose.ts +82 -33
- package/src/retarget.ts +199 -11
- package/src/run.ts +3 -2
- package/src/switch.ts +33 -5
- package/src/verify.ts +221 -17
package/src/retarget.ts
CHANGED
|
@@ -163,18 +163,29 @@ export interface Flip {
|
|
|
163
163
|
clipped: 'start' | 'end' | 'both' | null;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* One span of frames under `--short` (angle interpolated, `holdShort`) or under
|
|
168
|
+
* `SHORT_STEP_FRACTION` with a one-frame step over `SHORT_STEP_CAP` (step
|
|
169
|
+
* capped, `capShortSteps`, #110).
|
|
170
|
+
*/
|
|
167
171
|
export interface Short {
|
|
168
172
|
chain: string;
|
|
169
173
|
bone: string;
|
|
170
174
|
/** The segment the bone carries: from joint, to joint. */
|
|
171
175
|
joints: [string, string];
|
|
172
|
-
/** First and last frame
|
|
176
|
+
/** First and last frame whose angle was replaced; the frames either side keep the input's. */
|
|
173
177
|
frames: [number, number];
|
|
174
178
|
/** The lowest picture length on those frames, as a fraction of the segment's median picture length. */
|
|
175
179
|
minFraction: number;
|
|
180
|
+
/** Present when a foreshortened segment (< `SHORT_STEP_FRACTION`) had its per-frame step capped (#110): the one-frame step after capping and the largest step asked, degrees, signed. */
|
|
181
|
+
step?: number;
|
|
182
|
+
asked?: number;
|
|
176
183
|
}
|
|
177
184
|
|
|
185
|
+
/** A limb segment shorter than this fraction of its median picture length has its per-frame angle step capped at `SHORT_STEP_CAP` degrees (rig-c/pose#110). */
|
|
186
|
+
export const SHORT_STEP_FRACTION = 0.6;
|
|
187
|
+
export const SHORT_STEP_CAP = 30;
|
|
188
|
+
|
|
178
189
|
/**
|
|
179
190
|
* A segment under the short cutoff long enough to be a pose (the limb pointing
|
|
180
191
|
* at the camera), not a blur: short spans merged across a gap of `END_ON_GAP`
|
|
@@ -571,6 +582,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
571
582
|
// against its own median, on the points the angle was read from.
|
|
572
583
|
const short: Short[] = [];
|
|
573
584
|
const endOn: EndOn[] = [];
|
|
585
|
+
const fractions = new Map<string, { chain: string; joints: [string, string]; fraction: number[] }>();
|
|
574
586
|
if (opts.short !== 'off') {
|
|
575
587
|
const cutoff = opts.short;
|
|
576
588
|
for (const c of map.chains) {
|
|
@@ -586,7 +598,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
586
598
|
p.add('POSE_SHORT_MEDIAN', `chain "${c.name}" — bone "${b}"'s segment ${joints[0]} → ${joints[1]} has median picture length ${med}px over the ${n} frames; --short measures a frame's length against that median, and there is none to measure against`);
|
|
587
599
|
return;
|
|
588
600
|
}
|
|
589
|
-
const
|
|
601
|
+
const fraction = lengths.map((l) => l / med);
|
|
602
|
+
fractions.set(b, { chain: c.name, joints, fraction });
|
|
603
|
+
const spans = holdShort(targets.get(b)!, fraction, cutoff);
|
|
590
604
|
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
591
605
|
for (const run of mergeRuns(spans, END_ON_GAP)) {
|
|
592
606
|
const seconds = Math.round((endOf(run.frames[1]) - keyTimes[run.frames[0]]!) * 1e6) / 1e6;
|
|
@@ -692,12 +706,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
692
706
|
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`);
|
|
693
707
|
if (opts.floor !== undefined && !Number.isFinite(opts.floor)) p.add('FLOOR', `--floor is ${opts.floor}; it must be a finite world y`);
|
|
694
708
|
p.throwIfAny();
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
709
|
+
const askedScales = new Map<string, number[]>();
|
|
710
|
+
for (const ls of legScale) askedScales.set(ls.bone, ls.asked);
|
|
711
|
+
/** Every bone on the legs' path posed on frame i, by the targets and the given scale series. */
|
|
712
|
+
const poseLegsWith = (i: number, sc: ReadonlyMap<string, number[]>): PathPose => {
|
|
713
|
+
const r = posePath(rest, legPath!, (b): Posing | null => (targets.has(b) ? { turn: restTurn(b, i), target: targets.get(b)![i]!, aimX: aimX.get(b)! } : null), (b) => sc.get(b)?.[i] ?? null);
|
|
698
714
|
if (typeof r === 'string') throw new Refusal([{ code: 'LEGS_MAP', text: r }]);
|
|
699
715
|
return r;
|
|
700
716
|
};
|
|
717
|
+
/** Every bone on the legs' path posed on frame i, by the targets as they stand. */
|
|
718
|
+
const poseLegs = (i: number): PathPose => poseLegsWith(i, scales);
|
|
701
719
|
if (plantOn) {
|
|
702
720
|
// The plant is derived from the targets before the range clamp, so nothing after may turn a bone on the legs' path.
|
|
703
721
|
const onPath = new Set(legPath!);
|
|
@@ -706,15 +724,18 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
706
724
|
for (const b of [...Object.keys(a.turn), ...Object.keys(a.offsets)]) if (onPath.has(b)) p.add('LEGS_RANGE', `--plant-feet places the hip through bone "${b}", which set "${a.name}" turns or offsets; that would move the planted foot — drop it from the set or --plant-feet`);
|
|
707
725
|
}
|
|
708
726
|
p.throwIfAny();
|
|
727
|
+
// #107: the clamp floor holds the bone, not the hip; the plant places the hip from the scale the pose asked for.
|
|
709
728
|
const offsets = Array.from({ length: n }, (_, i) => {
|
|
710
|
-
const lp =
|
|
729
|
+
const lp = poseLegsWith(i, askedScales);
|
|
711
730
|
return { r: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.r.ankleBone : ''), l: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.l.ankleBone : '') };
|
|
712
731
|
});
|
|
713
732
|
const tb = rest.get(t!.bone)!;
|
|
714
733
|
// #85: every support span on one floor — the median of their planted heights, or --floor.
|
|
715
734
|
// #92: in place, the supporting ankle's own picture x, scaled, is the track its anchor follows.
|
|
716
|
-
|
|
717
|
-
const
|
|
735
|
+
// #119: a tracked support follows the lower ankle's own picture x, scaled, on the floor until it is still.
|
|
736
|
+
const track = (i: number, s: Side) => series.get(`${s}_ankle`)![i]![0] * scale!.factor;
|
|
737
|
+
const follow = inPlace ? track : undefined;
|
|
738
|
+
const planted = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY], opts.floor, follow, track);
|
|
718
739
|
const hip = planted.hip;
|
|
719
740
|
floor = planted.floor;
|
|
720
741
|
closure = planted.closure;
|
|
@@ -805,16 +826,20 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
805
826
|
* offset turned by the nearest mapped bone above — the key formula's own
|
|
806
827
|
* assumption (unit scale, no shear), which the landing check holds to account.
|
|
807
828
|
*/
|
|
829
|
+
let headNodBone: string | null = null;
|
|
830
|
+
let headNod: Pt[] | null = null;
|
|
831
|
+
let headNodTrack: MotionTrack | null = null;
|
|
808
832
|
const posedOrigin = (b: string, i: number): Pt => {
|
|
809
833
|
const own = rest.get(b)!;
|
|
810
834
|
if (t !== null && b === t.bone) return [own.worldX + (travel[i]?.[0] ?? 0), own.worldY + (travel[i]?.[1] ?? 0)];
|
|
811
|
-
|
|
835
|
+
const nod = b === headNodBone ? (headNod?.[i] ?? [0, 0]) : [0, 0];
|
|
836
|
+
if (own.parent === null) return [own.worldX + nod[0], own.worldY + nod[1]];
|
|
812
837
|
const parent = own.parent;
|
|
813
838
|
const po = posedOrigin(parent, i);
|
|
814
839
|
const r = turnAbove(parent, i) / DEG;
|
|
815
840
|
const ox = own.worldX - rest.get(parent)!.worldX;
|
|
816
841
|
const oy = own.worldY - rest.get(parent)!.worldY;
|
|
817
|
-
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy, po[1] + Math.sin(r) * ox + Math.cos(r) * oy];
|
|
842
|
+
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy + nod[0], po[1] + Math.sin(r) * ox + Math.cos(r) * oy + nod[1]];
|
|
818
843
|
};
|
|
819
844
|
/** A rig point (a bone, `name:tip`, `a+b`) posed on frame i the same way. */
|
|
820
845
|
const posedPoint = (spec: string, i: number): Pt => {
|
|
@@ -868,6 +893,13 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
868
893
|
};
|
|
869
894
|
const turn = (bone: string, i: number) => targets.get(bone)![i]! - restOf.get(bone)!;
|
|
870
895
|
const covered = coveredChains(sw.switches, n);
|
|
896
|
+
if (opts.short !== 'off') {
|
|
897
|
+
for (const [b, info] of fractions) {
|
|
898
|
+
for (const s of capShortSteps(targets.get(b)!, info.fraction, covered.get(info.chain))) {
|
|
899
|
+
short.push({ chain: info.chain, bone: b, joints: info.joints, ...s });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
871
903
|
const range: RangeDemand[] = [];
|
|
872
904
|
const order = map.chains.flatMap((c) => c.bones.map((b) => ({ c, b }))).filter(({ c, b }) => c.reach || b in c.range);
|
|
873
905
|
order.sort((x, y) => depth(x.b) - depth(y.b));
|
|
@@ -895,6 +927,74 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
895
927
|
}
|
|
896
928
|
p.throwIfAny();
|
|
897
929
|
|
|
930
|
+
// #111: the head chain's nod channel — the eyes' vertical travel against the
|
|
931
|
+
// neck, measured in the rig's own units, keyed as a translate on the head bone
|
|
932
|
+
// (whose rotate track carries roll only).
|
|
933
|
+
for (const c of map.chains) {
|
|
934
|
+
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
935
|
+
const ja = c.joints[k]!;
|
|
936
|
+
const jb = c.joints[k + 1]!;
|
|
937
|
+
if (!((ja === 'r_eye' && jb === 'l_eye') || (ja === 'l_eye' && jb === 'r_eye'))) continue;
|
|
938
|
+
const hb = c.bones[k]!;
|
|
939
|
+
const ha = c.aim[k]!;
|
|
940
|
+
if (t?.bone === hb || sw.offsetTracks.some((tr) => tr.bone === hb)) break;
|
|
941
|
+
let underScaled = false;
|
|
942
|
+
for (let a = rest.get(hb)!.parent; a !== null; a = rest.get(a)!.parent) if (scales.has(a)) underScaled = true;
|
|
943
|
+
if (underScaled) break;
|
|
944
|
+
const rEye = series.get('r_eye')!;
|
|
945
|
+
const lEye = series.get('l_eye')!;
|
|
946
|
+
const neckSeries = series.get('neck') ?? (() => {
|
|
947
|
+
const h = heldOf('neck');
|
|
948
|
+
return 'never' in h ? undefined : h.at;
|
|
949
|
+
})();
|
|
950
|
+
let nodScale: number | null = scale?.factor ?? null;
|
|
951
|
+
if (nodScale === null && ha.includes('>')) {
|
|
952
|
+
const [pa, pb] = aimEnds(hb, ha);
|
|
953
|
+
const a0 = rigPoint(rest, pa)!;
|
|
954
|
+
const b0 = rigPoint(rest, pb)!;
|
|
955
|
+
const rigEye = Math.hypot(b0[0] - a0[0], b0[1] - a0[1]);
|
|
956
|
+
const refEye = median(rEye.map((re, i) => Math.hypot(lEye[i]![0] - re[0], lEye[i]![1] - re[1])));
|
|
957
|
+
if (refEye > 1e-6 && rigEye > 1e-6) nodScale = rigEye / refEye;
|
|
958
|
+
}
|
|
959
|
+
if (neckSeries !== undefined && nodScale !== null) {
|
|
960
|
+
const refEN = rEye.map((re, i) => -((re[1] + lEye[i]![1]) / 2 - neckSeries[i]![1]));
|
|
961
|
+
const dy = refEN.map((v) => (v - refEN[0]!) * nodScale);
|
|
962
|
+
if (dy.some((v) => Math.abs(r3(v)) > 0)) {
|
|
963
|
+
const bone = rest.get(hb)!;
|
|
964
|
+
const parent = bone.parent === null ? null : rest.get(bone.parent)!;
|
|
965
|
+
const keys: MotionKey[] = [];
|
|
966
|
+
const nod: Pt[] = [];
|
|
967
|
+
for (let i = 0; i < n; i++) {
|
|
968
|
+
const wy = dy[i]!;
|
|
969
|
+
let lx = 0;
|
|
970
|
+
let ly = r3(wy);
|
|
971
|
+
let awx = 0;
|
|
972
|
+
let awy = ly;
|
|
973
|
+
if (parent !== null) {
|
|
974
|
+
const r = turnAbove(parent.name, i) / DEG;
|
|
975
|
+
const pa = Math.cos(r) * parent.a - Math.sin(r) * parent.c;
|
|
976
|
+
const pb = Math.cos(r) * parent.b - Math.sin(r) * parent.d;
|
|
977
|
+
const pc = Math.sin(r) * parent.a + Math.cos(r) * parent.c;
|
|
978
|
+
const pd = Math.sin(r) * parent.b + Math.cos(r) * parent.d;
|
|
979
|
+
const det = pa * pd - pb * pc;
|
|
980
|
+
lx = r3((-pb * wy) / det);
|
|
981
|
+
ly = r3((pa * wy) / det);
|
|
982
|
+
awx = pa * lx + pb * ly;
|
|
983
|
+
awy = pc * lx + pd * ly;
|
|
984
|
+
}
|
|
985
|
+
keys.push({ t: keyTimes[i]!, v: [lx, ly] });
|
|
986
|
+
nod.push([awx, awy]);
|
|
987
|
+
}
|
|
988
|
+
headNodBone = hb;
|
|
989
|
+
headNod = nod;
|
|
990
|
+
headNodTrack = { bone: hb, property: 'translate', keys };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
if (headNodTrack !== null) break;
|
|
996
|
+
}
|
|
997
|
+
|
|
898
998
|
// --raised-by-head (#62, HQ's ruling): while a set whose arm is drawn raised shows, its wrist goes where
|
|
899
999
|
// the reference's is against the head, in eye distances (each side's median over the span). Solved last
|
|
900
1000
|
// of the bones: every bone above the arm, the head and the trunk's reach included, has its final target
|
|
@@ -1176,6 +1276,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1176
1276
|
}
|
|
1177
1277
|
}
|
|
1178
1278
|
if (translateTrack !== null) tracks.push(translateTrack);
|
|
1279
|
+
if (headNodTrack !== null) tracks.push(headNodTrack);
|
|
1179
1280
|
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
|
1180
1281
|
|
|
1181
1282
|
return {
|
|
@@ -1302,10 +1403,97 @@ export function mergeRuns(spans: readonly ShortSpan[], gap: number): ShortSpan[]
|
|
|
1302
1403
|
return out;
|
|
1303
1404
|
}
|
|
1304
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* Cap one-frame steps on a foreshortened segment (#110), in place. Where a
|
|
1408
|
+
* segment's picture length is under `SHORT_STEP_FRACTION` (0.60) of its median
|
|
1409
|
+
* on either side of a frame step (above `--short`'s hold cutoff, where two end-on
|
|
1410
|
+
* pixels still swing the angle faster than the limb moved) and the one-frame turn
|
|
1411
|
+
* exceeds `SHORT_STEP_CAP` (30°), spread the turn across a symmetric window of
|
|
1412
|
+
* frames the same way `spreadFlips` does (`lo = k - h - 1`, `hi = k + h`), bounded
|
|
1413
|
+
* by the ends of the pose and by any active alternate set on the chain (`covered`).
|
|
1414
|
+
*/
|
|
1415
|
+
export function capShortSteps(
|
|
1416
|
+
angles: number[],
|
|
1417
|
+
fraction: readonly number[],
|
|
1418
|
+
covered?: readonly boolean[],
|
|
1419
|
+
fractionCutoff: number = SHORT_STEP_FRACTION,
|
|
1420
|
+
stepCap: number = SHORT_STEP_CAP,
|
|
1421
|
+
): ShortSpan[] {
|
|
1422
|
+
const n = angles.length;
|
|
1423
|
+
if (n < 2) return [];
|
|
1424
|
+
const out: ShortSpan[] = [];
|
|
1425
|
+
for (let segStart = 0; segStart < n;) {
|
|
1426
|
+
if (covered?.[segStart] === true) {
|
|
1427
|
+
segStart++;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
let segEnd = segStart;
|
|
1431
|
+
while (segEnd + 1 < n && covered?.[segEnd + 1] !== true) segEnd++;
|
|
1432
|
+
if (segEnd - segStart >= 1) {
|
|
1433
|
+
const u: number[] = new Array<number>(n).fill(0);
|
|
1434
|
+
u[segStart] = angles[segStart]!;
|
|
1435
|
+
for (let i = segStart + 1; i <= segEnd; i++) u[i] = u[i - 1]! + wrap(angles[i]! - angles[i - 1]!);
|
|
1436
|
+
const fits = (lo: number, hi: number) => Math.abs(u[hi]! - u[lo]!) <= stepCap * (hi - lo);
|
|
1437
|
+
interface Win { lo: number; hi: number; asked: number }
|
|
1438
|
+
const wins: Win[] = [];
|
|
1439
|
+
for (let k = segStart + 1; k <= segEnd; k++) {
|
|
1440
|
+
if (!(Math.min(fraction[k - 1]!, fraction[k]!) < fractionCutoff)) continue;
|
|
1441
|
+
const jump = u[k]! - u[k - 1]!;
|
|
1442
|
+
if (Math.abs(jump) <= stepCap) continue;
|
|
1443
|
+
let h = 1;
|
|
1444
|
+
let lo = Math.max(segStart, k - h - 1);
|
|
1445
|
+
let hi = Math.min(segEnd, k + h);
|
|
1446
|
+
while (!fits(lo, hi) && (lo > segStart || hi < segEnd)) {
|
|
1447
|
+
h++;
|
|
1448
|
+
lo = Math.max(segStart, k - h - 1);
|
|
1449
|
+
hi = Math.min(segEnd, k + h);
|
|
1450
|
+
}
|
|
1451
|
+
wins.push({ lo, hi, asked: jump });
|
|
1452
|
+
}
|
|
1453
|
+
for (let changed = true; changed;) {
|
|
1454
|
+
changed = false;
|
|
1455
|
+
wins.sort((a, b) => a.lo - b.lo);
|
|
1456
|
+
for (let i = 0; i + 1 < wins.length; i++) {
|
|
1457
|
+
const a = wins[i]!;
|
|
1458
|
+
const b = wins[i + 1]!;
|
|
1459
|
+
if (b.lo >= a.hi) continue;
|
|
1460
|
+
wins.splice(i, 2, { lo: a.lo, hi: Math.max(a.hi, b.hi), asked: Math.abs(b.asked) > Math.abs(a.asked) ? b.asked : a.asked });
|
|
1461
|
+
changed = true;
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
if (changed) continue;
|
|
1465
|
+
for (const w of wins) {
|
|
1466
|
+
if (fits(w.lo, w.hi) || (w.lo === segStart && w.hi === segEnd)) continue;
|
|
1467
|
+
w.lo = Math.max(segStart, w.lo - 1);
|
|
1468
|
+
w.hi = Math.min(segEnd, w.hi + 1);
|
|
1469
|
+
changed = true;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
for (const w of wins) {
|
|
1473
|
+
if (w.lo + 1 > w.hi - 1) continue;
|
|
1474
|
+
const step = (u[w.hi]! - u[w.lo]!) / (w.hi - w.lo);
|
|
1475
|
+
for (let i = w.lo + 1; i < w.hi; i++) angles[i] = wrap(u[w.lo]! + step * (i - w.lo));
|
|
1476
|
+
let minFraction = Infinity;
|
|
1477
|
+
for (let i = w.lo + 1; i <= w.hi - 1; i++) minFraction = Math.min(minFraction, fraction[i]!);
|
|
1478
|
+
if (!(minFraction < fractionCutoff)) {
|
|
1479
|
+
minFraction = Math.min(minFraction, fraction[w.lo]!, fraction[w.hi]!);
|
|
1480
|
+
}
|
|
1481
|
+
out.push({ frames: [w.lo + 1, w.hi - 1], minFraction, step: r3(step), asked: r3(w.asked) });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
segStart = segEnd + 1;
|
|
1485
|
+
}
|
|
1486
|
+
return out;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1305
1489
|
const spanOf = (frames: [number, number], times: readonly number[]): string =>
|
|
1306
1490
|
frames[0] === frames[1] ? `frame ${frames[0]} (t=${times[frames[0]]!.toFixed(3)}s)` : `frames ${frames[0]}–${frames[1]} (t=${times[frames[0]]!.toFixed(3)}–${times[frames[1]]!.toFixed(3)}s)`;
|
|
1307
1491
|
|
|
1308
1492
|
export function shortLine(s: Short, times: readonly number[], cutoff: number): string {
|
|
1493
|
+
if (s.step !== undefined && s.asked !== undefined) {
|
|
1494
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
1495
|
+
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${SHORT_STEP_FRACTION.toFixed(2)}, turned ${sg(s.asked)} in one frame (over ${SHORT_STEP_CAP}°); spread to ${sg(s.step)} per frame`;
|
|
1496
|
+
}
|
|
1309
1497
|
const lo = s.frames[0] - 1;
|
|
1310
1498
|
const hi = s.frames[1] + 1;
|
|
1311
1499
|
const held = lo < 0 && hi >= times.length
|
package/src/run.ts
CHANGED
|
@@ -146,6 +146,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
146
146
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
147
147
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
148
148
|
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
149
|
+
if (f.tracked !== undefined) say(` FEET tracked (the lower foot supports on the floor, following its scaled ankle track until still, rig-c/pose#119): ${f.tracked.map((x) => `${x.side} ${span(x.frames)}`).join(', ')}`);
|
|
149
150
|
if (r.closure !== null) say(` FEET ${travelLine(r.closure)}`);
|
|
150
151
|
else say(` FEET steps (the supporting ankle > 0.03 leg lengths a frame; re-planted after 2 frames < 0.01): ${f.steps.map((x) => `${x.side} ${span(x.frames)} → ${x.to === null ? 'the pose\'s travel' : `${x.to} supports`}, ${x.replant === null ? 'never re-planted' : `re-planted at ${x.replant}`}`).join('; ') || 'none'}`);
|
|
151
152
|
if (r.floor !== null) {
|
|
@@ -315,10 +316,10 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
315
316
|
raised: r.raised,
|
|
316
317
|
// Only when a set declares `place` (#93): a map without one writes the report it always has.
|
|
317
318
|
...(map.alternates.some((x) => x.place !== undefined && x.place !== null) ? { byHip: r.byHip } : {}),
|
|
318
|
-
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
319
|
+
legScale: r.legScale.map(({ scale: _scale, asked: _asked, ...rest }) => rest),
|
|
319
320
|
pivot: r.pivot,
|
|
320
321
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
321
|
-
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
322
|
+
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
322
323
|
shear,
|
|
323
324
|
idleDropped: idle === null ? null : idle.dropped,
|
|
324
325
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
package/src/switch.ts
CHANGED
|
@@ -590,22 +590,50 @@ export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
|
590
590
|
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it; ${e.clamped ? 'held at the limit' : 'the bone follows the pose'} — demand for another drawing of the set`;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* The art-range clamp eases in and out at a span's edges over at least
|
|
595
|
+
* `ART_EASE_FRAMES` frames, or enough frames to keep the clamp's step at the
|
|
596
|
+
* boundary within `ART_EASE_STEP` degrees per frame (#110).
|
|
597
|
+
*/
|
|
598
|
+
export const ART_EASE_FRAMES = 3;
|
|
599
|
+
export const ART_EASE_STEP = 3;
|
|
600
|
+
|
|
593
601
|
/**
|
|
594
602
|
* Inside each span, a bone with a `turn` in the set is held within it: its
|
|
595
|
-
* target world angle is drawn + clamp(wrap(target − drawn), min, max)
|
|
596
|
-
*
|
|
597
|
-
*
|
|
603
|
+
* target world angle is drawn + clamp(wrap(target − drawn), min, max), eased
|
|
604
|
+
* in and out over at least `ART_EASE_FRAMES` frames at an edge where the clamp
|
|
605
|
+
* switches on or off across the boundary (#110). Each bone is clamped on its
|
|
606
|
+
* own world angle, and keys are differences of world angles, so a forearm
|
|
607
|
+
* under a clamped upper arm keeps its own target.
|
|
598
608
|
*/
|
|
599
609
|
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
|
|
600
610
|
for (const sw of switches) {
|
|
601
611
|
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
612
|
+
const [a, b] = sw.frames;
|
|
602
613
|
for (const [bone, limits] of Object.entries(set.turn)) {
|
|
603
614
|
const drawn = set.drawn[bone]!;
|
|
604
615
|
const target = targets.get(bone)!;
|
|
605
|
-
|
|
616
|
+
const n = target.length;
|
|
617
|
+
const delta = (i: number): number => {
|
|
606
618
|
const t = wrap(target[i]! - drawn);
|
|
607
619
|
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
608
|
-
|
|
620
|
+
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
621
|
+
};
|
|
622
|
+
const da = delta(a);
|
|
623
|
+
const db = delta(b);
|
|
624
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
625
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
626
|
+
: 0;
|
|
627
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
628
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
629
|
+
: 0;
|
|
630
|
+
for (let i = a; i <= b; i++) {
|
|
631
|
+
const d = delta(i);
|
|
632
|
+
if (d === 0) continue;
|
|
633
|
+
const wIn = i - a < easeIn ? (i - a + 1) / (easeIn + 1) : 1;
|
|
634
|
+
const wOut = b - i < easeOut ? (b - i + 1) / (easeOut + 1) : 1;
|
|
635
|
+
const w = Math.min(wIn, wOut);
|
|
636
|
+
target[i] = wrap(target[i]! + w * d);
|
|
609
637
|
}
|
|
610
638
|
}
|
|
611
639
|
}
|