@rig-c/pose 0.20.1 → 0.20.3
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/package.json +1 -1
- package/src/legs.ts +92 -3
- package/src/map.ts +2 -0
- package/src/retarget.ts +13 -4
- package/src/rigc.ts +1 -1
- package/src/switch.ts +274 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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/legs.ts
CHANGED
|
@@ -683,6 +683,23 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
683
683
|
const tracked = new Array<Side | null>(n).fill(null);
|
|
684
684
|
for (const t of feet.tracked ?? []) for (let i = t.frames[0]; i <= t.frames[1]; i++) tracked[i] = t.side;
|
|
685
685
|
const trkX = trackFn ?? follow;
|
|
686
|
+
// #142: on a run (two or more flights), each support span's push-off ankle lift and side-to-side
|
|
687
|
+
// straight-length asymmetry would otherwise swing floorY - offset(i, s)[1] far past the pose's
|
|
688
|
+
// own bounce and ratchet the un-floored picture pass per cycle. Keep the support spans' mean floor
|
|
689
|
+
// shift across both support spans and flights, holding each grounded ankle on floorY via blends.
|
|
690
|
+
const isRun = feet.jumps.length >= 2 && feet.supports.length > 0;
|
|
691
|
+
let runShift = 0;
|
|
692
|
+
if (isRun && floorY !== null) {
|
|
693
|
+
const t0 = travel(0)[1];
|
|
694
|
+
const shifts: number[] = [];
|
|
695
|
+
for (let i = 0; i < n; i++) {
|
|
696
|
+
const s = feet.support[i] ?? null;
|
|
697
|
+
if (s === null) continue;
|
|
698
|
+
shifts.push(floorY - offset(i, s)[1] - (start[1] + travel(i)[1] - t0));
|
|
699
|
+
}
|
|
700
|
+
if (shifts.length > 0) runShift = shifts.reduce((a, b) => a + b, 0) / shifts.length;
|
|
701
|
+
}
|
|
702
|
+
const flightShift = runShift;
|
|
686
703
|
/** How the hip is derived on a frame: held over a foot's anchor, tracking the lower foot on the floor (#119), or carried by the pose's travel from a frame's hip (and for a jump against `floorY`, carried so the landing frame meets the floor and frames inside the flight never go below it, #103). */
|
|
687
704
|
type Mode =
|
|
688
705
|
| { kind: 'plant'; side: Side; anchor: Pt; from: number }
|
|
@@ -693,6 +710,13 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
693
710
|
const o = offset(i, mode.side);
|
|
694
711
|
// In place (#92) the anchor's x follows the supporting ankle's own track from the frame it planted.
|
|
695
712
|
const dx = follow === undefined ? 0 : follow(i, mode.side) - follow(mode.from, mode.side);
|
|
713
|
+
if (isRun) {
|
|
714
|
+
const yPose = start[1] + travel(i)[1] - travel(0)[1] + (floorY === null ? 0 : runShift);
|
|
715
|
+
if (floorY === null) return [mode.anchor[0] + dx - o[0], yPose];
|
|
716
|
+
const d = -o[1];
|
|
717
|
+
const y = d > 0 ? Math.min(Math.max(yPose, mode.anchor[1] + STANCE_BOUNDS[0] * d), mode.anchor[1] + STANCE_BOUNDS[1] * d) : yPose;
|
|
718
|
+
return [mode.anchor[0] + dx - o[0], y];
|
|
719
|
+
}
|
|
696
720
|
return [mode.anchor[0] + dx - o[0], mode.anchor[1] - o[1]];
|
|
697
721
|
}
|
|
698
722
|
if (mode.kind === 'track') {
|
|
@@ -703,6 +727,13 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
703
727
|
}
|
|
704
728
|
const a = travel(i);
|
|
705
729
|
const b = travel(mode.from);
|
|
730
|
+
if (isRun) {
|
|
731
|
+
let y = start[1] + a[1] - travel(0)[1] + (floorY === null ? 0 : flightShift);
|
|
732
|
+
if (mode.end !== undefined && floorY !== null && i <= mode.end) {
|
|
733
|
+
y = Math.max(y, floorY - Math.min(offset(i, 'r')[1], offset(i, 'l')[1]));
|
|
734
|
+
}
|
|
735
|
+
return [mode.at[0] + a[0] - b[0], y];
|
|
736
|
+
}
|
|
706
737
|
let y = mode.at[1] + a[1] - b[1];
|
|
707
738
|
if (mode.carry !== undefined && mode.land !== undefined && mode.end !== undefined && floorY !== null) {
|
|
708
739
|
if (mode.land < n && i <= mode.land && mode.land > mode.from) {
|
|
@@ -756,7 +787,7 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
756
787
|
// The new support is held where it is on this frame, under the hip the old derivation gives here.
|
|
757
788
|
const here = of(mode!, i);
|
|
758
789
|
const o = offset(i, s);
|
|
759
|
-
from = mode;
|
|
790
|
+
from = isRun ? null : mode;
|
|
760
791
|
mode = plant(i, s, [here[0] + o[0], here[1] + o[1]]);
|
|
761
792
|
blendAt = i;
|
|
762
793
|
}
|
|
@@ -784,6 +815,10 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
784
815
|
} else {
|
|
785
816
|
from = null;
|
|
786
817
|
hip.push(to);
|
|
818
|
+
const m = mode!;
|
|
819
|
+
if (isRun && floorY !== null && m.kind === 'plant' && Math.abs(to[1] - (m.anchor[1] - offset(i, m.side)[1])) > BLEND_HOLD_WITHIN) {
|
|
820
|
+
blends.push({ frame: i, side: m.side, y: m.anchor[1] });
|
|
821
|
+
}
|
|
787
822
|
}
|
|
788
823
|
}
|
|
789
824
|
return { hip, anchors, blends };
|
|
@@ -1014,7 +1049,61 @@ export interface StancePosed {
|
|
|
1014
1049
|
* free side and with no frame between, takes their state, and is listed with
|
|
1015
1050
|
* `absorbed`.
|
|
1016
1051
|
*/
|
|
1017
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* Under `--stance` and `--leg-scale` (rig-c/pose#137), a free foot read `both`
|
|
1054
|
+
* (planted beside the supporting one within `tolerance`) whose posed ankle sits
|
|
1055
|
+
* above the floor (`ankle[free][1] > ankle[support][1]`) while neither the
|
|
1056
|
+
* whole leg (`lo >= STANCE_BOUNDS[0]`) nor its thigh (`s0 >= STANCE_BOUNDS[0]`)
|
|
1057
|
+
* is folded below `STANCE_BOUNDS[0]`, yet its shin was shortened below it
|
|
1058
|
+
* (`s1 < STANCE_BOUNDS[0]`), owes that shin shortening to a low knee / high
|
|
1059
|
+
* ankle split along a straight stance leg in the picture. Keeping the shortened
|
|
1060
|
+
* shin scale makes `--stance` multiply both thigh and shin by `needed`,
|
|
1061
|
+
* over-stretching the thigh (pushing the knee down), leaving the shin and foot
|
|
1062
|
+
* squashed, and exceeding `STANCE_BOUNDS[1]` so the free ankle floats off #85's
|
|
1063
|
+
* floor. Lifting the shin's scale to `min(1, s0)` before `stanceOf` reads
|
|
1064
|
+
* `needed` restores the straight leg's shin proportion first so the chain is
|
|
1065
|
+
* stretched only by the remaining gap.
|
|
1066
|
+
*/
|
|
1067
|
+
export function unshortenStanceShin(
|
|
1068
|
+
scales: Map<string, number[]>,
|
|
1069
|
+
legs: Record<Side, { chain: Chain; ankleBone: string }>,
|
|
1070
|
+
posed: (i: number) => StancePosed,
|
|
1071
|
+
lower?: StanceLower,
|
|
1072
|
+
): (i: number, free?: Side) => StancePosed {
|
|
1073
|
+
const copied = new Set<string>();
|
|
1074
|
+
const ensureCopy = (b: string): number[] => {
|
|
1075
|
+
const cur = scales.get(b)!;
|
|
1076
|
+
if (copied.has(b)) return cur;
|
|
1077
|
+
const copy = [...cur];
|
|
1078
|
+
scales.set(b, copy);
|
|
1079
|
+
copied.add(b);
|
|
1080
|
+
return copy;
|
|
1081
|
+
};
|
|
1082
|
+
return (i: number, free?: Side): StancePosed => {
|
|
1083
|
+
const p = posed(i);
|
|
1084
|
+
if (free === undefined) return p;
|
|
1085
|
+
if (lowerOf(lower, free, i) < STANCE_BOUNDS[0]) return p;
|
|
1086
|
+
const support: Side = free === 'r' ? 'l' : 'r';
|
|
1087
|
+
const floor = p.ankle[support][1];
|
|
1088
|
+
const root = p.root[free][1];
|
|
1089
|
+
const a = p.ankle[free][1];
|
|
1090
|
+
if (!(root - a > 1e-9 && a > floor + 1e-9)) return p;
|
|
1091
|
+
const bones = legs[free].chain.bones;
|
|
1092
|
+
if (bones.length < 2) return p;
|
|
1093
|
+
const b0 = bones[0]!;
|
|
1094
|
+
const b1 = bones[bones.length - 1]!;
|
|
1095
|
+
const arr0 = scales.get(b0);
|
|
1096
|
+
const arr1 = scales.get(b1);
|
|
1097
|
+
if (arr0 === undefined || arr1 === undefined) return p;
|
|
1098
|
+
const s0 = arr0[i]!;
|
|
1099
|
+
const s1 = arr1[i]!;
|
|
1100
|
+
if (!(s0 >= STANCE_BOUNDS[0] && s1 < STANCE_BOUNDS[0])) return p;
|
|
1101
|
+
ensureCopy(b1)[i] = Math.min(1, s0);
|
|
1102
|
+
return posed(i);
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number, free?: Side) => StancePosed, secondsOf: (run: [number, number]) => number, lower?: StanceLower): Stance {
|
|
1018
1107
|
const n = feet.support.length;
|
|
1019
1108
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
1020
1109
|
const tracked = new Array<Side | null>(n).fill(null);
|
|
@@ -1085,7 +1174,7 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
1085
1174
|
rows[i] = { state: r.state, side: f, height, k: null, needed: null, over: false, left: 0, lo };
|
|
1086
1175
|
continue;
|
|
1087
1176
|
}
|
|
1088
|
-
const p = posed(i);
|
|
1177
|
+
const p = posed(i, f);
|
|
1089
1178
|
const floor = p.ankle[s][1];
|
|
1090
1179
|
const root = p.root[f][1];
|
|
1091
1180
|
const a = p.ankle[f][1];
|
package/src/map.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface JointMap {
|
|
|
111
111
|
export interface RigSlot {
|
|
112
112
|
setup: string | null;
|
|
113
113
|
attachments: ReadonlySet<string>;
|
|
114
|
+
/** The bone the slot is attached to, when read from a compiled build. */
|
|
115
|
+
bone?: string;
|
|
114
116
|
}
|
|
115
117
|
export type RigSlots = ReadonlyMap<string, RigSlot>;
|
|
116
118
|
|
package/src/retarget.ts
CHANGED
|
@@ -82,7 +82,7 @@ import { Problems, Refusal } from './problems.ts';
|
|
|
82
82
|
import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
83
|
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
-
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf } from './legs.ts';
|
|
85
|
+
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
86
|
|
|
87
87
|
const DEG = 180 / Math.PI;
|
|
88
88
|
|
|
@@ -775,11 +775,20 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
775
775
|
const ankleSeries = { r: series.get('r_ankle')!, l: series.get('l_ankle')! };
|
|
776
776
|
const lower = stanceRangeOf(ankleSeries, { r: series.get('r_hip')!, l: series.get('l_hip')! });
|
|
777
777
|
blendHold = holdBlend(planted.blends, n, posedFeet, lower);
|
|
778
|
+
const holdAnchorX = feet!.jumps.length >= 2 ? blendHold.stretch.map((b, i) => (b ? posedFeet(i).ankle[b.side][0] : 0)) : null;
|
|
778
779
|
stretchChain(blendHold.stretch);
|
|
780
|
+
if (holdAnchorX !== null) {
|
|
781
|
+
for (let i = 0; i < n; i++) {
|
|
782
|
+
const b = blendHold.stretch[i];
|
|
783
|
+
if (!b) continue;
|
|
784
|
+
hip[i]![0] += holdAnchorX[i]! - posedFeet(i).ankle[b.side][0];
|
|
785
|
+
travel[i]![0] = hip[i]![0] - tb.worldX;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
779
788
|
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
780
789
|
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
781
790
|
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
782
|
-
stance = stanceOf(feet!, ankleSeries, opts.stance, posedFeet, secondsOf, lower);
|
|
791
|
+
stance = stanceOf(feet!, ankleSeries, opts.stance, legScaleOn ? unshortenStanceShin(scales, L, posedFeet, lower) : posedFeet, secondsOf, lower);
|
|
783
792
|
// The supporting leg (the blend's) and the free leg (the stance's) are different chains on a frame.
|
|
784
793
|
for (let i = 0; i < n; i++) {
|
|
785
794
|
const b = blendHold.stretch[i];
|
|
@@ -897,7 +906,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
897
906
|
return null;
|
|
898
907
|
};
|
|
899
908
|
const turn = (bone: string, i: number) => targets.get(bone)![i]! - restOf.get(bone)!;
|
|
900
|
-
const covered = coveredChains(sw.switches, n);
|
|
909
|
+
const covered = coveredChains(sw.switches, n, sw.chainHolds);
|
|
901
910
|
if (opts.short !== 'off') {
|
|
902
911
|
for (const [b, info] of fractions) {
|
|
903
912
|
for (const s of capShortSteps(targets.get(b)!, info.fraction, covered.get(info.chain))) {
|
|
@@ -1188,7 +1197,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1188
1197
|
const own = measured.filter((e) => e.set === s.set && e.frames[0] >= s.frames[0] && e.frames[1] <= s.frames[1]);
|
|
1189
1198
|
artRange.push(...own.filter((e) => !solvedBy(e, s)), ...(raisedArt.find((x) => x.sw === s)?.entries ?? []));
|
|
1190
1199
|
}
|
|
1191
|
-
clampArt(sw.switches, map, targets);
|
|
1200
|
+
clampArt(sw.switches, map, targets, sw.chainHolds);
|
|
1192
1201
|
// Where each raised wrist lands as keyed: after the solve and the set's `turn`.
|
|
1193
1202
|
for (const job of raisedJobs) {
|
|
1194
1203
|
const after: Pt[] = [];
|
package/src/rigc.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function slotsOf(doc: CompiledDocument): RigSlots {
|
|
|
143
143
|
for (const s of doc.slots) {
|
|
144
144
|
const attachments = new Set<string>();
|
|
145
145
|
for (const k of doc.skins) for (const name of Object.keys(k.attachments[s.name] ?? {})) attachments.add(name);
|
|
146
|
-
out.set(s.name, { setup: s.setup, attachments });
|
|
146
|
+
out.set(s.name, { setup: s.setup, attachments, bone: s.bone });
|
|
147
147
|
}
|
|
148
148
|
return out;
|
|
149
149
|
}
|
package/src/switch.ts
CHANGED
|
@@ -154,19 +154,35 @@ export interface Switches {
|
|
|
154
154
|
shown: Map<string, Array<string | null>>;
|
|
155
155
|
/** Per set, its distance on each frame, degrees. */
|
|
156
156
|
distances: Map<string, number[]>;
|
|
157
|
+
/** Per chain, the behind-the-torso set spans (including same-set gap holds, #136) for `clampArt`. */
|
|
158
|
+
chainHolds?: Array<{ set: string; chain: string; frames: [number, number] }>;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
/** Per set, its distance to the pose on every frame: the largest segment difference over
|
|
160
|
-
export function
|
|
161
|
-
const out = new Map<string, number[]
|
|
161
|
+
/** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain. */
|
|
162
|
+
export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, Map<string, number[]>> {
|
|
163
|
+
const out = new Map<string, Map<string, number[]>>();
|
|
162
164
|
for (const a of map.alternates) {
|
|
163
|
-
const
|
|
165
|
+
const byChain = new Map<string, number[]>();
|
|
164
166
|
for (const cn of a.chains) {
|
|
165
167
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
166
|
-
c.bones.
|
|
168
|
+
const segs = c.bones.map((b, k) => ({ asked: targets.get(b)!, key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!) }));
|
|
169
|
+
const d: number[] = [];
|
|
170
|
+
for (let i = 0; i < n; i++) d.push(Math.max(...segs.map((s) => Math.abs(wrap(s.asked[i]! - s.key)))));
|
|
171
|
+
byChain.set(cn, d);
|
|
167
172
|
}
|
|
173
|
+
out.set(a.name, byChain);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
|
|
179
|
+
export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
|
|
180
|
+
const byChain = chainDistances(map, targets, n);
|
|
181
|
+
const out = new Map<string, number[]>();
|
|
182
|
+
for (const a of map.alternates) {
|
|
183
|
+
const cs = a.chains.map((cn) => byChain.get(a.name)!.get(cn)!);
|
|
168
184
|
const d: number[] = [];
|
|
169
|
-
for (let i = 0; i < n; i++) d.push(Math.max(...
|
|
185
|
+
for (let i = 0; i < n; i++) d.push(Math.max(...cs.map((c) => c[i]!)));
|
|
170
186
|
out.set(a.name, d);
|
|
171
187
|
}
|
|
172
188
|
return out;
|
|
@@ -210,6 +226,59 @@ function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
|
|
|
210
226
|
return [...groups.values()];
|
|
211
227
|
}
|
|
212
228
|
|
|
229
|
+
/**
|
|
230
|
+
* How a competing group's slots sit against the torso in the rig's draw order
|
|
231
|
+
* (`slots` is in draw order, back to front, #136): which slot is the never-hidden
|
|
232
|
+
* torso slot (when the group touches or sits beside one), which sets draw their
|
|
233
|
+
* limb slots behind it vs in front of it, whether the base setup slots sit in
|
|
234
|
+
* front of it, and which chain owns each non-torso slot.
|
|
235
|
+
*/
|
|
236
|
+
function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: RigSlots) {
|
|
237
|
+
const rank = new Map([...slots.keys()].map((s, i) => [s, i]));
|
|
238
|
+
const touched = [...new Set(group.flatMap((a) => [...Object.keys(a.slots), ...a.hides]))].filter((s) => rank.has(s));
|
|
239
|
+
const hiddenSetup = touched.filter((s) => slots.get(s)!.setup !== null && group.some((a) => a.hides.includes(s)));
|
|
240
|
+
const extra = touched.filter((s) => slots.get(s)!.setup === null);
|
|
241
|
+
const torsoSlot = touched.find((s) => slots.get(s)!.setup !== null && !group.some((a) => a.hides.includes(s))) ?? null;
|
|
242
|
+
let torsoRank: number | null = torsoSlot !== null ? rank.get(torsoSlot)! : null;
|
|
243
|
+
if (torsoRank === null && hiddenSetup.length > 0 && extra.length > 0) {
|
|
244
|
+
const minFront = Math.min(...hiddenSetup.map((s) => rank.get(s)!));
|
|
245
|
+
const maxBack = Math.max(...extra.filter((s) => rank.get(s)! < minFront).map((s) => rank.get(s)!));
|
|
246
|
+
if (Number.isFinite(maxBack)) {
|
|
247
|
+
const between = [...slots.keys()].find((s) => !touched.includes(s) && slots.get(s)!.setup !== null && rank.get(s)! > maxBack && rank.get(s)! < minFront);
|
|
248
|
+
torsoRank = between !== undefined ? rank.get(between)! : minFront - 0.5;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const limbSlotsOf = (a: AlternateSet) => Object.keys(a.slots).filter((s) => s !== torsoSlot && rank.has(s));
|
|
252
|
+
const isBehind = (a: AlternateSet): boolean => {
|
|
253
|
+
if (torsoRank === null) return false;
|
|
254
|
+
const ls = limbSlotsOf(a);
|
|
255
|
+
return ls.length > 0 && ls.every((s) => rank.get(s)! < torsoRank) && a.hides.some((h) => (rank.get(h) ?? -1) > torsoRank);
|
|
256
|
+
};
|
|
257
|
+
const isFront = (a: AlternateSet): boolean => {
|
|
258
|
+
if (torsoRank === null) return false;
|
|
259
|
+
const ls = limbSlotsOf(a);
|
|
260
|
+
return ls.length > 0 && ls.every((s) => rank.get(s)! > torsoRank);
|
|
261
|
+
};
|
|
262
|
+
const baseInFront = torsoRank !== null && hiddenSetup.length > 0 && hiddenSetup.every((s) => rank.get(s)! > torsoRank);
|
|
263
|
+
const groupChains = [...new Set(group.flatMap((a) => a.chains))];
|
|
264
|
+
const boneOwner = new Map<string, string>();
|
|
265
|
+
for (const cn of groupChains) for (const b of map.chains.find((c) => c.name === cn)!.bones) boneOwner.set(b, cn);
|
|
266
|
+
const slotChain = new Map<string, string>();
|
|
267
|
+
for (const s of touched) {
|
|
268
|
+
if (s === torsoSlot) continue;
|
|
269
|
+
const b = slots.get(s)?.bone;
|
|
270
|
+
if (b !== undefined && boneOwner.has(b)) {
|
|
271
|
+
slotChain.set(s, boneOwner.get(b)!);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const side = /(?:^|_)([rl])(?:_|$)/.exec(s)?.[1];
|
|
275
|
+
const bySide = side === undefined ? undefined : groupChains.find((c) => new RegExp(`(?:^|_)${side}(?:_|$)`).test(c));
|
|
276
|
+
if (bySide !== undefined) slotChain.set(s, bySide);
|
|
277
|
+
else if (groupChains.length === 1) slotChain.set(s, groupChains[0]!);
|
|
278
|
+
}
|
|
279
|
+
return { torsoSlot, torsoRank, touched, isBehind, isFront, baseInFront, slotChain };
|
|
280
|
+
}
|
|
281
|
+
|
|
213
282
|
/**
|
|
214
283
|
* Per frame, the largest |angular speed| over the given bones' target world
|
|
215
284
|
* angles, degrees per frame: a central difference, one-sided on the first and
|
|
@@ -305,9 +374,12 @@ const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 :
|
|
|
305
374
|
*/
|
|
306
375
|
export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
|
|
307
376
|
const n = times.length;
|
|
377
|
+
const byChain = chainDistances(map, targets, n);
|
|
308
378
|
const distances = setDistances(map, targets, n);
|
|
309
379
|
const switches: Switch[] = [];
|
|
310
|
-
|
|
380
|
+
const groups = competing(map.alternates);
|
|
381
|
+
for (const group of groups) {
|
|
382
|
+
const layer = groupLayering(group, map, slots);
|
|
311
383
|
const pick: Array<AlternateSet | null> = [];
|
|
312
384
|
for (let i = 0; i < n; i++) {
|
|
313
385
|
// A set with a `frames` window shows only inside it, and there it is chosen
|
|
@@ -328,22 +400,25 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
328
400
|
}
|
|
329
401
|
pick.push(best);
|
|
330
402
|
}
|
|
403
|
+
const bridgeSame = (arr: Array<AlternateSet | null>) => {
|
|
404
|
+
for (let i = 0; i < n;) {
|
|
405
|
+
const a = arr[i];
|
|
406
|
+
let j = i;
|
|
407
|
+
while (j + 1 < n && arr[j + 1] === a) j++;
|
|
408
|
+
const before = i > 0 ? arr[i - 1] : null;
|
|
409
|
+
const after = j + 1 < n ? arr[j + 1] : null;
|
|
410
|
+
if (before !== null && before !== undefined && before === after && a !== before) {
|
|
411
|
+
const gap = times[j + 1]! - times[i]!;
|
|
412
|
+
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) arr[k] = before;
|
|
413
|
+
}
|
|
414
|
+
i = j + 1;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
331
417
|
// A gap shorter than the minimum span between two runs of one set is that set
|
|
332
418
|
// too: the base art flashing for a frame between them is the flicker the
|
|
333
419
|
// minimum span exists to prevent (measured on the dancer: one-frame breaks at
|
|
334
420
|
// a blurred frame inside a span of one set).
|
|
335
|
-
|
|
336
|
-
const a = pick[i];
|
|
337
|
-
let j = i;
|
|
338
|
-
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
339
|
-
const before = i > 0 ? pick[i - 1] : null;
|
|
340
|
-
const after = j + 1 < n ? pick[j + 1] : null;
|
|
341
|
-
if (before !== null && before !== undefined && before === after && a !== before) {
|
|
342
|
-
const gap = times[j + 1]! - times[i]!;
|
|
343
|
-
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = before;
|
|
344
|
-
}
|
|
345
|
-
i = j + 1;
|
|
346
|
-
}
|
|
421
|
+
bridgeSame(pick);
|
|
347
422
|
// A run shorter than the minimum span shows the base art.
|
|
348
423
|
for (let i = 0; i < n;) {
|
|
349
424
|
const a = pick[i];
|
|
@@ -353,6 +428,31 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
353
428
|
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
|
|
354
429
|
i = j + 1;
|
|
355
430
|
}
|
|
431
|
+
// #136: a short front-set window (< 2 × switchMin) between two behind-set windows is dropped.
|
|
432
|
+
if (layer.torsoRank !== null) {
|
|
433
|
+
let dropped = false;
|
|
434
|
+
for (let i = 0; i < n;) {
|
|
435
|
+
const a = pick[i] ?? null;
|
|
436
|
+
let j = i;
|
|
437
|
+
while (j + 1 < n && (pick[j + 1] ?? null) === a) j++;
|
|
438
|
+
if (a !== null && layer.isFront(a)) {
|
|
439
|
+
let p = i - 1;
|
|
440
|
+
while (p >= 0 && (pick[p] ?? null) === null) p--;
|
|
441
|
+
let q = j + 1;
|
|
442
|
+
while (q < n && (pick[q] ?? null) === null) q++;
|
|
443
|
+
const prev = p >= 0 ? (pick[p] ?? null) : null;
|
|
444
|
+
const next = q < n ? (pick[q] ?? null) : null;
|
|
445
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
446
|
+
if (prev !== null && next !== null && layer.isBehind(prev) && layer.isBehind(next) && to - times[i]! < 2 * opts.switchMin - 1e-9) {
|
|
447
|
+
const fill = prev === next && p === i - 1 && q === j + 1 && Array.from({ length: j - i + 1 }, (_, k) => distances.get(prev.name)![i + k]! <= opts.switchTolerance).every(Boolean) ? prev : null;
|
|
448
|
+
for (let k = i; k <= j; k++) pick[k] = fill;
|
|
449
|
+
dropped = true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
i = j + 1;
|
|
453
|
+
}
|
|
454
|
+
if (dropped) bridgeSame(pick);
|
|
455
|
+
}
|
|
356
456
|
// The segments between boundaries, then each boundary moved (#31).
|
|
357
457
|
const vals: Array<AlternateSet | null> = [pick[0] ?? null];
|
|
358
458
|
const pos: number[] = [];
|
|
@@ -398,6 +498,112 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
398
498
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
399
499
|
}
|
|
400
500
|
}
|
|
501
|
+
// #136: when a group's base setup slots sit in front of the torso, an unswitched gap between two
|
|
502
|
+
// behind-the-torso switches keeps each chain on its behind-the-torso slots on the frames where that
|
|
503
|
+
// chain stays within tolerance of the surrounding behind sets (with the same minimum-span bridging),
|
|
504
|
+
// and across a joint behind-to-behind transition where all chains move from one behind set to another.
|
|
505
|
+
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
506
|
+
for (const group of groups) {
|
|
507
|
+
const layer = groupLayering(group, map, slots);
|
|
508
|
+
if (!layer.baseInFront) continue;
|
|
509
|
+
const names = new Set(group.map((a) => a.name));
|
|
510
|
+
const gSwitches = switches.filter((s) => names.has(s.set));
|
|
511
|
+
const groupChains = [...new Set(group.flatMap((a) => a.chains))];
|
|
512
|
+
const cClamp = new Map<string, Array<AlternateSet | null>>();
|
|
513
|
+
for (const cn of groupChains) {
|
|
514
|
+
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
515
|
+
for (const sw of gSwitches) {
|
|
516
|
+
if (!sw.chains.includes(cn)) continue;
|
|
517
|
+
const set = group.find((a) => a.name === sw.set)!;
|
|
518
|
+
if (layer.isBehind(set)) for (let i = sw.frames[0]; i <= sw.frames[1]; i++) arr[i] = set;
|
|
519
|
+
}
|
|
520
|
+
cClamp.set(cn, arr);
|
|
521
|
+
}
|
|
522
|
+
for (let m = 0; m + 1 < gSwitches.length; m++) {
|
|
523
|
+
const sw0 = gSwitches[m]!;
|
|
524
|
+
const sw1 = gSwitches[m + 1]!;
|
|
525
|
+
const set0 = group.find((a) => a.name === sw0.set)!;
|
|
526
|
+
const set1 = group.find((a) => a.name === sw1.set)!;
|
|
527
|
+
if (!layer.isBehind(set0) || !layer.isBehind(set1)) continue;
|
|
528
|
+
const g0 = sw0.frames[1] + 1;
|
|
529
|
+
const g1 = sw1.frames[0] - 1;
|
|
530
|
+
if (g0 > g1) continue;
|
|
531
|
+
const chains = [...new Set([...set0.chains, ...set1.chains])];
|
|
532
|
+
const cPick = new Map<string, Array<AlternateSet | null>>();
|
|
533
|
+
for (const cn of chains) {
|
|
534
|
+
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
535
|
+
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = set0;
|
|
536
|
+
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = set1;
|
|
537
|
+
const d0s = byChain.get(set0.name)?.get(cn);
|
|
538
|
+
const d1s = byChain.get(set1.name)?.get(cn);
|
|
539
|
+
for (let i = g0; i <= g1; i++) {
|
|
540
|
+
const d0 = d0s?.[i] ?? Infinity;
|
|
541
|
+
const d1 = d1s?.[i] ?? Infinity;
|
|
542
|
+
if (Math.min(d0, d1) <= opts.switchTolerance) arr[i] = d0 <= d1 ? set0 : set1;
|
|
543
|
+
}
|
|
544
|
+
for (let i = g0; i <= g1;) {
|
|
545
|
+
const a = arr[i];
|
|
546
|
+
let j = i;
|
|
547
|
+
while (j + 1 <= g1 && arr[j + 1] === a) j++;
|
|
548
|
+
const before = arr[i - 1] ?? null;
|
|
549
|
+
const after = arr[j + 1] ?? null;
|
|
550
|
+
if (before !== null && before === after && a !== before) {
|
|
551
|
+
const gap = times[j + 1]! - times[i]!;
|
|
552
|
+
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) arr[k] = before;
|
|
553
|
+
}
|
|
554
|
+
i = j + 1;
|
|
555
|
+
}
|
|
556
|
+
for (let i = g0; i <= g1;) {
|
|
557
|
+
const a = arr[i];
|
|
558
|
+
let j = i;
|
|
559
|
+
while (j + 1 <= g1 && arr[j + 1] === a) j++;
|
|
560
|
+
if (a !== null && i > g0 && j < g1 && times[j + 1]! - times[i]! < opts.switchMin - 1e-9) {
|
|
561
|
+
for (let k = i; k <= j; k++) arr[k] = null;
|
|
562
|
+
}
|
|
563
|
+
i = j + 1;
|
|
564
|
+
}
|
|
565
|
+
for (let i = g0; i <= g1; i++) {
|
|
566
|
+
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
567
|
+
}
|
|
568
|
+
cPick.set(cn, arr);
|
|
569
|
+
}
|
|
570
|
+
const anyHeldBehind = chains.some((cn) => {
|
|
571
|
+
const arr = cPick.get(cn)!;
|
|
572
|
+
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
573
|
+
return true;
|
|
574
|
+
});
|
|
575
|
+
if (!anyHeldBehind && set0 !== set1) {
|
|
576
|
+
const frontNear = group.filter((a) => layer.isFront(a)).some((a) => {
|
|
577
|
+
for (let i = g0; i <= g1; i++) if (distances.get(a.name)![i]! <= opts.switchTolerance) return true;
|
|
578
|
+
return false;
|
|
579
|
+
});
|
|
580
|
+
if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
581
|
+
}
|
|
582
|
+
for (const s of layer.touched) {
|
|
583
|
+
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
584
|
+
}
|
|
585
|
+
for (let i = g0; i <= g1; i++) {
|
|
586
|
+
for (const [s, cn] of layer.slotChain) {
|
|
587
|
+
const a = cPick.get(cn)?.[i] ?? null;
|
|
588
|
+
if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
|
|
589
|
+
}
|
|
590
|
+
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
|
|
591
|
+
const picked = chains.map((cn) => cPick.get(cn)![i]);
|
|
592
|
+
if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
for (const cn of groupChains) {
|
|
597
|
+
const arr = cClamp.get(cn)!;
|
|
598
|
+
for (let i = 0; i < n;) {
|
|
599
|
+
const a = arr[i];
|
|
600
|
+
let j = i;
|
|
601
|
+
while (j + 1 < n && arr[j + 1] === a) j++;
|
|
602
|
+
if (a !== null) chainHolds.push({ set: a.name, chain: cn, frames: [i, j] });
|
|
603
|
+
i = j + 1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
401
607
|
const slotTracks: SlotTrack[] = [];
|
|
402
608
|
for (const s of slotOrder) {
|
|
403
609
|
const row = shown.get(s);
|
|
@@ -425,11 +631,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
425
631
|
for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
|
|
426
632
|
offsetTracks.push({ bone: b, property: 'translate', keys });
|
|
427
633
|
}
|
|
428
|
-
return { switches, slotTracks, offsetTracks, shown, distances };
|
|
634
|
+
return { switches, slotTracks, offsetTracks, shown, distances, chainHolds };
|
|
429
635
|
}
|
|
430
636
|
|
|
431
637
|
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
432
|
-
export function coveredChains(switches: readonly Switch[], n: number): Map<string, boolean[]> {
|
|
638
|
+
export function coveredChains(switches: readonly Switch[], n: number, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): Map<string, boolean[]> {
|
|
433
639
|
const out = new Map<string, boolean[]>();
|
|
434
640
|
for (const sw of switches) {
|
|
435
641
|
for (const c of sw.chains) {
|
|
@@ -438,6 +644,11 @@ export function coveredChains(switches: readonly Switch[], n: number): Map<strin
|
|
|
438
644
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
|
|
439
645
|
}
|
|
440
646
|
}
|
|
647
|
+
for (const h of chainHolds ?? []) {
|
|
648
|
+
if (!out.has(h.chain)) out.set(h.chain, Array.from({ length: n }, () => false));
|
|
649
|
+
const row = out.get(h.chain)!;
|
|
650
|
+
for (let i = h.frames[0]; i <= h.frames[1]; i++) row[i] = true;
|
|
651
|
+
}
|
|
441
652
|
return out;
|
|
442
653
|
}
|
|
443
654
|
|
|
@@ -606,35 +817,50 @@ export const ART_EASE_STEP = 3;
|
|
|
606
817
|
* own world angle, and keys are differences of world angles, so a forearm
|
|
607
818
|
* under a clamped upper arm keeps its own target.
|
|
608
819
|
*/
|
|
609
|
-
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
|
|
820
|
+
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
821
|
+
const boneChain = new Map<string, string>();
|
|
822
|
+
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
823
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b]: [number, number]) => {
|
|
824
|
+
const limits = set.turn[bone];
|
|
825
|
+
if (limits === undefined) return;
|
|
826
|
+
const drawn = set.drawn[bone]!;
|
|
827
|
+
const target = targets.get(bone)!;
|
|
828
|
+
const n = target.length;
|
|
829
|
+
const delta = (i: number): number => {
|
|
830
|
+
const t = wrap(target[i]! - drawn);
|
|
831
|
+
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
832
|
+
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
833
|
+
};
|
|
834
|
+
const da = delta(a);
|
|
835
|
+
const db = delta(b);
|
|
836
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
837
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
838
|
+
: 0;
|
|
839
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
840
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
841
|
+
: 0;
|
|
842
|
+
for (let i = a; i <= b; i++) {
|
|
843
|
+
const d = delta(i);
|
|
844
|
+
if (d === 0) continue;
|
|
845
|
+
const wIn = i - a < easeIn ? (i - a + 1) / (easeIn + 1) : 1;
|
|
846
|
+
const wOut = b - i < easeOut ? (b - i + 1) / (easeOut + 1) : 1;
|
|
847
|
+
const w = Math.min(wIn, wOut);
|
|
848
|
+
target[i] = wrap(target[i]! + w * d);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const heldChains = new Set((chainHolds ?? []).map((h) => `${h.set}:${h.chain}`));
|
|
610
852
|
for (const sw of switches) {
|
|
611
853
|
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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);
|
|
637
|
-
}
|
|
854
|
+
for (const bone of Object.keys(set.turn)) {
|
|
855
|
+
const cn = boneChain.get(bone);
|
|
856
|
+
if (cn !== undefined && heldChains.has(`${sw.set}:${cn}`)) continue;
|
|
857
|
+
clampSpan(set, bone, sw.frames);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const h of chainHolds ?? []) {
|
|
861
|
+
const set = map.alternates.find((a) => a.name === h.set)!;
|
|
862
|
+
for (const bone of Object.keys(set.turn)) {
|
|
863
|
+
if (boneChain.get(bone) === h.chain) clampSpan(set, bone, h.frames);
|
|
638
864
|
}
|
|
639
865
|
}
|
|
640
866
|
}
|