@genex-ai/cli-demo 0.91.0-dev.228 → 0.92.0-dev.229
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 +302 -25
- package/package.json +1 -1
- package/templates/skills/genex-ai-character/SKILL.md +37 -17
- package/templates/skills/genex-ai-character/references/motion-generation.md +125 -0
- package/templates/skills/genex-game-director/references/routing-map.md +1 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +5 -4
- package/templates/skills/genex-threejs-creatures/SKILL.md +14 -4
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +2 -2
- package/templates/skills/genex-ai-character/references/motion.md +0 -145
package/dist/index.js
CHANGED
|
@@ -252,7 +252,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
|
|
|
252
252
|
var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
253
253
|
# Genex build contract (always in effect for this game)
|
|
254
254
|
|
|
255
|
-
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
255
|
+
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
256
256
|
|
|
257
257
|
1. Load the \`genex-game-director\` skill and follow it end to end before any game code. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
258
258
|
2. The design interview always happens: 2\u20134 build-forking questions, ONE round, before DESIGN.md is written \u2014 even when the request already names the game (then it confirms the pitch). Ask with your built-in question tool with clickable answer options when you have one; a short numbered list in plain chat otherwise. Only after asking may you proceed on stated assumptions if the player stays silent; record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
|
|
@@ -3536,7 +3536,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3536
3536
|
} catch {
|
|
3537
3537
|
}
|
|
3538
3538
|
}
|
|
3539
|
-
const referenced = (
|
|
3539
|
+
const referenced = (basename2) => sources.some((s) => s.text.includes(basename2));
|
|
3540
3540
|
const findings = [...viewportFindings];
|
|
3541
3541
|
const assetFiles = await walkFiles(absAssets);
|
|
3542
3542
|
const metaByName = /* @__PURE__ */ new Map();
|
|
@@ -4476,7 +4476,10 @@ var KIND_WAIT_TIMEOUT_MS = {
|
|
|
4476
4476
|
character_concept: 15 * 60 * 1e3,
|
|
4477
4477
|
character_preview: 30 * 60 * 1e3,
|
|
4478
4478
|
character: 30 * 60 * 1e3,
|
|
4479
|
-
character_animation: 20 * 60 * 1e3
|
|
4479
|
+
character_animation: 20 * 60 * 1e3,
|
|
4480
|
+
// Per clip this lane is fast (locomotion ~2s, video ~51s, described ~130s) —
|
|
4481
|
+
// the BATCH is what takes time. A 20-clip standard set is the sizing case.
|
|
4482
|
+
character_motion: 30 * 60 * 1e3
|
|
4480
4483
|
};
|
|
4481
4484
|
var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
|
|
4482
4485
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -4592,7 +4595,8 @@ function printHint(kind, view, files, log) {
|
|
|
4592
4595
|
character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
|
|
4593
4596
|
character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
|
|
4594
4597
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
4595
|
-
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing."
|
|
4598
|
+
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
|
|
4599
|
+
character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`
|
|
4596
4600
|
};
|
|
4597
4601
|
log.dim(` ${hint[kind]}`);
|
|
4598
4602
|
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
@@ -16486,6 +16490,226 @@ async function runCharacterAnimate(opts) {
|
|
|
16486
16490
|
});
|
|
16487
16491
|
}
|
|
16488
16492
|
|
|
16493
|
+
// src/commands/character-motion.ts
|
|
16494
|
+
import { readFile } from "fs/promises";
|
|
16495
|
+
import { basename } from "path";
|
|
16496
|
+
var VIDEO_SECONDS = [3, 4, 5, 6];
|
|
16497
|
+
var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
16498
|
+
async function context2(opts) {
|
|
16499
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
16500
|
+
if (!token) return null;
|
|
16501
|
+
const project = await readProject();
|
|
16502
|
+
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
16503
|
+
}
|
|
16504
|
+
async function readVideo(path19, log) {
|
|
16505
|
+
let bytes;
|
|
16506
|
+
try {
|
|
16507
|
+
bytes = await readFile(path19);
|
|
16508
|
+
} catch {
|
|
16509
|
+
log.error(`Can't read ${path19}.`);
|
|
16510
|
+
return null;
|
|
16511
|
+
}
|
|
16512
|
+
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
16513
|
+
log.error(`${basename(path19)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
16514
|
+
return null;
|
|
16515
|
+
}
|
|
16516
|
+
return bytes;
|
|
16517
|
+
}
|
|
16518
|
+
async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
|
|
16519
|
+
const contentType = /\.mov$/i.test(path19) ? "video/quicktime" : "video/mp4";
|
|
16520
|
+
const minted = await apiFetch(
|
|
16521
|
+
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
16522
|
+
{
|
|
16523
|
+
method: "POST",
|
|
16524
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
16525
|
+
body: JSON.stringify({ bytes: bytes.byteLength, contentType })
|
|
16526
|
+
}
|
|
16527
|
+
);
|
|
16528
|
+
if (printedStructuredError(minted)) return null;
|
|
16529
|
+
if (!minted.ok) {
|
|
16530
|
+
log.error(`Couldn't start the video upload (HTTP ${minted.status}).`);
|
|
16531
|
+
return null;
|
|
16532
|
+
}
|
|
16533
|
+
const { uploadUrl, videoUrl } = await minted.json();
|
|
16534
|
+
log.dim(` uploading ${basename(path19)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
16535
|
+
const put = await fetch(uploadUrl, {
|
|
16536
|
+
method: "PUT",
|
|
16537
|
+
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
16538
|
+
body: new Uint8Array(bytes)
|
|
16539
|
+
});
|
|
16540
|
+
if (!put.ok) {
|
|
16541
|
+
log.error(`Video upload failed (HTTP ${put.status}).`);
|
|
16542
|
+
return null;
|
|
16543
|
+
}
|
|
16544
|
+
return videoUrl;
|
|
16545
|
+
}
|
|
16546
|
+
function printPlan(plan, log, creature) {
|
|
16547
|
+
log.plain(c.bold(creature ? "Creature animation plan" : "Animation plan"));
|
|
16548
|
+
for (const item of plan.items) {
|
|
16549
|
+
const count = item.clips > 1 ? ` (${item.clips} clips)` : "";
|
|
16550
|
+
log.plain(` \xB7 ${c.bold(item.request)}${count} \u2014 ${item.explanation}`);
|
|
16551
|
+
if (item.supersededSlots.length > 0) {
|
|
16552
|
+
log.plain(
|
|
16553
|
+
` ${c.dim(`${item.supersededSlots.join(", ")} stay on the reviewed pack clips \u2014 those slots are pinned.`)}`
|
|
16554
|
+
);
|
|
16555
|
+
}
|
|
16556
|
+
}
|
|
16557
|
+
if (plan.secondsApplied === false) {
|
|
16558
|
+
log.plain(
|
|
16559
|
+
` ${c.dim("--duration sets the reference-video length; none of these verbs took the video route, so it did nothing.")}`
|
|
16560
|
+
);
|
|
16561
|
+
}
|
|
16562
|
+
log.plain("");
|
|
16563
|
+
if (plan.credits > 0) log.dim(` ${plan.totalClips} clips \xB7 ${plan.credits} credits`);
|
|
16564
|
+
else log.dim(` ${plan.totalClips} clips`);
|
|
16565
|
+
log.plain("");
|
|
16566
|
+
}
|
|
16567
|
+
async function runCharacterAnimateMotion(opts) {
|
|
16568
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
16569
|
+
const characterId = opts.characterId?.trim();
|
|
16570
|
+
if (!characterId) {
|
|
16571
|
+
log.error("Missing character id.");
|
|
16572
|
+
process.exitCode = 1;
|
|
16573
|
+
return;
|
|
16574
|
+
}
|
|
16575
|
+
const verbs = (opts.verbs ?? []).map((v) => v.trim()).filter(Boolean);
|
|
16576
|
+
if (verbs.length === 0 && !opts.locomotion) {
|
|
16577
|
+
log.error(
|
|
16578
|
+
`Say what the character should do. ${c.cyan(`genex character animate ${characterId} "overhead slam"`)}`
|
|
16579
|
+
);
|
|
16580
|
+
log.dim(` Or generate the movement set: ${c.cyan(`genex character animate ${characterId} --locomotion`)}`);
|
|
16581
|
+
process.exitCode = 1;
|
|
16582
|
+
return;
|
|
16583
|
+
}
|
|
16584
|
+
if (opts.video && verbs.length !== 1) {
|
|
16585
|
+
log.error("--video applies to exactly one verb \u2014 name the action the footage shows.");
|
|
16586
|
+
process.exitCode = 1;
|
|
16587
|
+
return;
|
|
16588
|
+
}
|
|
16589
|
+
if (opts.duration !== void 0 && !VIDEO_SECONDS.includes(opts.duration)) {
|
|
16590
|
+
log.error(`--duration must be one of ${VIDEO_SECONDS.join(", ")} seconds (got ${opts.duration}).`);
|
|
16591
|
+
process.exitCode = 1;
|
|
16592
|
+
return;
|
|
16593
|
+
}
|
|
16594
|
+
if (opts.duration !== void 0 && opts.video) {
|
|
16595
|
+
log.error("--duration sets the length of a GENERATED reference video \u2014 it cannot change footage you supplied.");
|
|
16596
|
+
process.exitCode = 1;
|
|
16597
|
+
return;
|
|
16598
|
+
}
|
|
16599
|
+
let videoBytes;
|
|
16600
|
+
if (opts.video) {
|
|
16601
|
+
const read = await readVideo(opts.video, log);
|
|
16602
|
+
if (!read) {
|
|
16603
|
+
process.exitCode = 1;
|
|
16604
|
+
return;
|
|
16605
|
+
}
|
|
16606
|
+
videoBytes = read;
|
|
16607
|
+
}
|
|
16608
|
+
const ctx = await context2(opts);
|
|
16609
|
+
if (!ctx) {
|
|
16610
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
16611
|
+
process.exitCode = 1;
|
|
16612
|
+
return;
|
|
16613
|
+
}
|
|
16614
|
+
let videoUrl;
|
|
16615
|
+
if (opts.video && videoBytes) {
|
|
16616
|
+
const uploaded = await uploadVideo(ctx.apiUrl, ctx.token, characterId, opts.video, videoBytes, log);
|
|
16617
|
+
if (!uploaded) {
|
|
16618
|
+
process.exitCode = 1;
|
|
16619
|
+
return;
|
|
16620
|
+
}
|
|
16621
|
+
videoUrl = uploaded;
|
|
16622
|
+
}
|
|
16623
|
+
const response = await apiFetch(
|
|
16624
|
+
`${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/plan`,
|
|
16625
|
+
{
|
|
16626
|
+
method: "POST",
|
|
16627
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
16628
|
+
body: JSON.stringify({
|
|
16629
|
+
verbs,
|
|
16630
|
+
locomotion: Boolean(opts.locomotion),
|
|
16631
|
+
lean: Boolean(opts.lean),
|
|
16632
|
+
...videoUrl ? { videoUrl } : {},
|
|
16633
|
+
...opts.duration === void 0 ? {} : { seconds: opts.duration }
|
|
16634
|
+
})
|
|
16635
|
+
}
|
|
16636
|
+
);
|
|
16637
|
+
if (printedStructuredError(response)) {
|
|
16638
|
+
process.exitCode = 1;
|
|
16639
|
+
return;
|
|
16640
|
+
}
|
|
16641
|
+
if (!response.ok) {
|
|
16642
|
+
const body = await response.json().catch(() => ({}));
|
|
16643
|
+
log.error(body.message ?? `Couldn't plan those animations (HTTP ${response.status}).`);
|
|
16644
|
+
process.exitCode = 1;
|
|
16645
|
+
return;
|
|
16646
|
+
}
|
|
16647
|
+
const planned = await response.json();
|
|
16648
|
+
if (opts.json) {
|
|
16649
|
+
writeJson({ kind: "character_motion", stage: "plan", ...planned.plan });
|
|
16650
|
+
} else {
|
|
16651
|
+
printPlan(planned.plan, log, Boolean(opts.creature));
|
|
16652
|
+
}
|
|
16653
|
+
await runGenerate("character_motion", {
|
|
16654
|
+
...opts,
|
|
16655
|
+
prompt: verbs.length > 0 ? `Animate character ${characterId}: ${verbs.join(", ")}` : `Generate the locomotion set for character ${characterId}`,
|
|
16656
|
+
token: ctx.token,
|
|
16657
|
+
apiUrl: ctx.apiUrl,
|
|
16658
|
+
generationOptions: planned.request.options
|
|
16659
|
+
});
|
|
16660
|
+
}
|
|
16661
|
+
async function runCharacterMotions(opts) {
|
|
16662
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
16663
|
+
const characterId = opts.characterId?.trim();
|
|
16664
|
+
if (!characterId) {
|
|
16665
|
+
log.error("Missing character id.");
|
|
16666
|
+
process.exitCode = 1;
|
|
16667
|
+
return;
|
|
16668
|
+
}
|
|
16669
|
+
const ctx = await context2(opts);
|
|
16670
|
+
if (!ctx) {
|
|
16671
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
16672
|
+
process.exitCode = 1;
|
|
16673
|
+
return;
|
|
16674
|
+
}
|
|
16675
|
+
const response = await apiFetch(
|
|
16676
|
+
`${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/manifest`,
|
|
16677
|
+
{ headers: { Authorization: `Bearer ${ctx.token}` } }
|
|
16678
|
+
);
|
|
16679
|
+
if (printedStructuredError(response)) {
|
|
16680
|
+
process.exitCode = 1;
|
|
16681
|
+
return;
|
|
16682
|
+
}
|
|
16683
|
+
if (!response.ok) {
|
|
16684
|
+
log.error(`Couldn't read that character (HTTP ${response.status}).`);
|
|
16685
|
+
process.exitCode = 1;
|
|
16686
|
+
return;
|
|
16687
|
+
}
|
|
16688
|
+
const { manifest } = await response.json();
|
|
16689
|
+
if (opts.json) {
|
|
16690
|
+
writeJson({
|
|
16691
|
+
kind: "character_motion",
|
|
16692
|
+
stage: "list",
|
|
16693
|
+
characterId,
|
|
16694
|
+
clips: manifest.clips.map((clip) => ({
|
|
16695
|
+
key: clip.key,
|
|
16696
|
+
name: clip.name,
|
|
16697
|
+
provider: clip.provider ?? "meshy",
|
|
16698
|
+
duration: clip.duration,
|
|
16699
|
+
slots: Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot)
|
|
16700
|
+
}))
|
|
16701
|
+
});
|
|
16702
|
+
return;
|
|
16703
|
+
}
|
|
16704
|
+
log.plain(c.bold(`${manifest.clips.length} clips on ${characterId}`));
|
|
16705
|
+
for (const clip of manifest.clips) {
|
|
16706
|
+
const slots = Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot);
|
|
16707
|
+
const where = slots.length > 0 ? ` \u2192 ${slots.join(", ")}` : "";
|
|
16708
|
+
const source = clip.provider === "uthana" ? c.dim(" generated") : "";
|
|
16709
|
+
log.plain(` ${clip.key}${source}${where}`);
|
|
16710
|
+
}
|
|
16711
|
+
}
|
|
16712
|
+
|
|
16489
16713
|
// src/commands/animations.ts
|
|
16490
16714
|
function animationSearchView(entry) {
|
|
16491
16715
|
return {
|
|
@@ -18201,20 +18425,26 @@ ${c.bold("Usage")}
|
|
|
18201
18425
|
genex character finalize <preview-id>
|
|
18202
18426
|
Finalize an approved preview as a 10,000-face
|
|
18203
18427
|
controller-ready Meshy character.
|
|
18204
|
-
genex character animate <id>
|
|
18428
|
+
genex character animate <id> "<verb>" \u2026
|
|
18429
|
+
Animate the character: say what it should DO,
|
|
18430
|
+
in plain words ("overhead slam"), one clip per
|
|
18431
|
+
verb. The platform picks how each is made and
|
|
18432
|
+
prints the plan before anything is charged.
|
|
18433
|
+
--locomotion generates the 8-way walk + run set;
|
|
18434
|
+
--video ./clip.mp4 uses your own footage;
|
|
18435
|
+
--duration 3-6 lengthens the reference video for
|
|
18436
|
+
multi-beat moves; --action <id-or-query> still
|
|
18437
|
+
picks a catalog clip.
|
|
18438
|
+
genex character motions <id> List the clips installed on that character.
|
|
18205
18439
|
genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
|
|
18206
18440
|
bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
|
|
18207
18441
|
bind library clips via --animation. No approval
|
|
18208
18442
|
ceremony, no player controller pack.
|
|
18443
|
+
genex creature animate <id> "<verb>" \u2026
|
|
18444
|
+
Same as character animate, for an enemy \u2014 a boss's
|
|
18445
|
+
signature move. --lean keeps movement to forward
|
|
18446
|
+
walk + run (enemies follow a path).
|
|
18209
18447
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
18210
|
-
genex motion <sub> [args] Custom character animations from text (ARDY):
|
|
18211
|
-
gen "<prompt>" (GPU service, bills per take) |
|
|
18212
|
-
verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
|
|
18213
|
-
install [--set rifle] | constraints \u2014 the
|
|
18214
|
-
local toolkit + vendored runtime the agent
|
|
18215
|
-
owns. Catalog actions (genex animations
|
|
18216
|
-
search) stay the first stop; motion covers
|
|
18217
|
-
the verbs the catalog lacks.
|
|
18218
18448
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
18219
18449
|
print its asset URL(s) when it finishes. Safe to
|
|
18220
18450
|
re-run \u2014 it never creates a new generation.
|
|
@@ -18248,7 +18478,10 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `te
|
|
|
18248
18478
|
appended environment-only clause AND the refusal on
|
|
18249
18479
|
prompts that name structures/objects (a sky is sky only;
|
|
18250
18480
|
landforms and weather always pass).
|
|
18251
|
-
--duration <sec> (sfx, video, music) target clip length in seconds
|
|
18481
|
+
--duration <sec> (sfx, video, music) target clip length in seconds;
|
|
18482
|
+
(character/creature animate) 3-6 s of reference video for
|
|
18483
|
+
verbs that route to AI video (default 3) \u2014 longer gives a
|
|
18484
|
+
multi-beat move room to land instead of rushing it.
|
|
18252
18485
|
(music: 10-300, default 90 \u2014 a looping gameplay track).
|
|
18253
18486
|
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
18254
18487
|
--glass (image) magenta-key glass lane: generate the panel on a
|
|
@@ -18370,8 +18603,6 @@ ${c.bold("Options for `controller character` / legacy `controller anims`")}
|
|
|
18370
18603
|
|
|
18371
18604
|
${UI_USAGE}
|
|
18372
18605
|
|
|
18373
|
-
${MOTION_USAGE}
|
|
18374
|
-
|
|
18375
18606
|
${c.bold("Global")}
|
|
18376
18607
|
--quiet Reduce output.
|
|
18377
18608
|
-h, --help Show this help.
|
|
@@ -18415,15 +18646,17 @@ ${c.bold("Examples")}
|
|
|
18415
18646
|
genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
|
|
18416
18647
|
genex character "stylized sci-fi courier" --direct-text --no-wait
|
|
18417
18648
|
genex character animate <character-id> --action <action-id>
|
|
18649
|
+
genex character animate <character-id> "overhead slam" "parry and recover" --no-wait
|
|
18650
|
+
genex character animate <character-id> --locomotion --no-wait
|
|
18651
|
+
genex character animate <character-id> "leap the gap and roll on landing" --duration 6
|
|
18652
|
+
genex character animate <character-id> "victory pose" --video ./take-3.mp4
|
|
18653
|
+
genex character motions <character-id>
|
|
18418
18654
|
genex creature "hulking bone seraph, upright stance" --animation <walk-id> --animation <attack-id> --no-wait
|
|
18655
|
+
genex creature animate <creature-id> "wind-up ground pound" --no-wait
|
|
18419
18656
|
genex controller character --character <character-id>
|
|
18420
18657
|
genex controller anims sword pistol
|
|
18421
18658
|
genex controller touch
|
|
18422
18659
|
genex controller networked-physics
|
|
18423
|
-
genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
|
|
18424
|
-
genex motion verify takes/ --json
|
|
18425
|
-
genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
|
|
18426
|
-
genex motion install --set rifle
|
|
18427
18660
|
`;
|
|
18428
18661
|
function parseArgs(argv) {
|
|
18429
18662
|
const parsed = {
|
|
@@ -18493,7 +18726,8 @@ function parseArgs(argv) {
|
|
|
18493
18726
|
"--gates",
|
|
18494
18727
|
"--config",
|
|
18495
18728
|
"--set",
|
|
18496
|
-
"--speed"
|
|
18729
|
+
"--speed",
|
|
18730
|
+
"--video"
|
|
18497
18731
|
]);
|
|
18498
18732
|
let i = 0;
|
|
18499
18733
|
while (i < argv.length) {
|
|
@@ -18529,6 +18763,12 @@ function parseArgs(argv) {
|
|
|
18529
18763
|
case "--direct-text":
|
|
18530
18764
|
parsed.options.directText = true;
|
|
18531
18765
|
break;
|
|
18766
|
+
case "--locomotion":
|
|
18767
|
+
parsed.options.locomotion = true;
|
|
18768
|
+
break;
|
|
18769
|
+
case "--lean":
|
|
18770
|
+
parsed.options.lean = true;
|
|
18771
|
+
break;
|
|
18532
18772
|
case "--in-place":
|
|
18533
18773
|
parsed.options.inPlace = true;
|
|
18534
18774
|
break;
|
|
@@ -18609,20 +18849,27 @@ function parseArgs(argv) {
|
|
|
18609
18849
|
} else if (parsed.command === "controller") {
|
|
18610
18850
|
(parsed.options.selectors ??= []).push(arg);
|
|
18611
18851
|
} else if (parsed.command === "character") {
|
|
18612
|
-
if (parsed.options.name === "animate" && !parsed.options.characterId) {
|
|
18852
|
+
if ((parsed.options.name === "animate" || parsed.options.name === "motions") && !parsed.options.characterId) {
|
|
18613
18853
|
parsed.options.characterId = arg;
|
|
18854
|
+
} else if (parsed.options.name === "animate") {
|
|
18855
|
+
(parsed.options.verbs ??= []).push(arg);
|
|
18614
18856
|
} else if (parsed.options.name === "preview" && !parsed.options.conceptId) {
|
|
18615
18857
|
parsed.options.conceptId = arg;
|
|
18616
18858
|
} else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
|
|
18617
18859
|
parsed.options.previewId = arg;
|
|
18618
|
-
} else if (!["animate", "preview", "finalize"].includes(parsed.options.name ?? "")) {
|
|
18860
|
+
} else if (!["animate", "motions", "preview", "finalize"].includes(parsed.options.name ?? "")) {
|
|
18619
18861
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18620
18862
|
} else {
|
|
18621
18863
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
18622
18864
|
return parsed;
|
|
18623
18865
|
}
|
|
18624
18866
|
} else if (parsed.command === "creature") {
|
|
18625
|
-
parsed.options.name
|
|
18867
|
+
if (parsed.options.name === "animate") {
|
|
18868
|
+
if (!parsed.options.characterId) parsed.options.characterId = arg;
|
|
18869
|
+
else (parsed.options.verbs ??= []).push(arg);
|
|
18870
|
+
} else {
|
|
18871
|
+
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18872
|
+
}
|
|
18626
18873
|
} else if (parsed.command === "motion") {
|
|
18627
18874
|
if (parsed.options.name === "gen") {
|
|
18628
18875
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
@@ -18860,6 +19107,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
18860
19107
|
case "--frame":
|
|
18861
19108
|
options.frameUrl = value;
|
|
18862
19109
|
break;
|
|
19110
|
+
case "--video":
|
|
19111
|
+
options.video = value;
|
|
19112
|
+
break;
|
|
18863
19113
|
case "--first-frame":
|
|
18864
19114
|
options.firstFrameUrl = value;
|
|
18865
19115
|
break;
|
|
@@ -18959,7 +19209,13 @@ async function main() {
|
|
|
18959
19209
|
break;
|
|
18960
19210
|
case "character":
|
|
18961
19211
|
if (parsed.options.name === "animate") {
|
|
18962
|
-
|
|
19212
|
+
if ((parsed.options.actions?.length ?? 0) > 0) {
|
|
19213
|
+
await runCharacterAnimate(parsed.options);
|
|
19214
|
+
} else {
|
|
19215
|
+
await runCharacterAnimateMotion(parsed.options);
|
|
19216
|
+
}
|
|
19217
|
+
} else if (parsed.options.name === "motions") {
|
|
19218
|
+
await runCharacterMotions(parsed.options);
|
|
18963
19219
|
} else if (parsed.options.name === "preview") {
|
|
18964
19220
|
await runCharacterPreview(parsed.options);
|
|
18965
19221
|
} else if (parsed.options.name === "finalize") {
|
|
@@ -18969,6 +19225,10 @@ async function main() {
|
|
|
18969
19225
|
}
|
|
18970
19226
|
break;
|
|
18971
19227
|
case "creature":
|
|
19228
|
+
if (parsed.options.name === "animate") {
|
|
19229
|
+
await runCharacterAnimateMotion({ ...parsed.options, creature: true });
|
|
19230
|
+
break;
|
|
19231
|
+
}
|
|
18972
19232
|
if (!parsed.options.json && !parsed.options.quiet) {
|
|
18973
19233
|
log.dim(
|
|
18974
19234
|
" Biped-shaped creatures only (Meshy rig limit) \u2014 for true quadrupeds/fliers use a static model + the genex-threejs-creatures procedural recipes."
|
|
@@ -19001,6 +19261,23 @@ async function main() {
|
|
|
19001
19261
|
case "ui":
|
|
19002
19262
|
await runUi({ ...parsed.options, sub: parsed.options.name });
|
|
19003
19263
|
break;
|
|
19264
|
+
// PARKED (AG-824 D12) — still dispatches, deliberately unlisted in --help
|
|
19265
|
+
// and gone from the skills, the same way `genex explore` is parked.
|
|
19266
|
+
//
|
|
19267
|
+
// Why: `genex character animate` replaced it. That lane generates motion
|
|
19268
|
+
// that binds to the character's OWN rig with no retargeting (measured: the
|
|
19269
|
+
// provider returns our exact skeleton), which is what the ~1,700 lines of
|
|
19270
|
+
// local retargeting behind this command existed to solve.
|
|
19271
|
+
//
|
|
19272
|
+
// Everything under it is intact and dormant: src/commands/motion.ts,
|
|
19273
|
+
// src/lib/motion/**, templates/motion/**, the API's runpod-motion provider
|
|
19274
|
+
// and its `motion` kind, and the RunPod worker (idle cost measured at $0).
|
|
19275
|
+
//
|
|
19276
|
+
// To re-enable: restore MOTION_USAGE + the examples in this file and the
|
|
19277
|
+
// skill cross-links. The trap that already cost time —
|
|
19278
|
+
// RUNPOD_MOTION_ENDPOINT_ID must be a GitHub Environment `vars.` entry,
|
|
19279
|
+
// NOT a secret of the same name: a same-named secret upserts EMPTY, and
|
|
19280
|
+
// the provider then silently falls back to the mock with no error anywhere.
|
|
19004
19281
|
case "motion":
|
|
19005
19282
|
await runMotion({ ...parsed.options, sub: parsed.options.name });
|
|
19006
19283
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0-dev.229",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -169,44 +169,64 @@ Catalog entries are metadata-reviewed. Inspect the preview before choosing a
|
|
|
169
169
|
specialty action. Only provider-declared `InPlace` loops or measured overrides
|
|
170
170
|
may fill locomotion slots automatically.
|
|
171
171
|
|
|
172
|
-
## Custom animations — `genex
|
|
172
|
+
## Custom animations — `genex character animate "<verb>"`
|
|
173
173
|
|
|
174
174
|
Catalog search is ALWAYS the first stop — its stock actions cover most verbs
|
|
175
|
-
with no
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
locally into a rig-independent set:
|
|
175
|
+
with no spend. When the catalog genuinely lacks the motion (a signature move, a
|
|
176
|
+
boss telegraph, a full 8-way movement set), say what the character should DO in
|
|
177
|
+
plain words and the platform generates it for this character's own rig:
|
|
179
178
|
|
|
180
179
|
```bash
|
|
181
|
-
npx genex
|
|
182
|
-
npx genex
|
|
183
|
-
npx genex
|
|
184
|
-
npx genex motion install --set rifle # vendored runtime + the proven rifle starter set
|
|
180
|
+
npx genex character animate <character-id> "overhead slam" "parry and recover" --no-wait
|
|
181
|
+
npx genex character animate <character-id> --locomotion --no-wait
|
|
182
|
+
npx genex character animate <character-id> "victory pose" --video ./take-3.mp4
|
|
185
183
|
```
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
One clip per verb. You never choose the method — the platform routes each verb
|
|
186
|
+
(reuse a shared-library motion, generate movement, act it out on video first, or
|
|
187
|
+
generate from a description) and prints the plan BEFORE anything is charged.
|
|
188
|
+
|
|
189
|
+
**Show the plan to the user and let them change it before you continue.** That
|
|
190
|
+
is the one stop in this lane; everything after it runs in the background while
|
|
191
|
+
you keep building.
|
|
192
|
+
|
|
193
|
+
Read [references/motion-generation.md](references/motion-generation.md) before
|
|
194
|
+
the first run: it carries what makes a good verb, which verbs this cannot do,
|
|
195
|
+
and the footage requirements for `--video`.
|
|
196
|
+
|
|
197
|
+
`--locomotion` generates the full 8-way walk + run set (16 clips) — the
|
|
198
|
+
directional slots the shipped controller resolves but the stock pack leaves
|
|
199
|
+
empty, so a strafe stops being a faked forward walk. For an enemy that just
|
|
200
|
+
follows a path, `--lean` keeps it to forward walk + run.
|
|
201
|
+
|
|
202
|
+
Animate an enemy the same way — `genex creature animate <id> "<verb>"`. A
|
|
203
|
+
creature is already a character underneath; the alias exists so you don't have
|
|
204
|
+
to know that.
|
|
195
205
|
|
|
196
206
|
## Add actions and refresh the game
|
|
197
207
|
|
|
198
208
|
```bash
|
|
199
209
|
npx genex character animate <character-id> --action <action-id>
|
|
200
210
|
npx genex character animate <character-id> --action <first-id> --action <second-id> --no-wait
|
|
211
|
+
npx genex character motions <character-id> # what is installed right now
|
|
201
212
|
npx genex controller character --character <character-id>
|
|
202
213
|
```
|
|
203
214
|
|
|
215
|
+
`--action` picks an existing catalog clip; a free-text verb generates one. Both
|
|
216
|
+
land in the same manifest and play through the same state machine.
|
|
217
|
+
|
|
204
218
|
Ambiguous text queries print ranked candidates instead of silently spending
|
|
205
219
|
credits. Already-installed actions return without another debit. After an
|
|
206
220
|
animation job completes, rerun `genex controller character --character <id>`
|
|
207
221
|
to refresh `public/assets/meshy-character.json`; existing controller source is
|
|
208
222
|
preserved unless `--force` is explicitly used.
|
|
209
223
|
|
|
224
|
+
Six slots belong to the reviewed controller pack and cannot be replaced —
|
|
225
|
+
`idle.default`, `walk.forward`, `run.forward`, `crouch.forward`, `crouch.idle`,
|
|
226
|
+
`jump.full`. A generated locomotion set fills the other fourteen directions; the
|
|
227
|
+
plan says so before you spend anything. A character built without the pack
|
|
228
|
+
(every `genex creature`) takes all sixteen.
|
|
229
|
+
|
|
210
230
|
Meshy manifests bypass browser cache, so newly installed actions must work
|
|
211
231
|
after preview without asking the player to disable cache.
|
|
212
232
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Generating character motion
|
|
2
|
+
|
|
3
|
+
`npx genex character animate <character-id> "<verb>"` — one clip per verb, on
|
|
4
|
+
this character's own rig. Read this before the first run; it is short, and the
|
|
5
|
+
first two sections are where the credits go.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx genex character animate <id> "overhead slam" --no-wait
|
|
9
|
+
npx genex character animate <id> --locomotion --no-wait
|
|
10
|
+
npx genex character animate <id> "leap the gap and roll on landing" --duration 6
|
|
11
|
+
npx genex character animate <id> "victory pose" --video ./take-3.mp4
|
|
12
|
+
npx genex character motions <id>
|
|
13
|
+
npx genex creature animate <id> "wind-up ground pound" --no-wait
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
You do not choose how a verb is made. The platform prints a plan first — which
|
|
17
|
+
verbs it will reuse, generate, or act out on video, and one total. **Show the
|
|
18
|
+
plan to the user.** That is the single stop in this lane; after it, everything
|
|
19
|
+
runs in the background while you keep building. Collect it later with
|
|
20
|
+
`genex wait --all`.
|
|
21
|
+
|
|
22
|
+
## Write the verb like a stage direction
|
|
23
|
+
|
|
24
|
+
The generator turns your words into a body performance, so name the BODY, not
|
|
25
|
+
the intent. "Overhead slam" is a motion; "attack" is a category.
|
|
26
|
+
|
|
27
|
+
| instead of | write |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `attack` | `heavy two-handed overhead slam, ending in a low crouch` |
|
|
30
|
+
| `hurt` | `stagger back two steps from a chest impact, arms flailing` |
|
|
31
|
+
| `win` | `plant both feet, raise one fist overhead, chest out` |
|
|
32
|
+
| `die` | `drop to the knees, then fall forward onto the chest` |
|
|
33
|
+
|
|
34
|
+
Three things make a verb generate well:
|
|
35
|
+
|
|
36
|
+
- **Name the beats.** A wind-up, a committed main beat, a recovery. A motion with
|
|
37
|
+
one beat reads as a twitch.
|
|
38
|
+
- **Say where the weight goes.** "Sinks into the knees as it lands" is the single
|
|
39
|
+
most useful phrase you can add — without it you tend to get a bow from the
|
|
40
|
+
waist where you wanted a crouch.
|
|
41
|
+
- **Keep it one person, standing, on the floor.** No props that define the pose,
|
|
42
|
+
no partner, no furniture.
|
|
43
|
+
|
|
44
|
+
## What this cannot do
|
|
45
|
+
|
|
46
|
+
Do not spend on these — pick a different approach instead:
|
|
47
|
+
|
|
48
|
+
- **Fingers and fine manipulation** — picking a lock, typing, a trigger squeeze,
|
|
49
|
+
threading a needle. Generated rigs have no finger bones, so the hand simply
|
|
50
|
+
will not do it. Fake it with a prop animation or a cut.
|
|
51
|
+
- **Two characters interacting** — a handshake, a grapple, a carry. Only one body
|
|
52
|
+
is generated. Animate each side separately and time them in code.
|
|
53
|
+
- **Anything a prop defines** — swinging on a rope, climbing a specific ladder,
|
|
54
|
+
sitting in a specific chair. The clip does not know your geometry; drive those
|
|
55
|
+
with `$genex-threejs-procedural-animation` instead.
|
|
56
|
+
- **Non-bipeds** — quadrupeds, fliers, blobs. See `$genex-threejs-creatures`;
|
|
57
|
+
those stay static models plus procedural motion.
|
|
58
|
+
|
|
59
|
+
## Room to land the beats: `--duration`
|
|
60
|
+
|
|
61
|
+
Some verbs are made by generating a short reference video of a performer and
|
|
62
|
+
converting THAT into motion — the plan says which ones ("acted out on video").
|
|
63
|
+
The footage is a hard ceiling on the animation, and the default 3 seconds is a
|
|
64
|
+
tight fit for anything with more than one beat: a performer given too little
|
|
65
|
+
time rushes, and a rushed wind-up-slam-recover comes back as a bow.
|
|
66
|
+
|
|
67
|
+
`--duration 3|4|5|6` gives those verbs more room. Use it when the verb you wrote
|
|
68
|
+
genuinely has three beats — a leap with a landing roll, a stagger that recovers,
|
|
69
|
+
a combo. Leave it alone for a single action; longer is not better, it is just
|
|
70
|
+
longer, and it costs more.
|
|
71
|
+
|
|
72
|
+
It only reaches verbs the platform routes to video, and the plan tells you
|
|
73
|
+
whether it applied. It cannot change footage you supplied with `--video` —
|
|
74
|
+
that clip already has a length.
|
|
75
|
+
|
|
76
|
+
## Movement: `--locomotion`
|
|
77
|
+
|
|
78
|
+
Generates the full 8-way walk + run set — 16 clips. This fills the directional
|
|
79
|
+
slots the shipped controller already resolves (`walk.forward-left`,
|
|
80
|
+
`run.back-right`, and so on) but that the stock pack leaves empty, so strafing
|
|
81
|
+
stops being a forward walk played sideways.
|
|
82
|
+
|
|
83
|
+
Six slots belong to the reviewed controller pack and cannot be replaced:
|
|
84
|
+
`idle.default`, `walk.forward`, `run.forward`, `crouch.forward`, `crouch.idle`,
|
|
85
|
+
`jump.full`. A generated set fills the other fourteen; the plan says which. A
|
|
86
|
+
character built without the pack — every `genex creature` — takes all sixteen.
|
|
87
|
+
|
|
88
|
+
For an enemy that follows a path, `--lean` generates forward walk + run only.
|
|
89
|
+
Enemies have no controller state machine driving a direction picker, so the
|
|
90
|
+
other six directions would never play. Ask for the full set when a boss
|
|
91
|
+
genuinely circles the player.
|
|
92
|
+
|
|
93
|
+
## Your own footage: `--video`
|
|
94
|
+
|
|
95
|
+
`--video ./clip.mp4` converts a real performance instead of a generated one.
|
|
96
|
+
The footage is a hard ceiling on the result — nothing can add weight the
|
|
97
|
+
performer never committed — so it has to meet the extractor's requirements:
|
|
98
|
+
|
|
99
|
+
- **Exactly one person**, alone in frame.
|
|
100
|
+
- **Whole body visible**, head to feet, for the entire clip. A limb that leaves
|
|
101
|
+
the frame is a gap that cannot be recovered.
|
|
102
|
+
- **Locked-off camera.** No pan, no zoom, no handheld drift, no cuts.
|
|
103
|
+
- **Plain contrasting background**, and clothing in distinct solid colors — no
|
|
104
|
+
long coat, cape, or skirt over the legs.
|
|
105
|
+
- **Even lighting.** Heavy shadows or a silhouette degrade the extraction.
|
|
106
|
+
- **2–60 seconds.** `.mp4` or `.mov`.
|
|
107
|
+
|
|
108
|
+
Phone recordings work. So do screen captures and gameplay clips.
|
|
109
|
+
|
|
110
|
+
The video is checked against these rules before anything is converted, and a
|
|
111
|
+
failure tells you which requirement broke and roughly when — re-shoot against
|
|
112
|
+
that note rather than re-running the same file.
|
|
113
|
+
|
|
114
|
+
## After the clips land
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx genex controller character --character <character-id>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then play the game and watch the motion on the real character. Numbers and a
|
|
121
|
+
completed job prove the clip installed; only your eyes prove it reads right.
|
|
122
|
+
|
|
123
|
+
A clip that plays as a T-pose or does not play at all means it did not bind —
|
|
124
|
+
check `genex character motions <id>` to see what is actually installed. Do not
|
|
125
|
+
"fix" it with runtime bone corrections; regenerate it.
|
|
@@ -22,6 +22,7 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
22
22
|
| shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, mouse-aimed action (shooter, FPS/first-person, sniper, turret, crosshair/reticle), mouse-look, hand-rolled steering/pan/look input signs (screen-direction contract), floating origins | `$genex-threejs-camera-direction` |
|
|
23
23
|
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, the player's body loader, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
|
|
24
24
|
| the game's own generated character — the player's BODY wherever a human body appears on screen (first-person included; the exemption is "no human body ever appears", not "the camera is in the head"), enqueued with the first art actions; one user stop riding the concept review, owner-ratified auto-pick on silence (director §7) — or Meshy animation coverage beyond UAL: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
|
|
25
|
+
| a character or enemy needs a motion the catalog lacks — a signature move, a boss telegraph, a death, a full 8-way movement set, or a performance from the user's own footage: free-text verbs, platform-routed, plan shown before any spend (`genex character animate <id> "<verb>"`, `genex creature animate`, `--locomotion`, `--video`) | `$genex-ai-character` (motion section + `references/motion-generation.md`) |
|
|
25
26
|
| remote player bodies in multiplayer — NEVER hand-built primitives: the game's generated character when it has one (everyone wears it), the player's `p.avatarUrl` VRM only when it doesn't | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
|
|
26
27
|
| the game has enemies, NPCs, or creatures — **load whenever an enemy roster exists**: rigged bipeds via `npx genex creature`, static + procedural motion for other body shapes, plus the mechanical floor every enemy owes (collider, verified facing, hit reaction, death moment) | `$genex-threejs-creatures` |
|
|
27
28
|
| the player drives or flies something: cars, drones, vehicle physics, gearbox, enter/exit between character and vehicle | `$genex-threejs-vehicle-controllers` |
|
|
@@ -56,10 +56,11 @@ way, so the character arriving mid-build is a file drop
|
|
|
56
56
|
(`npx genex controller character --character <id>`) and not an edit to your
|
|
57
57
|
most load-bearing code. Wire the block under "Minimal wiring" once, at hour 0.
|
|
58
58
|
|
|
59
|
-
For a full custom LOCOMOTION SET beyond the UAL packs
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
For a full custom LOCOMOTION SET beyond the UAL packs, see `$genex-ai-character`'s
|
|
60
|
+
motion section: `npx genex character animate <id> --locomotion` generates the
|
|
61
|
+
8-way walk + run set onto the character's own rig, filling the directional slots
|
|
62
|
+
this controller already resolves but the stock pack leaves empty. Signature moves
|
|
63
|
+
are the same command with a verb.
|
|
63
64
|
|
|
64
65
|
Before generating a Meshy character, discuss two or three visual directions.
|
|
65
66
|
When the user names a visual reference, inspect references before writing the
|
|
@@ -51,10 +51,20 @@ post-mixer limb corrections. Prompt the body UPRIGHT and unpropped (held
|
|
|
51
51
|
props fuse into bodies); prompt "facing the viewer" but never trust it —
|
|
52
52
|
facing is verified below.
|
|
53
53
|
|
|
54
|
-
When the catalog has no clip for a bespoke attack, telegraph, or death move
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
When the catalog has no clip for a bespoke attack, telegraph, or death move on
|
|
55
|
+
a rigged biped, generate one for this creature:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx genex creature animate <creature-id> "wind-up ground pound" --no-wait
|
|
59
|
+
npx genex creature animate <creature-id> --locomotion --lean --no-wait
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Say what the body does, in beats, with the weight named — "rears back, both
|
|
63
|
+
arms overhead, slams down and sinks into a low crouch" beats "attack". The
|
|
64
|
+
clips land in the creature's own manifest and play through the raw mixer wired
|
|
65
|
+
below; `--lean` keeps movement to forward walk + run, which is all an enemy
|
|
66
|
+
following a path can ever play. See the motion section of `$genex-ai-character`
|
|
67
|
+
for what this can and cannot generate.
|
|
58
68
|
|
|
59
69
|
### State → clip wiring (one mixer per creature)
|
|
60
70
|
|
|
@@ -11,8 +11,8 @@ coordinate frames, velocities, and ownership before writing per-frame updates.
|
|
|
11
11
|
Boundary: procedural animation owns held-object and analytic motion (arcs,
|
|
12
12
|
springs, docking, debris). WHOLE-BODY character verbs (a gait, a weapon
|
|
13
13
|
hold, a signature move) belong to the character lane — the catalog first,
|
|
14
|
-
then `genex
|
|
15
|
-
`$genex-ai-character`.
|
|
14
|
+
then `genex character animate <id> "<verb>"` for verbs it lacks; see the
|
|
15
|
+
motion section of `$genex-ai-character`.
|
|
16
16
|
|
|
17
17
|
## Build order
|
|
18
18
|
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# Custom animations with `genex motion` — the full procedure
|
|
2
|
-
|
|
3
|
-
Text → raw motion takes on the platform GPU service → objective verify →
|
|
4
|
-
YOUR pick → local compile → a rig-independent set the vendored runtime plays
|
|
5
|
-
on any humanoid. The skeleton is a 27-joint humanoid core at 20 fps; takes
|
|
6
|
-
arrive as `.npz` files plus a machine verify report. Everything after `gen`
|
|
7
|
-
runs locally and free — regenerate compiled sets as often as you like.
|
|
8
|
-
|
|
9
|
-
**Rig limits (say them to the user before promising a move):** biped,
|
|
10
|
-
human-proportioned characters only. Rigs without finger bones (many Meshy
|
|
11
|
-
characters) cannot curl fingers around a grip — prefer weapon-carry looks
|
|
12
|
-
that read well with flat hands there, or use a rig with fingers for hero
|
|
13
|
-
characters. Takes are 2–10 seconds.
|
|
14
|
-
|
|
15
|
-
## The loop
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
# 1. Generate (bills per take, default 4; ALWAYS --no-wait — jobs run minutes)
|
|
19
|
-
npx genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
|
|
20
|
-
npx genex wait <id> # prints one URL per take + the report
|
|
21
|
-
|
|
22
|
-
# 2. Download the takes it printed and RENAME them — filenames drive everything
|
|
23
|
-
mkdir -p takes && curl -o takes/rifle-idle.npz "<take-url>"
|
|
24
|
-
|
|
25
|
-
# 3. Verify locally (same math the service ran; free, reproducible)
|
|
26
|
-
npx genex motion verify takes/
|
|
27
|
-
|
|
28
|
-
# 4. Pick the takes whose MOTION reads right (the report is data, not taste),
|
|
29
|
-
# then compile the set
|
|
30
|
-
npx genex motion compile takes/ --out src/motion/rifle.json
|
|
31
|
-
|
|
32
|
-
# 5. Vendor the runtime once per game (+ the proven rifle starter set)
|
|
33
|
-
npx genex motion install --set rifle
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Take naming — the routing contract
|
|
37
|
-
|
|
38
|
-
`verify` and `compile` classify each take by its FILENAME stem. Rename
|
|
39
|
-
downloaded takes before running them:
|
|
40
|
-
|
|
41
|
-
| Stem pattern | Compiles into |
|
|
42
|
-
|---|---|
|
|
43
|
-
| `<set>-run-forward` / `-back` | `forward` / `back` gait |
|
|
44
|
-
| `<set>-strafe-left` / `-strafe-right` | `strafeLeft` / `strafeRight` |
|
|
45
|
-
| `<set>-run-fl` / `-fr` / `-bl` / `-br` | the four diagonal gaits |
|
|
46
|
-
| `<set>-idle` or `…stance…` | the `idle` loop |
|
|
47
|
-
| `…jump…` (`jump-stand`, `jump-run`) | `jumpStand` / `jumpRun` one-shots |
|
|
48
|
-
| anything else | a named freeform loop |
|
|
49
|
-
|
|
50
|
-
A stem containing `aim`, `rifle`, or `pistol` marks the take as a WEAPON
|
|
51
|
-
HOLD: verify adds the hold-shape gates, and compile grip-line-normalizes the
|
|
52
|
-
idle (the whole body blades as one) and plucks `aim0`/`aim0Up`/`aim0Dn`
|
|
53
|
-
upper-body mask poses from the hold takes.
|
|
54
|
-
|
|
55
|
-
## The prompt wording ladder (hard-won — wrong words cost paid takes)
|
|
56
|
-
|
|
57
|
-
- **Never ask for "sights" or a "cheek weld"** — you get a hunched
|
|
58
|
-
face-height hold that reads absurd on game rigs. Ask for the third-person
|
|
59
|
-
hold: *"weapon at chest height, arms extended"*, *"low-ready"* as the
|
|
60
|
-
fallback.
|
|
61
|
-
- **Always add "fully upright, head held high"** — the model's prior slouches.
|
|
62
|
-
- **Describe hand geometry literally** (*"left hand forward on the handguard,
|
|
63
|
-
right hand at the grip"*), not by weapon jargon.
|
|
64
|
-
- The model NEVER squares a two-handed hold to the pelvis — real marksmen
|
|
65
|
-
blade, and compile normalizes the heading by the grip line. Do not fight
|
|
66
|
-
it with prompt words; it is not a defect.
|
|
67
|
-
- **Gaits need constraints, not prose.** Direction words alone drift; pin
|
|
68
|
-
the trajectory:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npx genex motion constraints --dir strafe-left --speed 2.5 --duration 5 --out strafe-left.json
|
|
72
|
-
npx genex motion gen "soldier strafing with a rifle at low-ready" --constraints strafe-left.json --no-wait
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
The constraint pins the root path AND the heading (facing stays +Z) — that
|
|
76
|
-
pin is what makes a strafe a strafe. One direction per request; a full
|
|
77
|
-
8-direction set is 8 requests + one idle (run them in parallel with
|
|
78
|
-
`--no-wait`, like any batch).
|
|
79
|
-
|
|
80
|
-
## Verify semantics
|
|
81
|
-
|
|
82
|
-
`PASS` (≥70) / `WARN` (≥50) / `FAIL`, with a hard-zero cap: a zero on any
|
|
83
|
-
load-bearing gate fails the take no matter the average. The reasons column
|
|
84
|
-
names the defect in plain words (feet skate, grip line wanders, cheek weld…).
|
|
85
|
-
The report is DATA — freeform verbs have no semantic gate, so watch the take
|
|
86
|
-
on the rig before shipping it. Tune bands via `--gates gates.json` (partial
|
|
87
|
-
JSON, deep-merged: `{"aim": {"hunch": {"bad": 25}}}`).
|
|
88
|
-
|
|
89
|
-
## Compile semantics + tunables
|
|
90
|
-
|
|
91
|
-
Compile cuts loop cycles at left-foot strikes (gaits) or the best seam window
|
|
92
|
-
(idles), removes the linear trajectory (residual sway + full Y bob stay),
|
|
93
|
-
normalizes heading to +Z, phase-aligns gaits (frame 0 = left strike), and for
|
|
94
|
-
weapon holds levels the arm assembly, raises the gaze, clamps tall stances,
|
|
95
|
-
and yaws mask poses in the SPINE. All rigid whole-body/assembly operations —
|
|
96
|
-
no joint ever moves relative to another (the no-bake law; it is why compiled
|
|
97
|
-
sets survive retargeting). Every threshold lives in
|
|
98
|
-
`src/motion/motion.config.json` after install — pass `--config` to use it.
|
|
99
|
-
|
|
100
|
-
## Wiring the runtime
|
|
101
|
-
|
|
102
|
-
`genex motion install` puts the runtime IN YOUR GAME — `src/motion/rigs.js`
|
|
103
|
-
(both retarget formulas + the load-time normalizations), `anim-runtime.js`
|
|
104
|
-
(ClipSet/Animator/dir8Weights/aimMaskStep), `ik.js`, and the config. They are
|
|
105
|
-
yours: read them, tune them, the comments explain every formula. Load order
|
|
106
|
-
matters:
|
|
107
|
-
|
|
108
|
-
```js
|
|
109
|
-
import { loadRig, captureRestAnkle, reanchorFeet, groundCalibrate, curlFingers } from "./motion/rigs.js";
|
|
110
|
-
import { ClipSet, Animator, dir8Weights, aimMaskStep } from "./motion/anim-runtime.js";
|
|
111
|
-
|
|
112
|
-
const set = await (await fetch("./motion-sets/rifle.json")).json(); // copy sets where your bundler serves them
|
|
113
|
-
const rig = await loadRig("./assets/character.glb", set, modelFrame); // GLB or VRM — auto-detected
|
|
114
|
-
rig.computeCorrection();
|
|
115
|
-
const restAnkle = captureRestAnkle(rig, scene); // BIND pose, before any clip
|
|
116
|
-
reanchorFeet(rig, set, scene); // steep-boot fix — BEFORE ClipSet
|
|
117
|
-
const clipSet = new ClipSet(rig, set, set.gaits);
|
|
118
|
-
groundCalibrate(rig, clipSet, scene, restAnkle); // short-leg fix — AFTER ClipSet
|
|
119
|
-
curlFingers(rig, scene); // grips (no-op on finger-less rigs)
|
|
120
|
-
const anim = new Animator(clipSet);
|
|
121
|
-
|
|
122
|
-
// per frame: your input → blend weights; the mask follows the COMMAND magnitude
|
|
123
|
-
anim.setLoops({ idle: 1 - mag, ...dir8Weights(x, z, mag) }, dt);
|
|
124
|
-
maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });
|
|
125
|
-
anim.setMask("aim0", maskW, pitch / 0.6); // ±40° at pitch extremes
|
|
126
|
-
anim.update(dt);
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
- Physics owns the world transform: hips play LOCAL values only (residual
|
|
130
|
-
x/z + absolute y); your character root carries position + yaw.
|
|
131
|
-
- Jumps: `anim.playOneShot("jumpRun", { yCap: 0.1 })` — cap hips Y when the
|
|
132
|
-
physics capsule owns the jump, and gate the impulse on the clip's own
|
|
133
|
-
takeoff frame. The `--set rifle` install includes the shared jumps set;
|
|
134
|
-
merge it: `Object.assign(clipSet.tracks, new ClipSet(rig, jumps, jumps.gaits).tracks)`.
|
|
135
|
-
- The aim mask WINS over one-shots on masked bones — a jump keeps aiming.
|
|
136
|
-
- Standing aim = the UNMASKED bladed idle; the spine-yawed mask pose is only
|
|
137
|
-
for moving/airborne/pitched states. `aimMaskStep` already encodes this.
|
|
138
|
-
|
|
139
|
-
## Cost + discipline
|
|
140
|
-
|
|
141
|
-
Per-take billing (a 4-take request ≈ one model generation). One regenerate
|
|
142
|
-
per verb is a fair budget when takes fail the gates — reword with the ladder
|
|
143
|
-
above before spending again, and compare candidates from ONE batch first
|
|
144
|
-
(that is what `--takes 4` is for). Never re-run `motion gen` to "pick up" a
|
|
145
|
-
result — `genex wait <id>` re-attaches free.
|