@rig-c/pose 0.22.2 → 0.24.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 +8 -6
- package/package.json +1 -1
- package/src/face.ts +295 -2
- package/src/map.ts +165 -14
- package/src/retarget.ts +297 -31
- package/src/rigc.ts +29 -2
- package/src/run.ts +34 -5
- package/src/switch.ts +172 -18
- package/src/verify.ts +30 -1
package/src/retarget.ts
CHANGED
|
@@ -74,17 +74,17 @@
|
|
|
74
74
|
import { screenToSpineDegrees } from 'rig-c/transform';
|
|
75
75
|
import type { Body18 } from './joints.ts';
|
|
76
76
|
import { isBody18, VIRTUAL } from './joints.ts';
|
|
77
|
-
import type { Chain, JointMap, Rest, RigSlots } from './map.ts';
|
|
78
|
-
import { aimAngle, aimEnds, descends, partBone, pointParts, rigPoint } from './map.ts';
|
|
77
|
+
import type { Chain, JointMap, Rest, RigSets, RigSlots } from './map.ts';
|
|
78
|
+
import { aimAngle, aimEnds, bodyYawSets, descends, faceYawSets, knownSetsOf, partBone, pointParts, rigPoint } from './map.ts';
|
|
79
79
|
import type { Joint, Travel } from './pose.ts';
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
|
-
import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
-
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
82
|
+
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
+
import { clampArt, coveredChains, isRaised, measureArt, overflow, RAISED_WRIST_EYES, 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
85
|
import { figureScaleOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
|
-
import type { FaceReading } from './face.ts';
|
|
87
|
-
import { readFace } from './face.ts';
|
|
86
|
+
import type { BodyReading, FaceReading, YawSetSwitch } from './face.ts';
|
|
87
|
+
import { readBody, readFace, readFaceAngles, selectYawSets } from './face.ts';
|
|
88
88
|
|
|
89
89
|
const DEG = 180 / Math.PI;
|
|
90
90
|
|
|
@@ -326,6 +326,8 @@ export interface Retarget {
|
|
|
326
326
|
byHip: ByHip[];
|
|
327
327
|
/** With "face" in the map (#138): per-frame yaw/pitch reading and range-compressed mapping. */
|
|
328
328
|
face: FaceReading | null;
|
|
329
|
+
/** Per-frame body yaw read from camera-normalised shoulder and hip width (#173), and optional "body.yaw" dial mapping. */
|
|
330
|
+
body: BodyReading | null;
|
|
329
331
|
/** Continuous per-frame figure scale relative to frame 0 (`#163`, `#161`). */
|
|
330
332
|
figureScale: FigureScaleReading;
|
|
331
333
|
}
|
|
@@ -430,7 +432,7 @@ function pivotOf(rest: Rest, bone: string | null, legs: ReturnType<typeof legsOf
|
|
|
430
432
|
* good, when a hinge's side cannot be read off a straight rest, and — with
|
|
431
433
|
* `bends: 'refuse'` — when a hinge bends the wrong way.
|
|
432
434
|
*/
|
|
433
|
-
export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, times: readonly number[], opts: RetargetOptions, slots: RigSlots = new Map()): Retarget {
|
|
435
|
+
export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, times: readonly number[], opts: RetargetOptions, slots: RigSlots = new Map(), rigSets: RigSets = new Map()): Retarget {
|
|
434
436
|
const p = new Problems();
|
|
435
437
|
const n = frames.length;
|
|
436
438
|
const t0 = times[0]!;
|
|
@@ -462,6 +464,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
462
464
|
needed.add('r_eye');
|
|
463
465
|
needed.add('l_eye');
|
|
464
466
|
}
|
|
467
|
+
if (map.body !== undefined) {
|
|
468
|
+
needed.add('r_shoulder');
|
|
469
|
+
needed.add('l_shoulder');
|
|
470
|
+
}
|
|
465
471
|
const series = new Map<string, Pt[]>();
|
|
466
472
|
const good = new Map<string, boolean[]>();
|
|
467
473
|
const holds: Hold[] = [];
|
|
@@ -722,6 +728,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
722
728
|
});
|
|
723
729
|
};
|
|
724
730
|
const dist2 = (a: Pt, b: Pt): number => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
731
|
+
let fixedCamera = false;
|
|
725
732
|
const rawFigureHeight: number[] = (() => {
|
|
726
733
|
const ar = heldPt('r_ankle');
|
|
727
734
|
const al = heldPt('l_ankle');
|
|
@@ -774,9 +781,15 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
774
781
|
return minR > 1 ? minR : maxR < 1 ? maxR : 1;
|
|
775
782
|
});
|
|
776
783
|
if (cons.every((c) => Math.abs(c - 1) <= 0.02)) {
|
|
784
|
+
fixedCamera = true;
|
|
777
785
|
return cons.map((c) => c * legSum[0]!);
|
|
778
786
|
}
|
|
779
787
|
}
|
|
788
|
+
const top = heldPt('nose') ?? heldPt('neck');
|
|
789
|
+
if (top !== null) {
|
|
790
|
+
const h = ar.map((r, i) => Math.max(r[1], al[i]![1]) - top[i]![1]);
|
|
791
|
+
if (h.every((v) => v > 1e-6)) return h;
|
|
792
|
+
}
|
|
780
793
|
const out = winPercentile(legSum, 1, STRAIGHT_PERCENTILE);
|
|
781
794
|
out[0] = legSum[0]!;
|
|
782
795
|
return out;
|
|
@@ -907,7 +920,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
907
920
|
const lo = supported ? supFloor : limits[0];
|
|
908
921
|
return Math.min(Math.max(r, lo), limits[1]);
|
|
909
922
|
});
|
|
910
|
-
if (effRatio.some((v) => Math.abs(v - 1) > 0.04) && sy.some((v) => Math.abs(r6(v) - 1) > 0)) {
|
|
923
|
+
if ((fixedCamera || (n <= 160 && minR < 0.9)) && effRatio.some((v) => Math.abs(v - 1) > 0.04) && sy.some((v) => Math.abs(r6(v) - 1) > 0)) {
|
|
911
924
|
trunkScaleBone = tb;
|
|
912
925
|
trunkScale = sy;
|
|
913
926
|
}
|
|
@@ -1106,7 +1119,31 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1106
1119
|
p.throwIfAny();
|
|
1107
1120
|
|
|
1108
1121
|
// The switches: with every target known, each set's distance and its spans.
|
|
1109
|
-
const
|
|
1122
|
+
const shortInfo = opts.short !== 'off' ? { cutoff: opts.short, fractions: new Map([...fractions].map(([b, info]) => [b, info.fraction])) } : undefined;
|
|
1123
|
+
const handOffsets = new Map<string, Array<[number, number]>>();
|
|
1124
|
+
{
|
|
1125
|
+
const rs = series.get('r_shoulder');
|
|
1126
|
+
const ls = series.get('l_shoulder');
|
|
1127
|
+
for (const c of map.chains) {
|
|
1128
|
+
if (c.reach || c.joints.length < 2) continue;
|
|
1129
|
+
const sh = series.get(c.joints[0]!);
|
|
1130
|
+
const wr = series.get(c.joints[c.joints.length - 1]!);
|
|
1131
|
+
if (sh === undefined || wr === undefined) continue;
|
|
1132
|
+
const arr: Array<[number, number]> = [];
|
|
1133
|
+
for (let i = 0; i < n; i++) {
|
|
1134
|
+
const sc = rs !== undefined && ls !== undefined
|
|
1135
|
+
? Math.hypot(ls[i]![0] - rs[i]![0], ls[i]![1] - rs[i]![1])
|
|
1136
|
+
: c.joints.slice(0, -1).reduce((s, j, k) => {
|
|
1137
|
+
const p0 = series.get(j)?.[i];
|
|
1138
|
+
const p1 = series.get(c.joints[k + 1]!)?.[i];
|
|
1139
|
+
return p0 !== undefined && p1 !== undefined ? s + Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) : s;
|
|
1140
|
+
}, 0);
|
|
1141
|
+
arr.push(sc > 1e-6 ? [(wr[i]![0] - sh[i]![0]) / sc, (wr[i]![1] - sh[i]![1]) / sc] : [0, 0]);
|
|
1142
|
+
}
|
|
1143
|
+
handOffsets.set(c.name, arr);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets });
|
|
1110
1147
|
|
|
1111
1148
|
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
1112
1149
|
// first, so a bone below takes its clamped parent into its key; a reach is aimed
|
|
@@ -1151,9 +1188,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1151
1188
|
}
|
|
1152
1189
|
p.throwIfAny();
|
|
1153
1190
|
|
|
1154
|
-
// #111: the head chain's nod channel — the eyes' vertical travel against the
|
|
1155
|
-
// neck, measured in the rig's own units
|
|
1156
|
-
// (whose rotate track carries roll only).
|
|
1191
|
+
// #111 / #181: the head chain's nod channel — the eyes' vertical travel against the
|
|
1192
|
+
// neck, measured in the rig's own units against a detrended running median (window ≈ 1 s),
|
|
1193
|
+
// keyed as a translate on the head bone (whose rotate track carries roll only).
|
|
1157
1194
|
for (const c of map.chains) {
|
|
1158
1195
|
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
1159
1196
|
const ja = c.joints[k]!;
|
|
@@ -1184,25 +1221,52 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1184
1221
|
const bone = rest.get(hb)!;
|
|
1185
1222
|
const parent = bone.parent === null ? null : rest.get(bone.parent)!;
|
|
1186
1223
|
let neckRestLen = 0;
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1224
|
+
if (rest.has('neck') && descends(rest, hb, 'neck')) {
|
|
1225
|
+
const nk = rest.get('neck')!;
|
|
1226
|
+
neckRestLen = Math.hypot(bone.worldX - nk.worldX, bone.worldY - nk.worldY);
|
|
1227
|
+
}
|
|
1228
|
+
if (neckRestLen <= 1e-6) {
|
|
1229
|
+
for (let a = parent; a !== null; a = a.parent === null ? null : rest.get(a.parent)!) {
|
|
1230
|
+
const d = Math.hypot(bone.worldX - a.worldX, bone.worldY - a.worldY);
|
|
1231
|
+
if (d > 1e-6) {
|
|
1232
|
+
neckRestLen = d;
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1192
1235
|
}
|
|
1193
1236
|
}
|
|
1194
1237
|
const maxNod = !inPlace && neckRestLen > 1e-6 ? 0.02 * neckRestLen : Infinity;
|
|
1195
|
-
const refEN = rEye.map((re, i) => -((re[1] + lEye[i]![1]) / 2 - neckSeries[i]![1])
|
|
1196
|
-
const
|
|
1238
|
+
const refEN = rEye.map((re, i) => -((re[1] + lEye[i]![1]) / 2 - neckSeries[i]![1]));
|
|
1239
|
+
const halfWin = Math.max(1, Math.round(0.5 / Math.max(1e-6, (keyTimes[n - 1]! - keyTimes[0]!) / Math.max(1, n - 1))));
|
|
1240
|
+
const nodMed = maxNod < Infinity && n > 2 * halfWin
|
|
1241
|
+
? refEN.map((_, i) => median(refEN.slice(Math.max(0, i - halfWin), Math.min(n, i + halfWin + 1))))
|
|
1242
|
+
: refEN.map(() => refEN[0]!);
|
|
1243
|
+
const nodBase = maxNod < Infinity && n > 2 * halfWin
|
|
1244
|
+
? nodMed.map((_, i) => {
|
|
1245
|
+
const sl = nodMed.slice(Math.max(0, i - halfWin), Math.min(n, i + halfWin + 1));
|
|
1246
|
+
return sl.reduce((a, b) => a + b, 0) / sl.length;
|
|
1247
|
+
})
|
|
1248
|
+
: nodMed;
|
|
1249
|
+
const rawNod = refEN.map((v, i) => (v - nodBase[i]!) * nodScale!);
|
|
1250
|
+
const p90Nod = maxNod < Infinity ? percentile(rawNod.map((v) => Math.abs(v)), 0.92) : 0;
|
|
1251
|
+
const kneeNod = 0.82 * maxNod;
|
|
1252
|
+
const dy = rawNod.map((v) => {
|
|
1253
|
+
if (p90Nod > maxNod && Math.abs(v) > kneeNod) {
|
|
1254
|
+
const tail = kneeNod + (Math.abs(v) - kneeNod) * ((maxNod - kneeNod) / (p90Nod - kneeNod));
|
|
1255
|
+
return Math.sign(v) * Math.min(maxNod, tail);
|
|
1256
|
+
}
|
|
1257
|
+
return Math.max(-maxNod, Math.min(maxNod, v));
|
|
1258
|
+
});
|
|
1197
1259
|
if (dy.some((v) => Math.abs(r3(v)) > 0)) {
|
|
1198
1260
|
const keys: MotionKey[] = [];
|
|
1199
1261
|
const nod: Pt[] = [];
|
|
1262
|
+
const ref0 = refEN[0]! / (poseScale?.[0] ?? 1);
|
|
1200
1263
|
for (let i = 0; i < n; i++) {
|
|
1201
1264
|
const wy = dy[i]!;
|
|
1265
|
+
const wy0 = Math.max(-maxNod, Math.min(maxNod, (refEN[i]! / (poseScale?.[i] ?? 1) - ref0) * nodScale!));
|
|
1202
1266
|
let lx = 0;
|
|
1203
1267
|
let ly = r3(wy);
|
|
1204
1268
|
let awx = 0;
|
|
1205
|
-
let awy =
|
|
1269
|
+
let awy = r3(wy0);
|
|
1206
1270
|
if (parent !== null) {
|
|
1207
1271
|
const r = turnAbove(parent.name, i) / DEG;
|
|
1208
1272
|
const pa = Math.cos(r) * parent.a - Math.sin(r) * parent.c;
|
|
@@ -1212,8 +1276,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1212
1276
|
const det = pa * pd - pb * pc;
|
|
1213
1277
|
lx = r3((-pb * wy) / det);
|
|
1214
1278
|
ly = r3((pa * wy) / det);
|
|
1215
|
-
|
|
1216
|
-
|
|
1279
|
+
const lx0 = r3((-pb * wy0) / det);
|
|
1280
|
+
const ly0 = r3((pa * wy0) / det);
|
|
1281
|
+
awx = pa * lx0 + pb * ly0;
|
|
1282
|
+
awy = pc * lx0 + pd * ly0;
|
|
1217
1283
|
}
|
|
1218
1284
|
keys.push({ t: keyTimes[i]!, v: [lx, ly] });
|
|
1219
1285
|
nod.push([awx, awy]);
|
|
@@ -1228,8 +1294,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1228
1294
|
if (headNodTrack !== null) break;
|
|
1229
1295
|
}
|
|
1230
1296
|
|
|
1231
|
-
// --raised-by-head (#62,
|
|
1232
|
-
//
|
|
1297
|
+
// --raised-by-head (#62, #179): while a set shows whose key skeleton's wrist lies within
|
|
1298
|
+
// `RAISED_WRIST_EYES` (2) eye distances of the head centre, its wrist goes where the
|
|
1299
|
+
// reference's is against the head, in eye distances (each side's median over the span). Solved last
|
|
1233
1300
|
// of the bones: every bone above the arm, the head and the trunk's reach included, has its final target
|
|
1234
1301
|
// (range clamp done), so the head and the shoulder are posed from them frame by frame; the arm moves
|
|
1235
1302
|
// neither. A target out of reach is placed at the reach limit along the head-relative direction
|
|
@@ -1252,14 +1319,69 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1252
1319
|
const b = posedPoint(an.points[1], i);
|
|
1253
1320
|
return { centre: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], dist: Math.hypot(b[0] - a[0], b[1] - a[1]) };
|
|
1254
1321
|
};
|
|
1322
|
+
const isChainHeadPlaced = (set: (typeof map.alternates)[number], c: Chain, drawn: number | undefined): boolean => {
|
|
1323
|
+
if (headOk === null) return false;
|
|
1324
|
+
const wJoint = c.joints[c.joints.length - 1];
|
|
1325
|
+
const ea = set.pose[headOk.joints[0]];
|
|
1326
|
+
const eb = set.pose[headOk.joints[1]];
|
|
1327
|
+
const wr = wJoint !== undefined ? set.pose[wJoint] : undefined;
|
|
1328
|
+
if (ea !== undefined && eb !== undefined && wr !== undefined) {
|
|
1329
|
+
const ed = Math.hypot(eb[0] - ea[0], eb[1] - ea[1]);
|
|
1330
|
+
if (ed > 1e-6) {
|
|
1331
|
+
const hc: Pt = [(ea[0] + eb[0]) / 2, (ea[1] + eb[1]) / 2];
|
|
1332
|
+
return Math.hypot(wr[0] - hc[0], wr[1] - hc[1]) <= RAISED_WRIST_EYES * ed;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (c.bones.length === 2 && !c.reach && !c.aim.some((x) => x.includes('>')) && c.joints.length >= 3) {
|
|
1336
|
+
const ha0 = rigPoint(rest, headOk.points[0]);
|
|
1337
|
+
const hb0 = rigPoint(rest, headOk.points[1]);
|
|
1338
|
+
const S0 = rigPoint(rest, c.bones[0]!);
|
|
1339
|
+
const E0 = rigPoint(rest, c.bones[1]!);
|
|
1340
|
+
const wSpec = c.aim[1] === 'x' ? `${c.bones[1]!}:tip` : c.aim[1]!;
|
|
1341
|
+
const W0 = rigPoint(rest, wSpec);
|
|
1342
|
+
const p0 = set.pose[c.joints[0]!];
|
|
1343
|
+
const p1 = set.pose[c.joints[1]!];
|
|
1344
|
+
const p2 = set.pose[c.joints[2]!];
|
|
1345
|
+
if (ha0 !== null && hb0 !== null && S0 !== null && E0 !== null && W0 !== null && p0 !== undefined && p1 !== undefined && p2 !== undefined) {
|
|
1346
|
+
const rigEye0 = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1347
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
1348
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
1349
|
+
if (rigEye0 > 1e-6) {
|
|
1350
|
+
if (l0 / rigEye0 > 3.5) return drawn !== undefined && isRaised(drawn);
|
|
1351
|
+
const H0: Pt = [(ha0[0] + hb0[0]) / 2, (ha0[1] + hb0[1]) / 2];
|
|
1352
|
+
const k0 = pictureAngle(p0, p1) / DEG;
|
|
1353
|
+
const k1 = pictureAngle(p1, p2) / DEG;
|
|
1354
|
+
const Wx = S0[0] + l0 * Math.cos(k0) + l1 * Math.cos(k1);
|
|
1355
|
+
const Wy = S0[1] + l0 * Math.sin(k0) + l1 * Math.sin(k1);
|
|
1356
|
+
return (
|
|
1357
|
+
Math.hypot(Wx - H0[0], Wy - H0[1]) <= RAISED_WRIST_EYES * rigEye0 ||
|
|
1358
|
+
(Math.abs(Wy - H0[1]) <= RAISED_WRIST_EYES * rigEye0 && Math.hypot(Wx - S0[0], Wy - S0[1]) <= RAISED_WRIST_EYES * rigEye0)
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return drawn !== undefined && isRaised(drawn);
|
|
1364
|
+
};
|
|
1255
1365
|
for (const s of sw.switches) {
|
|
1256
1366
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1257
1367
|
const art: ArtRange[] = [];
|
|
1258
1368
|
for (const cn of s.chains) {
|
|
1259
1369
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
1260
1370
|
const drawn = set.drawn[c.bones[0]!];
|
|
1261
|
-
const up =
|
|
1371
|
+
const up = isChainHeadPlaced(set, c, drawn);
|
|
1262
1372
|
const low = hipsOk !== null && set.place?.chains.includes(c.name) === true;
|
|
1373
|
+
if (!up && !low && headOk !== null && drawn !== undefined && isRaised(drawn) && c.bones.length >= 1 && c.joints.length >= 2) {
|
|
1374
|
+
const p0 = set.pose[c.joints[0]!];
|
|
1375
|
+
const p1 = set.pose[c.joints[1]!];
|
|
1376
|
+
if (p0 !== undefined && p1 !== undefined) {
|
|
1377
|
+
const key0 = pictureAngle(p0, p1);
|
|
1378
|
+
const t0 = targets.get(c.bones[0]!)!;
|
|
1379
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) {
|
|
1380
|
+
const diff = Math.max(-3, Math.min(3, wrap(t0[i]! - key0)));
|
|
1381
|
+
t0[i] = wrap(drawn + diff);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1263
1385
|
if (up && low) {
|
|
1264
1386
|
p.add('PLACE_BOTH', `set "${set.name}" chain "${c.name}": --raised-by-head places its wrist against the head (bone "${c.bones[0]}" drawn at ${drawn}°, raised), and "place": "by-hip" against the hip (its key skeleton's wrist within one hip width of the mid-hip); a wrist is placed against one — drop "place" from the set or the flag`);
|
|
1265
1387
|
continue;
|
|
@@ -1331,13 +1453,47 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1331
1453
|
const w = wristOf(i);
|
|
1332
1454
|
job.reference.push(ref);
|
|
1333
1455
|
job.before.push([(w[0] - h.centre[0]) / rigEye, (w[1] - h.centre[1]) / rigEye]);
|
|
1334
|
-
const target: Pt = [h.centre[0] + ref[0] * rigEye, h.centre[1] + ref[1] * rigEye];
|
|
1335
1456
|
const shoulder = posedOrigin(b0, i);
|
|
1336
|
-
|
|
1337
|
-
|
|
1457
|
+
const target: Pt = [h.centre[0] + ref[0] * rigEye, h.centre[1] + ref[1] * rigEye];
|
|
1458
|
+
let solveTarget: Pt = target;
|
|
1459
|
+
if (up && l0 / rigEye <= 3.5 && (turn0 !== undefined || turn1 !== undefined)) {
|
|
1460
|
+
const side0: 1 | -1 = wrap(t1[i]! + d1 - (t0[i]! + d0)) >= 0 ? 1 : -1;
|
|
1461
|
+
const place0 = reachLimit(h.centre, solveTarget, shoulder, l0, l1);
|
|
1462
|
+
const sol0 = twoBone(shoulder, place0.point, l0, l1, side0);
|
|
1463
|
+
if (sol0.upper !== null) {
|
|
1464
|
+
let a0Cl = wrap(sol0.upper - d0);
|
|
1465
|
+
let a1Cl = wrap(sol0.fore - d1);
|
|
1466
|
+
if (turn0 !== undefined && set.drawn[b0] !== undefined) {
|
|
1467
|
+
const tt0 = wrap(a0Cl - set.drawn[b0]!);
|
|
1468
|
+
const cl0 = Math.min(Math.max(tt0, turn0[0]), turn0[1]);
|
|
1469
|
+
if (Math.abs(cl0 - tt0) > RANGE_SLACK) {
|
|
1470
|
+
a0Cl = wrap(set.drawn[b0]! + cl0);
|
|
1471
|
+
const r0Cl = (a0Cl + d0) / DEG;
|
|
1472
|
+
const ECl: Pt = [shoulder[0] + l0 * Math.cos(r0Cl), shoulder[1] + l0 * Math.sin(r0Cl)];
|
|
1473
|
+
a1Cl = wrap(Math.atan2(place0.point[1] - ECl[1], place0.point[0] - ECl[0]) * DEG - d1);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (turn1 !== undefined && set.drawn[b1] !== undefined) {
|
|
1477
|
+
const asked1 = wrap(a1Cl - set.drawn[b1]!);
|
|
1478
|
+
if (overflow(asked1, turn1) > RANGE_SLACK) {
|
|
1479
|
+
a1Cl = wrap(set.drawn[b1]! + Math.min(Math.max(asked1, turn1[0]), turn1[1]));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const r0Cl = (a0Cl + d0) / DEG;
|
|
1483
|
+
const r1Cl = (a1Cl + d1) / DEG;
|
|
1484
|
+
const WCl: Pt = [shoulder[0] + l0 * Math.cos(r0Cl) + l1 * Math.cos(r1Cl), shoulder[1] + l0 * Math.sin(r0Cl) + l1 * Math.sin(r1Cl)];
|
|
1485
|
+
const miss0 = Math.hypot(place0.point[0] - WCl[0], place0.point[1] - WCl[1]) / rigEye;
|
|
1486
|
+
if (miss0 > 0.86) {
|
|
1487
|
+
const k = (0.86 * rigEye) / Math.hypot(place0.point[0] - WCl[0], place0.point[1] - WCl[1]);
|
|
1488
|
+
solveTarget = [WCl[0] + (place0.point[0] - WCl[0]) * k, WCl[1] + (place0.point[1] - WCl[1]) * k];
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
job.farthest = Math.max(job.farthest, Math.hypot(solveTarget[0] - shoulder[0], solveTarget[1] - shoulder[1]));
|
|
1493
|
+
const place = reachLimit(h.centre, solveTarget, shoulder, l0, l1);
|
|
1338
1494
|
const P = place.point;
|
|
1339
1495
|
job.placed.push([(P[0] - h.centre[0]) / rigEye, (P[1] - h.centre[1]) / rigEye]);
|
|
1340
|
-
if (place.how !== 'in') job.short.push({ i, how: place.how, shortfall: Math.hypot(
|
|
1496
|
+
if (place.how !== 'in') job.short.push({ i, how: place.how, shortfall: Math.hypot(solveTarget[0] - P[0], solveTarget[1] - P[1]) / rigEye });
|
|
1341
1497
|
// The pose's bend side, read on the vectors the two bones carry.
|
|
1342
1498
|
const side: 1 | -1 = wrap(t1[i]! + d1 - (t0[i]! + d0)) >= 0 ? 1 : -1;
|
|
1343
1499
|
const sol = twoBone(shoulder, P, l0, l1, side);
|
|
@@ -1378,7 +1534,14 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1378
1534
|
const r0 = (a0 + d0) / DEG;
|
|
1379
1535
|
const r1 = (a1 + d1) / DEG;
|
|
1380
1536
|
const W: Pt = [shoulder[0] + l0 * Math.cos(r0) + l1 * Math.cos(r1), shoulder[1] + l0 * Math.sin(r0) + l1 * Math.sin(r1)];
|
|
1381
|
-
|
|
1537
|
+
if (up && l0 / rigEye <= 3.5 && Math.hypot(W[0] - P[0], W[1] - P[1]) / rigEye > 0.86) {
|
|
1538
|
+
const k = (0.86 * rigEye) / Math.hypot(W[0] - P[0], W[1] - P[1]);
|
|
1539
|
+
const Pc: Pt = [W[0] + (P[0] - W[0]) * k, W[1] + (P[1] - W[1]) * k];
|
|
1540
|
+
job.placed[job.placed.length - 1] = [(Pc[0] - h.centre[0]) / rigEye, (Pc[1] - h.centre[1]) / rigEye];
|
|
1541
|
+
heldMiss.push(held ? 0.86 : 0);
|
|
1542
|
+
} else {
|
|
1543
|
+
heldMiss.push(held ? Math.hypot(W[0] - P[0], W[1] - P[1]) / rigEye : 0);
|
|
1544
|
+
}
|
|
1382
1545
|
t0[i] = a0;
|
|
1383
1546
|
t1[i] = a1;
|
|
1384
1547
|
}
|
|
@@ -1520,6 +1683,109 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1520
1683
|
targets.set(ax.bone, ax.keys.map((v, i) => wrap(rest0 + (above === null ? 0 : turn(above, i)) + v)));
|
|
1521
1684
|
tracks.push({ bone: ax.bone, property: 'rotate', keys: ax.keys.map((v, i) => ({ t: keyTimes[i]!, v: [v] })) });
|
|
1522
1685
|
}
|
|
1686
|
+
const fSets = faceYawSets(map.face);
|
|
1687
|
+
if (fSets !== undefined) face.sets = selectYawSets(face.read.rawYaw, fSets, keyTimes, opts.switchMin);
|
|
1688
|
+
}
|
|
1689
|
+
let body: BodyReading | null = null;
|
|
1690
|
+
{
|
|
1691
|
+
const rSh = heldPt('r_shoulder');
|
|
1692
|
+
const lSh = heldPt('l_shoulder');
|
|
1693
|
+
if (rSh !== null && lSh !== null) {
|
|
1694
|
+
const rHip = heldPt('r_hip');
|
|
1695
|
+
const lHip = heldPt('l_hip');
|
|
1696
|
+
const neck = heldPt('neck');
|
|
1697
|
+
const faceYaw = face !== null ? face.read.rawYaw : (() => {
|
|
1698
|
+
const nose = heldPt('nose');
|
|
1699
|
+
const rEye = heldPt('r_eye');
|
|
1700
|
+
const lEye = heldPt('l_eye');
|
|
1701
|
+
if (nose === null || rEye === null || lEye === null) return null;
|
|
1702
|
+
return readFaceAngles(nose, rEye, lEye, heldPt('r_ear'), heldPt('l_ear')).yaw;
|
|
1703
|
+
})();
|
|
1704
|
+
body = readBody(map.body, rSh, lSh, rHip, lHip, neck, !inPlace && n > 25 ? continuousScale : null, faceYaw);
|
|
1705
|
+
const bodyAx = body.yaw;
|
|
1706
|
+
if (bodyAx !== undefined) {
|
|
1707
|
+
const rest0 = restAngle(rest, bodyAx.bone, 'x');
|
|
1708
|
+
const above = mappedAbove(bodyAx.bone);
|
|
1709
|
+
targets.set(bodyAx.bone, bodyAx.keys.map((v, i) => wrap(rest0 + (above === null ? 0 : turn(above, i)) + v)));
|
|
1710
|
+
tracks.push({ bone: bodyAx.bone, property: 'rotate', keys: bodyAx.keys.map((v, i) => ({ t: keyTimes[i]!, v: [v] })) });
|
|
1711
|
+
}
|
|
1712
|
+
const bSets = bodyYawSets(map.body);
|
|
1713
|
+
if (bSets !== undefined) body.sets = selectYawSets(body.read.rawYaw, bSets, keyTimes, opts.switchMin);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const activeYawSets: YawSetSwitch[] = [...(face?.sets ?? []), ...(body?.sets ?? [])];
|
|
1717
|
+
if (activeYawSets.some((s) => s.spans.length > 0)) {
|
|
1718
|
+
const known = knownSetsOf(map, rigSets);
|
|
1719
|
+
const slotOrder: string[] = [];
|
|
1720
|
+
for (const tr of sw.slotTracks) if (!slotOrder.includes(tr.slot)) slotOrder.push(tr.slot);
|
|
1721
|
+
for (const s of activeYawSets) {
|
|
1722
|
+
const def = known.get(s.set);
|
|
1723
|
+
if (def === undefined) continue;
|
|
1724
|
+
for (const sl of [...Object.keys(def.slots), ...def.hides]) {
|
|
1725
|
+
if (!slotOrder.includes(sl)) slotOrder.push(sl);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const yawExactAt = new Map<string, Set<number>>();
|
|
1729
|
+
for (const s of activeYawSets) {
|
|
1730
|
+
const def = known.get(s.set);
|
|
1731
|
+
if (def === undefined) continue;
|
|
1732
|
+
const values: Array<[string, string | null]> = [...Object.entries(def.slots), ...def.hides.map((h): [string, null] => [h, null])];
|
|
1733
|
+
for (const [sl, v] of values) {
|
|
1734
|
+
if (!sw.shown.has(sl)) sw.shown.set(sl, Array.from({ length: n }, () => slots.get(sl)?.setup ?? null));
|
|
1735
|
+
const row = sw.shown.get(sl)!;
|
|
1736
|
+
for (const [lo, hi] of s.spans) {
|
|
1737
|
+
for (let i = lo; i <= hi; i++) row[i] = v;
|
|
1738
|
+
const exact = yawExactAt.get(sl) ?? new Set<number>();
|
|
1739
|
+
exact.add(lo);
|
|
1740
|
+
if (hi + 1 < n) exact.add(hi + 1);
|
|
1741
|
+
yawExactAt.set(sl, exact);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const rebuilt: SlotTrack[] = [];
|
|
1746
|
+
for (const sl of slotOrder) {
|
|
1747
|
+
const row = sw.shown.get(sl);
|
|
1748
|
+
if (row === undefined) continue;
|
|
1749
|
+
let prev = slots.get(sl)?.setup ?? null;
|
|
1750
|
+
const keys: AttachmentKey[] = [];
|
|
1751
|
+
const exact = yawExactAt.get(sl);
|
|
1752
|
+
row.forEach((v, i) => {
|
|
1753
|
+
if (v === prev) return;
|
|
1754
|
+
const t = i === 0 ? 0 : exact?.has(i) === true ? keyTimes[i]! : r6((keyTimes[i - 1]! + keyTimes[i]!) / 2);
|
|
1755
|
+
keys.push({ t, v });
|
|
1756
|
+
prev = v;
|
|
1757
|
+
});
|
|
1758
|
+
if (keys.length > 0) rebuilt.push({ slot: sl, property: 'attachment', keys });
|
|
1759
|
+
}
|
|
1760
|
+
sw.slotTracks.splice(0, sw.slotTracks.length, ...rebuilt);
|
|
1761
|
+
}
|
|
1762
|
+
if (activeYawSets.length > 0) {
|
|
1763
|
+
const known = knownSetsOf(map, rigSets);
|
|
1764
|
+
const otherSlotKeyers = new Map<string, string[]>();
|
|
1765
|
+
const addKeyer = (sl: string, who: string) => {
|
|
1766
|
+
const list = otherSlotKeyers.get(sl) ?? [];
|
|
1767
|
+
if (!list.includes(who)) list.push(who);
|
|
1768
|
+
otherSlotKeyers.set(sl, list);
|
|
1769
|
+
};
|
|
1770
|
+
for (const aSw of sw.switches) {
|
|
1771
|
+
for (const sl of [...Object.keys(aSw.slots), ...aSw.hides]) addKeyer(sl, `set "${aSw.set}"`);
|
|
1772
|
+
}
|
|
1773
|
+
for (const s of activeYawSets) {
|
|
1774
|
+
if (s.spans.length === 0) continue;
|
|
1775
|
+
const def = known.get(s.set);
|
|
1776
|
+
if (def === undefined) continue;
|
|
1777
|
+
for (const sl of [...Object.keys(def.slots), ...def.hides]) addKeyer(sl, `set "${s.set}"`);
|
|
1778
|
+
}
|
|
1779
|
+
for (const s of activeYawSets) {
|
|
1780
|
+
const def = known.get(s.set);
|
|
1781
|
+
if (def === undefined) continue;
|
|
1782
|
+
for (const h of def.hides) {
|
|
1783
|
+
const keyers = (otherSlotKeyers.get(h) ?? []).filter((who) => who !== `set "${s.set}"`);
|
|
1784
|
+
if (keyers.length > 0) {
|
|
1785
|
+
s.hidesKeyed.push({ slot: h, by: `slot "${h}" attachment (${keyers.join(', ')})` });
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1523
1789
|
}
|
|
1524
1790
|
// Scale keys: a scaled leg segment's own scale, or the scale that lands a bone under one at its asked length.
|
|
1525
1791
|
for (const c of map.chains) {
|
|
@@ -1539,7 +1805,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1539
1805
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
1540
1806
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
1541
1807
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
1542
|
-
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip, face, figureScale,
|
|
1808
|
+
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, closure, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised, byHip, face, body, figureScale,
|
|
1543
1809
|
};
|
|
1544
1810
|
}
|
|
1545
1811
|
|
package/src/rigc.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
|
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { poseRawSetup, readModel, type CompiledDocument } from 'rig-c/core';
|
|
16
|
-
import type { InheritMode, Rest, RestBone, RigSlot, RigSlots } from './map.ts';
|
|
16
|
+
import type { InheritMode, Rest, RestBone, RigSet, RigSets, RigSlot, RigSlots } from './map.ts';
|
|
17
17
|
import { isObject, parseJson, Problems, Refusal, typeName } from './problems.ts';
|
|
18
18
|
|
|
19
19
|
export interface RigInput {
|
|
@@ -23,6 +23,32 @@ export interface RigInput {
|
|
|
23
23
|
name: string;
|
|
24
24
|
/** The motion spec the rig is built with, or null when it has none. */
|
|
25
25
|
motion: string | null;
|
|
26
|
+
/** Any alternate sets declared in `sets.json` (`rig-parts-sets/1`) beside `rig.json`. */
|
|
27
|
+
sets: RigSets;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readRigSets(path: string): RigSets {
|
|
31
|
+
const out = new Map<string, RigSet>();
|
|
32
|
+
if (!existsSync(path)) return out;
|
|
33
|
+
const doc = parseJson(readFileSync(path, 'utf8'), path, 'SETS_JSON');
|
|
34
|
+
if (!isObject(doc) || (doc.spec !== 'rig-parts-sets/1' && doc.spec !== 'spine-parts-sets/1') || (!isObject(doc.sets) && !Array.isArray(doc.sets))) {
|
|
35
|
+
throw new Refusal([{ code: 'SETS_SPEC', text: `${path} is not a rig-parts-sets/1 spec with a "sets" object` }]);
|
|
36
|
+
}
|
|
37
|
+
const addSet = (name: string, s: unknown) => {
|
|
38
|
+
if (!isObject(s) || !isObject(s.slots)) return;
|
|
39
|
+
const slots: Record<string, string> = {};
|
|
40
|
+
for (const [k, v] of Object.entries(s.slots)) if (typeof v === 'string') slots[k] = v;
|
|
41
|
+
const hides = Array.isArray(s.hides) ? s.hides.filter((h): h is string => typeof h === 'string') : [];
|
|
42
|
+
out.set(name, { slots, hides });
|
|
43
|
+
};
|
|
44
|
+
if (Array.isArray(doc.sets)) {
|
|
45
|
+
for (const s of doc.sets) {
|
|
46
|
+
if (isObject(s) && typeof s.name === 'string') addSet(s.name, s);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
for (const [name, s] of Object.entries(doc.sets)) addSet(name, s);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
/**
|
|
@@ -49,7 +75,8 @@ export function resolveRig(path: string, baseMotion: string | undefined): RigInp
|
|
|
49
75
|
if (!isObject(doc) || doc.spec !== 'rigc-rig/1' || typeof doc.name !== 'string') {
|
|
50
76
|
throw new Refusal([{ code: 'RIG_SPEC', text: `${rig} is not a rig spec: "spec" is ${isObject(doc) ? typeName(doc.spec) : typeName(doc)} and "name" ${isObject(doc) ? typeName(doc.name) : '-'}; it must be "rigc-rig/1" with a name` }]);
|
|
51
77
|
}
|
|
52
|
-
|
|
78
|
+
const sets = readRigSets(join(dirname(rig), 'sets.json'));
|
|
79
|
+
return { rig, name: doc.name, motion, sets };
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
/** The motion spec the rig is built with, or an empty one under its name. */
|
package/src/run.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
|
-
import { readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
10
|
+
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
11
|
import { cleanLine, inferredSource } from './clean.ts';
|
|
12
12
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
13
13
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
@@ -19,7 +19,7 @@ import { landed, measureFeet, measureLanding, measurePosture, measureShear, pose
|
|
|
19
19
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
20
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
21
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
22
|
-
import { faceLine } from './face.ts';
|
|
22
|
+
import { bodyLine, faceLine } from './face.ts';
|
|
23
23
|
|
|
24
24
|
export interface RetargetArgs extends RetargetOptions {
|
|
25
25
|
rig: string;
|
|
@@ -108,9 +108,25 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
108
108
|
const rigc = rigcVersion();
|
|
109
109
|
const rest = restOf(doc0);
|
|
110
110
|
const slots = slotsOf(doc0);
|
|
111
|
-
resolveMap(map, rest, a.map, slots);
|
|
111
|
+
resolveMap(map, rest, a.map, slots, input.sets);
|
|
112
112
|
|
|
113
|
-
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}) }, slots);
|
|
113
|
+
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}) }, slots, input.sets);
|
|
114
|
+
if (fragment.length > 0 && ((r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
|
|
115
|
+
const known = knownSetsOf(map, input.sets);
|
|
116
|
+
for (const s of [...(r.face?.sets ?? []), ...(r.body?.sets ?? [])]) {
|
|
117
|
+
const def = known.get(s.set);
|
|
118
|
+
if (def === undefined) continue;
|
|
119
|
+
for (const h of def.hides) {
|
|
120
|
+
for (const tr of fragment) {
|
|
121
|
+
const tgt = targetOf(tr);
|
|
122
|
+
if (tgt?.kind === 'slot' && tgt.name === h) {
|
|
123
|
+
const by = `slot "${h}" ${tr.property} (--merge ${a.merge})`;
|
|
124
|
+
if (!s.hidesKeyed.some((x) => x.slot === h && x.by === by)) s.hidesKeyed.push({ slot: h, by });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
114
130
|
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
115
131
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
116
132
|
say(` CLEAN ${cleanLine(seq.cleaned!)}`);
|
|
@@ -161,6 +177,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
161
177
|
}
|
|
162
178
|
if (r.stance !== null) for (const l of stanceLines(r.stance, r.times)) say(` STANCE ${l}`);
|
|
163
179
|
if (r.face !== null) say(` FACE ${faceLine(r.face)}`);
|
|
180
|
+
if (r.body !== null) say(` BODY ${bodyLine(r.body)}`);
|
|
164
181
|
|
|
165
182
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
166
183
|
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip);
|
|
@@ -192,6 +209,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
192
209
|
...(map.translate === null ? [] : [map.translate.bone]),
|
|
193
210
|
...(map.face?.yaw === undefined ? [] : [map.face.yaw.bone]),
|
|
194
211
|
...(map.face?.pitch === undefined ? [] : [map.face.pitch.bone]),
|
|
212
|
+
...(map.body?.yaw === undefined ? [] : [map.body.yaw.bone]),
|
|
195
213
|
];
|
|
196
214
|
const below = new Map<string, string[]>();
|
|
197
215
|
for (const m of new Set(mapped)) {
|
|
@@ -283,7 +301,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
283
301
|
else if (f.meshes.length === 0) say(` FOLD rigc render --folds at ${fps} fps: no mesh folds`);
|
|
284
302
|
else for (const m of f.meshes) say(` FOLD mesh "${m.attachment}" (slot "${m.slot}") folds on ${m.times.length} frame(s) at ${fps} fps: t=${spans(m.times, 1 / fps)}`);
|
|
285
303
|
const uncovered = f.meshes === null ? null : uncoveredFolds(f.meshes, r.shown, r.times, slots, (slot, att) => chainsMoving(attachmentBones(doc, slot, att), rest, map));
|
|
286
|
-
if (uncovered !== null && map.alternates.length > 0) {
|
|
304
|
+
if (uncovered !== null && (map.alternates.length > 0 || (r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
|
|
287
305
|
for (const u of uncovered) say(` UNCOVERED mesh "${u.attachment}" (slot "${u.slot}"; chains ${u.chains.length === 0 ? 'none' : u.chains.join(', ')}) folds with no switch covering it on ${u.times.length} frame(s): t=${spans(u.times, 1 / fps)}`);
|
|
288
306
|
}
|
|
289
307
|
|
|
@@ -363,8 +381,19 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
363
381
|
read: { yaw: r.face.read.yaw, pitch: r.face.read.pitch },
|
|
364
382
|
...(r.face.yaw === undefined ? {} : { yaw: { bone: r.face.yaw.bone, limits: r.face.yaw.limits, knee: r.face.yaw.knee, read: r.face.yaw.read, mapped: r.face.yaw.mapped, scale: r.face.yaw.scale, corr: r.face.yaw.corr } }),
|
|
365
383
|
...(r.face.pitch === undefined ? {} : { pitch: { bone: r.face.pitch.bone, limits: r.face.pitch.limits, knee: r.face.pitch.knee, read: r.face.pitch.read, mapped: r.face.pitch.mapped, scale: r.face.pitch.scale, corr: r.face.pitch.corr } }),
|
|
384
|
+
...(r.face.sets === undefined ? {} : { sets: r.face.sets }),
|
|
366
385
|
},
|
|
367
386
|
}),
|
|
387
|
+
body: r.body === null
|
|
388
|
+
? null
|
|
389
|
+
: {
|
|
390
|
+
standing: r.body.standing,
|
|
391
|
+
read: { yaw: r.body.read.yaw, opening: r.body.read.opening },
|
|
392
|
+
over45: r.body.over45,
|
|
393
|
+
framesOver45: r.body.framesOver45,
|
|
394
|
+
...(r.body.yaw === undefined ? {} : { yaw: { bone: r.body.yaw.bone, limits: r.body.yaw.limits, knee: r.body.yaw.knee, read: r.body.yaw.read, mapped: r.body.yaw.mapped, scale: r.body.yaw.scale, corr: r.body.yaw.corr } }),
|
|
395
|
+
...(r.body.sets === undefined ? {} : { sets: r.body.sets }),
|
|
396
|
+
},
|
|
368
397
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, ...rest }) => rest),
|
|
369
398
|
pivot: r.pivot,
|
|
370
399
|
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 },
|