@genex-ai/cli-demo 0.53.0-dev.121 → 0.54.0-dev.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/index.js +9535 -162
- package/package.json +2 -1
- package/templates/controllers/character/animation-packs.ts +3 -23
- package/templates/controllers/character/character-animations.ts +197 -8
- package/templates/controllers/character/character-controller.ts +56 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +137 -0
- package/templates/controllers/character/motion-actions.ts +135 -0
- package/templates/controllers/character/vrm/vrm-retarget.ts +4 -4
- package/templates/skills/genex-ai-character/SKILL.md +97 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +39 -10
- package/templates/skills/genex-threejs-character-controller/references/animations.md +116 -16
- package/templates/skills/genex-threejs-skill-router/SKILL.md +12 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import type { CharacterAnimations } from "./character-animations.ts";
|
|
3
|
+
import type { CharacterActionMotion } from "./character-controller.ts";
|
|
4
|
+
|
|
5
|
+
export type MotionTrajectorySample = readonly [time: number, x: number, y: number, z: number];
|
|
6
|
+
|
|
7
|
+
export interface MotionTrajectory {
|
|
8
|
+
schema: 1;
|
|
9
|
+
clip: string;
|
|
10
|
+
samples: MotionTrajectorySample[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MotionActionController {
|
|
14
|
+
setActionMotion(motion: CharacterActionMotion | null): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadMotionTrajectory(url: string): Promise<MotionTrajectory> {
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
if (!response.ok) throw new Error(`[motion-action] ${url} returned HTTP ${response.status}`);
|
|
20
|
+
const value = await response.json() as Partial<MotionTrajectory>;
|
|
21
|
+
if (value.schema !== 1 || !value.clip || !Array.isArray(value.samples) || value.samples.length < 1) {
|
|
22
|
+
throw new Error(`[motion-action] invalid trajectory ${url}`);
|
|
23
|
+
}
|
|
24
|
+
const samples = value.samples as MotionTrajectorySample[];
|
|
25
|
+
let previous = -Infinity;
|
|
26
|
+
for (const sample of samples) {
|
|
27
|
+
if (
|
|
28
|
+
sample.length !== 4 ||
|
|
29
|
+
sample.some((item) => !Number.isFinite(item)) ||
|
|
30
|
+
sample[0] < previous
|
|
31
|
+
) {
|
|
32
|
+
throw new Error(`[motion-action] invalid trajectory sample in ${url}`);
|
|
33
|
+
}
|
|
34
|
+
previous = sample[0];
|
|
35
|
+
}
|
|
36
|
+
return { schema: 1, clip: value.clip, samples };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Samples an extracted GLB root curve into ECCTRL's dynamic Rapier body.
|
|
41
|
+
* The animation GLB stays in-place; only this driver can advance the capsule.
|
|
42
|
+
*/
|
|
43
|
+
export class MotionActionDriver {
|
|
44
|
+
#controller: MotionActionController;
|
|
45
|
+
#animations: CharacterAnimations;
|
|
46
|
+
#trajectory: MotionTrajectory | null = null;
|
|
47
|
+
#elapsed = 0;
|
|
48
|
+
#onDone: (() => void) | undefined;
|
|
49
|
+
|
|
50
|
+
constructor(controller: MotionActionController, animations: CharacterAnimations) {
|
|
51
|
+
this.#controller = controller;
|
|
52
|
+
this.#animations = animations;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get active(): boolean {
|
|
56
|
+
return this.#trajectory !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
play(trajectory: MotionTrajectory, onDone?: () => void): boolean {
|
|
60
|
+
this.cancel();
|
|
61
|
+
if (!this.#animations.playOneShot(trajectory.clip)) return false;
|
|
62
|
+
this.#trajectory = trajectory;
|
|
63
|
+
this.#elapsed = 0;
|
|
64
|
+
this.#onDone = onDone;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Call once in the fixed-step callback immediately before controller.update(). */
|
|
69
|
+
update(dt: number): void {
|
|
70
|
+
const trajectory = this.#trajectory;
|
|
71
|
+
if (!trajectory || !(dt > 0)) return;
|
|
72
|
+
const duration = trajectory.samples.at(-1)?.[0] ?? 0;
|
|
73
|
+
// Release on the callback AFTER the final interval was applied. Clearing
|
|
74
|
+
// immediately after setActionMotion would erase that velocity before the
|
|
75
|
+
// caller's following controller.update().
|
|
76
|
+
if (this.#elapsed >= duration) {
|
|
77
|
+
this.#finish();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const nextTime = Math.min(this.#elapsed + dt, duration);
|
|
81
|
+
const from = sampleTrajectory(trajectory.samples, this.#elapsed);
|
|
82
|
+
const to = sampleTrajectory(trajectory.samples, nextTime);
|
|
83
|
+
const sampleDt = nextTime - this.#elapsed;
|
|
84
|
+
if (sampleDt > 0) {
|
|
85
|
+
this.#controller.setActionMotion({
|
|
86
|
+
x: (to[1] - from[1]) / sampleDt,
|
|
87
|
+
z: (to[3] - from[3]) / sampleDt,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
this.#elapsed = nextTime;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cancel(): void {
|
|
94
|
+
if (!this.#trajectory) return;
|
|
95
|
+
this.#controller.setActionMotion(null);
|
|
96
|
+
this.#trajectory = null;
|
|
97
|
+
this.#elapsed = 0;
|
|
98
|
+
this.#onDone = undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#finish(): void {
|
|
102
|
+
const done = this.#onDone;
|
|
103
|
+
this.#controller.setActionMotion(null);
|
|
104
|
+
this.#trajectory = null;
|
|
105
|
+
this.#elapsed = 0;
|
|
106
|
+
this.#onDone = undefined;
|
|
107
|
+
done?.();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function sampleTrajectory(
|
|
112
|
+
samples: readonly MotionTrajectorySample[],
|
|
113
|
+
time: number,
|
|
114
|
+
): MotionTrajectorySample {
|
|
115
|
+
if (samples.length === 0) return [0, 0, 0, 0];
|
|
116
|
+
if (time <= samples[0]![0]) return samples[0]!;
|
|
117
|
+
const last = samples.at(-1)!;
|
|
118
|
+
if (time >= last[0]) return last;
|
|
119
|
+
let high = samples.length - 1;
|
|
120
|
+
let low = 0;
|
|
121
|
+
while (high - low > 1) {
|
|
122
|
+
const middle = Math.floor((low + high) / 2);
|
|
123
|
+
if (samples[middle]![0] <= time) low = middle;
|
|
124
|
+
else high = middle;
|
|
125
|
+
}
|
|
126
|
+
const a = samples[low]!;
|
|
127
|
+
const b = samples[high]!;
|
|
128
|
+
const alpha = (time - a[0]) / Math.max(b[0] - a[0], Number.EPSILON);
|
|
129
|
+
return [
|
|
130
|
+
time,
|
|
131
|
+
a[1] + (b[1] - a[1]) * alpha,
|
|
132
|
+
a[2] + (b[2] - a[2]) * alpha,
|
|
133
|
+
a[3] + (b[3] - a[3]) * alpha,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// and auto-detected: the free UAL's Blender Rigify `DEF-*` skeleton and the UAL
|
|
5
5
|
// Pro's UE-mannequin-style skeleton (`pelvis`/`spine_01`/…) that the bundled
|
|
6
6
|
// core library and every CDN animation pack use. Adapted from the
|
|
7
|
-
// official three-vrm
|
|
7
|
+
// official three-vrm humanoid retargeting example (@pixiv/three-vrm examples, MIT):
|
|
8
8
|
// rewrite each bone track into the VRM's normalized-bone local space using the
|
|
9
9
|
// SOURCE rig's rest-pose world rotations, and scale the hips translation by the
|
|
10
10
|
// height ratio. Because vrm-loader calls VRMUtils.rotateVRM0, VRM 0.x and 1.0
|
|
11
11
|
// share this one path — no per-version flip.
|
|
12
12
|
//
|
|
13
13
|
// The HIPS POSITION track is kept (delta from the source rest pose, scaled by
|
|
14
|
-
// the hips-height ratio, like the
|
|
14
|
+
// the hips-height ratio, like the upstream recipe). Dropping it pins the hips at
|
|
15
15
|
// bind height, so any pose that lowers the hips (idle stance, walk contact,
|
|
16
16
|
// punches) lifts the feet off the floor instead — the UAL idle alone holds the
|
|
17
17
|
// hips ~4.5 cm below rest. All other position tracks are still dropped: the
|
|
@@ -134,7 +134,7 @@ export function retargetClips(
|
|
|
134
134
|
// (PropertyBinding strips `[].:/ ` and turns spaces into `_`), so a Rigify bone
|
|
135
135
|
// "DEF-upper_arm.L" shows up in tracks as "DEF-upper_armL". Map those sanitized
|
|
136
136
|
// names back to the real bones, whose actual names carry the dots the map
|
|
137
|
-
// keys on. (
|
|
137
|
+
// keys on. (Many source rigs are dotless, so the upstream example never needed this.)
|
|
138
138
|
const sanitize = (name: string): string => name.replace(/\s/g, "_").replace(/[[\]./:]/g, "");
|
|
139
139
|
const sourceByTrackName = new Map<string, THREE.Object3D>();
|
|
140
140
|
animationRoot.traverse((o) => {
|
|
@@ -220,7 +220,7 @@ export function retargetClips(
|
|
|
220
220
|
|
|
221
221
|
// Full bind-pose retarget: reproduce the SOURCE bone's world-space motion
|
|
222
222
|
// on the TARGET bone, accounting for BOTH rigs' bind orientations, then
|
|
223
|
-
// express it in the target's local space. Unlike the simplified
|
|
223
|
+
// express it in the target's local space. Unlike the simplified source-rig
|
|
224
224
|
// recipe (source-rest only), this also uses the VRM normalized bone's bind
|
|
225
225
|
// world rotation — necessary because T-pose limbs are far from identity,
|
|
226
226
|
// which is what left arms pointing straight up before. Reduces to identity
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-character
|
|
3
|
+
description: Generate a controller-ready Meshy humanoid with Genex, search the committed Meshy animation catalog by gameplay intent, add exact same-rig actions, and install the shared physics controller's Meshy-native adapter. Use when a game needs a custom playable character or motion not covered by the default VRM + UAL lane.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI Character
|
|
7
|
+
|
|
8
|
+
Use the existing VRM + UAL character controller by default. Use this lane when
|
|
9
|
+
the game needs a custom generated humanoid or an action unavailable in UAL.
|
|
10
|
+
Both lanes use the same ECCTRL-derived Rapier controller, camera, inputs,
|
|
11
|
+
crossfades, transition rules, and multiplayer authority contract.
|
|
12
|
+
|
|
13
|
+
## Generate a controller-ready character
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx genex character "stylized desert courier, practical layered clothing"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The default request uses Meshy 6 to generate and texture an A-pose humanoid,
|
|
20
|
+
remeshes and rigs it, adds the validated idle/walk/run/jump controller pack,
|
|
21
|
+
and stores every successful model
|
|
22
|
+
and clip at permanent Genex asset URLs. It prints the complete Genex-credit
|
|
23
|
+
quote before enqueueing. The Meshy API key remains server-side; never ask the
|
|
24
|
+
user for one or call Meshy directly from game code.
|
|
25
|
+
|
|
26
|
+
Useful options:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx genex character "compact fantasy knight" --height 1.7 --polycount 40000
|
|
30
|
+
npx genex character "retro space pilot" --animation 466 --no-wait
|
|
31
|
+
npx genex wait <generation-id>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`--animation <id-or-query>` is repeatable. `--no-controller-pack` deliberately
|
|
35
|
+
omits the default locomotion pack. `--no-wait` returns a generation id for
|
|
36
|
+
`genex wait`; it does not create a second paid request.
|
|
37
|
+
|
|
38
|
+
## Search first; use action IDs
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx genex animations search "rifle reload" --json
|
|
42
|
+
npx genex animations search "dance" --category Action --limit 8
|
|
43
|
+
npx genex animations search "walk backward" --in-place
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Search is local and free: it reads the committed Meshy catalog and returns the
|
|
47
|
+
numeric action ID, stable key, preview URL, motion policy, controller slots,
|
|
48
|
+
requirements, review status, and estimated Genex cost. Use a returned action
|
|
49
|
+
ID. Never invent an ID or assume an unsupported motion exists. For example,
|
|
50
|
+
the current committed catalog has no skateboard action; build that mechanic
|
|
51
|
+
only after search returns real coverage.
|
|
52
|
+
|
|
53
|
+
Catalog entries are metadata-reviewed. Inspect the preview before choosing a
|
|
54
|
+
specialty action. Only provider-declared `InPlace` loops or measured overrides
|
|
55
|
+
may fill locomotion slots automatically.
|
|
56
|
+
|
|
57
|
+
## Add actions and refresh the game
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx genex character animate <character-id> --action <action-id>
|
|
61
|
+
npx genex character animate <character-id> --action <first-id> --action <second-id> --no-wait
|
|
62
|
+
npx genex controller character --character <character-id>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Ambiguous text queries print ranked candidates instead of silently spending
|
|
66
|
+
credits. Already-installed actions return without another debit. After an
|
|
67
|
+
animation job completes, rerun `genex controller character --character <id>`
|
|
68
|
+
to refresh `public/assets/meshy-character.json`; existing controller source is
|
|
69
|
+
preserved unless `--force` is explicitly used.
|
|
70
|
+
|
|
71
|
+
Load `$genex-threejs-character-controller` for the actual wiring. The install
|
|
72
|
+
command copies the shared controller plus `character/meshy/meshy-loader.ts`.
|
|
73
|
+
The manifest points at the current rigged model and compact animation-only GLBs
|
|
74
|
+
in R2, and carries the skeleton signature, locomotion slots, fallbacks,
|
|
75
|
+
durations, nominal speeds, trajectories, and gameplay requirements.
|
|
76
|
+
|
|
77
|
+
## Compatibility and motion authority
|
|
78
|
+
|
|
79
|
+
- Meshy clips play only on the exact character revision whose skeleton
|
|
80
|
+
signature matches. There is no runtime retargeting in this lane.
|
|
81
|
+
- Rapier owns the player transform. Animation tracks are normalized
|
|
82
|
+
horizontally in place; never move the rendered character root separately.
|
|
83
|
+
- `controller-loop` clips follow controller-local intent and measured speed.
|
|
84
|
+
The state machine crossfades idle, walk, run, jump, directional slots, and
|
|
85
|
+
explicit fallbacks exactly as it does for VRM + UAL.
|
|
86
|
+
- `anchored-action` is a one-shot at the current controller pose.
|
|
87
|
+
- `planar-root-action` is not enabled for physics movement until the manifest
|
|
88
|
+
says `rootMotionValidated: true`. The current catalog does not validate root
|
|
89
|
+
motion, so do not drive a roll or lunge trajectory merely from its name.
|
|
90
|
+
- `choreography` needs game logic for the named prop, partner, ledge, ladder,
|
|
91
|
+
obstacle, seat, or other environment contract. A clip does not create that
|
|
92
|
+
mechanic.
|
|
93
|
+
|
|
94
|
+
For multiplayer, only the owning client advances the dynamic body and publishes
|
|
95
|
+
its pose plus compact animation state. Remote characters are visual-only: they
|
|
96
|
+
interpolate the owner's transform and replay the matching clip progress. Never
|
|
97
|
+
run a motion driver or a second physics controller for a remote player.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-character-controller
|
|
3
|
-
description: Add
|
|
3
|
+
description: Add Genex's tuned ECCTRL-derived physics character controller with `npx genex controller character`: dynamic-capsule movement, follow camera, touch input, personal VRM + UAL animation, or an exact same-rig Meshy character. Use for every on-foot player or third-person movement request.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Character Controller
|
|
@@ -19,15 +19,32 @@ animation retargeting, capsule auto-fit, foot IK) and the 12-clip core
|
|
|
19
19
|
~1.3 MB) into `public/assets/`. Need more — swords, pistols, magic, climbing,
|
|
20
20
|
swimming, emotes? Install exactly what the game uses with
|
|
21
21
|
`npx genex controller anims <tags|clip names…>` (see Animations below). The
|
|
22
|
-
command also writes the player's avatar to `public/assets/avatar.vrm` —
|
|
22
|
+
default command also writes the player's avatar to `public/assets/avatar.vrm` —
|
|
23
23
|
**your** avatar when you're signed in,
|
|
24
24
|
otherwise a bundled CC0 default (attribution in `src/controllers/NOTICE.md`).
|
|
25
|
-
|
|
25
|
+
By default, the character plays as that VRM. The copied files are then owned by the game —
|
|
26
26
|
edit them freely; re-running skips existing files unless `--force`. Do not write
|
|
27
27
|
a character controller from scratch and do not swap in a kinematic-controller
|
|
28
28
|
tutorial: this one is a real dynamic body that pushes crates, rides moving
|
|
29
29
|
platforms, climbs stairs and slides on too-steep slopes out of the box.
|
|
30
30
|
|
|
31
|
+
Use the VRM + UAL lane above by default. When the game needs a custom generated
|
|
32
|
+
humanoid or an action unavailable in UAL, load `$genex-ai-character`, search
|
|
33
|
+
Meshy's library first, generate the character, and install it by character ID:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx genex animations search "rifle reload" --json
|
|
37
|
+
npx genex character "stylized sci-fi courier, practical clothing" --animation 466
|
|
38
|
+
npx genex controller character --character <character-id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That is a separate, **same-rig Meshy-native lane**. Its animation-only GLBs are
|
|
42
|
+
accepted only when their skeleton signature matches the active character
|
|
43
|
+
revision; this lane does no runtime retargeting. The shared ECCTRL-derived
|
|
44
|
+
dynamic controller stays authoritative for collision, grounding, facing, and
|
|
45
|
+
world translation. The animation layer poses the visual rig; it never
|
|
46
|
+
translates the visual root.
|
|
47
|
+
|
|
31
48
|
Because these files are game-owned, never run `genex controller character --force` over an edited
|
|
32
49
|
fork as a migration strategy. Install a fresh copy elsewhere and port only the named changes.
|
|
33
50
|
|
|
@@ -42,11 +59,13 @@ fork as a migration strategy. Install a fresh copy elsewhere and port only the n
|
|
|
42
59
|
| `character/follow-camera.ts` | `FollowCamera` | orbit/zoom chase camera with collision pullback and an opt-in pointer-lock aim mode |
|
|
43
60
|
| `character/keyboard-input.ts` | `KeyboardInput` | WASD/arrows/Shift/Space/F state, no per-frame polling setup |
|
|
44
61
|
| `character/touch-joystick.ts` | `TouchJoystick`, `VirtualButton` | mobile controls |
|
|
45
|
-
| `character/character-animations.ts` | `CharacterAnimations` | animation state machine
|
|
46
|
-
| `character/animation-packs.ts` | `loadCharacterClips` |
|
|
62
|
+
| `character/character-animations.ts` | `CharacterAnimations` | animation state machine, directional profiles, speed-matched cadence, `playOneShot`, procedural fallback |
|
|
63
|
+
| `character/animation-packs.ts` | `loadCharacterClips` | loads the core + installed UAL packs and retargets them to the active VRM |
|
|
64
|
+
| `character/meshy/meshy-loader.ts` | `loadMeshyCharacter` | loads a Meshy manifest, exact-signature model/clips, locomotion slots, and fallbacks |
|
|
65
|
+
| `character/motion-actions.ts` | `MotionActionDriver` | applies only validated planar trajectories through the physics controller, never the visual root |
|
|
47
66
|
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | load the VRM avatar, retarget library clips onto its humanoid rig, auto-fit the capsule, ground the feet |
|
|
48
67
|
|
|
49
|
-
## Minimal wiring
|
|
68
|
+
## Minimal wiring: personal VRM lane
|
|
50
69
|
|
|
51
70
|
```ts
|
|
52
71
|
import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
|
|
@@ -147,7 +166,7 @@ preset table with provenance, the density/spring scaling rule, and the
|
|
|
147
166
|
|
|
148
167
|
## Animations + animation packs
|
|
149
168
|
|
|
150
|
-
`CharacterAnimations` resolves
|
|
169
|
+
`CharacterAnimations` resolves locomotion states (IDLE / WALK / RUN /
|
|
151
170
|
CROUCH_IDLE / CROUCH_MOVE / JUMP_START / JUMP_IDLE / JUMP_FALL / JUMP_LAND)
|
|
152
171
|
from the controller's live flags and crossfades mixer actions. Every OTHER
|
|
153
172
|
clip — punches, sword swings, pistol fire, spells, sit, dance, hit reactions —
|
|
@@ -155,16 +174,23 @@ plays through `anims.playOneShot("Punch_Jab")`, which layers over locomotion
|
|
|
155
174
|
and returns to it when done (punch-on-click is the default).
|
|
156
175
|
|
|
157
176
|
The bundled library carries only the 12 core clips. **Install what the game's
|
|
158
|
-
theme needs** from the 120-clip catalog
|
|
177
|
+
theme needs** from the 120-clip UAL catalog by tag/name. For motion UAL does
|
|
178
|
+
not cover, use the separate Meshy character lane:
|
|
159
179
|
|
|
160
180
|
```bash
|
|
161
181
|
npx genex controller anims sword pistol # a sword+shooter game
|
|
162
182
|
npx genex controller anims stealth climb crawl # a ninja game
|
|
163
183
|
npx genex controller anims --list # browse tags; --list <tag> for per-clip details
|
|
184
|
+
npx genex animations search "rifle reload" --json
|
|
185
|
+
npx genex character animate <character-id> --action <action-id>
|
|
186
|
+
npx genex controller character --character <character-id> # refresh the manifest
|
|
164
187
|
```
|
|
165
188
|
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
Meshy search results include an action ID, stable key, preview, loop mode,
|
|
190
|
+
motion policy, controller slots, requirements, review status, and estimated
|
|
191
|
+
cost. The installed Meshy manifest supplies exact-signature clips, directional
|
|
192
|
+
slots, fallbacks, and cadence data. UAL clips still land in
|
|
193
|
+
`public/assets/anims/` and are additive. Read
|
|
168
194
|
[references/animations.md](references/animations.md) for the tag catalog with
|
|
169
195
|
genre hints, `playOneShot` options, overrides, foot IK, and remote-player
|
|
170
196
|
animation.
|
|
@@ -227,6 +253,9 @@ different world.
|
|
|
227
253
|
- To animate remotes, sync the six animation booleans (`isOnGround`,
|
|
228
254
|
`isFalling`, `isMoving`, `runActive`, `jumpActive`, `crouchActive`) and feed
|
|
229
255
|
them to a per-remote `CharacterAnimations` — see the animations reference.
|
|
256
|
+
- Only the owning client runs `MotionActionDriver`. Remotes play the same
|
|
257
|
+
one-shot event while following smoothed owner-authored position/rotation;
|
|
258
|
+
their animation mixer never moves them through the world.
|
|
230
259
|
- Load `$genex-threejs-multiplayer` before writing any networking code; it is
|
|
231
260
|
mandatory for any 2+ player game.
|
|
232
261
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Character animations
|
|
2
2
|
|
|
3
3
|
`CharacterAnimations` turns the controller's live flags into crossfaded
|
|
4
|
-
`THREE.AnimationMixer` playback:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
clips
|
|
4
|
+
`THREE.AnimationMixer` playback: core states, directional locomotion profiles,
|
|
5
|
+
alias-based clip binding, one-shots, and a procedural bob/lean fallback. The
|
|
6
|
+
default lane retargets UAL packs to VRM; the Meshy lane plays animation-only
|
|
7
|
+
clips only on the exact matching generated rig revision.
|
|
8
8
|
|
|
9
9
|
## The bundled assets + animation packs
|
|
10
10
|
|
|
@@ -70,6 +70,100 @@ cloth). `anims.update` takes the RAW render delta — pause/slow-motion go throu
|
|
|
70
70
|
`anims.setPaused(true)` / `anims.setTimeScale(0.5)` (fade durations stretch with
|
|
71
71
|
the time scale so slow motion doesn't pop).
|
|
72
72
|
|
|
73
|
+
## Same-rig Meshy specialty lane
|
|
74
|
+
|
|
75
|
+
Use this lane when a game needs a custom generated humanoid or catalog depth
|
|
76
|
+
that the personal-VRM library does not cover: sports, dance, tactical actions,
|
|
77
|
+
prop interactions, or authored traversal. Load `$genex-ai-character`, search
|
|
78
|
+
first, generate the controller-ready character, and add exact action IDs:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx genex animations search "rifle reload" --json
|
|
82
|
+
npx genex character "stylized sci-fi courier, practical clothing" --animation 466
|
|
83
|
+
npx genex character animate <character-id> --action <action-id>
|
|
84
|
+
npx genex controller character --character <character-id>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Search output exposes the numeric action ID, stable key, preview, loop mode,
|
|
88
|
+
motion policy, controller slots, gameplay requirements, review status, and
|
|
89
|
+
estimated cost. Use a returned ID; do not guess from a name. Search is free and
|
|
90
|
+
local. Character and animation generation print a Genex-credit quote before
|
|
91
|
+
enqueueing, and successful outputs are copied to permanent R2 URLs.
|
|
92
|
+
|
|
93
|
+
The install command writes `public/assets/meshy-character.json`. Wire that
|
|
94
|
+
manifest through the **same** physics controller and camera:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { loadMeshyCharacter } from "./controllers/character/meshy/meshy-loader.ts";
|
|
98
|
+
|
|
99
|
+
const native = await loadMeshyCharacter("./assets/meshy-character.json");
|
|
100
|
+
const fit = capsuleFromModel(native.scene);
|
|
101
|
+
const character = new CharacterController(physics.world, camera, {
|
|
102
|
+
...characterPresets["default"].options,
|
|
103
|
+
...fit,
|
|
104
|
+
position: { x: 0, y: 2, z: 0 },
|
|
105
|
+
});
|
|
106
|
+
character.root.add(native.scene);
|
|
107
|
+
native.scene.position.y = fit.modelOffsetY;
|
|
108
|
+
const anims = new CharacterAnimations(native.scene, native.clips, {
|
|
109
|
+
locomotionProfile: native.locomotionProfile,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The loader rejects clips whose manifest skeleton signature differs from the
|
|
114
|
+
current model revision or whose tracks target missing bones. This is deliberate
|
|
115
|
+
same-rig playback, not runtime retargeting. After adding an action, rerun the
|
|
116
|
+
controller command to refresh the manifest; existing controller source remains
|
|
117
|
+
untouched unless `--force` is explicitly used.
|
|
118
|
+
|
|
119
|
+
### Directional locomotion and cadence
|
|
120
|
+
|
|
121
|
+
Meshy manifests can fill slots such as `idle.default`, `walk.forward`,
|
|
122
|
+
`walk.backward`, `walk.left`, `walk.right`, their diagonal variants, matching
|
|
123
|
+
run/crouch slots, transitions, and jump phases. Under `lockForward`, the state
|
|
124
|
+
machine quantizes controller-local input into eight directions with hysteresis;
|
|
125
|
+
under free-facing movement it uses forward locomotion. Playback cadence follows
|
|
126
|
+
controller move speed divided by each band's authored nominal speed, clamped to
|
|
127
|
+
the profile range, so ECCTRL remains authoritative while feet track velocity.
|
|
128
|
+
|
|
129
|
+
### Motion policies and authored root actions
|
|
130
|
+
|
|
131
|
+
Meshy results distinguish four policies:
|
|
132
|
+
|
|
133
|
+
- `controller-loop` — looping locomotion; ECCTRL supplies all translation.
|
|
134
|
+
- `anchored-action` — a one-shot that stays at the current controller pose.
|
|
135
|
+
- `planar-root-action` — a one-shot that may carry an extracted trajectory, but
|
|
136
|
+
it remains disabled until real output is validated.
|
|
137
|
+
- `choreography` — a multi-actor or constrained sequence; game code owns the
|
|
138
|
+
participants, anchors, props, and environment contract.
|
|
139
|
+
|
|
140
|
+
All installed GLBs stay horizontally in-place. A planar trajectory may request
|
|
141
|
+
movement only when its manifest entry says `rootMotionValidated: true`; current
|
|
142
|
+
Meshy manifests publish `false`, so these actions remain anchored. Once a real
|
|
143
|
+
canary validates extraction and synchronization, `MotionActionDriver` feeds the
|
|
144
|
+
trajectory into `CharacterController` immediately before
|
|
145
|
+
`character.update()`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { loadMotionTrajectory, MotionActionDriver } from "./controllers/character/motion-actions.ts";
|
|
149
|
+
|
|
150
|
+
const trajectory = await loadMotionTrajectory("./assets/anims/<trajectory-file>.json");
|
|
151
|
+
const motion = new MotionActionDriver(character, anims);
|
|
152
|
+
motion.play(trajectory);
|
|
153
|
+
|
|
154
|
+
physics.onBeforeStep(() => {
|
|
155
|
+
character.setMovement(kb.getCharacterMovement());
|
|
156
|
+
motion.update(physics.world.timestep);
|
|
157
|
+
character.update();
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Resolve the URL from `meshy-character.json`'s `trajectoryUrl` rather than
|
|
162
|
+
guessing it, and refuse it unless `rootMotionValidated` is true. Fulfill every
|
|
163
|
+
declared requirement (`prop`, `partner`, or `environment`) before starting an
|
|
164
|
+
action. Only the owning client runs the driver; remotes play the event while
|
|
165
|
+
following the owner's smoothed transform.
|
|
166
|
+
|
|
73
167
|
### Foot IK (optional)
|
|
74
168
|
|
|
75
169
|
`vrm/foot-ik.ts` plants feet on uneven ground (no skating/floating on steps and
|
|
@@ -176,18 +270,21 @@ run `--list <tag>` for per-clip descriptions before wiring one-shots:
|
|
|
176
270
|
| `shop` | Counter enter/exit/idle/give/show/angry (~0.4 MB) | shopkeeper NPCs |
|
|
177
271
|
| `drive` | Driving_Loop (~0.05 MB) | vehicles (see `$genex-threejs-vehicle-controllers`) |
|
|
178
272
|
|
|
179
|
-
For looping poses that should **persist** (aiming, sitting,
|
|
273
|
+
For legacy-library looping poses that should **persist** (aiming, sitting,
|
|
274
|
+
swimming) rather than
|
|
180
275
|
play once, drive them through the public `anims.mixer` escape hatch instead; the
|
|
181
|
-
`$genex-threejs-vehicle-controllers` skill shows the seated pattern.
|
|
182
|
-
are in-place
|
|
276
|
+
`$genex-threejs-vehicle-controllers` skill shows the seated pattern. UAL clips
|
|
277
|
+
are in-place. Meshy planar actions remain anchored until their manifest
|
|
278
|
+
explicitly validates the extracted trajectory; in every case the physics
|
|
279
|
+
controller owns world translation.
|
|
183
280
|
|
|
184
|
-
## Binding arbitrary
|
|
281
|
+
## Binding arbitrary clip names on a compatible rig
|
|
185
282
|
|
|
186
283
|
`buildClipMap(clips, overrides?)` resolves each state in priority order:
|
|
187
284
|
explicit override (exact, then case-insensitive) → library exact names →
|
|
188
285
|
case-insensitive → each alias as a case-insensitive **substring**, shortest
|
|
189
|
-
matching clip name wins (so `Walk_Loop` beats `Walk_Bwd_Loop`, and
|
|
190
|
-
|
|
286
|
+
matching clip name wins (so `Walk_Loop` beats `Walk_Bwd_Loop`, and `walking`
|
|
287
|
+
beats `walking_backwards`). Aliases include `idle`, `walk`, `run`,
|
|
191
288
|
`jog`, `sprint`, `crouch_idle`, `sneak`, `jump_start`, `takeoff`, `fall`,
|
|
192
289
|
`land`, and a bare `jump` catch-all so a rig whose only airborne clip is
|
|
193
290
|
"Jumping" still binds all four jump states. Unbound loop states chain
|
|
@@ -195,7 +292,9 @@ matching clip name wins (so `Walk_Loop` beats `Walk_Bwd_Loop`, and Mixamo's
|
|
|
195
292
|
without crouch clips sneak in a standing pose instead of T-posing); unbound
|
|
196
293
|
one-shots stay silent so the previous loop keeps playing.
|
|
197
294
|
|
|
198
|
-
|
|
295
|
+
This resolves names; it does **not** make incompatible skeletons compatible.
|
|
296
|
+
Only pass clips already retargeted to the VRM or authored for the exact Meshy
|
|
297
|
+
rig revision. If a compatible clip name refuses to bind, pass overrides:
|
|
199
298
|
|
|
200
299
|
```ts
|
|
201
300
|
const anims = new CharacterAnimations(model, gltf.animations, {
|
|
@@ -260,11 +359,12 @@ remoteAnims.update(remoteState.flags, delta);
|
|
|
260
359
|
resolves correctly (a missing flag reads as not-crouched).
|
|
261
360
|
|
|
262
361
|
The mixer crossfades exactly as it does locally, so remote players animate
|
|
263
|
-
correctly without simulating anything.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
362
|
+
correctly without simulating anything. Use the same visual lane as the owner:
|
|
363
|
+
`loadVrm` + retargeted UAL clips for a VRM game (and call `vrm.update(delta)`
|
|
364
|
+
per remote), or `loadMeshyCharacter` for a Meshy game. Relay one-shot events (punch,
|
|
365
|
+
hit, validated planar-action start) alongside the flags and call the matching
|
|
366
|
+
`remoteAnims.playOneShot(...)` on receipt. Never run `MotionActionDriver` for a
|
|
367
|
+
remote: its smoothed owner-authored transform is the sole movement authority.
|
|
268
368
|
Full networking patterns: `$genex-threejs-multiplayer`.
|
|
269
369
|
|
|
270
370
|
## Gotchas
|
|
@@ -16,7 +16,8 @@ map, execution order, and acceptance gate.
|
|
|
16
16
|
| Work needed | Load |
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| 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, floating origins | `$genex-threejs-camera-direction` |
|
|
19
|
-
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms,
|
|
19
|
+
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, personal VRM animation, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
|
|
20
|
+
| a custom generated playable humanoid or Meshy animation coverage beyond UAL: search, generate, rig, add exact action IDs, install the same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
|
|
20
21
|
| the player drives or flies something: cars, drones, vehicle physics, gearbox, enter/exit between character and vehicle | `$genex-threejs-vehicle-controllers` |
|
|
21
22
|
| anything falls, collides, gets pushed, or needs physics: Rapier world setup, colliders for meshes and GLBs, collision events | `$genex-threejs-physics-rapier` |
|
|
22
23
|
| launch and docking timelines, procedural transform phases, springs, staging, rotating-frame alignment, debris motion | `$genex-threejs-procedural-animation` |
|
|
@@ -53,6 +54,14 @@ map, execution order, and acceptance gate.
|
|
|
53
54
|
mentions sign-in, saves, progress, per-player state, a persistent world, or
|
|
54
55
|
leaderboards. Multiplayer auth (`getColyseusAuth`) comes from it too.
|
|
55
56
|
|
|
57
|
+
**Character-animation routing:** use the existing VRM + UAL character
|
|
58
|
+
controller by default. Use `npx genex character` when the game needs a custom
|
|
59
|
+
generated humanoid or an action unavailable in UAL. Load `$genex-ai-character`,
|
|
60
|
+
search Meshy actions first with `npx genex animations search "<intent>" --json`,
|
|
61
|
+
and use returned action IDs; never invent IDs or move the visual root. In both
|
|
62
|
+
lanes, the ECCTRL-derived character controller owns collision and world
|
|
63
|
+
translation.
|
|
64
|
+
|
|
56
65
|
**UI is mandatory routing:** every game has an interface — load
|
|
57
66
|
`$genex-threejs-game-ui` for every NEW game and run its "Plan the UI first" gate
|
|
58
67
|
(screen inventory, one shared style brief, tier decisions) right after the game
|
|
@@ -113,7 +122,8 @@ commit. Each skill has the exact Three.js loader code.
|
|
|
113
122
|
|
|
114
123
|
| Work needed | Generate with | Skill |
|
|
115
124
|
| --- | --- | --- |
|
|
116
|
-
| a specific prop / item /
|
|
125
|
+
| a specific prop / item / non-playable static figure / vehicle body as a real mesh | `npx genex model "<prompt>"` | `$genex-ai-model` |
|
|
126
|
+
| a custom generated playable humanoid with a rig and controller-ready animation pack | `npx genex character "<prompt>"` | `$genex-ai-character` |
|
|
117
127
|
| a described 360° sky / backdrop + image-based lighting | `npx genex skybox "<prompt>"` | `$genex-ai-skybox` |
|
|
118
128
|
| a specific sound effect tied to an event | `npx genex sfx "<prompt>"` | `$genex-ai-sfx` |
|
|
119
129
|
| a photoreal surface/material on a mesh or terrain | `npx genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
|