@genex-ai/cli-demo 0.87.0-dev.218 → 0.90.0-dev.227
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/dist/index.js +1664 -45
- package/package.json +1 -1
- package/templates/motion/anim-runtime.js +265 -0
- package/templates/motion/ik.js +75 -0
- package/templates/motion/motion.config.json +11 -0
- package/templates/motion/rigs.js +563 -0
- package/templates/motion/sets/jumps.json +1 -0
- package/templates/motion/sets/rifle.json +1 -0
- package/templates/skills/genex-ai-character/SKILL.md +24 -0
- package/templates/skills/genex-ai-character/references/motion.md +145 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +4 -0
- package/templates/skills/genex-threejs-creatures/SKILL.md +5 -0
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +6 -0
package/dist/index.js
CHANGED
|
@@ -402,7 +402,7 @@ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), ver
|
|
|
402
402
|
if (await readSkillsMarker(skillsDir) === version) return false;
|
|
403
403
|
const src = target.full ? templatesDir : path4.join(templatesDir, "skills");
|
|
404
404
|
const dest = target.full ? target.baseDir : skillsDir;
|
|
405
|
-
await copyTemplates(src, dest, { exclude: ["controllers"] });
|
|
405
|
+
await copyTemplates(src, dest, { exclude: ["controllers", "motion"] });
|
|
406
406
|
await pruneRemovedSkills(skillsDir, log);
|
|
407
407
|
await writeSkillsMarker(skillsDir, version);
|
|
408
408
|
return true;
|
|
@@ -1315,7 +1315,7 @@ async function runInit(opts) {
|
|
|
1315
1315
|
const dest = t.full ? t.baseDir : path9.join(t.baseDir, "skills");
|
|
1316
1316
|
const { copied, updated } = await copyTemplates(src, dest, {
|
|
1317
1317
|
force: opts.force,
|
|
1318
|
-
exclude: ["controllers"]
|
|
1318
|
+
exclude: ["controllers", "motion"]
|
|
1319
1319
|
});
|
|
1320
1320
|
await pruneRemovedSkills(path9.join(t.baseDir, "skills"), log);
|
|
1321
1321
|
await writeSkillsMarker(path9.join(t.baseDir, "skills"));
|
|
@@ -2071,9 +2071,9 @@ function measureSeam(png, tolerance = DEFAULT_SEAM_TOLERANCE) {
|
|
|
2071
2071
|
}
|
|
2072
2072
|
baseH = Math.max(baseH / Math.max(1, nH), 1e-6);
|
|
2073
2073
|
baseV = Math.max(baseV / Math.max(1, nV), 1e-6);
|
|
2074
|
-
const
|
|
2075
|
-
const ratioHorizontal =
|
|
2076
|
-
const ratioVertical =
|
|
2074
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
2075
|
+
const ratioHorizontal = round2(seamH / baseH);
|
|
2076
|
+
const ratioVertical = round2(seamV / baseV);
|
|
2077
2077
|
const worstRatio = Math.max(ratioHorizontal, ratioVertical);
|
|
2078
2078
|
return {
|
|
2079
2079
|
width: w,
|
|
@@ -3606,11 +3606,11 @@ async function uiAudit(opts, log) {
|
|
|
3606
3606
|
return;
|
|
3607
3607
|
}
|
|
3608
3608
|
const hard = findings.filter((finding) => HARD_AUDIT_KINDS.has(finding.kind));
|
|
3609
|
-
const
|
|
3609
|
+
const soft2 = findings.filter((finding) => !HARD_AUDIT_KINDS.has(finding.kind));
|
|
3610
3610
|
for (const finding of hard) log.error(`[${finding.kind}] ${finding.message}`);
|
|
3611
|
-
if (
|
|
3612
|
-
log.warn(`${
|
|
3613
|
-
for (const finding of
|
|
3611
|
+
if (soft2.length > 0) {
|
|
3612
|
+
log.warn(`${soft2.length} finding(s). These are heuristics \u2014 check each one; some may be fine:`);
|
|
3613
|
+
for (const finding of soft2) log.warn(` [${finding.kind}] ${finding.message}`);
|
|
3614
3614
|
}
|
|
3615
3615
|
if (opts.strict || hard.length > 0) process.exitCode = 1;
|
|
3616
3616
|
}
|
|
@@ -4259,6 +4259,8 @@ var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
4259
4259
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
4260
4260
|
video: 15 * 60 * 1e3,
|
|
4261
4261
|
// 15 min
|
|
4262
|
+
motion: 20 * 60 * 1e3,
|
|
4263
|
+
// 8 takes ≈ 8-9 GPU min + a serverless cold start
|
|
4262
4264
|
character_concept: 15 * 60 * 1e3,
|
|
4263
4265
|
character_preview: 30 * 60 * 1e3,
|
|
4264
4266
|
character: 30 * 60 * 1e3,
|
|
@@ -4374,6 +4376,7 @@ function printHint(kind, view, files, log) {
|
|
|
4374
4376
|
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
4375
4377
|
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
|
|
4376
4378
|
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
|
|
4379
|
+
motion: `Raw ARDY takes + verify report \u2014 download the takes, rename them meaningfully (routing is by filename), then: genex motion verify \u2192 compile \u2192 install. Full procedure: the genex-ai-character skill's motion reference.`,
|
|
4377
4380
|
character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
|
|
4378
4381
|
character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
|
|
4379
4382
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
@@ -4724,6 +4727,17 @@ async function deployGame(ctx, opts, log) {
|
|
|
4724
4727
|
log.error(`No index.html in ${rel} \u2014 a game needs one to serve. Nothing deployed.`);
|
|
4725
4728
|
return false;
|
|
4726
4729
|
}
|
|
4730
|
+
const lfsPointers = files.filter(
|
|
4731
|
+
(f) => f.bytes.length < 1024 && f.bytes.subarray(0, 64).toString("latin1").startsWith("version https://git-lfs.github.com/spec/v")
|
|
4732
|
+
);
|
|
4733
|
+
if (lfsPointers.length > 0) {
|
|
4734
|
+
log.error(
|
|
4735
|
+
`${lfsPointers.length} file${lfsPointers.length > 1 ? "s" : ""} in ${rel} ${lfsPointers.length > 1 ? "are" : "is"} an unresolved Git LFS pointer, not the real asset \u2014 the game would fail to load at runtime. Run \`git lfs pull\` and rebuild. Nothing deployed.`
|
|
4736
|
+
);
|
|
4737
|
+
for (const p of lfsPointers.slice(0, 8)) log.dim(` ${p.relPath}`);
|
|
4738
|
+
if (lfsPointers.length > 8) log.dim(` \u2026and ${lfsPointers.length - 8} more`);
|
|
4739
|
+
return false;
|
|
4740
|
+
}
|
|
4727
4741
|
printMobilePreflight(files, log);
|
|
4728
4742
|
await printUiAuditPreflight(log);
|
|
4729
4743
|
await printPipelineStatePreflight(log);
|
|
@@ -14986,17 +15000,17 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
14986
15000
|
if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
|
|
14987
15001
|
if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
|
|
14988
15002
|
const matched = [];
|
|
14989
|
-
let
|
|
15003
|
+
let score2 = 0;
|
|
14990
15004
|
if (String(entry.actionId) === rawQuery) {
|
|
14991
|
-
|
|
15005
|
+
score2 += 1e5;
|
|
14992
15006
|
matched.push("action id");
|
|
14993
15007
|
}
|
|
14994
15008
|
if (entry.key.toLowerCase() === rawQuery) {
|
|
14995
|
-
|
|
15009
|
+
score2 += 8e4;
|
|
14996
15010
|
matched.push("stable key");
|
|
14997
15011
|
}
|
|
14998
15012
|
if (entry.name.toLowerCase() === rawQuery) {
|
|
14999
|
-
|
|
15013
|
+
score2 += 6e4;
|
|
15000
15014
|
matched.push("exact name");
|
|
15001
15015
|
}
|
|
15002
15016
|
const haystack = new Set(tokens([
|
|
@@ -15014,13 +15028,13 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
15014
15028
|
coveredGroups++;
|
|
15015
15029
|
const original = queryTokens[index];
|
|
15016
15030
|
const best = hits.includes(original) ? original : hits[0];
|
|
15017
|
-
|
|
15031
|
+
score2 += best === original ? 400 : 140;
|
|
15018
15032
|
matched.push(best);
|
|
15019
15033
|
}
|
|
15020
|
-
if (entry.inPlace && (expanded.has("walk") || expanded.has("run")))
|
|
15021
|
-
const exact =
|
|
15022
|
-
if (
|
|
15023
|
-
results.push({ entry, score, matched: [...new Set(matched)] });
|
|
15034
|
+
if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score2 += 35;
|
|
15035
|
+
const exact = score2 >= 6e4;
|
|
15036
|
+
if (score2 > 0 && (exact || coveredGroups === groups.length)) {
|
|
15037
|
+
results.push({ entry, score: score2, matched: [...new Set(matched)] });
|
|
15024
15038
|
}
|
|
15025
15039
|
}
|
|
15026
15040
|
return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
|
|
@@ -15604,9 +15618,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
15604
15618
|
`
|
|
15605
15619
|
);
|
|
15606
15620
|
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
15607
|
-
const
|
|
15608
|
-
if (typeof
|
|
15609
|
-
args.log.success(`Meshy controller pack ${
|
|
15621
|
+
const pack2 = manifest.controllerPack;
|
|
15622
|
+
if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
|
|
15623
|
+
args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
|
|
15610
15624
|
} else {
|
|
15611
15625
|
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
|
|
15612
15626
|
}
|
|
@@ -15667,37 +15681,37 @@ function incompletePack(message) {
|
|
|
15667
15681
|
throw new Error(`Incomplete Meshy controller pack: ${message}`);
|
|
15668
15682
|
}
|
|
15669
15683
|
function assertCompleteMeshyControllerPack(manifest) {
|
|
15670
|
-
const
|
|
15671
|
-
if (
|
|
15672
|
-
if (typeof
|
|
15684
|
+
const pack2 = manifest.controllerPack;
|
|
15685
|
+
if (pack2 === void 0) return;
|
|
15686
|
+
if (typeof pack2.key !== "string" || pack2.key.length === 0 || typeof pack2.version !== "number" || !Number.isInteger(pack2.version)) {
|
|
15673
15687
|
incompletePack("invalid key or version.");
|
|
15674
15688
|
}
|
|
15675
|
-
if (typeof
|
|
15676
|
-
incompletePack(`${
|
|
15689
|
+
if (typeof pack2.fingerprint !== "string" || pack2.fingerprint.length === 0) {
|
|
15690
|
+
incompletePack(`${pack2.key} v${pack2.version} has no immutable fingerprint.`);
|
|
15677
15691
|
}
|
|
15678
|
-
if (!Array.isArray(
|
|
15679
|
-
incompletePack(`${
|
|
15692
|
+
if (!Array.isArray(pack2.actionIds) || pack2.actionIds.length === 0 || pack2.actionIds.some((actionId) => typeof actionId !== "number" || !Number.isInteger(actionId))) {
|
|
15693
|
+
incompletePack(`${pack2.key} v${pack2.version} has no valid action snapshot.`);
|
|
15680
15694
|
}
|
|
15681
|
-
const actionIds =
|
|
15695
|
+
const actionIds = pack2.actionIds;
|
|
15682
15696
|
const packActionIds = new Set(actionIds);
|
|
15683
15697
|
if (packActionIds.size !== actionIds.length) {
|
|
15684
|
-
incompletePack(`${
|
|
15698
|
+
incompletePack(`${pack2.key} v${pack2.version} repeats an action ID.`);
|
|
15685
15699
|
}
|
|
15686
15700
|
const bindings = manifest.locomotion?.bindings;
|
|
15687
|
-
if (!bindings) incompletePack(`${
|
|
15701
|
+
if (!bindings) incompletePack(`${pack2.key} v${pack2.version} has no authoritative locomotion bindings.`);
|
|
15688
15702
|
const missingSlots = REQUIRED_MESHY_CONTROLLER_SLOTS.filter((slot) => bindings[slot] === void 0);
|
|
15689
15703
|
if (missingSlots.length > 0) {
|
|
15690
|
-
incompletePack(`${
|
|
15704
|
+
incompletePack(`${pack2.key} v${pack2.version} is missing ${missingSlots.join(", ")}.`);
|
|
15691
15705
|
}
|
|
15692
15706
|
const clips = manifest.clips ?? [];
|
|
15693
15707
|
const boundActionIds = /* @__PURE__ */ new Set();
|
|
15694
15708
|
const snapshotBindings = {};
|
|
15695
15709
|
for (const [slot, binding] of Object.entries(bindings)) {
|
|
15696
15710
|
if (typeof binding.actionId !== "number" || !Number.isInteger(binding.actionId) || typeof binding.clip !== "string" || binding.clip.length === 0 || binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
15697
|
-
incompletePack(`${
|
|
15711
|
+
incompletePack(`${pack2.key} v${pack2.version} has an invalid ${slot} binding.`);
|
|
15698
15712
|
}
|
|
15699
15713
|
if (binding.phase !== void 0 && (typeof binding.phase !== "number" || !Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
15700
|
-
incompletePack(`${
|
|
15714
|
+
incompletePack(`${pack2.key} v${pack2.version} has an invalid ${slot} phase.`);
|
|
15701
15715
|
}
|
|
15702
15716
|
const actionId = binding.actionId;
|
|
15703
15717
|
boundActionIds.add(actionId);
|
|
@@ -15716,21 +15730,21 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
15716
15730
|
}
|
|
15717
15731
|
const unboundActionIds = actionIds.filter((actionId) => !boundActionIds.has(actionId));
|
|
15718
15732
|
if (unboundActionIds.length > 0) {
|
|
15719
|
-
incompletePack(`${
|
|
15733
|
+
incompletePack(`${pack2.key} v${pack2.version} action snapshot contains unbound actions ${unboundActionIds.join(", ")}.`);
|
|
15720
15734
|
}
|
|
15721
15735
|
let computedFingerprint;
|
|
15722
15736
|
try {
|
|
15723
15737
|
computedFingerprint = createMeshyControllerPackSnapshot({
|
|
15724
|
-
key:
|
|
15725
|
-
version:
|
|
15738
|
+
key: pack2.key,
|
|
15739
|
+
version: pack2.version,
|
|
15726
15740
|
actionIds,
|
|
15727
15741
|
bindings: snapshotBindings
|
|
15728
15742
|
}).fingerprint;
|
|
15729
15743
|
} catch (error) {
|
|
15730
15744
|
incompletePack(error instanceof Error ? error.message : "the immutable snapshot is invalid.");
|
|
15731
15745
|
}
|
|
15732
|
-
if (computedFingerprint !==
|
|
15733
|
-
incompletePack(`${
|
|
15746
|
+
if (computedFingerprint !== pack2.fingerprint) {
|
|
15747
|
+
incompletePack(`${pack2.key} v${pack2.version} fingerprint does not match its action bindings.`);
|
|
15734
15748
|
}
|
|
15735
15749
|
}
|
|
15736
15750
|
async function installOwnerAvatar(args) {
|
|
@@ -16375,16 +16389,1559 @@ function rank(items, query) {
|
|
|
16375
16389
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
16376
16390
|
return items.map((p) => {
|
|
16377
16391
|
const hay = `${p.title} ${p.description} ${p.categories.join(" ")}`.toLowerCase();
|
|
16378
|
-
let
|
|
16392
|
+
let score2 = 0;
|
|
16379
16393
|
for (const t of terms) {
|
|
16380
|
-
if (p.title.toLowerCase().includes(t))
|
|
16381
|
-
if (p.categories.some((cat) => cat.includes(t)))
|
|
16382
|
-
if (hay.includes(t))
|
|
16394
|
+
if (p.title.toLowerCase().includes(t)) score2 += 3;
|
|
16395
|
+
if (p.categories.some((cat) => cat.includes(t))) score2 += 2;
|
|
16396
|
+
if (hay.includes(t)) score2 += 1;
|
|
16383
16397
|
}
|
|
16384
|
-
return { p, score };
|
|
16398
|
+
return { p, score: score2 };
|
|
16385
16399
|
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || b.p.playsCount - a.p.playsCount).map((x) => x.p);
|
|
16386
16400
|
}
|
|
16387
16401
|
|
|
16402
|
+
// src/commands/motion.ts
|
|
16403
|
+
import fs18 from "fs/promises";
|
|
16404
|
+
import path18 from "path";
|
|
16405
|
+
|
|
16406
|
+
// src/lib/motion/npz.ts
|
|
16407
|
+
import zlib from "zlib";
|
|
16408
|
+
function decodeUcs4(b) {
|
|
16409
|
+
let out = "";
|
|
16410
|
+
for (let i = 0; i + 4 <= b.length; i += 4) {
|
|
16411
|
+
const cp = b.readUInt32LE(i);
|
|
16412
|
+
if (cp === 0) break;
|
|
16413
|
+
out += cp > 1114111 ? "\uFFFD" : String.fromCodePoint(cp);
|
|
16414
|
+
}
|
|
16415
|
+
return out;
|
|
16416
|
+
}
|
|
16417
|
+
function readNpy(b) {
|
|
16418
|
+
if (b.toString("latin1", 1, 6) !== "NUMPY") throw new Error("not an .npy member");
|
|
16419
|
+
const major = b[6];
|
|
16420
|
+
const hlen = major >= 2 ? b.readUInt32LE(8) : b.readUInt16LE(8);
|
|
16421
|
+
const hstart = major >= 2 ? 12 : 10;
|
|
16422
|
+
const header = b.toString("latin1", hstart, hstart + hlen);
|
|
16423
|
+
const descr = /'descr':\s*'([^']+)'/.exec(header)?.[1];
|
|
16424
|
+
const fortran = /'fortran_order':\s*(True|False)/.exec(header)?.[1] === "True";
|
|
16425
|
+
const shapeRaw = /'shape':\s*\(([^)]*)\)/.exec(header)?.[1];
|
|
16426
|
+
if (!descr || shapeRaw === void 0) throw new Error(`unparseable .npy header: ${header}`);
|
|
16427
|
+
if (fortran) throw new Error("fortran-order .npy is unsupported");
|
|
16428
|
+
const shape = shapeRaw.split(",").map((s) => s.trim()).filter(Boolean).map(Number);
|
|
16429
|
+
const data = Buffer.from(b.subarray(hstart + hlen));
|
|
16430
|
+
if (descr === "<f4") return { shape, descr, data: new Float32Array(data.buffer, data.byteOffset, data.length / 4) };
|
|
16431
|
+
if (descr === "<f8") return { shape, descr, data: new Float64Array(data.buffer, data.byteOffset, data.length / 8) };
|
|
16432
|
+
if (descr === "<i8") return { shape, descr, data: new BigInt64Array(data.buffer, data.byteOffset, data.length / 8) };
|
|
16433
|
+
if (descr === "|b1") return { shape, descr, data: new Uint8Array(data.buffer, data.byteOffset, data.length) };
|
|
16434
|
+
if (descr.startsWith("<U")) return { shape, descr, data: decodeUcs4(data) };
|
|
16435
|
+
if (descr === "|O") throw new Error(".npz member is a pickled object array \u2014 regenerate the take with a current ARDY");
|
|
16436
|
+
throw new Error(`unsupported .npy dtype ${descr}`);
|
|
16437
|
+
}
|
|
16438
|
+
function readNpz(buf) {
|
|
16439
|
+
let e = buf.length - 22;
|
|
16440
|
+
while (e >= 0 && buf.readUInt32LE(e) !== 101010256) e--;
|
|
16441
|
+
if (e < 0) throw new Error("not a ZIP/.npz file (no end-of-central-directory)");
|
|
16442
|
+
const count = buf.readUInt16LE(e + 10);
|
|
16443
|
+
let off = buf.readUInt32LE(e + 16);
|
|
16444
|
+
const out = {};
|
|
16445
|
+
for (let i = 0; i < count; i++) {
|
|
16446
|
+
if (buf.readUInt32LE(off) !== 33639248) throw new Error("corrupt ZIP central directory");
|
|
16447
|
+
const method = buf.readUInt16LE(off + 10);
|
|
16448
|
+
const csize = buf.readUInt32LE(off + 20);
|
|
16449
|
+
const nameLen = buf.readUInt16LE(off + 28);
|
|
16450
|
+
const extraLen = buf.readUInt16LE(off + 30);
|
|
16451
|
+
const cmtLen = buf.readUInt16LE(off + 32);
|
|
16452
|
+
const lho = buf.readUInt32LE(off + 42);
|
|
16453
|
+
const name = buf.toString("utf8", off + 46, off + 46 + nameLen);
|
|
16454
|
+
const lNameLen = buf.readUInt16LE(lho + 26);
|
|
16455
|
+
const lExtraLen = buf.readUInt16LE(lho + 28);
|
|
16456
|
+
const dataStart = lho + 30 + lNameLen + lExtraLen;
|
|
16457
|
+
const raw = buf.subarray(dataStart, dataStart + csize);
|
|
16458
|
+
const bytes = method === 8 ? zlib.inflateRawSync(raw) : Buffer.from(raw);
|
|
16459
|
+
if (method !== 0 && method !== 8) throw new Error(`unsupported ZIP method ${method} for ${name}`);
|
|
16460
|
+
out[name.replace(/\.npy$/, "")] = readNpy(bytes);
|
|
16461
|
+
off += 46 + nameLen + extraLen + cmtLen;
|
|
16462
|
+
}
|
|
16463
|
+
return out;
|
|
16464
|
+
}
|
|
16465
|
+
function floats(npz, key) {
|
|
16466
|
+
const m = npz[key];
|
|
16467
|
+
if (!m) throw new Error(`.npz is missing "${key}" \u2014 not an ARDY take?`);
|
|
16468
|
+
const d = m.data;
|
|
16469
|
+
if (d instanceof Float64Array) return { shape: m.shape, a: d };
|
|
16470
|
+
if (d instanceof Float32Array) return { shape: m.shape, a: Float64Array.from(d) };
|
|
16471
|
+
if (d instanceof Uint8Array && m.descr === "|b1") return { shape: m.shape, a: Float64Array.from(d) };
|
|
16472
|
+
throw new Error(`"${key}" has dtype ${m.descr}, expected floats`);
|
|
16473
|
+
}
|
|
16474
|
+
|
|
16475
|
+
// src/lib/motion/core27.ts
|
|
16476
|
+
var JOINTS = ["Hips", "Spine", "Spine1", "Spine2", "Spine3", "Neck", "Head", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "RightHandEnd", "RightHandThumb1", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", "LeftHandEnd", "LeftHandThumb1", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase"];
|
|
16477
|
+
var PARENTS = [-1, 0, 1, 2, 3, 4, 5, 4, 7, 8, 9, 10, 10, 4, 13, 14, 15, 16, 16, 0, 19, 20, 21, 0, 23, 24, 25];
|
|
16478
|
+
var REST_OFFSETS = [[0, 0, 0], [0, 0.071, -0.0473], [0, 0.0932, -0.0164], [0, 0.0943, -82e-4], [0, 0.0947, -0], [0, 0.2485, 0.0355], [-0, 0.1282, 0.0226], [-0.032, 0.1728, 0.0533], [-0.1589, -0, 0], [-0.2954, -0, -0], [-0.2327, -0, -0], [-0.0696, -0, 0], [-0.0278, -0.0186, 0.0464], [0.032, 0.1728, 0.0533], [0.1589, -0, -0], [0.2954, 0, 0], [0.2327, -0, 0], [0.0696, 0, 0], [0.0278, -0.0186, 0.0464], [-0.0949, -0.0277, -0], [-0, -0.4121, -0], [0, -0.4561, -0], [0, -0.0585, 0.1607], [0.0949, -0.0277, 0], [-0, -0.4121, 0], [-0, -0.4561, -0], [0, -0.0585, 0.1607]];
|
|
16479
|
+
var REST_POSITIONS = [[0, 0, 0], [0, 0.071, -0.0473], [0, 0.1642, -0.0637], [0, 0.2585, -0.0719], [0, 0.3532, -0.0719], [0, 0.6017, -0.0364], [0, 0.7299, -0.0138], [-0.032, 0.526, -0.0186], [-0.1909, 0.526, -0.0186], [-0.4863, 0.526, -0.0186], [-0.719, 0.526, -0.0186], [-0.7886, 0.526, -0.0186], [-0.7468, 0.5074, 0.0278], [0.032, 0.526, -0.0186], [0.1909, 0.526, -0.0186], [0.4863, 0.526, -0.0186], [0.719, 0.526, -0.0186], [0.7886, 0.526, -0.0186], [0.7468, 0.5074, 0.0278], [-0.0949, -0.0277, 0], [-0.0949, -0.4398, 0], [-0.0949, -0.8959, 0], [-0.0949, -0.9544, 0.1607], [0.0949, -0.0277, 0], [0.0949, -0.4398, 0], [0.0949, -0.8959, 0], [0.0949, -0.9544, 0.1607]];
|
|
16480
|
+
var HIPS_REST_Y = 0.9544;
|
|
16481
|
+
var J = JOINTS.length;
|
|
16482
|
+
|
|
16483
|
+
// src/lib/motion/skeleton.ts
|
|
16484
|
+
var ji = (name) => {
|
|
16485
|
+
const i = JOINTS.indexOf(name);
|
|
16486
|
+
if (i < 0) throw new Error(`unknown CoreSkeleton27 joint ${name}`);
|
|
16487
|
+
return i;
|
|
16488
|
+
};
|
|
16489
|
+
function matMul(a, b) {
|
|
16490
|
+
return [
|
|
16491
|
+
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
|
16492
|
+
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
|
16493
|
+
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
|
16494
|
+
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
|
16495
|
+
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
|
16496
|
+
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
|
16497
|
+
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
|
16498
|
+
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
|
16499
|
+
a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
|
|
16500
|
+
];
|
|
16501
|
+
}
|
|
16502
|
+
function matVec(m, v) {
|
|
16503
|
+
return [
|
|
16504
|
+
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
|
|
16505
|
+
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
|
16506
|
+
m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
|
|
16507
|
+
];
|
|
16508
|
+
}
|
|
16509
|
+
function matT(m) {
|
|
16510
|
+
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
|
|
16511
|
+
}
|
|
16512
|
+
function rotY(a) {
|
|
16513
|
+
const c2 = Math.cos(a), s = Math.sin(a);
|
|
16514
|
+
return [c2, 0, s, 0, 1, 0, -s, 0, c2];
|
|
16515
|
+
}
|
|
16516
|
+
function rotAxis(axis, angle) {
|
|
16517
|
+
const [x, y, z] = [axis[0], axis[1], axis[2]];
|
|
16518
|
+
const c2 = Math.cos(angle), s = Math.sin(angle), t = 1 - c2;
|
|
16519
|
+
return [
|
|
16520
|
+
t * x * x + c2,
|
|
16521
|
+
t * x * y - s * z,
|
|
16522
|
+
t * x * z + s * y,
|
|
16523
|
+
t * x * y + s * z,
|
|
16524
|
+
t * y * y + c2,
|
|
16525
|
+
t * y * z - s * x,
|
|
16526
|
+
t * x * z - s * y,
|
|
16527
|
+
t * y * z + s * x,
|
|
16528
|
+
t * z * z + c2
|
|
16529
|
+
];
|
|
16530
|
+
}
|
|
16531
|
+
function quatFromMat(m) {
|
|
16532
|
+
const tr = m[0] + m[4] + m[8];
|
|
16533
|
+
let x, y, z, w;
|
|
16534
|
+
if (tr > 0) {
|
|
16535
|
+
const s = Math.sqrt(tr + 1) * 2;
|
|
16536
|
+
w = s / 4;
|
|
16537
|
+
x = (m[7] - m[5]) / s;
|
|
16538
|
+
y = (m[2] - m[6]) / s;
|
|
16539
|
+
z = (m[3] - m[1]) / s;
|
|
16540
|
+
} else if (m[0] > m[4] && m[0] > m[8]) {
|
|
16541
|
+
const s = Math.sqrt(1 + m[0] - m[4] - m[8]) * 2;
|
|
16542
|
+
w = (m[7] - m[5]) / s;
|
|
16543
|
+
x = s / 4;
|
|
16544
|
+
y = (m[1] + m[3]) / s;
|
|
16545
|
+
z = (m[2] + m[6]) / s;
|
|
16546
|
+
} else if (m[4] > m[8]) {
|
|
16547
|
+
const s = Math.sqrt(1 + m[4] - m[0] - m[8]) * 2;
|
|
16548
|
+
w = (m[2] - m[6]) / s;
|
|
16549
|
+
x = (m[1] + m[3]) / s;
|
|
16550
|
+
y = s / 4;
|
|
16551
|
+
z = (m[5] + m[7]) / s;
|
|
16552
|
+
} else {
|
|
16553
|
+
const s = Math.sqrt(1 + m[8] - m[0] - m[4]) * 2;
|
|
16554
|
+
w = (m[3] - m[1]) / s;
|
|
16555
|
+
x = (m[2] + m[6]) / s;
|
|
16556
|
+
y = (m[5] + m[7]) / s;
|
|
16557
|
+
z = s / 4;
|
|
16558
|
+
}
|
|
16559
|
+
const n = Math.hypot(x, y, z, w);
|
|
16560
|
+
return [x / n, y / n, z / n, w / n];
|
|
16561
|
+
}
|
|
16562
|
+
function matFromQuat(q) {
|
|
16563
|
+
const [x, y, z, w] = [q[0], q[1], q[2], q[3]];
|
|
16564
|
+
const n = x * x + y * y + z * z + w * w || 1;
|
|
16565
|
+
const s = 2 / n;
|
|
16566
|
+
const xx = x * x * s, yy = y * y * s, zz = z * z * s;
|
|
16567
|
+
const xy = x * y * s, xz = x * z * s, yz = y * z * s;
|
|
16568
|
+
const wx = w * x * s, wy = w * y * s, wz = w * z * s;
|
|
16569
|
+
return [
|
|
16570
|
+
1 - yy - zz,
|
|
16571
|
+
xy - wz,
|
|
16572
|
+
xz + wy,
|
|
16573
|
+
xy + wz,
|
|
16574
|
+
1 - xx - zz,
|
|
16575
|
+
yz - wx,
|
|
16576
|
+
xz - wy,
|
|
16577
|
+
yz + wx,
|
|
16578
|
+
1 - xx - yy
|
|
16579
|
+
];
|
|
16580
|
+
}
|
|
16581
|
+
function quatMul(a, b) {
|
|
16582
|
+
const [ax, ay, az, aw] = [a[0], a[1], a[2], a[3]];
|
|
16583
|
+
const [bx, by, bz, bw] = [b[0], b[1], b[2], b[3]];
|
|
16584
|
+
return [
|
|
16585
|
+
aw * bx + ax * bw + ay * bz - az * by,
|
|
16586
|
+
aw * by - ax * bz + ay * bw + az * bx,
|
|
16587
|
+
aw * bz + ax * by - ay * bx + az * bw,
|
|
16588
|
+
aw * bw - ax * bx - ay * by - az * bz
|
|
16589
|
+
];
|
|
16590
|
+
}
|
|
16591
|
+
var mean = (a) => {
|
|
16592
|
+
let s = 0;
|
|
16593
|
+
for (let i = 0; i < a.length; i++) s += a[i];
|
|
16594
|
+
return a.length ? s / a.length : NaN;
|
|
16595
|
+
};
|
|
16596
|
+
function percentile(values, q) {
|
|
16597
|
+
const a = Array.from(values).sort((x, y) => x - y);
|
|
16598
|
+
if (a.length === 0) return NaN;
|
|
16599
|
+
const pos = (a.length - 1) * q / 100;
|
|
16600
|
+
const lo = Math.floor(pos), hi = Math.ceil(pos);
|
|
16601
|
+
if (lo === hi) return a[lo];
|
|
16602
|
+
return a[lo] * (hi - pos) + a[hi] * (pos - lo);
|
|
16603
|
+
}
|
|
16604
|
+
var median2 = (a) => percentile(a, 50);
|
|
16605
|
+
function std(a) {
|
|
16606
|
+
const m = mean(a);
|
|
16607
|
+
let s = 0;
|
|
16608
|
+
for (let i = 0; i < a.length; i++) s += (a[i] - m) ** 2;
|
|
16609
|
+
return Math.sqrt(a.length ? s / a.length : NaN);
|
|
16610
|
+
}
|
|
16611
|
+
function unwrap(a) {
|
|
16612
|
+
const out = [a[0] ?? 0];
|
|
16613
|
+
for (let i = 1; i < a.length; i++) {
|
|
16614
|
+
let d = a[i] - a[i - 1];
|
|
16615
|
+
d -= 2 * Math.PI * Math.round(d / (2 * Math.PI));
|
|
16616
|
+
out.push(out[i - 1] + d);
|
|
16617
|
+
}
|
|
16618
|
+
return out;
|
|
16619
|
+
}
|
|
16620
|
+
function lstsqLinear(t, y) {
|
|
16621
|
+
const n = t.length;
|
|
16622
|
+
const mt = mean(t), my = mean(y);
|
|
16623
|
+
let sxx = 0, sxy = 0;
|
|
16624
|
+
for (let i = 0; i < n; i++) {
|
|
16625
|
+
sxx += (t[i] - mt) ** 2;
|
|
16626
|
+
sxy += (t[i] - mt) * (y[i] - my);
|
|
16627
|
+
}
|
|
16628
|
+
const slope = sxx > 0 ? sxy / sxx : 0;
|
|
16629
|
+
return { slope, intercept: my - slope * mt };
|
|
16630
|
+
}
|
|
16631
|
+
function loadTake(buf) {
|
|
16632
|
+
const npz = readNpz(buf);
|
|
16633
|
+
const posed = floats(npz, "posed_joints");
|
|
16634
|
+
const rootRaw = floats(npz, "root_positions");
|
|
16635
|
+
const lm = floats(npz, "local_rot_mats");
|
|
16636
|
+
const fcRaw = floats(npz, "foot_contacts");
|
|
16637
|
+
const F = posed.shape[0];
|
|
16638
|
+
if (lm.shape[1] !== J) throw new Error(`take has ${lm.shape[1]} joints, expected ${J}`);
|
|
16639
|
+
const P = [];
|
|
16640
|
+
const root = [];
|
|
16641
|
+
const localMats = [];
|
|
16642
|
+
const quat = [];
|
|
16643
|
+
const heading = [];
|
|
16644
|
+
const fc = [];
|
|
16645
|
+
for (let f = 0; f < F; f++) {
|
|
16646
|
+
const pf = [];
|
|
16647
|
+
for (let j = 0; j < J; j++) {
|
|
16648
|
+
const o = (f * J + j) * 3;
|
|
16649
|
+
pf.push([posed.a[o], posed.a[o + 1], posed.a[o + 2]]);
|
|
16650
|
+
}
|
|
16651
|
+
P.push(pf);
|
|
16652
|
+
root.push([rootRaw.a[f * 3], rootRaw.a[f * 3 + 1], rootRaw.a[f * 3 + 2]]);
|
|
16653
|
+
const mf = [];
|
|
16654
|
+
const qf = [];
|
|
16655
|
+
for (let j = 0; j < J; j++) {
|
|
16656
|
+
const o = (f * J + j) * 9;
|
|
16657
|
+
const m = Array.from(lm.a.subarray(o, o + 9));
|
|
16658
|
+
mf.push(m);
|
|
16659
|
+
qf.push(quatFromMat(m));
|
|
16660
|
+
}
|
|
16661
|
+
localMats.push(mf);
|
|
16662
|
+
quat.push(qf);
|
|
16663
|
+
const m0 = mf[0];
|
|
16664
|
+
heading.push(Math.atan2(m0[2], m0[8]));
|
|
16665
|
+
fc.push([fcRaw.a[f * 4] !== 0, fcRaw.a[f * 4 + 1] !== 0, fcRaw.a[f * 4 + 2] !== 0, fcRaw.a[f * 4 + 3] !== 0]);
|
|
16666
|
+
}
|
|
16667
|
+
for (let j = 0; j < J; j++) {
|
|
16668
|
+
for (let f = 1; f < F; f++) {
|
|
16669
|
+
const a = quat[f][j], b = quat[f - 1][j];
|
|
16670
|
+
if (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0) {
|
|
16671
|
+
quat[f][j] = [-a[0], -a[1], -a[2], -a[3]];
|
|
16672
|
+
}
|
|
16673
|
+
}
|
|
16674
|
+
}
|
|
16675
|
+
let gm = null;
|
|
16676
|
+
if (npz.global_rot_mats) {
|
|
16677
|
+
const g = floats(npz, "global_rot_mats");
|
|
16678
|
+
gm = [];
|
|
16679
|
+
for (let f = 0; f < F; f++) {
|
|
16680
|
+
const gf = [];
|
|
16681
|
+
for (let j = 0; j < J; j++) {
|
|
16682
|
+
const o = (f * J + j) * 9;
|
|
16683
|
+
gf.push(Array.from(g.a.subarray(o, o + 9)));
|
|
16684
|
+
}
|
|
16685
|
+
gm.push(gf);
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16688
|
+
const fpsMember = npz.fps;
|
|
16689
|
+
const fps = fpsMember && typeof fpsMember.data !== "string" ? Number(fpsMember.data[0]) : 20;
|
|
16690
|
+
const text = typeof npz.text?.data === "string" ? npz.text.data : "";
|
|
16691
|
+
return { F, fps, text, P, root, quat, heading, fc, gm, localMats };
|
|
16692
|
+
}
|
|
16693
|
+
function fkRots(localFrame) {
|
|
16694
|
+
const W = new Array(J);
|
|
16695
|
+
for (let j = 0; j < J; j++) {
|
|
16696
|
+
const p = PARENTS[j];
|
|
16697
|
+
W[j] = p < 0 ? localFrame[j] : matMul(W[p], localFrame[j]);
|
|
16698
|
+
}
|
|
16699
|
+
return W;
|
|
16700
|
+
}
|
|
16701
|
+
var I_HIPS = 0;
|
|
16702
|
+
function charFrame(P, heading) {
|
|
16703
|
+
const out = [];
|
|
16704
|
+
for (let f = 0; f < P.length; f++) {
|
|
16705
|
+
const rot = rotY(-heading[f]);
|
|
16706
|
+
const hx = P[f][I_HIPS][0], hz = P[f][I_HIPS][2];
|
|
16707
|
+
const cf = [];
|
|
16708
|
+
for (let j = 0; j < J; j++) {
|
|
16709
|
+
cf.push(matVec(rot, [P[f][j][0] - hx, P[f][j][1], P[f][j][2] - hz]));
|
|
16710
|
+
}
|
|
16711
|
+
out.push(cf);
|
|
16712
|
+
}
|
|
16713
|
+
return out;
|
|
16714
|
+
}
|
|
16715
|
+
function poseDist(quat, a, b) {
|
|
16716
|
+
let s = 0;
|
|
16717
|
+
for (let j = 0; j < J; j++) {
|
|
16718
|
+
const qa = quat[a][j], qb = quat[b][j];
|
|
16719
|
+
const d = Math.min(1, Math.abs(qa[0] * qb[0] + qa[1] * qb[1] + qa[2] * qb[2] + qa[3] * qb[3]));
|
|
16720
|
+
s += 2 * Math.acos(d);
|
|
16721
|
+
}
|
|
16722
|
+
return s / J;
|
|
16723
|
+
}
|
|
16724
|
+
|
|
16725
|
+
// src/lib/motion/normalize.ts
|
|
16726
|
+
var ARM_ROOTS = [ji("RightArm"), ji("LeftArm")];
|
|
16727
|
+
var I_RARM = ji("RightArm");
|
|
16728
|
+
var I_LARM = ji("LeftArm");
|
|
16729
|
+
var NECK_HEAD = [ji("Neck"), ji("Head")];
|
|
16730
|
+
var SPINE_IDX = ["Spine", "Spine1", "Spine2", "Spine3"].map(ji);
|
|
16731
|
+
var I_HIPS2 = ji("Hips");
|
|
16732
|
+
var I_RH = ji("RightHand");
|
|
16733
|
+
var I_LH = ji("LeftHand");
|
|
16734
|
+
var I_HEAD = ji("Head");
|
|
16735
|
+
var deg = Math.PI / 180;
|
|
16736
|
+
function deroot(quatCyc, rootCyc, fps, refHeading, targetHeading = 0) {
|
|
16737
|
+
const F = rootCyc.length;
|
|
16738
|
+
const ts = [], xs = [], zs = [];
|
|
16739
|
+
for (let f = 0; f < F; f++) {
|
|
16740
|
+
ts.push(f / fps);
|
|
16741
|
+
xs.push(rootCyc[f][0]);
|
|
16742
|
+
zs.push(rootCyc[f][2]);
|
|
16743
|
+
}
|
|
16744
|
+
const fx = lstsqLinear(ts, xs), fz = lstsqLinear(ts, zs);
|
|
16745
|
+
for (let f = 0; f < F; f++) {
|
|
16746
|
+
rootCyc[f][0] -= fx.intercept + fx.slope * ts[f];
|
|
16747
|
+
rootCyc[f][2] -= fz.intercept + fz.slope * ts[f];
|
|
16748
|
+
}
|
|
16749
|
+
const theta = targetHeading - refHeading;
|
|
16750
|
+
const rotM = rotY(theta);
|
|
16751
|
+
const half = theta / 2;
|
|
16752
|
+
const rotQ = [0, Math.sin(half), 0, Math.cos(half)];
|
|
16753
|
+
for (let f = 0; f < F; f++) {
|
|
16754
|
+
const r = matVec(rotM, rootCyc[f]);
|
|
16755
|
+
rootCyc[f][0] = r[0];
|
|
16756
|
+
rootCyc[f][2] = r[2];
|
|
16757
|
+
quatCyc[f][0] = quatMul(rotQ, quatCyc[f][0]);
|
|
16758
|
+
}
|
|
16759
|
+
const v = matVec(rotM, [fx.slope, 0, fz.slope]);
|
|
16760
|
+
const speed = Math.hypot(v[0], v[2]);
|
|
16761
|
+
return { speed, dir: speed > 1e-6 ? [v[0] / speed, v[2] / speed] : [0, 0] };
|
|
16762
|
+
}
|
|
16763
|
+
function levelArms(qframe, Pf, pitchDeg) {
|
|
16764
|
+
const ax = [
|
|
16765
|
+
Pf[I_LARM][0] - Pf[I_RARM][0],
|
|
16766
|
+
Pf[I_LARM][1] - Pf[I_RARM][1],
|
|
16767
|
+
Pf[I_LARM][2] - Pf[I_RARM][2]
|
|
16768
|
+
];
|
|
16769
|
+
const n = Math.hypot(...ax) + 1e-9;
|
|
16770
|
+
const Rax = rotAxis([ax[0] / n, ax[1] / n, ax[2] / n], pitchDeg * deg);
|
|
16771
|
+
const local = qframe.map(matFromQuat);
|
|
16772
|
+
const W = fkRots(local);
|
|
16773
|
+
const out = qframe.slice();
|
|
16774
|
+
for (const j of ARM_ROOTS) {
|
|
16775
|
+
const Wp = W[PARENTS[j]];
|
|
16776
|
+
out[j] = quatFromMat(matMul(matT(Wp), matMul(Rax, matMul(Wp, local[j]))));
|
|
16777
|
+
}
|
|
16778
|
+
return out;
|
|
16779
|
+
}
|
|
16780
|
+
function levelHead(qframe, Pf, excessDeg) {
|
|
16781
|
+
const ax = [
|
|
16782
|
+
Pf[I_LARM][0] - Pf[I_RARM][0],
|
|
16783
|
+
Pf[I_LARM][1] - Pf[I_RARM][1],
|
|
16784
|
+
Pf[I_LARM][2] - Pf[I_RARM][2]
|
|
16785
|
+
];
|
|
16786
|
+
const n = Math.hypot(...ax) + 1e-9;
|
|
16787
|
+
const Rax = rotAxis([ax[0] / n, ax[1] / n, ax[2] / n], -excessDeg / 2 * deg);
|
|
16788
|
+
const local = qframe.map(matFromQuat);
|
|
16789
|
+
const W = fkRots(local);
|
|
16790
|
+
const out = qframe.slice();
|
|
16791
|
+
for (const j of NECK_HEAD) {
|
|
16792
|
+
const Wp = W[PARENTS[j]];
|
|
16793
|
+
out[j] = quatFromMat(matMul(matT(Wp), matMul(Rax, matMul(Wp, local[j]))));
|
|
16794
|
+
local[j] = matFromQuat(out[j]);
|
|
16795
|
+
W[j] = matMul(Wp, local[j]);
|
|
16796
|
+
}
|
|
16797
|
+
return out;
|
|
16798
|
+
}
|
|
16799
|
+
function headPitchFrames(localMats, s, e) {
|
|
16800
|
+
const vals = [];
|
|
16801
|
+
for (let f = s; f < e; f++) {
|
|
16802
|
+
const W = fkRots(localMats[f]);
|
|
16803
|
+
const fzY = W[NECK_HEAD[1]][5];
|
|
16804
|
+
vals.push(Math.asin(Math.max(-1, Math.min(1, -fzY))) / deg);
|
|
16805
|
+
}
|
|
16806
|
+
return mean(vals);
|
|
16807
|
+
}
|
|
16808
|
+
function pitched(q, theta) {
|
|
16809
|
+
const out = q.slice();
|
|
16810
|
+
const half = theta / SPINE_IDX.length / 2;
|
|
16811
|
+
const step = [Math.sin(half), 0, 0, Math.cos(half)];
|
|
16812
|
+
for (const si of SPINE_IDX) out[si] = quatMul(step, out[si]);
|
|
16813
|
+
return out;
|
|
16814
|
+
}
|
|
16815
|
+
function yawed(q, theta) {
|
|
16816
|
+
const out = q.slice();
|
|
16817
|
+
const half = theta / SPINE_IDX.length / 2;
|
|
16818
|
+
const step = [0, Math.sin(half), 0, Math.cos(half)];
|
|
16819
|
+
for (const si of SPINE_IDX) out[si] = quatMul(step, out[si]);
|
|
16820
|
+
return out;
|
|
16821
|
+
}
|
|
16822
|
+
function gripLineChar(P, heading) {
|
|
16823
|
+
const yaws = [], pitches = [];
|
|
16824
|
+
for (let f = 0; f < P.length; f++) {
|
|
16825
|
+
const rot = rotY(-heading[f]);
|
|
16826
|
+
const rel = (j) => matVec(rot, [
|
|
16827
|
+
P[f][j][0] - P[f][I_HIPS2][0],
|
|
16828
|
+
P[f][j][1] - P[f][I_HIPS2][1],
|
|
16829
|
+
P[f][j][2] - P[f][I_HIPS2][2]
|
|
16830
|
+
]);
|
|
16831
|
+
const rh = rel(I_RH), lh = rel(I_LH);
|
|
16832
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
16833
|
+
yaws.push(Math.atan2(v[0], Math.max(v[2], 1e-6)) / deg);
|
|
16834
|
+
pitches.push(Math.atan2(v[1], Math.hypot(v[0], v[2]) + 1e-9) / deg);
|
|
16835
|
+
}
|
|
16836
|
+
return { yaws, pitches };
|
|
16837
|
+
}
|
|
16838
|
+
|
|
16839
|
+
// src/lib/motion/verify.ts
|
|
16840
|
+
var I_HIPS3 = ji("Hips");
|
|
16841
|
+
var I_HEAD2 = ji("Head");
|
|
16842
|
+
var I_RSH = ji("RightShoulder");
|
|
16843
|
+
var I_LSH = ji("LeftShoulder");
|
|
16844
|
+
var I_RF = ji("RightFoot");
|
|
16845
|
+
var I_RT = ji("RightToeBase");
|
|
16846
|
+
var I_LF = ji("LeftFoot");
|
|
16847
|
+
var I_LT = ji("LeftToeBase");
|
|
16848
|
+
var I_NECK = ji("Neck");
|
|
16849
|
+
var I_RH2 = ji("RightHand");
|
|
16850
|
+
var I_LH2 = ji("LeftHand");
|
|
16851
|
+
var I_CHEST = ji("Spine2");
|
|
16852
|
+
var RAMP = 50;
|
|
16853
|
+
var TAIL = 10;
|
|
16854
|
+
var D = Math.sqrt(0.5);
|
|
16855
|
+
var EXPECT_DIR = {
|
|
16856
|
+
// ARDY frame: +z forward, +x LEFT of the character
|
|
16857
|
+
forward: [0, 1],
|
|
16858
|
+
back: [0, -1],
|
|
16859
|
+
"strafe-left": [1, 0],
|
|
16860
|
+
"strafe-right": [-1, 0],
|
|
16861
|
+
fl: [D, D],
|
|
16862
|
+
fr: [-D, D],
|
|
16863
|
+
bl: [D, -D],
|
|
16864
|
+
br: [-D, -D]
|
|
16865
|
+
};
|
|
16866
|
+
function classify(stemRaw) {
|
|
16867
|
+
const stem = stemRaw.replace(/-s\d+$/, "");
|
|
16868
|
+
if (stem.includes("jump") || stem.includes("ollie") || stem.includes("trick")) {
|
|
16869
|
+
return { kind: "oneshot", expect: null };
|
|
16870
|
+
}
|
|
16871
|
+
for (const [key, dxz] of Object.entries(EXPECT_DIR)) {
|
|
16872
|
+
if (stem.endsWith(`-${key}`) || stem.includes(`-run-${key}`)) return { kind: "loco", expect: dxz };
|
|
16873
|
+
}
|
|
16874
|
+
if (stem.includes("-idle") || stem.includes("stance")) return { kind: "idle", expect: null };
|
|
16875
|
+
if (stem.includes("push")) return { kind: "travel", expect: null };
|
|
16876
|
+
return { kind: "freeform", expect: null };
|
|
16877
|
+
}
|
|
16878
|
+
function mSlip(t, w) {
|
|
16879
|
+
const { P, fc, fps } = t;
|
|
16880
|
+
const vals = [];
|
|
16881
|
+
for (const [cA, cB, iF, iT] of [[0, 1, I_LF, I_LT], [2, 3, I_RF, I_RT]]) {
|
|
16882
|
+
const s = [];
|
|
16883
|
+
for (let f = w[0]; f < w[1] - 1; f++) {
|
|
16884
|
+
if (!(fc[f][cA] || fc[f][cB])) continue;
|
|
16885
|
+
const sp = (j) => Math.hypot(P[f + 1][j][0] - P[f][j][0], P[f + 1][j][2] - P[f][j][2]) * fps;
|
|
16886
|
+
s.push(Math.min(sp(iF), sp(iT)));
|
|
16887
|
+
}
|
|
16888
|
+
if (s.length < 3) continue;
|
|
16889
|
+
vals.push(mean(s));
|
|
16890
|
+
}
|
|
16891
|
+
return vals.length ? mean(vals) * 100 : null;
|
|
16892
|
+
}
|
|
16893
|
+
function mTravel(t, w) {
|
|
16894
|
+
const ts = [], xs = [], zs = [];
|
|
16895
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16896
|
+
ts.push(f / t.fps);
|
|
16897
|
+
xs.push(t.root[f][0]);
|
|
16898
|
+
zs.push(t.root[f][2]);
|
|
16899
|
+
}
|
|
16900
|
+
const vx = lstsqLinear(ts, xs).slope;
|
|
16901
|
+
const vz = lstsqLinear(ts, zs).slope;
|
|
16902
|
+
const v = matVec(rotY(-mean(t.heading.slice(w[0], w[1]))), [vx, 0, vz]);
|
|
16903
|
+
const speed = Math.hypot(v[0], v[2]);
|
|
16904
|
+
return { speed, dir: speed > 1e-6 ? [v[0] / speed, v[2] / speed] : null };
|
|
16905
|
+
}
|
|
16906
|
+
var mHeadingStd = (t, w) => std(unwrap(t.heading.slice(w[0], w[1]))) * 180 / Math.PI;
|
|
16907
|
+
function mBlade(C, w) {
|
|
16908
|
+
let ss = 0, cs = 0, n = 0;
|
|
16909
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16910
|
+
const shx = C[f][I_RSH][0] - C[f][I_LSH][0];
|
|
16911
|
+
const shz = C[f][I_RSH][2] - C[f][I_LSH][2];
|
|
16912
|
+
const ang = Math.atan2(shz, -shx);
|
|
16913
|
+
ss += Math.sin(ang);
|
|
16914
|
+
cs += Math.cos(ang);
|
|
16915
|
+
n++;
|
|
16916
|
+
}
|
|
16917
|
+
return Math.abs(Math.atan2(ss / n, cs / n)) * (180 / Math.PI);
|
|
16918
|
+
}
|
|
16919
|
+
function mSpeedCv(t, w) {
|
|
16920
|
+
const n = Math.max(2, Math.floor(t.fps / 2));
|
|
16921
|
+
const sp = [];
|
|
16922
|
+
for (let s = w[0]; s < w[1] - n; s += n) {
|
|
16923
|
+
sp.push(Math.hypot(t.root[s + n][0] - t.root[s][0], t.root[s + n][2] - t.root[s][2]) / (n / t.fps));
|
|
16924
|
+
}
|
|
16925
|
+
return sp.length >= 3 ? std(sp) / (mean(sp) + 1e-6) : null;
|
|
16926
|
+
}
|
|
16927
|
+
function mRoughness(t, w) {
|
|
16928
|
+
const F = w[1] - w[0];
|
|
16929
|
+
if (F < 5) return null;
|
|
16930
|
+
let accSq = 0, velSq = 0, n = 0;
|
|
16931
|
+
for (let f = w[0] + 1; f < w[1] - 1; f++) {
|
|
16932
|
+
for (let j = 0; j < t.P[f].length; j++) {
|
|
16933
|
+
for (let k = 0; k < 3; k++) {
|
|
16934
|
+
const a = t.P[f + 1][j][k] - 2 * t.P[f][j][k] + t.P[f - 1][j][k];
|
|
16935
|
+
const v = (t.P[f + 1][j][k] - t.P[f - 1][j][k]) / 2;
|
|
16936
|
+
accSq += a * a;
|
|
16937
|
+
velSq += v * v;
|
|
16938
|
+
n++;
|
|
16939
|
+
}
|
|
16940
|
+
}
|
|
16941
|
+
}
|
|
16942
|
+
const velRms = Math.max(Math.sqrt(velSq / n), 6e-3);
|
|
16943
|
+
return Math.sqrt(accSq / n) / velRms;
|
|
16944
|
+
}
|
|
16945
|
+
function mHeadyaw(t, w) {
|
|
16946
|
+
if (!t.gm) return null;
|
|
16947
|
+
let ss = 0, cs = 0, n = 0;
|
|
16948
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16949
|
+
const yawOf = (idx) => {
|
|
16950
|
+
const m = t.gm[f][idx];
|
|
16951
|
+
return Math.atan2(m[2], m[8]) * 180 / Math.PI;
|
|
16952
|
+
};
|
|
16953
|
+
const dDeg = ((yawOf(I_HEAD2) - yawOf(I_HIPS3) + 180) % 360 + 360) % 360 - 180;
|
|
16954
|
+
const d = dDeg * Math.PI / 180;
|
|
16955
|
+
ss += Math.sin(d);
|
|
16956
|
+
cs += Math.cos(d);
|
|
16957
|
+
n++;
|
|
16958
|
+
}
|
|
16959
|
+
return Math.abs(Math.atan2(ss / n, cs / n) * 180 / Math.PI);
|
|
16960
|
+
}
|
|
16961
|
+
function mHeadpitch(t, w) {
|
|
16962
|
+
if (!t.gm) return null;
|
|
16963
|
+
let s = 0, n = 0;
|
|
16964
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16965
|
+
s += t.gm[f][I_HEAD2][5];
|
|
16966
|
+
n++;
|
|
16967
|
+
}
|
|
16968
|
+
const v = Math.max(-1, Math.min(1, -(s / n)));
|
|
16969
|
+
return Math.asin(v) * 180 / Math.PI;
|
|
16970
|
+
}
|
|
16971
|
+
function mGround(t, w) {
|
|
16972
|
+
const feet = [I_LF, I_LT, I_RF, I_RT];
|
|
16973
|
+
let minAll = Infinity;
|
|
16974
|
+
const minOn = [];
|
|
16975
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16976
|
+
let mn = Infinity;
|
|
16977
|
+
for (const j of feet) mn = Math.min(mn, t.P[f][j][1]);
|
|
16978
|
+
minAll = Math.min(minAll, mn);
|
|
16979
|
+
if (t.fc[f].some(Boolean)) minOn.push(mn);
|
|
16980
|
+
}
|
|
16981
|
+
return {
|
|
16982
|
+
pen: Math.max(0, -minAll) * 100,
|
|
16983
|
+
float: minOn.length >= 3 ? median2(minOn) * 100 : null
|
|
16984
|
+
};
|
|
16985
|
+
}
|
|
16986
|
+
function mHipsRatio(t, w) {
|
|
16987
|
+
const ys = [];
|
|
16988
|
+
for (let f = w[0]; f < w[1]; f++) if (t.fc[f].some(Boolean)) ys.push(t.P[f][I_HIPS3][1]);
|
|
16989
|
+
return ys.length >= 3 ? median2(ys) / HIPS_REST_Y : null;
|
|
16990
|
+
}
|
|
16991
|
+
function mSeam(t, w, kind) {
|
|
16992
|
+
if (kind === "loco" || kind === "travel") {
|
|
16993
|
+
const ls = [];
|
|
16994
|
+
for (let f = w[0] + 1; f < w[1]; f++) {
|
|
16995
|
+
const on = (i) => t.fc[i][0] || t.fc[i][1];
|
|
16996
|
+
if (on(f) && !on(f - 1)) ls.push(f);
|
|
16997
|
+
}
|
|
16998
|
+
let best2 = null;
|
|
16999
|
+
for (let i = 0; i < ls.length - 1; i++) {
|
|
17000
|
+
const s = ls[i];
|
|
17001
|
+
for (const e of [ls[i + 1] - 1, ls[i + 1], ls[i + 1] + 1]) {
|
|
17002
|
+
if (e <= s + 6 || e >= w[1]) continue;
|
|
17003
|
+
const d = poseDist(t.quat, s, e);
|
|
17004
|
+
if (best2 === null || d < best2) best2 = d;
|
|
17005
|
+
}
|
|
17006
|
+
}
|
|
17007
|
+
return best2 === null ? null : best2 * 180 / Math.PI;
|
|
17008
|
+
}
|
|
17009
|
+
let best = null;
|
|
17010
|
+
const [lo, hi] = w;
|
|
17011
|
+
for (let s = lo; s < hi - 30; s += 2) {
|
|
17012
|
+
for (let L = Math.floor(t.fps); L < Math.floor(t.fps * 3); L += 4) {
|
|
17013
|
+
const e = s + L;
|
|
17014
|
+
if (e >= hi) continue;
|
|
17015
|
+
const d = poseDist(t.quat, s, e);
|
|
17016
|
+
if (best === null || d < best) best = d;
|
|
17017
|
+
}
|
|
17018
|
+
}
|
|
17019
|
+
return best === null ? null : best * 180 / Math.PI;
|
|
17020
|
+
}
|
|
17021
|
+
function mOneshot(t, w) {
|
|
17022
|
+
let maxY = -Infinity;
|
|
17023
|
+
const early = [];
|
|
17024
|
+
let air = 0;
|
|
17025
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
17026
|
+
maxY = Math.max(maxY, t.root[f][1]);
|
|
17027
|
+
if (f < w[0] + 10) early.push(t.root[f][1]);
|
|
17028
|
+
if (!t.fc[f].some(Boolean)) air++;
|
|
17029
|
+
}
|
|
17030
|
+
return { rise: maxY - median2(early), air };
|
|
17031
|
+
}
|
|
17032
|
+
function mAimhold(C, w) {
|
|
17033
|
+
const hunchF = [], dyF = [], gapF = [];
|
|
17034
|
+
const yawSigned = [], pitchF = [], archF = [];
|
|
17035
|
+
const fwdSrc = [];
|
|
17036
|
+
const handFwd = [], rhClear = [];
|
|
17037
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
17038
|
+
const up = [
|
|
17039
|
+
C[f][I_HEAD2][0] - C[f][I_HIPS3][0],
|
|
17040
|
+
C[f][I_HEAD2][1] - C[f][I_HIPS3][1],
|
|
17041
|
+
C[f][I_HEAD2][2] - C[f][I_HIPS3][2]
|
|
17042
|
+
];
|
|
17043
|
+
const upN = Math.hypot(...up) + 1e-9;
|
|
17044
|
+
hunchF.push(Math.acos(Math.max(-1, Math.min(1, up[1] / upN))) * 180 / Math.PI);
|
|
17045
|
+
const rh = C[f][I_RH2], lh = C[f][I_LH2];
|
|
17046
|
+
dyF.push(0.5 * (rh[1] + lh[1]) - C[f][I_HEAD2][1]);
|
|
17047
|
+
handFwd.push(0.5 * (rh[2] + lh[2]));
|
|
17048
|
+
const a = C[f][I_HIPS3], b = C[f][I_NECK], p = rh;
|
|
17049
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
17050
|
+
const tt = Math.max(0, Math.min(
|
|
17051
|
+
1,
|
|
17052
|
+
((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1] + (p[2] - a[2]) * ab[2]) / (ab[0] ** 2 + ab[1] ** 2 + ab[2] ** 2 + 1e-9)
|
|
17053
|
+
));
|
|
17054
|
+
rhClear.push(Math.hypot(p[0] - (a[0] + tt * ab[0]), p[1] - (a[1] + tt * ab[1]), p[2] - (a[2] + tt * ab[2])));
|
|
17055
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
17056
|
+
const L = Math.hypot(...v);
|
|
17057
|
+
gapF.push(L);
|
|
17058
|
+
fwdSrc.push({ v, L });
|
|
17059
|
+
yawSigned.push(Math.atan2(v[0], Math.max(v[2], 1e-6)) * 180 / Math.PI);
|
|
17060
|
+
pitchF.push(Math.atan2(v[1], Math.hypot(v[0], v[2]) + 1e-9) * 180 / Math.PI);
|
|
17061
|
+
const c2 = C[f][I_CHEST], h2 = C[f][I_HEAD2];
|
|
17062
|
+
const ab2 = [h2[0] - a[0], h2[1] - a[1], h2[2] - a[2]];
|
|
17063
|
+
const t2 = ((c2[0] - a[0]) * ab2[0] + (c2[1] - a[1]) * ab2[1] + (c2[2] - a[2]) * ab2[2]) / (ab2[0] ** 2 + ab2[1] ** 2 + ab2[2] ** 2 + 1e-9);
|
|
17064
|
+
archF.push(c2[2] - (a[2] + t2 * ab2[2]));
|
|
17065
|
+
}
|
|
17066
|
+
const yawMed = median2(yawSigned);
|
|
17067
|
+
const dev = yawSigned.map((y) => Math.abs(y - yawMed));
|
|
17068
|
+
const r = yawMed * Math.PI / 180;
|
|
17069
|
+
const axis = [Math.sin(r), 0, Math.cos(r)];
|
|
17070
|
+
const fwdF = fwdSrc.map(({ v, L }) => (v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]) / (L + 1e-9));
|
|
17071
|
+
let run2 = 0, best = 0;
|
|
17072
|
+
for (let i = 0; i < fwdF.length; i++) {
|
|
17073
|
+
const ok = dev[i] < 15 && dyF[i] < -0.22 && gapF[i] > 0.18 && gapF[i] < 0.46 && fwdF[i] > 0.75 && hunchF[i] < 16;
|
|
17074
|
+
run2 = ok ? run2 + 1 : 0;
|
|
17075
|
+
best = Math.max(best, run2);
|
|
17076
|
+
}
|
|
17077
|
+
let holdBest = 0, holdBestScore = -Infinity;
|
|
17078
|
+
for (let i = 0; i < fwdF.length; i++) {
|
|
17079
|
+
const s = fwdF[i] - dev[i] / 45 - Math.abs(gapF[i] - 0.32);
|
|
17080
|
+
if (s > holdBestScore) {
|
|
17081
|
+
holdBestScore = s;
|
|
17082
|
+
holdBest = i;
|
|
17083
|
+
}
|
|
17084
|
+
}
|
|
17085
|
+
return {
|
|
17086
|
+
hunch_deg: percentile(hunchF, 90),
|
|
17087
|
+
hand_dy_m: mean(dyF),
|
|
17088
|
+
hand_fwd_m: mean(handFwd),
|
|
17089
|
+
rh_clear_m: mean(rhClear),
|
|
17090
|
+
hand_gap_m: percentile(gapF, 90),
|
|
17091
|
+
line_fwd: mean(fwdF),
|
|
17092
|
+
line_yaw_deg: percentile(dev, 90),
|
|
17093
|
+
line_yaw_med_deg: yawMed,
|
|
17094
|
+
line_pitch_deg: median2(pitchF),
|
|
17095
|
+
arch_m: median2(archF),
|
|
17096
|
+
hold_run: best,
|
|
17097
|
+
hold_best_f: holdBest + w[0]
|
|
17098
|
+
};
|
|
17099
|
+
}
|
|
17100
|
+
var DEFAULT_GATES = {
|
|
17101
|
+
pass: 70,
|
|
17102
|
+
warn: 50,
|
|
17103
|
+
common: {
|
|
17104
|
+
slip: { w: 3, good: 12, bad: 45 },
|
|
17105
|
+
rough: { w: 2, good: 0.4, bad: 0.8 },
|
|
17106
|
+
pen: { w: 1, good: 1.5, bad: 8 },
|
|
17107
|
+
float: { w: 1, good: 4, bad: 12 },
|
|
17108
|
+
headyaw: { w: 2, good: 15, bad: 45 }
|
|
17109
|
+
},
|
|
17110
|
+
aim: {
|
|
17111
|
+
hunch: { w: 3, good: 10, bad: 18 },
|
|
17112
|
+
handhigh: { w: 2, good: -0.32, bad: -0.22 },
|
|
17113
|
+
handfwd: { w: 2, good: 0.3, bad: 0.12, invert: true },
|
|
17114
|
+
handclear: { w: 2, good: 0.16, bad: 0.07, invert: true },
|
|
17115
|
+
twohand: { w: 3, good: 0.35, bad: 0.48 },
|
|
17116
|
+
lineFwd: { w: 3, good: 0.88, bad: 0.75, invert: true },
|
|
17117
|
+
lineYaw: { w: 3, good: 8, bad: 18 },
|
|
17118
|
+
lineMed: { w: 2, good: 45, bad: 65 },
|
|
17119
|
+
holdRun: { w: 3, good: 15, bad: 6, invert: true },
|
|
17120
|
+
linePitch: { w: 2, good: 8, bad: 22 },
|
|
17121
|
+
arch: { w: 1, good: -0.1, bad: -0.16, invert: true },
|
|
17122
|
+
headDown: { w: 2, good: 15, bad: 30 },
|
|
17123
|
+
tiptoe: { w: 1, good: 1.03, bad: 1.07 }
|
|
17124
|
+
},
|
|
17125
|
+
loco: {
|
|
17126
|
+
dir: { w: 3, good: 0.92, bad: 0.6, invert: true },
|
|
17127
|
+
headstd: { w: 1, good: 8, bad: 30 },
|
|
17128
|
+
blade: { w: 2, good: 28, bad: 60 },
|
|
17129
|
+
speedcv: { w: 1, good: 0.18, bad: 0.5 },
|
|
17130
|
+
seam: { w: 2, good: 6, bad: 20 },
|
|
17131
|
+
hips: { w: 1, good: 0.82, bad: 0.55, invert: true }
|
|
17132
|
+
},
|
|
17133
|
+
idle: {
|
|
17134
|
+
still: { w: 2, good: 0.06, bad: 0.35 },
|
|
17135
|
+
blade: { w: 1, good: 30, bad: 65 },
|
|
17136
|
+
seam: { w: 2, good: 6, bad: 20 },
|
|
17137
|
+
hips: { w: 2, good: 0.82, bad: 0.55, invert: true }
|
|
17138
|
+
},
|
|
17139
|
+
oneshot: {
|
|
17140
|
+
hips: { w: 1, good: 0.8, bad: 0.5, invert: true }
|
|
17141
|
+
},
|
|
17142
|
+
freeform: {
|
|
17143
|
+
seam: { w: 1, good: 8, bad: 25 }
|
|
17144
|
+
}
|
|
17145
|
+
};
|
|
17146
|
+
function soft(x, good, bad, invert = false) {
|
|
17147
|
+
if (x === null || x === void 0) return null;
|
|
17148
|
+
if (invert) {
|
|
17149
|
+
x = -x;
|
|
17150
|
+
const g = -good, b = -bad;
|
|
17151
|
+
good = g;
|
|
17152
|
+
bad = b;
|
|
17153
|
+
}
|
|
17154
|
+
return Math.max(0, Math.min(1, (bad - x) / (bad - good)));
|
|
17155
|
+
}
|
|
17156
|
+
var WHY = {
|
|
17157
|
+
slip: { fmt: (v) => `${v.toFixed(0)} cm/s`, why: "feet skate during stance" },
|
|
17158
|
+
rough: { fmt: (v) => v.toFixed(2), why: "jittery motion" },
|
|
17159
|
+
pen: { fmt: (v) => `${v.toFixed(1)} cm`, why: "feet clip the floor" },
|
|
17160
|
+
float: { fmt: (v) => `${v.toFixed(1)} cm`, why: "floats above the floor" },
|
|
17161
|
+
headyaw: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "head/body twisted \u2014 bladed stance" },
|
|
17162
|
+
hunch: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "hunched marksman crouch" },
|
|
17163
|
+
handhigh: { fmt: (v) => `${v.toFixed(2)} m`, why: "hands at face height (cheek weld)" },
|
|
17164
|
+
handfwd: { fmt: (v) => `${v.toFixed(2)} m`, why: "weapon held tight to the chest" },
|
|
17165
|
+
handclear: { fmt: (v) => `${v.toFixed(2)} m`, why: "trigger hand hugs the torso" },
|
|
17166
|
+
twohand: { fmt: (v) => `${v.toFixed(2)} m`, why: "support hand off the weapon (one-handed hold)" },
|
|
17167
|
+
lineFwd: { fmt: (v) => v.toFixed(2), why: "support hand off the take's own barrel axis" },
|
|
17168
|
+
lineYaw: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "grip line wanders \u2014 no stable barrel axis" },
|
|
17169
|
+
lineMed: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "median line beyond bladed-stance range (cross-chest carry)" },
|
|
17170
|
+
holdRun: { fmt: (v) => `${v.toFixed(0)}f`, why: "no sustained clean hold segment to pluck/loop" },
|
|
17171
|
+
linePitch: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "grip line slopes up/down \u2014 barrel not level" },
|
|
17172
|
+
arch: { fmt: (v) => `${v.toFixed(2)} m`, why: "leaning-back spine bow (hips forward, chest behind)" },
|
|
17173
|
+
headDown: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "head tilted down at the hold" },
|
|
17174
|
+
tiptoe: { fmt: (v) => v.toFixed(2), why: "stands taller than rest \u2014 short-legged rigs dangle/float" },
|
|
17175
|
+
dir: { fmt: (v) => v.toFixed(2), why: "doesn't travel the commanded direction" },
|
|
17176
|
+
headstd: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "heading wanders" },
|
|
17177
|
+
blade: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "bladed stance" },
|
|
17178
|
+
speedcv: { fmt: (v) => v.toFixed(2), why: "speed surges" },
|
|
17179
|
+
seam: { fmt: (v) => `${v.toFixed(1)}\xB0`, why: "no clean loop boundary" },
|
|
17180
|
+
hips: { fmt: (v) => v.toFixed(2), why: "crouched/collapsed" },
|
|
17181
|
+
still: { fmt: (v) => `${v.toFixed(2)} m/s`, why: "idle drifts" }
|
|
17182
|
+
};
|
|
17183
|
+
function score(kind, m, gates = DEFAULT_GATES) {
|
|
17184
|
+
const subs = {};
|
|
17185
|
+
const reasons = [];
|
|
17186
|
+
const add = (name, band, val, whyKey = name) => {
|
|
17187
|
+
if (!band) return;
|
|
17188
|
+
const s = soft(val ?? null, band.good, band.bad, band.invert);
|
|
17189
|
+
if (s === null) return;
|
|
17190
|
+
subs[name] = [s, band.w];
|
|
17191
|
+
const info = WHY[whyKey];
|
|
17192
|
+
if (s < 0.5 && info && val !== null && val !== void 0) {
|
|
17193
|
+
reasons.push(`${name} ${info.fmt(val)} (${info.why})`);
|
|
17194
|
+
}
|
|
17195
|
+
};
|
|
17196
|
+
const num = (k) => typeof m[k] === "number" && Number.isFinite(m[k]) ? m[k] : null;
|
|
17197
|
+
add("slip", gates.common.slip, num("slip_cms"));
|
|
17198
|
+
add("rough", gates.common.rough, num("roughness"));
|
|
17199
|
+
add("pen", gates.common.pen, num("pen_cm"));
|
|
17200
|
+
if (num("float_cm") !== null) add("float", gates.common.float, num("float_cm"));
|
|
17201
|
+
if (kind !== "oneshot" && kind !== "freeform" && !m.twist_ok) {
|
|
17202
|
+
add("headyaw", gates.common.headyaw, num("headyaw_deg"));
|
|
17203
|
+
}
|
|
17204
|
+
if (m.aim) {
|
|
17205
|
+
add("hunch", gates.aim.hunch, num("hunch_deg"));
|
|
17206
|
+
add("handhigh", gates.aim.handhigh, num("hand_dy_m"));
|
|
17207
|
+
add("handfwd", gates.aim.handfwd, num("hand_fwd_m"));
|
|
17208
|
+
add("handclear", gates.aim.handclear, num("rh_clear_m"));
|
|
17209
|
+
add("twohand", gates.aim.twohand, num("hand_gap_m"));
|
|
17210
|
+
add("lineFwd", gates.aim.lineFwd, num("line_fwd"));
|
|
17211
|
+
add("lineYaw", gates.aim.lineYaw, num("line_yaw_deg"));
|
|
17212
|
+
const lm = num("line_yaw_med_deg");
|
|
17213
|
+
add("lineMed", gates.aim.lineMed, lm === null ? null : Math.abs(lm));
|
|
17214
|
+
add("holdRun", gates.aim.holdRun, num("hold_run"));
|
|
17215
|
+
const lp = num("line_pitch_deg");
|
|
17216
|
+
add("linePitch", gates.aim.linePitch, lp === null ? null : Math.abs(lp));
|
|
17217
|
+
add("arch", gates.aim.arch, num("arch_m"));
|
|
17218
|
+
add("headDown", gates.aim.headDown, num("headpitch_deg"));
|
|
17219
|
+
add("tiptoe", gates.aim.tiptoe, num("hips_ratio"));
|
|
17220
|
+
}
|
|
17221
|
+
if (kind === "loco") {
|
|
17222
|
+
add("dir", gates.loco.dir, num("dir_dot"));
|
|
17223
|
+
add("headstd", gates.loco.headstd, num("heading_std"));
|
|
17224
|
+
add("blade", gates.loco.blade, num("blade_deg"));
|
|
17225
|
+
add("speedcv", gates.loco.speedcv, num("speed_cv"));
|
|
17226
|
+
add("seam", gates.loco.seam, num("seam_deg"));
|
|
17227
|
+
add("hips", gates.loco.hips, num("hips_ratio"));
|
|
17228
|
+
} else if (kind === "idle" || kind === "travel") {
|
|
17229
|
+
if (kind === "idle") add("still", gates.idle.still, num("speed"));
|
|
17230
|
+
add("blade", gates.idle.blade, num("blade_deg"));
|
|
17231
|
+
add("seam", gates.idle.seam, num("seam_deg"));
|
|
17232
|
+
add("hips", gates.idle.hips, num("hips_ratio"), "hips");
|
|
17233
|
+
} else if (kind === "oneshot") {
|
|
17234
|
+
const rise = num("rise_m") ?? 0, air = num("air_frames") ?? 0;
|
|
17235
|
+
if (air >= 3 && rise < 0.12) {
|
|
17236
|
+
subs.airlie = [0, 2];
|
|
17237
|
+
reasons.push(`contacts say airborne x${air} but rise ${rise.toFixed(2)} m`);
|
|
17238
|
+
}
|
|
17239
|
+
add("hips", gates.oneshot.hips, num("hips_ratio"));
|
|
17240
|
+
} else {
|
|
17241
|
+
add("seam", gates.freeform.seam, num("seam_deg"));
|
|
17242
|
+
}
|
|
17243
|
+
let total = 0, wsum = 0;
|
|
17244
|
+
for (const [s, w] of Object.values(subs)) {
|
|
17245
|
+
total += s * w;
|
|
17246
|
+
wsum += w;
|
|
17247
|
+
}
|
|
17248
|
+
const pts = wsum ? Math.round(100 * total / wsum) : 0;
|
|
17249
|
+
const zeros = Object.values(subs).filter(([s]) => s === 0).map(([, w]) => w);
|
|
17250
|
+
const cap = zeros.some((w) => w >= 3) || zeros.length >= 2 ? "FAIL" : zeros.length ? "WARN" : null;
|
|
17251
|
+
const subscores = {};
|
|
17252
|
+
for (const [k, [s]] of Object.entries(subs)) subscores[k] = Math.round(s * 100) / 100;
|
|
17253
|
+
return { pts, subscores, reasons, cap };
|
|
17254
|
+
}
|
|
17255
|
+
function analyzeTake(stem, buf, gates = DEFAULT_GATES) {
|
|
17256
|
+
const t = loadTake(buf);
|
|
17257
|
+
const { kind, expect } = classify(stem);
|
|
17258
|
+
const F = t.F;
|
|
17259
|
+
const lo = kind !== "oneshot" ? Math.min(RAMP, Math.max(4, Math.floor(F / 4))) : 8;
|
|
17260
|
+
const w = [lo, F - Math.min(TAIL, Math.max(2, Math.floor(F / 8)))];
|
|
17261
|
+
const C = charFrame(t.P, t.heading);
|
|
17262
|
+
const m = { frames: F, fps: t.fps, kind };
|
|
17263
|
+
m.twist_ok = stem.startsWith("skate-");
|
|
17264
|
+
m.slip_cms = mSlip(t, w);
|
|
17265
|
+
m.roughness = mRoughness(t, w);
|
|
17266
|
+
const g = mGround(t, w);
|
|
17267
|
+
m.pen_cm = g.pen;
|
|
17268
|
+
m.float_cm = g.float;
|
|
17269
|
+
m.hips_ratio = mHipsRatio(t, w);
|
|
17270
|
+
m.blade_deg = mBlade(C, w);
|
|
17271
|
+
m.headyaw_deg = mHeadyaw(t, w);
|
|
17272
|
+
m.aim = /aim|rifle|pistol/.test(stem);
|
|
17273
|
+
if (m.aim) {
|
|
17274
|
+
Object.assign(m, mAimhold(C, w));
|
|
17275
|
+
m.headpitch_deg = mHeadpitch(t, w);
|
|
17276
|
+
}
|
|
17277
|
+
const travel = mTravel(t, w);
|
|
17278
|
+
m.speed = travel.speed;
|
|
17279
|
+
if (kind === "loco") {
|
|
17280
|
+
m.dir_dot = travel.dir && expect ? travel.dir[0] * expect[0] + travel.dir[1] * expect[1] : 0;
|
|
17281
|
+
m.heading_std = mHeadingStd(t, w);
|
|
17282
|
+
m.speed_cv = mSpeedCv(t, w);
|
|
17283
|
+
}
|
|
17284
|
+
if (kind === "loco" || kind === "idle" || kind === "travel" || kind === "freeform") {
|
|
17285
|
+
m.seam_deg = mSeam(t, w, kind);
|
|
17286
|
+
}
|
|
17287
|
+
if (kind === "oneshot") {
|
|
17288
|
+
const o = mOneshot(t, w);
|
|
17289
|
+
m.rise_m = o.rise;
|
|
17290
|
+
m.air_frames = o.air;
|
|
17291
|
+
}
|
|
17292
|
+
const { pts, subscores, reasons, cap } = score(kind, m, gates);
|
|
17293
|
+
let verdict = pts >= gates.pass ? "PASS" : pts >= gates.warn ? "WARN" : "FAIL";
|
|
17294
|
+
if (cap === "WARN" && verdict === "PASS") verdict = "WARN";
|
|
17295
|
+
else if (cap === "FAIL") verdict = "FAIL";
|
|
17296
|
+
const metrics = {};
|
|
17297
|
+
for (const [k, v] of Object.entries(m)) {
|
|
17298
|
+
metrics[k] = typeof v === "number" ? Math.round(v * 1e3) / 1e3 : v;
|
|
17299
|
+
}
|
|
17300
|
+
return { take: stem, kind, score: pts, verdict, reasons, metrics, subscores, prompt: t.text.slice(0, 140) };
|
|
17301
|
+
}
|
|
17302
|
+
|
|
17303
|
+
// src/lib/motion/compile.ts
|
|
17304
|
+
var deg2 = Math.PI / 180;
|
|
17305
|
+
var D2 = Math.sqrt(0.5);
|
|
17306
|
+
var DEFAULT_MOTION_CONFIG = {
|
|
17307
|
+
rampFrames: 50,
|
|
17308
|
+
tailSkip: 10,
|
|
17309
|
+
idleLoop: { minFrames: 40, maxFrames: 70, step: 2 },
|
|
17310
|
+
headGazeTargetDeg: 6,
|
|
17311
|
+
aimPitchDeg: 40,
|
|
17312
|
+
aimCandidates: 3,
|
|
17313
|
+
hipsClampM: 5e-3,
|
|
17314
|
+
minDirDot: 0.9
|
|
17315
|
+
};
|
|
17316
|
+
var GAIT_BY_SUFFIX = [
|
|
17317
|
+
["forward", "forward", [0, 1]],
|
|
17318
|
+
["back", "back", [0, -1]],
|
|
17319
|
+
["strafe-left", "strafeLeft", [1, 0]],
|
|
17320
|
+
["strafe-right", "strafeRight", [-1, 0]],
|
|
17321
|
+
["fl", "forwardLeft", [D2, D2]],
|
|
17322
|
+
["fr", "forwardRight", [-D2, D2]],
|
|
17323
|
+
["bl", "backLeft", [D2, -D2]],
|
|
17324
|
+
["br", "backRight", [-D2, -D2]]
|
|
17325
|
+
];
|
|
17326
|
+
var round = (v, p) => {
|
|
17327
|
+
const m = 10 ** p;
|
|
17328
|
+
return Math.round(v * m) / m;
|
|
17329
|
+
};
|
|
17330
|
+
function pack(quatCyc, hipsPos, contacts, fps, speed, dxz, loop = true) {
|
|
17331
|
+
return {
|
|
17332
|
+
fps,
|
|
17333
|
+
frames: quatCyc.length,
|
|
17334
|
+
speed: round(speed, 3),
|
|
17335
|
+
dir: [round(dxz[0], 3), round(dxz[1], 3)],
|
|
17336
|
+
loop,
|
|
17337
|
+
localQuat: quatCyc.map((f) => f.map((q) => q.map((v) => round(v, 6)))),
|
|
17338
|
+
hipsPos: hipsPos.map((p) => p.map((v) => round(v, 5))),
|
|
17339
|
+
contacts
|
|
17340
|
+
};
|
|
17341
|
+
}
|
|
17342
|
+
var folded = (t) => t.fc.map((f) => [f[0] || f[1] ? 1 : 0, f[2] || f[3] ? 1 : 0]);
|
|
17343
|
+
function strikes(cc, col, lo, hi) {
|
|
17344
|
+
const out = [];
|
|
17345
|
+
for (let f = lo + 1; f < hi; f++) if (cc[f][col] === 1 && cc[f - 1][col] === 0) out.push(f);
|
|
17346
|
+
return out;
|
|
17347
|
+
}
|
|
17348
|
+
var slice2 = (a, s, e) => a.slice(s, e);
|
|
17349
|
+
var copyQuats = (q, s, e) => q.slice(s, e).map((f) => f.map((x) => x.slice()));
|
|
17350
|
+
var copyRoots = (r, s, e) => r.slice(s, e).map((v) => v.slice());
|
|
17351
|
+
function cutCycle(stem, t, expect, cfg) {
|
|
17352
|
+
const cc = folded(t);
|
|
17353
|
+
const lo = cfg.rampFrames, hi = t.F - cfg.tailSkip;
|
|
17354
|
+
const ls = strikes(cc, 0, lo, hi);
|
|
17355
|
+
if (ls.length < 3) {
|
|
17356
|
+
throw new Error(
|
|
17357
|
+
`${stem}: only ${ls.length} left-foot strikes in the steady window \u2014 the take never settles into a gait (longer --duration, or regenerate).`
|
|
17358
|
+
);
|
|
17359
|
+
}
|
|
17360
|
+
let best = null;
|
|
17361
|
+
for (let i = 0; i < ls.length - 1; i++) {
|
|
17362
|
+
const s2 = ls[i];
|
|
17363
|
+
for (const e2 of [ls[i + 1] - 1, ls[i + 1], ls[i + 1] + 1]) {
|
|
17364
|
+
if (e2 <= s2 + 6 || e2 >= hi) continue;
|
|
17365
|
+
const d = poseDist(t.quat, s2, e2);
|
|
17366
|
+
if (!best || d < best[0]) best = [d, s2, e2];
|
|
17367
|
+
}
|
|
17368
|
+
}
|
|
17369
|
+
if (!best) throw new Error(`${stem}: no valid strike pair for a loop cycle.`);
|
|
17370
|
+
const [seam, s, e] = best;
|
|
17371
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17372
|
+
const rc = copyRoots(t.root, s, e);
|
|
17373
|
+
const { speed, dir } = deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)));
|
|
17374
|
+
const dot = dir[0] * expect[0] + dir[1] * expect[1];
|
|
17375
|
+
if (dot <= cfg.minDirDot) {
|
|
17376
|
+
throw new Error(
|
|
17377
|
+
`${stem}: cycle travels (${dir[0].toFixed(2)}, ${dir[1].toFixed(2)}) vs expected (${expect[0].toFixed(2)}, ${expect[1].toFixed(2)}) \u2014 wrong direction; check the constraint/prompt.`
|
|
17378
|
+
);
|
|
17379
|
+
}
|
|
17380
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, speed, dir), seamDeg: seam / deg2 };
|
|
17381
|
+
}
|
|
17382
|
+
function bestLoop(t, cfg) {
|
|
17383
|
+
const lo = 10, hi = t.F - 5;
|
|
17384
|
+
let best = null;
|
|
17385
|
+
for (let s = lo; s < hi - cfg.idleLoop.minFrames; s++) {
|
|
17386
|
+
for (let L = cfg.idleLoop.minFrames; L <= cfg.idleLoop.maxFrames; L += cfg.idleLoop.step) {
|
|
17387
|
+
const e = s + L;
|
|
17388
|
+
if (e >= hi) continue;
|
|
17389
|
+
const d = poseDist(t.quat, s, e);
|
|
17390
|
+
if (!best || d < best[0]) best = [d, s, e];
|
|
17391
|
+
}
|
|
17392
|
+
}
|
|
17393
|
+
if (!best) throw new Error("take too short for an idle loop window");
|
|
17394
|
+
return { seam: best[0], s: best[1], e: best[2] };
|
|
17395
|
+
}
|
|
17396
|
+
function compileIdle(stem, t, aim, cfg) {
|
|
17397
|
+
const log = [];
|
|
17398
|
+
const { seam, s, e } = bestLoop(t, cfg);
|
|
17399
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17400
|
+
const rc = copyRoots(t.root, s, e);
|
|
17401
|
+
const cc = folded(t);
|
|
17402
|
+
if (!aim) {
|
|
17403
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)));
|
|
17404
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0]), seamDeg: seam / deg2, log };
|
|
17405
|
+
}
|
|
17406
|
+
const { yaws, pitches } = gripLineChar(t.P, t.heading);
|
|
17407
|
+
const yawMed = median2(yaws.slice(s, e));
|
|
17408
|
+
const pitchMed = median2(pitches.slice(s, e));
|
|
17409
|
+
const headExcess = headPitchFrames(t.localMats, s, e) - cfg.headGazeTargetDeg;
|
|
17410
|
+
log.push(
|
|
17411
|
+
`idle root-yaw ${(-yawMed).toFixed(1)}\xB0 (whole body blades) \xB7 arm-level ${(-pitchMed).toFixed(1)}\xB0 \xB7 head-raise ${Math.max(0, headExcess).toFixed(1)}\xB0`
|
|
17412
|
+
);
|
|
17413
|
+
for (let f = 0; f < qc.length; f++) {
|
|
17414
|
+
qc[f] = levelArms(qc[f], t.P[s + f], pitchMed);
|
|
17415
|
+
if (headExcess > 0) qc[f] = levelHead(qc[f], t.P[s + f], headExcess);
|
|
17416
|
+
}
|
|
17417
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)) + yawMed * deg2);
|
|
17418
|
+
const over = mean(rc.map((r) => r[1])) - HIPS_REST_Y;
|
|
17419
|
+
if (over > cfg.hipsClampM) {
|
|
17420
|
+
log.push(`hips tall-stance clamp: -${over.toFixed(3)} m`);
|
|
17421
|
+
for (const r of rc) r[1] -= over;
|
|
17422
|
+
}
|
|
17423
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0]), seamDeg: seam / deg2, log };
|
|
17424
|
+
}
|
|
17425
|
+
function cutOneshot(t, cfg) {
|
|
17426
|
+
const cc = folded(t);
|
|
17427
|
+
const air = cc.map((c2) => c2[0] === 0 && c2[1] === 0);
|
|
17428
|
+
let best = null;
|
|
17429
|
+
let run2 = null;
|
|
17430
|
+
for (let f = 0; f < air.length; f++) {
|
|
17431
|
+
if (air[f]) run2 = run2 ? [run2[0], f] : [f, f];
|
|
17432
|
+
else {
|
|
17433
|
+
if (run2 && (!best || run2[1] - run2[0] > best[1] - best[0])) best = run2;
|
|
17434
|
+
run2 = null;
|
|
17435
|
+
}
|
|
17436
|
+
}
|
|
17437
|
+
if (run2 && (!best || run2[1] - run2[0] > best[1] - best[0])) best = run2;
|
|
17438
|
+
let s, e;
|
|
17439
|
+
if (best) {
|
|
17440
|
+
s = best[0];
|
|
17441
|
+
while (s > 8 && !air[s - 1]) s--;
|
|
17442
|
+
e = Math.min(t.F - 4, Math.floor(best[1] + 0.6 * t.fps));
|
|
17443
|
+
} else {
|
|
17444
|
+
s = 8;
|
|
17445
|
+
e = t.F - 4;
|
|
17446
|
+
}
|
|
17447
|
+
void cfg;
|
|
17448
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17449
|
+
const rc = copyRoots(t.root, s, e);
|
|
17450
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, Math.min(s + 10, e))));
|
|
17451
|
+
return pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0], false);
|
|
17452
|
+
}
|
|
17453
|
+
function aimPoseTracks(takes, cfg) {
|
|
17454
|
+
const I_HIPS4 = ji("Hips"), I_HEAD3 = ji("Head");
|
|
17455
|
+
const I_RH3 = ji("RightHand"), I_LH3 = ji("LeftHand");
|
|
17456
|
+
const cands = [];
|
|
17457
|
+
for (const { stem, take: t } of takes) {
|
|
17458
|
+
const { yaws, pitches } = gripLineChar(t.P, t.heading);
|
|
17459
|
+
const lo = Math.min(cfg.rampFrames, Math.floor(t.F / 3));
|
|
17460
|
+
const hiY = t.F - cfg.tailSkip;
|
|
17461
|
+
const yawMed = median2(yaws.slice(lo, hiY));
|
|
17462
|
+
const handVel = [];
|
|
17463
|
+
for (let f = 0; f < t.F - 1; f++) {
|
|
17464
|
+
const sp = (j) => Math.hypot(
|
|
17465
|
+
t.P[f + 1][j][0] - t.P[f][j][0],
|
|
17466
|
+
t.P[f + 1][j][1] - t.P[f][j][1],
|
|
17467
|
+
t.P[f + 1][j][2] - t.P[f][j][2]
|
|
17468
|
+
) * t.fps;
|
|
17469
|
+
handVel.push(Math.max(sp(I_RH3), sp(I_LH3)));
|
|
17470
|
+
}
|
|
17471
|
+
for (let f = lo; f < hiY; f++) {
|
|
17472
|
+
const rot = rotY(-t.heading[f]);
|
|
17473
|
+
const rel = (j) => matVec(rot, [
|
|
17474
|
+
t.P[f][j][0] - t.P[f][I_HIPS4][0],
|
|
17475
|
+
t.P[f][j][1] - t.P[f][I_HIPS4][1],
|
|
17476
|
+
t.P[f][j][2] - t.P[f][I_HIPS4][2]
|
|
17477
|
+
]);
|
|
17478
|
+
const rh = rel(I_RH3), lh = rel(I_LH3), head = rel(I_HEAD3), hips = rel(I_HIPS4);
|
|
17479
|
+
const up = [head[0] - hips[0], head[1] - hips[1], head[2] - hips[2]];
|
|
17480
|
+
const hunch = Math.acos(Math.max(-1, Math.min(1, up[1] / (Math.hypot(...up) + 1e-9)))) / deg2;
|
|
17481
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
17482
|
+
const L = Math.hypot(...v);
|
|
17483
|
+
if (L < 0.15) continue;
|
|
17484
|
+
const yaw = Math.atan2(v[0], Math.max(v[2], 1e-6)) / deg2;
|
|
17485
|
+
const ry = yawMed * deg2;
|
|
17486
|
+
const fwdN = (v[0] * Math.sin(ry) + v[2] * Math.cos(ry)) / L;
|
|
17487
|
+
const dy = 0.5 * (rh[1] + lh[1]) - head[1];
|
|
17488
|
+
const score2 = fwdN - Math.abs(yaw - yawMed) / 30 - 4 * Math.max(0, dy + 0.25) - 2.5 * Math.abs(L - 0.32) - Math.max(0, hunch - 14) / 10 - 0.5 * Math.min(handVel[Math.min(f, handVel.length - 1)], 1);
|
|
17489
|
+
cands.push({ score: score2, stem, f, take: t, yaw, pitch: pitches[f] });
|
|
17490
|
+
}
|
|
17491
|
+
}
|
|
17492
|
+
cands.sort((a, b) => b.score - a.score);
|
|
17493
|
+
const picked = [];
|
|
17494
|
+
for (const c2 of cands) {
|
|
17495
|
+
if (picked.some((p) => p.stem === c2.stem)) continue;
|
|
17496
|
+
picked.push(c2);
|
|
17497
|
+
if (picked.length === cfg.aimCandidates) break;
|
|
17498
|
+
}
|
|
17499
|
+
const tracks = {};
|
|
17500
|
+
const log = [];
|
|
17501
|
+
const PITCH = cfg.aimPitchDeg * deg2;
|
|
17502
|
+
picked.forEach((c2, i) => {
|
|
17503
|
+
log.push(
|
|
17504
|
+
`aim${i}: ${c2.stem} frame ${c2.f} score ${c2.score >= 0 ? "+" : ""}${c2.score.toFixed(2)} spine-yaw ${(-c2.yaw).toFixed(1)}\xB0 arm-level ${(-c2.pitch).toFixed(1)}\xB0`
|
|
17505
|
+
);
|
|
17506
|
+
let q = levelArms(c2.take.quat[c2.f].map((x) => x.slice()), c2.take.P[c2.f], c2.pitch);
|
|
17507
|
+
const hx = headPitchFrames(c2.take.localMats, c2.f, c2.f + 1) - cfg.headGazeTargetDeg;
|
|
17508
|
+
if (hx > 0) q = levelHead(q, c2.take.P[c2.f], hx);
|
|
17509
|
+
q = yawed(q, -c2.yaw * deg2);
|
|
17510
|
+
const hip = [0, c2.take.P[c2.f][ji("Hips")][1], 0];
|
|
17511
|
+
for (const [suffix, qv] of [["", q], ["Up", pitched(q, -PITCH)], ["Dn", pitched(q, PITCH)]]) {
|
|
17512
|
+
tracks[`aim${i}${suffix}`] = pack([qv], [hip], [[0, 0]], 30, 0, [0, 0]);
|
|
17513
|
+
}
|
|
17514
|
+
});
|
|
17515
|
+
return { tracks, log };
|
|
17516
|
+
}
|
|
17517
|
+
function compileSet(inputs, setName, cfg = DEFAULT_MOTION_CONFIG) {
|
|
17518
|
+
const log = [];
|
|
17519
|
+
const gaits = {};
|
|
17520
|
+
const aimTakes = [];
|
|
17521
|
+
for (const { stem, take } of inputs) {
|
|
17522
|
+
const { kind } = classify(stem);
|
|
17523
|
+
const aim = /aim|rifle|pistol/.test(stem);
|
|
17524
|
+
if (aim && (kind === "idle" || kind === "freeform")) aimTakes.push({ stem, take });
|
|
17525
|
+
if (kind === "loco") {
|
|
17526
|
+
const hit = GAIT_BY_SUFFIX.find(
|
|
17527
|
+
([suffix]) => stem.replace(/-s\d+$/, "").endsWith(`-${suffix}`) || stem.includes(`-run-${suffix}`)
|
|
17528
|
+
);
|
|
17529
|
+
if (!hit) throw new Error(`${stem}: locomotion take with no recognizable direction suffix.`);
|
|
17530
|
+
const { track, seamDeg } = cutCycle(stem, take, hit[2], cfg);
|
|
17531
|
+
gaits[hit[1]] = track;
|
|
17532
|
+
log.push(`${hit[1]}: ${track.frames} frames speed ${track.speed} m/s seam ${seamDeg.toFixed(1)}\xB0`);
|
|
17533
|
+
} else if (kind === "idle") {
|
|
17534
|
+
const { track, seamDeg, log: l } = compileIdle(stem, take, aim, cfg);
|
|
17535
|
+
gaits.idle = track;
|
|
17536
|
+
log.push(`idle: ${track.frames} frames seam ${seamDeg.toFixed(1)}\xB0`, ...l);
|
|
17537
|
+
} else if (kind === "oneshot") {
|
|
17538
|
+
const clean2 = stem.replace(/-s\d+$/, "");
|
|
17539
|
+
const name = /jump.*run|run.*jump/.test(clean2) ? "jumpRun" : /jump/.test(clean2) ? "jumpStand" : clean2.replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
|
|
17540
|
+
const track = cutOneshot(take, cfg);
|
|
17541
|
+
gaits[name] = track;
|
|
17542
|
+
log.push(`${name}: ${track.frames} frames (one-shot)`);
|
|
17543
|
+
} else if (kind === "travel" || kind === "freeform") {
|
|
17544
|
+
const { track, seamDeg } = compileIdle(stem, take, false, cfg);
|
|
17545
|
+
const name = stem.replace(/-s\d+$/, "").replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
|
|
17546
|
+
gaits[name] = track;
|
|
17547
|
+
log.push(`${name}: ${track.frames} frames seam ${seamDeg.toFixed(1)}\xB0 (freeform loop)`);
|
|
17548
|
+
}
|
|
17549
|
+
}
|
|
17550
|
+
if (aimTakes.length > 0) {
|
|
17551
|
+
const { tracks, log: l } = aimPoseTracks(aimTakes, cfg);
|
|
17552
|
+
Object.assign(gaits, tracks);
|
|
17553
|
+
log.push(...l);
|
|
17554
|
+
}
|
|
17555
|
+
return {
|
|
17556
|
+
data: {
|
|
17557
|
+
version: 1,
|
|
17558
|
+
set: setName,
|
|
17559
|
+
skeleton: "core27",
|
|
17560
|
+
joints: [...JOINTS],
|
|
17561
|
+
parents: [...PARENTS],
|
|
17562
|
+
restOffsets: REST_OFFSETS.map((o) => [...o]),
|
|
17563
|
+
restPositions: REST_POSITIONS.map((o) => [...o]),
|
|
17564
|
+
hipsRestY: HIPS_REST_Y,
|
|
17565
|
+
gaits
|
|
17566
|
+
},
|
|
17567
|
+
log
|
|
17568
|
+
};
|
|
17569
|
+
}
|
|
17570
|
+
|
|
17571
|
+
// src/lib/motion/constraints.ts
|
|
17572
|
+
var FPS = 20;
|
|
17573
|
+
var RAMP_S = 1.5;
|
|
17574
|
+
var WAYPOINT_EVERY = 10;
|
|
17575
|
+
var FIRST_FRAME = 10;
|
|
17576
|
+
var D3 = Math.sqrt(0.5);
|
|
17577
|
+
var MOTION_DIRECTIONS = {
|
|
17578
|
+
forward: [0, 1],
|
|
17579
|
+
back: [0, -1],
|
|
17580
|
+
"strafe-left": [1, 0],
|
|
17581
|
+
"strafe-right": [-1, 0],
|
|
17582
|
+
"forward-left": [D3, D3],
|
|
17583
|
+
"forward-right": [-D3, D3],
|
|
17584
|
+
"back-left": [D3, -D3],
|
|
17585
|
+
"back-right": [-D3, -D3]
|
|
17586
|
+
};
|
|
17587
|
+
function distAt(t, speed) {
|
|
17588
|
+
if (t <= RAMP_S) return 0.5 * speed * t * t / RAMP_S;
|
|
17589
|
+
return 0.5 * speed * RAMP_S + speed * (t - RAMP_S);
|
|
17590
|
+
}
|
|
17591
|
+
function directionConstraint(dir, speed, durationS) {
|
|
17592
|
+
const frames = [];
|
|
17593
|
+
const pts = [];
|
|
17594
|
+
const nFrames = Math.floor(durationS * FPS);
|
|
17595
|
+
for (let f = FIRST_FRAME; f <= nFrames; f += WAYPOINT_EVERY) {
|
|
17596
|
+
const t = f / FPS;
|
|
17597
|
+
const d = distAt(t, speed);
|
|
17598
|
+
frames.push(f);
|
|
17599
|
+
pts.push([Math.round(dir[0] * d * 1e4) / 1e4, Math.round(dir[1] * d * 1e4) / 1e4]);
|
|
17600
|
+
}
|
|
17601
|
+
return [
|
|
17602
|
+
{
|
|
17603
|
+
type: "root2d",
|
|
17604
|
+
frame_indices: frames,
|
|
17605
|
+
root_2d: pts,
|
|
17606
|
+
global_root_heading: frames.map(() => 0)
|
|
17607
|
+
}
|
|
17608
|
+
];
|
|
17609
|
+
}
|
|
17610
|
+
|
|
17611
|
+
// src/commands/motion.ts
|
|
17612
|
+
var MOTION_USAGE = `${c.bold("Options for `motion` (custom character animations \u2014 text \u2192 CoreSkeleton27 takes \u2192 compiled set)")}
|
|
17613
|
+
motion gen "<prompt>" Generate N raw motion takes + the objective verify
|
|
17614
|
+
report on the GPU service (bills per take; default 4).
|
|
17615
|
+
[--takes 1-8] [--duration 2-10 s] [--seeds 0,1,2]
|
|
17616
|
+
[--constraints file.json] [--no-wait]
|
|
17617
|
+
motion verify <takes\u2026> LOCALLY score .npz takes/dirs against the objective
|
|
17618
|
+
gates (same math the service ran \u2014 reproducible).
|
|
17619
|
+
[--gates gates.json] [--json]
|
|
17620
|
+
motion compile <takes\u2026> --out src/motion/<set>.json \u2014 cut loop cycles,
|
|
17621
|
+
deroot, grip-line normalize, pack the game-ready set.
|
|
17622
|
+
Take FILENAMES drive routing (rifle-run-forward.npz,
|
|
17623
|
+
rifle-idle.npz, jump-stand.npz\u2026). [--set <name>]
|
|
17624
|
+
[--config motion.config.json]
|
|
17625
|
+
motion install Vendor the motion runtime INTO this game: retarget
|
|
17626
|
+
formulas (src/motion/rigs.js), the clip player
|
|
17627
|
+
(anim-runtime.js), and motion.config.json \u2014 files the
|
|
17628
|
+
agent then owns and can edit. [--set rifle: also drop
|
|
17629
|
+
the proven rifle starter set] [--force]
|
|
17630
|
+
motion constraints Write an ARDY root-2d constraint JSON (heading-pinned
|
|
17631
|
+
straight run) for gen. [--dir forward|back|strafe-left|
|
|
17632
|
+
strafe-right|forward-left|\u2026] [--speed 2.5] [--duration 5]
|
|
17633
|
+
[--out constraints.json]`;
|
|
17634
|
+
async function motionGen(opts, log) {
|
|
17635
|
+
const prompt = opts.query?.trim();
|
|
17636
|
+
if (!prompt) {
|
|
17637
|
+
log.error(`Missing prompt. Usage: ${c.cyan('genex motion gen "<prompt>"')}`);
|
|
17638
|
+
process.exitCode = 1;
|
|
17639
|
+
return;
|
|
17640
|
+
}
|
|
17641
|
+
const generationOptions = {};
|
|
17642
|
+
if (opts.takes !== void 0) generationOptions.takes = opts.takes;
|
|
17643
|
+
if (opts.duration !== void 0) generationOptions.duration = opts.duration;
|
|
17644
|
+
if (opts.seeds !== void 0) {
|
|
17645
|
+
const seeds = opts.seeds.split(",").map((s) => Number(s.trim()));
|
|
17646
|
+
if (seeds.length === 0 || seeds.length > 8 || seeds.some((s) => !Number.isInteger(s) || s < 0)) {
|
|
17647
|
+
log.error(`Invalid --seeds value: ${opts.seeds} (expected 1-8 comma-separated non-negative integers).`);
|
|
17648
|
+
process.exitCode = 1;
|
|
17649
|
+
return;
|
|
17650
|
+
}
|
|
17651
|
+
generationOptions.seeds = seeds;
|
|
17652
|
+
}
|
|
17653
|
+
if (opts.constraintsPath !== void 0) {
|
|
17654
|
+
try {
|
|
17655
|
+
const raw = await fs18.readFile(opts.constraintsPath, "utf8");
|
|
17656
|
+
generationOptions.constraints = JSON.parse(raw);
|
|
17657
|
+
} catch {
|
|
17658
|
+
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
17659
|
+
process.exitCode = 1;
|
|
17660
|
+
return;
|
|
17661
|
+
}
|
|
17662
|
+
}
|
|
17663
|
+
const genOpts = {
|
|
17664
|
+
prompt,
|
|
17665
|
+
generationOptions,
|
|
17666
|
+
apiUrl: opts.apiUrl,
|
|
17667
|
+
envPath: opts.envPath,
|
|
17668
|
+
token: opts.token,
|
|
17669
|
+
noWait: opts.noWait,
|
|
17670
|
+
json: opts.json,
|
|
17671
|
+
quiet: opts.quiet,
|
|
17672
|
+
open: opts.open
|
|
17673
|
+
};
|
|
17674
|
+
await runGenerate("motion", genOpts);
|
|
17675
|
+
}
|
|
17676
|
+
async function expandTakes(selectors) {
|
|
17677
|
+
const out = [];
|
|
17678
|
+
for (const sel of selectors) {
|
|
17679
|
+
const st = await fs18.stat(sel).catch(() => null);
|
|
17680
|
+
if (st?.isDirectory()) {
|
|
17681
|
+
const names = await fs18.readdir(sel);
|
|
17682
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path18.join(sel, n));
|
|
17683
|
+
} else if (st?.isFile()) {
|
|
17684
|
+
out.push(sel);
|
|
17685
|
+
} else {
|
|
17686
|
+
throw new Error(`No such take file or directory: ${sel}`);
|
|
17687
|
+
}
|
|
17688
|
+
}
|
|
17689
|
+
return out;
|
|
17690
|
+
}
|
|
17691
|
+
function mergeGates(base, patch) {
|
|
17692
|
+
const out = structuredClone(base);
|
|
17693
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
17694
|
+
if (k === "pass" || k === "warn") {
|
|
17695
|
+
if (typeof v === "number") out[k] = v;
|
|
17696
|
+
} else if (v && typeof v === "object" && k in out) {
|
|
17697
|
+
const section = out[k];
|
|
17698
|
+
for (const [name, band] of Object.entries(v)) {
|
|
17699
|
+
if (band && typeof band === "object") {
|
|
17700
|
+
section[name] = { ...section[name], ...band };
|
|
17701
|
+
}
|
|
17702
|
+
}
|
|
17703
|
+
}
|
|
17704
|
+
}
|
|
17705
|
+
return out;
|
|
17706
|
+
}
|
|
17707
|
+
async function motionVerify(opts, log) {
|
|
17708
|
+
const selectors = opts.selectors ?? [];
|
|
17709
|
+
if (selectors.length === 0) {
|
|
17710
|
+
log.error(`Missing takes. Usage: ${c.cyan("genex motion verify <take.npz\u2026|dir> [--gates gates.json] [--json]")}`);
|
|
17711
|
+
process.exitCode = 1;
|
|
17712
|
+
return;
|
|
17713
|
+
}
|
|
17714
|
+
let gates = DEFAULT_GATES;
|
|
17715
|
+
if (opts.gatesPath) {
|
|
17716
|
+
try {
|
|
17717
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs18.readFile(opts.gatesPath, "utf8")));
|
|
17718
|
+
} catch {
|
|
17719
|
+
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
17720
|
+
process.exitCode = 1;
|
|
17721
|
+
return;
|
|
17722
|
+
}
|
|
17723
|
+
}
|
|
17724
|
+
let files;
|
|
17725
|
+
try {
|
|
17726
|
+
files = await expandTakes(selectors);
|
|
17727
|
+
} catch (err) {
|
|
17728
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17729
|
+
process.exitCode = 1;
|
|
17730
|
+
return;
|
|
17731
|
+
}
|
|
17732
|
+
if (files.length === 0) {
|
|
17733
|
+
log.error("No .npz takes found.");
|
|
17734
|
+
process.exitCode = 1;
|
|
17735
|
+
return;
|
|
17736
|
+
}
|
|
17737
|
+
const reports = [];
|
|
17738
|
+
for (const file of files) {
|
|
17739
|
+
const stem = path18.basename(file).replace(/\.npz$/, "");
|
|
17740
|
+
try {
|
|
17741
|
+
reports.push(analyzeTake(stem, await fs18.readFile(file), gates));
|
|
17742
|
+
} catch (err) {
|
|
17743
|
+
reports.push({
|
|
17744
|
+
take: stem,
|
|
17745
|
+
kind: "?",
|
|
17746
|
+
score: 0,
|
|
17747
|
+
verdict: "ERROR",
|
|
17748
|
+
reasons: [err instanceof Error ? err.message : String(err)],
|
|
17749
|
+
metrics: {},
|
|
17750
|
+
subscores: {},
|
|
17751
|
+
prompt: ""
|
|
17752
|
+
});
|
|
17753
|
+
}
|
|
17754
|
+
}
|
|
17755
|
+
reports.sort((a, b) => a.score - b.score);
|
|
17756
|
+
if (opts.json) {
|
|
17757
|
+
writeJson(reports);
|
|
17758
|
+
return;
|
|
17759
|
+
}
|
|
17760
|
+
const wide = Math.max(...reports.map((r) => r.take.length));
|
|
17761
|
+
for (const r of reports) {
|
|
17762
|
+
const why = r.reasons.length ? ` \u2014 ${r.reasons.join("; ")}` : "";
|
|
17763
|
+
log.plain(`${r.verdict.padEnd(5)} ${String(r.score).padStart(3)} ${r.take.padEnd(wide)} [${r.kind}]${why}`);
|
|
17764
|
+
}
|
|
17765
|
+
log.plain("");
|
|
17766
|
+
log.dim(" The report is data, not a verdict on the verb \u2014 pick the takes whose motion");
|
|
17767
|
+
log.dim(" reads right, then: genex motion compile <picked\u2026> --out src/motion/<set>.json");
|
|
17768
|
+
}
|
|
17769
|
+
async function motionCompile(opts, log) {
|
|
17770
|
+
const selectors = opts.selectors ?? [];
|
|
17771
|
+
if (selectors.length === 0 || !opts.out) {
|
|
17772
|
+
log.error(`Usage: ${c.cyan("genex motion compile <take.npz\u2026|dir> --out src/motion/<set>.json [--config motion.config.json]")}`);
|
|
17773
|
+
process.exitCode = 1;
|
|
17774
|
+
return;
|
|
17775
|
+
}
|
|
17776
|
+
let cfg = DEFAULT_MOTION_CONFIG;
|
|
17777
|
+
if (opts.configPath) {
|
|
17778
|
+
try {
|
|
17779
|
+
const patch = JSON.parse(await fs18.readFile(opts.configPath, "utf8"));
|
|
17780
|
+
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
17781
|
+
} catch {
|
|
17782
|
+
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
17783
|
+
process.exitCode = 1;
|
|
17784
|
+
return;
|
|
17785
|
+
}
|
|
17786
|
+
}
|
|
17787
|
+
let files;
|
|
17788
|
+
try {
|
|
17789
|
+
files = await expandTakes(selectors);
|
|
17790
|
+
} catch (err) {
|
|
17791
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17792
|
+
process.exitCode = 1;
|
|
17793
|
+
return;
|
|
17794
|
+
}
|
|
17795
|
+
const inputs = [];
|
|
17796
|
+
for (const file of files) {
|
|
17797
|
+
const stem = path18.basename(file).replace(/\.npz$/, "");
|
|
17798
|
+
try {
|
|
17799
|
+
inputs.push({ stem, take: loadTake(await fs18.readFile(file)) });
|
|
17800
|
+
} catch (err) {
|
|
17801
|
+
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17802
|
+
process.exitCode = 1;
|
|
17803
|
+
return;
|
|
17804
|
+
}
|
|
17805
|
+
}
|
|
17806
|
+
const setName = opts.set ?? path18.basename(opts.out).replace(/\.json$/, "");
|
|
17807
|
+
let result;
|
|
17808
|
+
try {
|
|
17809
|
+
result = compileSet(inputs, setName, cfg);
|
|
17810
|
+
} catch (err) {
|
|
17811
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17812
|
+
process.exitCode = 1;
|
|
17813
|
+
return;
|
|
17814
|
+
}
|
|
17815
|
+
const trackCount = Object.keys(result.data.gaits).length;
|
|
17816
|
+
if (trackCount === 0) {
|
|
17817
|
+
log.error("No tracks compiled \u2014 check the take filenames (routing is by stem: rifle-run-forward, rifle-idle, jump-stand\u2026).");
|
|
17818
|
+
process.exitCode = 1;
|
|
17819
|
+
return;
|
|
17820
|
+
}
|
|
17821
|
+
await fs18.mkdir(path18.dirname(path18.resolve(opts.out)), { recursive: true });
|
|
17822
|
+
const json = JSON.stringify(result.data);
|
|
17823
|
+
await fs18.writeFile(opts.out, json);
|
|
17824
|
+
if (opts.json) {
|
|
17825
|
+
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
17826
|
+
return;
|
|
17827
|
+
}
|
|
17828
|
+
for (const line of result.log) log.dim(` ${line}`);
|
|
17829
|
+
log.success(`Wrote ${opts.out} (${(json.length / 1e3).toFixed(0)} KB, ${trackCount} tracks).`);
|
|
17830
|
+
log.dim(" Play it with the vendored runtime (genex motion install) \u2014 see the genex-ai-character skill's motion reference.");
|
|
17831
|
+
}
|
|
17832
|
+
var MOTION_RUNTIME_FILES = [
|
|
17833
|
+
"rigs.js",
|
|
17834
|
+
"anim-runtime.js",
|
|
17835
|
+
"ik.js",
|
|
17836
|
+
"motion.config.json"
|
|
17837
|
+
];
|
|
17838
|
+
var MOTION_PRESETS = {
|
|
17839
|
+
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
17840
|
+
};
|
|
17841
|
+
var MOTION_DEST = path18.join("src", "motion");
|
|
17842
|
+
async function motionInstall(opts, log) {
|
|
17843
|
+
const srcDir = path18.join(getTemplatesDir(), "motion");
|
|
17844
|
+
const root = opts.cwd ?? process.cwd();
|
|
17845
|
+
const preset = opts.set;
|
|
17846
|
+
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
17847
|
+
log.error(`Unknown motion preset "${preset}". Available: ${Object.keys(MOTION_PRESETS).join(", ")}.`);
|
|
17848
|
+
process.exitCode = 1;
|
|
17849
|
+
return;
|
|
17850
|
+
}
|
|
17851
|
+
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
17852
|
+
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
17853
|
+
log.plain("");
|
|
17854
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path18.sep)}`);
|
|
17855
|
+
let copied = 0, skipped = 0;
|
|
17856
|
+
try {
|
|
17857
|
+
for (const rel of files) {
|
|
17858
|
+
const dest = path18.join(root, MOTION_DEST, rel);
|
|
17859
|
+
const exists4 = await fs18.access(dest).then(() => true, () => false);
|
|
17860
|
+
if (!opts.force && exists4) {
|
|
17861
|
+
skipped++;
|
|
17862
|
+
log.dim(` skipped ${path18.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
17863
|
+
continue;
|
|
17864
|
+
}
|
|
17865
|
+
await fs18.mkdir(path18.dirname(dest), { recursive: true });
|
|
17866
|
+
await fs18.copyFile(path18.join(srcDir, rel), dest);
|
|
17867
|
+
copied++;
|
|
17868
|
+
log.dim(` ${path18.join(MOTION_DEST, rel)}`);
|
|
17869
|
+
}
|
|
17870
|
+
} catch (err) {
|
|
17871
|
+
log.error(`Copy failed: ${String(err)}`);
|
|
17872
|
+
process.exitCode = 1;
|
|
17873
|
+
return;
|
|
17874
|
+
}
|
|
17875
|
+
log.success(`Motion runtime ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`);
|
|
17876
|
+
log.plain("");
|
|
17877
|
+
log.plain(c.bold("Next steps"));
|
|
17878
|
+
log.plain(" 1. No new dependencies for GLB rigs (three is in the scaffold); VRM avatars also need @pixiv/three-vrm.");
|
|
17879
|
+
log.plain(` 2. Read the ${c.cyan("genex-ai-character")} skill's motion reference for wiring, the prompt wording ladder, and the gen \u2192 verify \u2192 compile loop.`);
|
|
17880
|
+
log.plain(" 3. Wiring sketch:");
|
|
17881
|
+
for (const line of [
|
|
17882
|
+
`const rig = await loadRig("./assets/character.glb", set, scene); rig.computeCorrection();`,
|
|
17883
|
+
`const restAnkle = captureRestAnkle(rig, scene); reanchorFeet(rig, set, scene); // BEFORE ClipSet`,
|
|
17884
|
+
`const clipSet = new ClipSet(rig, set, set.gaits); const anim = new Animator(clipSet);`,
|
|
17885
|
+
`groundCalibrate(rig, clipSet, scene, restAnkle); curlFingers(rig, scene); // once, at load`,
|
|
17886
|
+
`anim.setLoops({ idle: 1 - mag, ...dir8Weights(x, z, mag) }, dt); maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });`,
|
|
17887
|
+
`anim.setMask("aim0", maskW, pitch / 0.6); anim.update(dt); // per frame \u2014 src/motion/ is YOURS to tune`
|
|
17888
|
+
]) {
|
|
17889
|
+
log.dim(` ${line}`);
|
|
17890
|
+
}
|
|
17891
|
+
}
|
|
17892
|
+
async function motionConstraints(opts, log) {
|
|
17893
|
+
const dirName = opts.dir ?? "forward";
|
|
17894
|
+
const dir = MOTION_DIRECTIONS[dirName];
|
|
17895
|
+
if (!dir) {
|
|
17896
|
+
log.error(`Unknown --dir "${dirName}". Directions: ${Object.keys(MOTION_DIRECTIONS).join(", ")}.`);
|
|
17897
|
+
process.exitCode = 1;
|
|
17898
|
+
return;
|
|
17899
|
+
}
|
|
17900
|
+
const speed = opts.speed ?? 2.5;
|
|
17901
|
+
const duration = opts.duration ?? 5;
|
|
17902
|
+
if (!(speed > 0) || !(duration >= 2 && duration <= 10)) {
|
|
17903
|
+
log.error("Expected --speed > 0 and --duration 2-10 seconds.");
|
|
17904
|
+
process.exitCode = 1;
|
|
17905
|
+
return;
|
|
17906
|
+
}
|
|
17907
|
+
const doc = directionConstraint(dir, speed, duration);
|
|
17908
|
+
const out = opts.out ?? "constraints.json";
|
|
17909
|
+
await fs18.writeFile(out, JSON.stringify(doc));
|
|
17910
|
+
if (opts.json) {
|
|
17911
|
+
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
17912
|
+
return;
|
|
17913
|
+
}
|
|
17914
|
+
log.success(`Wrote ${out} \u2014 ${dirName} at ${speed} m/s for ${duration}s (heading pinned +Z).`);
|
|
17915
|
+
log.dim(` Use it with: ${c.cyan(`genex motion gen "<gait prompt>" --constraints ${out}`)}`);
|
|
17916
|
+
}
|
|
17917
|
+
async function runMotion(opts) {
|
|
17918
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
17919
|
+
switch (opts.sub?.trim()) {
|
|
17920
|
+
case "gen":
|
|
17921
|
+
await motionGen(opts, log);
|
|
17922
|
+
return;
|
|
17923
|
+
case "verify":
|
|
17924
|
+
await motionVerify(opts, log);
|
|
17925
|
+
return;
|
|
17926
|
+
case "compile":
|
|
17927
|
+
await motionCompile(opts, log);
|
|
17928
|
+
return;
|
|
17929
|
+
case "install":
|
|
17930
|
+
await motionInstall(opts, log);
|
|
17931
|
+
return;
|
|
17932
|
+
case "constraints":
|
|
17933
|
+
await motionConstraints(opts, log);
|
|
17934
|
+
return;
|
|
17935
|
+
default: {
|
|
17936
|
+
const bare = createLogger({ quiet: opts.quiet });
|
|
17937
|
+
bare.error(
|
|
17938
|
+
opts.sub ? `Unknown motion command "${opts.sub}". Commands: gen, verify, compile, install, constraints.` : 'Missing motion command. Usage: genex motion <gen|verify|compile|install|constraints> \u2014 e.g. genex motion gen "soldier low-ready rifle idle".'
|
|
17939
|
+
);
|
|
17940
|
+
process.exitCode = 1;
|
|
17941
|
+
}
|
|
17942
|
+
}
|
|
17943
|
+
}
|
|
17944
|
+
|
|
16388
17945
|
// src/index.ts
|
|
16389
17946
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
|
|
16390
17947
|
var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
|
|
@@ -16427,6 +17984,14 @@ ${c.bold("Usage")}
|
|
|
16427
17984
|
bind library clips via --animation. No approval
|
|
16428
17985
|
ceremony, no player controller pack.
|
|
16429
17986
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
17987
|
+
genex motion <sub> [args] Custom character animations from text (ARDY):
|
|
17988
|
+
gen "<prompt>" (GPU service, bills per take) |
|
|
17989
|
+
verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
|
|
17990
|
+
install [--set rifle] | constraints \u2014 the
|
|
17991
|
+
local toolkit + vendored runtime the agent
|
|
17992
|
+
owns. Catalog actions (genex animations
|
|
17993
|
+
search) stay the first stop; motion covers
|
|
17994
|
+
the verbs the catalog lacks.
|
|
16430
17995
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
16431
17996
|
print its asset URL(s) when it finishes. Safe to
|
|
16432
17997
|
re-run \u2014 it never creates a new generation.
|
|
@@ -16578,6 +18143,8 @@ ${c.bold("Options for `controller character` / legacy `controller anims`")}
|
|
|
16578
18143
|
|
|
16579
18144
|
${UI_USAGE}
|
|
16580
18145
|
|
|
18146
|
+
${MOTION_USAGE}
|
|
18147
|
+
|
|
16581
18148
|
${c.bold("Global")}
|
|
16582
18149
|
--quiet Reduce output.
|
|
16583
18150
|
-h, --help Show this help.
|
|
@@ -16626,6 +18193,10 @@ ${c.bold("Examples")}
|
|
|
16626
18193
|
genex controller anims sword pistol
|
|
16627
18194
|
genex controller touch
|
|
16628
18195
|
genex controller networked-physics
|
|
18196
|
+
genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
|
|
18197
|
+
genex motion verify takes/ --json
|
|
18198
|
+
genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
|
|
18199
|
+
genex motion install --set rifle
|
|
16629
18200
|
`;
|
|
16630
18201
|
function parseArgs(argv) {
|
|
16631
18202
|
const parsed = {
|
|
@@ -16687,7 +18258,15 @@ function parseArgs(argv) {
|
|
|
16687
18258
|
"--candidate",
|
|
16688
18259
|
"--approve-remesh",
|
|
16689
18260
|
"--category",
|
|
16690
|
-
"--limit"
|
|
18261
|
+
"--limit",
|
|
18262
|
+
// `genex motion` value flags.
|
|
18263
|
+
"--takes",
|
|
18264
|
+
"--seeds",
|
|
18265
|
+
"--constraints",
|
|
18266
|
+
"--gates",
|
|
18267
|
+
"--config",
|
|
18268
|
+
"--set",
|
|
18269
|
+
"--speed"
|
|
16691
18270
|
]);
|
|
16692
18271
|
let i = 0;
|
|
16693
18272
|
while (i < argv.length) {
|
|
@@ -16814,6 +18393,12 @@ function parseArgs(argv) {
|
|
|
16814
18393
|
}
|
|
16815
18394
|
} else if (parsed.command === "creature") {
|
|
16816
18395
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18396
|
+
} else if (parsed.command === "motion") {
|
|
18397
|
+
if (parsed.options.name === "gen") {
|
|
18398
|
+
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
18399
|
+
} else {
|
|
18400
|
+
(parsed.options.selectors ??= []).push(arg);
|
|
18401
|
+
}
|
|
16817
18402
|
} else if (parsed.command === "animations") {
|
|
16818
18403
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
16819
18404
|
} else {
|
|
@@ -16929,6 +18514,37 @@ function applyValueFlag(options, flag, value) {
|
|
|
16929
18514
|
case "--category":
|
|
16930
18515
|
options.category = value;
|
|
16931
18516
|
break;
|
|
18517
|
+
case "--takes": {
|
|
18518
|
+
const n = Number(value);
|
|
18519
|
+
if (!Number.isInteger(n) || n < 1 || n > 8) {
|
|
18520
|
+
throw new Error(`Invalid --takes value: ${value} (expected an integer 1-8)`);
|
|
18521
|
+
}
|
|
18522
|
+
options.takes = n;
|
|
18523
|
+
break;
|
|
18524
|
+
}
|
|
18525
|
+
case "--seeds":
|
|
18526
|
+
options.seeds = value;
|
|
18527
|
+
break;
|
|
18528
|
+
case "--constraints":
|
|
18529
|
+
options.constraintsPath = value;
|
|
18530
|
+
break;
|
|
18531
|
+
case "--gates":
|
|
18532
|
+
options.gatesPath = value;
|
|
18533
|
+
break;
|
|
18534
|
+
case "--config":
|
|
18535
|
+
options.configPath = value;
|
|
18536
|
+
break;
|
|
18537
|
+
case "--set":
|
|
18538
|
+
options.set = value;
|
|
18539
|
+
break;
|
|
18540
|
+
case "--speed": {
|
|
18541
|
+
const n = Number(value);
|
|
18542
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
18543
|
+
throw new Error(`Invalid --speed value: ${value}`);
|
|
18544
|
+
}
|
|
18545
|
+
options.speed = n;
|
|
18546
|
+
break;
|
|
18547
|
+
}
|
|
16932
18548
|
case "--limit": {
|
|
16933
18549
|
const n = Number(value);
|
|
16934
18550
|
if (!Number.isInteger(n) || n < 1 || n > 100) {
|
|
@@ -17155,6 +18771,9 @@ async function main() {
|
|
|
17155
18771
|
case "ui":
|
|
17156
18772
|
await runUi({ ...parsed.options, sub: parsed.options.name });
|
|
17157
18773
|
break;
|
|
18774
|
+
case "motion":
|
|
18775
|
+
await runMotion({ ...parsed.options, sub: parsed.options.name });
|
|
18776
|
+
break;
|
|
17158
18777
|
case "wait":
|
|
17159
18778
|
await runWait({ ...parsed.options, id: parsed.options.name });
|
|
17160
18779
|
break;
|