@genex-ai/cli-demo 0.90.0-dev.227 → 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/README.md +4 -1
- package/dist/index.js +598 -91
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +6 -4
- package/templates/controllers/character/player-character.ts +217 -0
- package/templates/skills/genex-ai-character/SKILL.md +91 -25
- package/templates/skills/genex-ai-character/references/motion-generation.md +125 -0
- package/templates/skills/genex-ai-image/SKILL.md +5 -1
- package/templates/skills/genex-ai-menu/SKILL.md +14 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +19 -6
- package/templates/skills/genex-game-director/SKILL.md +29 -11
- package/templates/skills/genex-game-director/references/routing-map.md +4 -3
- package/templates/skills/genex-threejs-character-controller/SKILL.md +105 -52
- package/templates/skills/genex-threejs-character-controller/references/animations.md +62 -50
- package/templates/skills/genex-threejs-creatures/SKILL.md +14 -4
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +5 -3
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +10 -8
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +8 -7
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +2 -2
- package/templates/skills/genex-ai-character/references/motion.md +0 -145
|
@@ -318,17 +318,35 @@ way this skill is worn by the main agent: you stay the director.
|
|
|
318
318
|
holding the only free slot"). The table is accountability, not ceremony —
|
|
319
319
|
inline can be the right call.
|
|
320
320
|
|
|
321
|
-
## 7. The game's character (Meshy) —
|
|
322
|
-
|
|
323
|
-
**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
321
|
+
## 7. The game's character (Meshy) — the player's body
|
|
322
|
+
|
|
323
|
+
**The game's own generated character IS the player's body**, wherever a
|
|
324
|
+
human body appears on screen. Third-person obviously; first-person too, the
|
|
325
|
+
moment remotes, a look-down body, a shadow, a death or spectator camera, or
|
|
326
|
+
a menu portrait shows one. "The camera is in the head" is not an exemption —
|
|
327
|
+
**"no human body ever appears in this game" is**, and a game whose player is
|
|
328
|
+
genuinely not a person (a car, a ship, an RTS cursor, a board) generates
|
|
329
|
+
that object with `npx genex model` instead.
|
|
330
|
+
|
|
331
|
+
Put its row in the Assets table up front and **enqueue it with your first
|
|
332
|
+
art actions** — its concepts ride the same review beat as the Stage-1 HUD
|
|
333
|
+
concept, so firing at minute 0 lands it around the v0 preview instead of
|
|
334
|
+
after it. `npx genex controller character` installs the controller and the
|
|
335
|
+
fallback body in one command; the boot path is written once and never
|
|
336
|
+
rewritten (`loadPlayerCharacter` — see
|
|
337
|
+
`$genex-threejs-character-controller`), so when the character lands,
|
|
338
|
+
`npx genex controller character --character <id>` is the entire switch.
|
|
339
|
+
|
|
340
|
+
The profile VRM avatar is the FALLBACK, in two shapes and both spoken
|
|
341
|
+
aloud: a temporary body while the character renders (say so plainly — it's
|
|
342
|
+
a fully textured animated humanoid, so nothing on screen will look
|
|
343
|
+
unfinished enough to remind you), or the stand-in when generation genuinely
|
|
344
|
+
could not happen (out of credits, failed, unverified), recorded in DESIGN.md
|
|
345
|
+
as `Player character: VRM — <reason>`. A capsule or hand-built primitive
|
|
346
|
+
standing in for a person is never a shipped state, for the local player or a
|
|
347
|
+
remote one. In a game with a generated character, every remote wears it —
|
|
348
|
+
a mixed roster of one themed hero plus stock avatars is the same incoherence
|
|
349
|
+
as capsule-and-cone remotes.
|
|
332
350
|
|
|
333
351
|
**The default lane has ONE user stop, and it rides the concept review.**
|
|
334
352
|
When the user names a visual reference, inspect references before writing
|
|
@@ -20,9 +20,10 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
20
20
|
| Work needed | Load |
|
|
21
21
|
| --- | --- |
|
|
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
|
-
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms,
|
|
24
|
-
| the game's
|
|
25
|
-
|
|
|
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
|
+
| 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`) |
|
|
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` |
|
|
28
29
|
| playable on phones: touch/mobile input for any game — joystick, virtual buttons, drag zones, per-genre touch recipes, rotate-device overlay — wired by default for every NEW game when a recipe fits (skip with a one-line reason) | `$genex-threejs-touch-controls` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-character-controller
|
|
3
|
-
description: Add Genex's tuned ECCTRL-derived physics character controller with `npx genex controller character`: dynamic-capsule movement, follow camera, touch input,
|
|
3
|
+
description: Add Genex's tuned ECCTRL-derived physics character controller with `npx genex controller character`: dynamic-capsule movement, follow camera, touch input, and one loader that resolves the player's body — the game's own generated character by default, the profile VRM avatar as the fallback. Use for every on-foot player or third-person movement request.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Character Controller
|
|
@@ -19,24 +19,48 @@ 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
|
-
|
|
23
|
-
**your** avatar when you're signed in,
|
|
24
|
-
otherwise a bundled CC0 default (attribution in `src/controllers/NOTICE.md`).
|
|
25
|
-
At runtime the character plays as the **visiting player's own** picked avatar
|
|
26
|
-
(`user.avatarUrl` from the embed identity — see the wiring below); the baked
|
|
27
|
-
file is only the fallback for local dev and load failures. The copied files are then owned by the game —
|
|
22
|
+
copied files are then owned by the game —
|
|
28
23
|
edit them freely; re-running skips existing files unless `--force`. Do not write
|
|
29
24
|
a character controller from scratch and do not swap in a kinematic-controller
|
|
30
25
|
tutorial: this one is a real dynamic body that pushes crates, rides moving
|
|
31
26
|
platforms, climbs stairs and slides on too-steep slopes out of the box.
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
## The player's body: the game's own character
|
|
29
|
+
|
|
30
|
+
**The player wears the character this game generated for itself.** That is the
|
|
31
|
+
default for any game where a human body appears on screen — third-person
|
|
32
|
+
obviously, and first-person too the moment remotes, a look-down body, a shadow,
|
|
33
|
+
a death cam, or a menu portrait shows one. Load `$genex-ai-character` and start
|
|
34
|
+
it EARLY, with your first art actions, so it lands around the v0 preview
|
|
35
|
+
instead of after it.
|
|
36
|
+
|
|
37
|
+
The profile VRM avatar is the **fallback**, and the command bakes a neutral CC0
|
|
38
|
+
copy to `public/assets/avatar.vrm` (attribution in `src/controllers/NOTICE.md`)
|
|
39
|
+
so a fresh game always has a working body. It is used when:
|
|
40
|
+
|
|
41
|
+
- the generated character hasn't landed yet — say in one plain line that this
|
|
42
|
+
body is temporary; or
|
|
43
|
+
- it genuinely could not be made (out of credits, generation failed, email
|
|
44
|
+
unverified) — say so in one line, record `Player character: VRM — <reason>`
|
|
45
|
+
in DESIGN.md, and keep building.
|
|
46
|
+
|
|
47
|
+
At runtime the fallback lane loads the **visiting player's own** picked avatar
|
|
48
|
+
(`user.avatarUrl` from the embed identity); the baked file covers local dev and
|
|
49
|
+
load failures. Games whose player is not a person at all — a car, a ship, an
|
|
50
|
+
RTS cursor, a board — generate that object with `npx genex model` instead and
|
|
51
|
+
never enter this lane.
|
|
52
|
+
|
|
53
|
+
**You never write two boot paths for this.** `loadPlayerCharacter()` decides at
|
|
54
|
+
runtime from `public/assets/meshy-character.json` and returns one shape either
|
|
55
|
+
way, so the character arriving mid-build is a file drop
|
|
56
|
+
(`npx genex controller character --character <id>`) and not an edit to your
|
|
57
|
+
most load-bearing code. Wire the block under "Minimal wiring" once, at hour 0.
|
|
58
|
+
|
|
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.
|
|
40
64
|
|
|
41
65
|
Before generating a Meshy character, discuss two or three visual directions.
|
|
42
66
|
When the user names a visual reference, inspect references before writing the
|
|
@@ -64,12 +88,12 @@ that model in R2. Before rigging, ask the user to approve a separate
|
|
|
64
88
|
rigged and animated. (For these approvals, use your question tool when you
|
|
65
89
|
have one; if you have none, a short numbered list in chat.)
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
accepted only when their skeleton signature matches the active
|
|
69
|
-
revision; this lane does no runtime retargeting. The shared
|
|
70
|
-
dynamic controller stays authoritative for collision, grounding,
|
|
71
|
-
world translation
|
|
72
|
-
translates the visual root.
|
|
91
|
+
The generated character is a **same-rig Meshy-native lane**. Its animation-only
|
|
92
|
+
GLBs are accepted only when their skeleton signature matches the active
|
|
93
|
+
character revision; this lane does no runtime retargeting. The shared
|
|
94
|
+
ECCTRL-derived dynamic controller stays authoritative for collision, grounding,
|
|
95
|
+
facing, and world translation — identical in both lanes. The animation layer
|
|
96
|
+
poses the visual rig; it never translates the visual root.
|
|
73
97
|
|
|
74
98
|
Meshy manifests bypass browser cache, so newly installed actions must work
|
|
75
99
|
after preview without asking the player to disable cache. `playOneShot()`
|
|
@@ -101,10 +125,11 @@ fork as a migration strategy. Install a fresh copy elsewhere and port only the n
|
|
|
101
125
|
| `character/character-animations.ts` | `CharacterAnimations` | animation state machine, directional profiles, speed-matched cadence, `playOneShot`, procedural fallback |
|
|
102
126
|
| `character/animation-packs.ts` | `loadCharacterClips` | loads the core + installed UAL packs and retargets them to the active VRM |
|
|
103
127
|
| `character/meshy/meshy-loader.ts` | `loadMeshyCharacter` | loads a Meshy manifest, exact-signature model/clips, locomotion slots, and fallbacks |
|
|
128
|
+
| `character/player-character.ts` | `loadPlayerCharacter`, `loadRemotePlayerCharacter` | **the player's body** — the game's generated character when it has one, the avatar fallback when it doesn't, one shape either way |
|
|
104
129
|
| `character/motion-actions.ts` | `MotionActionDriver` | applies only validated planar trajectories through the physics controller, never the visual root |
|
|
105
|
-
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | load
|
|
130
|
+
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | the FALLBACK lane's parts: load a VRM avatar, retarget library clips onto its humanoid rig, auto-fit the capsule (works on both lanes), ground the feet |
|
|
106
131
|
|
|
107
|
-
## Minimal wiring
|
|
132
|
+
## Minimal wiring
|
|
108
133
|
|
|
109
134
|
```ts
|
|
110
135
|
import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
|
|
@@ -115,25 +140,27 @@ import { characterPresets } from "./controllers/character/presets.ts";
|
|
|
115
140
|
import { FollowCamera } from "./controllers/character/follow-camera.ts";
|
|
116
141
|
import { createAimCue } from "./controllers/character/aim-cue.ts";
|
|
117
142
|
import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
|
|
118
|
-
import {
|
|
143
|
+
import { loadPlayerCharacter } from "./controllers/character/player-character.ts";
|
|
119
144
|
import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
|
|
120
145
|
import { waitForPlayer } from "@genex-ai/embed-sdk";
|
|
121
146
|
|
|
122
147
|
const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run before this resolves
|
|
123
148
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
149
|
+
// The player's body — ONE call, both lanes. It plays as the character THIS
|
|
150
|
+
// GAME generated whenever `public/assets/meshy-character.json` is present
|
|
151
|
+
// (with that character's exact-rig clips and locomotion profile), and falls
|
|
152
|
+
// back to the visiting player's own profile avatar when it isn't — retargeting
|
|
153
|
+
// the bundled core library plus any packs installed by `genex controller
|
|
154
|
+
// anims`. `user.avatarUrl` comes from the embed identity
|
|
155
|
+
// ($genex-threejs-embed-auth boots before this) and is used only in that
|
|
156
|
+
// fallback lane; the baked `./assets/avatar.vrm` covers local dev and load
|
|
157
|
+
// failures. WRITE THIS ONCE: when the generated character lands mid-build,
|
|
158
|
+
// `genex controller character --character <id>` drops the manifest in and the
|
|
159
|
+
// next reload swaps the body. Nothing below changes.
|
|
131
160
|
const { user } = await waitForPlayer(); // from "@genex-ai/embed-sdk"
|
|
132
|
-
const
|
|
133
|
-
.catch(() => loadVrm("./assets/avatar.vrm")); // network failure → bundled fallback
|
|
134
|
-
const clips = await loadCharacterClips(vrm);
|
|
161
|
+
const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl });
|
|
135
162
|
|
|
136
|
-
const fit = capsuleFromModel(
|
|
163
|
+
const fit = capsuleFromModel(player.scene); // collider fits THIS body's bounds
|
|
137
164
|
const character = new CharacterController(physics.world, camera, {
|
|
138
165
|
...characterPresets["default"].options,
|
|
139
166
|
...fit,
|
|
@@ -141,11 +168,15 @@ const character = new CharacterController(physics.world, camera, {
|
|
|
141
168
|
userData: { controller: { excludeVehicleRay: true } }, // car wheels must never drive on the player
|
|
142
169
|
});
|
|
143
170
|
scene.add(character.root);
|
|
144
|
-
character.root.add(
|
|
145
|
-
|
|
171
|
+
character.root.add(player.scene); // parent the body under the character root
|
|
172
|
+
player.scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
146
173
|
physics.registerBody(character.body, character.root); // root now follows the body, interpolated
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
// locomotionProfile is present in the generated lane and undefined in the
|
|
176
|
+
// fallback — pass it either way; CharacterAnimations handles both.
|
|
177
|
+
const anims = new CharacterAnimations(player.scene, player.clips, {
|
|
178
|
+
locomotionProfile: player.locomotionProfile,
|
|
179
|
+
});
|
|
149
180
|
addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // punch on click
|
|
150
181
|
|
|
151
182
|
const kb = new KeyboardInput();
|
|
@@ -184,7 +215,10 @@ renderer.setAnimationLoop(() => {
|
|
|
184
215
|
followCam.update(delta);
|
|
185
216
|
|
|
186
217
|
anims.update(character, delta); // see the animations reference
|
|
187
|
-
|
|
218
|
+
player.update(delta); // REQUIRED — ticks the VRM humanoid + spring bones
|
|
219
|
+
// in the fallback lane, a no-op for native rigs.
|
|
220
|
+
// Always call it: that is why the lane can change
|
|
221
|
+
// under you without this loop changing.
|
|
188
222
|
renderer.render(scene, camera);
|
|
189
223
|
});
|
|
190
224
|
```
|
|
@@ -312,20 +346,39 @@ and the style-matching rules live in `$genex-threejs-touch-controls`.
|
|
|
312
346
|
|
|
313
347
|
**The local player is physics-authoritative; remote players are interpolated
|
|
314
348
|
visuals only.** Exactly one `CharacterController` exists — yours. For every
|
|
315
|
-
remote player:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
remote player:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { loadRemotePlayerCharacter } from "./controllers/character/player-character.ts";
|
|
353
|
+
|
|
354
|
+
const remote = await loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl });
|
|
355
|
+
scene.add(remote.scene);
|
|
356
|
+
const remoteAnims = new CharacterAnimations(remote.scene, remote.clips, {
|
|
357
|
+
locomotionProfile: remote.locomotionProfile,
|
|
358
|
+
});
|
|
359
|
+
// on 'leave': remote.dispose()
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Same rule as the local player, and the same one call: **in a game with its own
|
|
363
|
+
generated character, every remote wears it** — the game should look like the
|
|
364
|
+
game that was designed, and a mixed roster (one themed knight plus three stock
|
|
365
|
+
avatars) is the same incoherence as capsule-and-cone remotes. Only when this
|
|
366
|
+
game has NO generated character does each remote wear their own
|
|
367
|
+
`p.avatarUrl` VRM, which is then the right look. Several generated characters
|
|
368
|
+
(per class, per team, a picker) are fine when the player asks for them — the
|
|
369
|
+
rule is a coherent themed cast, not exactly one model. Name tags distinguish
|
|
370
|
+
players; their bodies don't have to.
|
|
371
|
+
|
|
372
|
+
The loader shares one parsed base across every remote (N remotes ≈ 1 body of
|
|
373
|
+
GPU memory) and `remote.dispose()` detaches the clone without touching those
|
|
374
|
+
shared resources. Move it with the interpolator from
|
|
375
|
+
`$genex-threejs-multiplayer`, and **never** create a rigid body, a
|
|
376
|
+
`CharacterController`, or any physics for it. Simulating remote players'
|
|
377
|
+
physics locally guarantees divergence — every client would compute a different
|
|
378
|
+
world. And a remote player's body is **never hand-built primitives** (no
|
|
379
|
+
capsule-plus-cone "person"). First-person games are not exempt — the local
|
|
380
|
+
player may be invisible to themselves (`setFirstPersonBody`), but every remote
|
|
381
|
+
is a full character on screen.
|
|
329
382
|
|
|
330
383
|
- **Publish `character.netState()`** on the fixed 10–20 Hz tick (never per frame):
|
|
331
384
|
`room.me.set(character.netState())`. It bundles the network-safe position, a four-number
|
|
@@ -8,12 +8,16 @@ clips only on the exact matching generated rig revision.
|
|
|
8
8
|
|
|
9
9
|
## The bundled assets + animation packs
|
|
10
10
|
|
|
11
|
-
`npx genex controller character`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
`npx genex controller character` installs both lanes of the player's body — the
|
|
12
|
+
game's own generated character (the default) and the VRM avatar fallback:
|
|
13
|
+
|
|
14
|
+
- `public/assets/meshy-character.json` — written by
|
|
15
|
+
`npx genex controller character --character <id>` once the game's character
|
|
16
|
+
has been generated. Its presence is what `loadPlayerCharacter` routes on.
|
|
17
|
+
- `public/assets/avatar.vrm` — the FALLBACK body (the bundled CC0 default).
|
|
18
|
+
Always present; always one path. At runtime the fallback lane loads the
|
|
19
|
+
visiting player's own picked avatar instead (`user.avatarUrl` from the embed
|
|
20
|
+
identity) — this file covers local dev and load failures.
|
|
17
21
|
- `public/assets/animation-library.glb` (~1.3 MB) — the 12-clip core
|
|
18
22
|
(idle/walk/jog/sprint, the jump trio, crouch idle+move, hit, death, interact)
|
|
19
23
|
on a shared Quaternius rig (provenance in `src/controllers/NOTICE.md`).
|
|
@@ -26,38 +30,39 @@ clips only on the exact matching generated rig revision.
|
|
|
26
30
|
VRM helpers live in `src/controllers/character/vrm/`. Install three-vrm once:
|
|
27
31
|
`npm i @pixiv/three-vrm`.
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
`loadPlayerCharacter` loads the body and everything the game can play on it in
|
|
34
|
+
one call — the generated character's exact-rig clips, or the core library plus
|
|
35
|
+
installed packs retargeted onto the avatar's humanoid rig. All with
|
|
36
|
+
**relative** paths so the published game works under its subpath:
|
|
32
37
|
|
|
33
38
|
```ts
|
|
34
|
-
import {
|
|
35
|
-
import { loadCharacterClips } from "./controllers/character/animation-packs.ts";
|
|
39
|
+
import { loadPlayerCharacter } from "./controllers/character/player-character.ts";
|
|
36
40
|
import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
|
|
37
41
|
import { CharacterController } from "./controllers/character/character-controller.ts";
|
|
38
42
|
import { CharacterAnimations } from "./controllers/character/character-animations.ts";
|
|
39
43
|
import { characterPresets } from "./controllers/character/presets.ts";
|
|
40
44
|
import { waitForPlayer } from "@genex-ai/embed-sdk";
|
|
41
45
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
46
|
+
// This game's own generated character when it has one; otherwise the playing
|
|
47
|
+
// user's own avatar (their profile pick; per-session for guests), with the
|
|
48
|
+
// baked file as the local-dev / failure fallback.
|
|
44
49
|
const { user } = await waitForPlayer();
|
|
45
|
-
const
|
|
46
|
-
.catch(() => loadVrm("./assets/avatar.vrm"));
|
|
47
|
-
const clips = await loadCharacterClips(vrm); // core + every installed pack, retargeted
|
|
50
|
+
const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl });
|
|
48
51
|
|
|
49
|
-
// capsuleFromModel derives the collider from the
|
|
50
|
-
// per-
|
|
51
|
-
const fit = capsuleFromModel(scene);
|
|
52
|
+
// capsuleFromModel derives the collider from the body's bounds — no manual
|
|
53
|
+
// per-model tuning even as heights and proportions vary between lanes.
|
|
54
|
+
const fit = capsuleFromModel(player.scene);
|
|
52
55
|
const character = new CharacterController(physics.world, camera, {
|
|
53
56
|
...characterPresets["default"].options,
|
|
54
57
|
...fit,
|
|
55
58
|
position: { x: 0, y: 2, z: 0 },
|
|
56
59
|
});
|
|
57
|
-
character.root.add(scene);
|
|
58
|
-
scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
60
|
+
character.root.add(player.scene);
|
|
61
|
+
player.scene.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
59
62
|
|
|
60
|
-
const anims = new CharacterAnimations(scene, clips
|
|
63
|
+
const anims = new CharacterAnimations(player.scene, player.clips, {
|
|
64
|
+
locomotionProfile: player.locomotionProfile, // undefined in the fallback lane — fine
|
|
65
|
+
});
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
(Advanced: to load a single GLB by hand, `retargetClips(vrm, gltf.scene,
|
|
@@ -68,12 +73,15 @@ Per render frame, **after** `physics.step(delta)`:
|
|
|
68
73
|
|
|
69
74
|
```ts
|
|
70
75
|
anims.update(character, delta); // the controller itself satisfies the snapshot type
|
|
71
|
-
|
|
76
|
+
player.update(delta); // REQUIRED — ticks the humanoid rig + spring bones
|
|
72
77
|
```
|
|
73
78
|
|
|
74
|
-
`
|
|
75
|
-
animated normalized pose onto the render
|
|
76
|
-
|
|
79
|
+
`player.update(delta)` MUST run every frame, AFTER `anims.update`. In the
|
|
80
|
+
fallback (VRM) lane it applies the animated normalized pose onto the render
|
|
81
|
+
mesh and advances spring bones (hair, cloth); native generated rigs need no
|
|
82
|
+
such pass, so it costs nothing there. Call it unconditionally — that is what
|
|
83
|
+
lets the body change lanes without this loop changing.
|
|
84
|
+
`anims.update` takes the RAW render delta — pause/slow-motion go through
|
|
77
85
|
`anims.setPaused(true)` / `anims.setTimeScale(0.5)` (fade durations stretch with
|
|
78
86
|
the time scale so slow motion doesn't pop).
|
|
79
87
|
|
|
@@ -107,26 +115,25 @@ estimated cost. Use a returned ID; do not guess from a name. Search is free and
|
|
|
107
115
|
local. Character and animation generation print a Genex-credit quote before
|
|
108
116
|
enqueueing, and successful outputs are copied to permanent R2 URLs.
|
|
109
117
|
|
|
110
|
-
The install command writes `public/assets/meshy-character.json`.
|
|
111
|
-
|
|
118
|
+
The install command writes `public/assets/meshy-character.json`. **There is
|
|
119
|
+
nothing to wire** — the boot block above already routes on that file, so the
|
|
120
|
+
next reload plays the generated character with its exact-rig clips and
|
|
121
|
+
locomotion profile, through the same physics controller and camera.
|
|
122
|
+
|
|
123
|
+
`loadMeshyCharacter` is the direct handle underneath, for the rare case that
|
|
124
|
+
needs the manifest itself (reading `rigSignature`, a second character in the
|
|
125
|
+
scene):
|
|
112
126
|
|
|
113
127
|
```ts
|
|
114
128
|
import { loadMeshyCharacter } from "./controllers/character/meshy/meshy-loader.ts";
|
|
115
129
|
|
|
116
130
|
const native = await loadMeshyCharacter("./assets/meshy-character.json");
|
|
117
|
-
|
|
118
|
-
const character = new CharacterController(physics.world, camera, {
|
|
119
|
-
...characterPresets["default"].options,
|
|
120
|
-
...fit,
|
|
121
|
-
position: { x: 0, y: 2, z: 0 },
|
|
122
|
-
});
|
|
123
|
-
character.root.add(native.scene);
|
|
124
|
-
native.scene.position.y = fit.modelOffsetY;
|
|
125
|
-
const anims = new CharacterAnimations(native.scene, native.clips, {
|
|
126
|
-
locomotionProfile: native.locomotionProfile,
|
|
127
|
-
});
|
|
131
|
+
// native.scene / native.clips / native.locomotionProfile / native.manifest
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
Never make it the player's boot path: that is the two-lane split
|
|
135
|
+
`loadPlayerCharacter` exists to remove.
|
|
136
|
+
|
|
130
137
|
During Meshy validation, record the action ID actually bound to every slot.
|
|
131
138
|
A public preview is not evidence when the game is playing a different clip or
|
|
132
139
|
a rig-basic fallback. Meshy limb rotations play unchanged. Never freeze hand
|
|
@@ -210,15 +217,19 @@ const footIK = new FootIK(vrm, (foot) => {
|
|
|
210
217
|
});
|
|
211
218
|
```
|
|
212
219
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`
|
|
220
|
+
Foot IK is a VRM-lane feature (it poses a VRM's normalized bones), so it
|
|
221
|
+
applies only when the player is on the fallback body — check
|
|
222
|
+
`player.kind === "avatar"` before creating it.
|
|
223
|
+
|
|
224
|
+
ORDER IS LOAD-BEARING — foot IK poses the VRM's *normalized* bones, which the
|
|
225
|
+
VRM's own update then copies onto the render mesh. Run it BETWEEN the two
|
|
226
|
+
(after that copy it has no visible effect at all — the next frame's mixer tick
|
|
216
227
|
overwrites it before the copy):
|
|
217
228
|
|
|
218
229
|
```ts
|
|
219
230
|
anims.update(character, delta); // 1. mixer poses the normalized rig
|
|
220
|
-
footIK
|
|
221
|
-
|
|
231
|
+
footIK?.update(delta); // 2. plant the feet on that pose (fallback lane only)
|
|
232
|
+
player.update(delta); // 3. copy normalized -> render mesh + spring bones
|
|
222
233
|
```
|
|
223
234
|
|
|
224
235
|
## States and default clips
|
|
@@ -409,12 +420,13 @@ remoteAnims.update(remoteState.flags, delta);
|
|
|
409
420
|
resolves correctly (a missing flag reads as not-crouched).
|
|
410
421
|
|
|
411
422
|
The mixer crossfades exactly as it does locally, so remote players animate
|
|
412
|
-
correctly without simulating anything.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
423
|
+
correctly without simulating anything. Build every remote's body with
|
|
424
|
+
`loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl })` — the same routing the
|
|
425
|
+
local player gets, so in a game with a generated character everyone wears it,
|
|
426
|
+
and only a game without one falls back to per-player avatars (`p.avatarUrl` is
|
|
427
|
+
the multiplayer SDK's verified per-player field; empty means fall back). It
|
|
428
|
+
clones a shared parsed base, so N remotes cost about one body of GPU memory;
|
|
429
|
+
call `remote.dispose()` on leave. Relay one-shot events (punch,
|
|
418
430
|
hit, validated planar-action start) alongside the flags and call the matching
|
|
419
431
|
`remoteAnims.playOneShot(...)` on receipt. Never run `MotionActionDriver` for a
|
|
420
432
|
remote: its smoothed owner-authored transform is the sole movement authority.
|
|
@@ -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
|
|
|
@@ -72,9 +72,11 @@ import { waitForPlayer, waitForAuth, getColyseusAuth, getEmbedToken } from "@gen
|
|
|
72
72
|
const { user, guest } = await waitForPlayer();
|
|
73
73
|
// user.id / user.name — real account identity, or guest:<id> / "Guest-1234"
|
|
74
74
|
// user.avatarUrl — the player's OWN VRM avatar (profile pick; per-session for
|
|
75
|
-
// guests).
|
|
76
|
-
//
|
|
77
|
-
//
|
|
75
|
+
// guests). It is the FALLBACK body, not the game's character: pass it to
|
|
76
|
+
// loadPlayerCharacter({ avatarUrl: user.avatarUrl }) and it is used only when
|
|
77
|
+
// this game has no generated character of its own, with the baked
|
|
78
|
+
// ./assets/avatar.vrm behind it (absent on old APIs / local test mode).
|
|
79
|
+
// Peers resolve the same way via the multiplayer SDK's player.avatarUrl.
|
|
78
80
|
|
|
79
81
|
// ACCOUNT gate — resolves ONLY for signed-in players (stays pending for
|
|
80
82
|
// guests; resolves later if they sign in mid-game). Use ONLY for /state
|
|
@@ -537,14 +537,16 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
537
537
|
`{ id, name, avatarUrl, connected, state, stateRaw }`: `state` is auto-smoothed (remotes) / live (you);
|
|
538
538
|
`stateRaw` is the raw latest (hit-tests, discrete values). A reconnect-grace seat remains in this
|
|
539
539
|
map with `connected: false`. `avatarUrl` is that player's verified VRM pick (server-set, `''`
|
|
540
|
-
when unknown) —
|
|
541
|
-
`
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
540
|
+
when unknown) — render each remote with
|
|
541
|
+
`loadRemotePlayerCharacter({ avatarUrl: p.avatarUrl })` (the vendored controller kit's
|
|
542
|
+
loader) and `remote.dispose()` on leave; never publish avatar URLs through `me.set`.
|
|
543
|
+
That one call carries the whole rule: **in a game with its own generated character,
|
|
544
|
+
every remote wears it** — `p.avatarUrl` is used only when this game has no generated
|
|
545
|
+
character, which is exactly when per-player looks are the right answer. It shares one
|
|
546
|
+
parsed base across remotes (N remotes ≈ 1 body of geometry/textures instead of a parse
|
|
547
|
+
per player) and clips are retargeted once on that base. **A remote player's body is
|
|
548
|
+
NEVER hand-built primitives** (no capsule + cone "person" — the single most common
|
|
549
|
+
shipped co-op defect). This applies with full force to
|
|
548
550
|
first-person games — the local player may be invisible to themselves, but every
|
|
549
551
|
remote hunter/partner is a full character on screen. On phone tiers, animate and fully draw only the
|
|
550
552
|
nearest `tier.remoteAvatarCap` remotes (`$genex-threejs-adaptive-quality`) — freeze the
|
|
@@ -273,13 +273,14 @@ room.me.set({
|
|
|
273
273
|
r: character.runActive, j: character.jumpActive, c: character.crouchActive,
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
-
// Remote players: a VISUAL-ONLY
|
|
277
|
-
// Build each remote
|
|
278
|
-
//
|
|
279
|
-
// relay replicates
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
276
|
+
// Remote players: a VISUAL-ONLY body — NO Rapier body, NO controller instance for remotes.
|
|
277
|
+
// Build each remote with loadRemotePlayerCharacter({ avatarUrl: pl.avatarUrl }) — in a game
|
|
278
|
+
// with its own generated character everyone wears it, and pl.avatarUrl (the verified
|
|
279
|
+
// per-player pick the relay replicates; '' = unknown → fall back) is used only when this
|
|
280
|
+
// game has no generated character. It clones a shared parsed base, so remotes share GPU
|
|
281
|
+
// geometry/textures instead of re-parsing per player, and retargets clips once on that
|
|
282
|
+
// base; drive each clone's own AnimationMixer and call remote.dispose() on 'leave'.
|
|
283
|
+
// Never reuse your own body OBJECT for a remote (clones, not references).
|
|
283
284
|
// Phone tiers animate only the nearest tier.remoteAvatarCap remotes (adaptive-quality skill). Position/rotation from smoothed state; animation from the
|
|
284
285
|
// synced flags via the avatar's own update(flags, dt). The character-controller skill's
|
|
285
286
|
// animations reference owns the flag set.
|
|
@@ -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
|
|