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