@genex-ai/cli-demo 0.37.0 → 0.38.0
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 +1 -0
- package/dist/index.js +344 -20
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +8 -5
- package/templates/controllers/assets/animation-library.glb +0 -0
- package/templates/controllers/assets/anims-manifest.json +1306 -0
- package/templates/controllers/character/animation-packs.ts +70 -0
- package/templates/controllers/character/character-animations.ts +51 -8
- package/templates/controllers/character/character-controller.ts +153 -5
- package/templates/controllers/character/keyboard-input.ts +12 -1
- package/templates/controllers/character/presets.ts +19 -1
- package/templates/controllers/character/vrm/foot-ik.ts +71 -15
- package/templates/controllers/character/vrm/vrm-retarget.ts +72 -12
- package/templates/skills/genex-threejs-character-controller/SKILL.md +55 -22
- package/templates/skills/genex-threejs-character-controller/references/animations.md +91 -46
- package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +3 -0
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +7 -6
- package/templates/skills/genex-threejs-skill-router/SKILL.md +1 -1
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +3 -2
- package/templates/controllers/assets/character.glb +0 -0
|
@@ -68,6 +68,16 @@ export interface FootIKOptions {
|
|
|
68
68
|
* so airborne legs keep their jump pose). Default: always active.
|
|
69
69
|
*/
|
|
70
70
|
isActive?: () => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Gate for the terrain-relative reach (downward offsets + pelvis drop, and
|
|
73
|
+
* full plant-up). Return false while a full-body one-shot is playing
|
|
74
|
+
* (`() => !anims.oneShotActive`): those clips are choreography — a foot
|
|
75
|
+
* swinging over a lower step must not drag the pelvis down into a staircase,
|
|
76
|
+
* and a raised stance foot must not be yanked a full step up. While gated,
|
|
77
|
+
* feet are lifted only as much as needed to keep the sole out of the contact
|
|
78
|
+
* under them (anti dig-in). Default: always allowed.
|
|
79
|
+
*/
|
|
80
|
+
allowReachDown?: () => boolean;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// three-vrm's own node type — same THREE.Object3D at runtime, but using the
|
|
@@ -96,6 +106,12 @@ const NORMAL_DAMPING = 12; // ground-normal smoothing
|
|
|
96
106
|
const PLANTED_LIFT_MIN = 0.04; // below this lift the foot is fully planted (align at full weight)
|
|
97
107
|
const PLANTED_LIFT_MAX = 0.16; // above this lift the foot is fully lifted (no align)
|
|
98
108
|
const MAX_FOOT_TILT = 0.6; // clamp foot-to-slope tilt (rad)
|
|
109
|
+
const PENETRATION_SLACK = 0.02; // ankle may dip this far below rest before the no-sink lift kicks in
|
|
110
|
+
// Max anti-dig-in lift while reach-down is gated (one-shots). Retarget dips are
|
|
111
|
+
// a few cm; a hard cap keeps a stance foot carried INTO a stair riser by the
|
|
112
|
+
// choreography from being lifted a whole step ("knee to the chest") — beyond
|
|
113
|
+
// the cap it stays buried, which the step itself mostly occludes.
|
|
114
|
+
const GATED_LIFT_CAP = 0.12;
|
|
99
115
|
const MIN_BONE_LENGTH = 1e-4;
|
|
100
116
|
const IK_EPSILON = 1e-4;
|
|
101
117
|
|
|
@@ -136,6 +152,7 @@ export class FootIK {
|
|
|
136
152
|
#pelvisDrop: boolean;
|
|
137
153
|
#alignFeet: boolean;
|
|
138
154
|
#isActive: (() => boolean) | undefined;
|
|
155
|
+
#allowReachDown: (() => boolean) | undefined;
|
|
139
156
|
#enabled = true;
|
|
140
157
|
#weight = 0;
|
|
141
158
|
#restFootHeight = 0;
|
|
@@ -148,6 +165,7 @@ export class FootIK {
|
|
|
148
165
|
this.#pelvisDrop = options.pelvisDrop ?? true;
|
|
149
166
|
this.#alignFeet = options.alignFeet ?? true;
|
|
150
167
|
this.#isActive = options.isActive;
|
|
168
|
+
this.#allowReachDown = options.allowReachDown;
|
|
151
169
|
this.#modelRoot = vrm.scene;
|
|
152
170
|
|
|
153
171
|
const h = vrm.humanoid;
|
|
@@ -157,7 +175,14 @@ export class FootIK {
|
|
|
157
175
|
const lower = h.getNormalizedBoneNode(l);
|
|
158
176
|
const foot = h.getNormalizedBoneNode(f);
|
|
159
177
|
return upper && lower && foot
|
|
160
|
-
? {
|
|
178
|
+
? {
|
|
179
|
+
upper,
|
|
180
|
+
lower,
|
|
181
|
+
foot,
|
|
182
|
+
offset: 0,
|
|
183
|
+
normal: new THREE.Vector3(0, 1, 0),
|
|
184
|
+
animFootPos: new THREE.Vector3(),
|
|
185
|
+
}
|
|
161
186
|
: null;
|
|
162
187
|
};
|
|
163
188
|
const left = mk(VRMHumanBoneName.LeftUpperLeg, VRMHumanBoneName.LeftLowerLeg, VRMHumanBoneName.LeftFoot);
|
|
@@ -192,14 +217,27 @@ export class FootIK {
|
|
|
192
217
|
|
|
193
218
|
const k = 1 - Math.exp(-this.#smoothing * dt);
|
|
194
219
|
const kNormal = 1 - Math.exp(-NORMAL_DAMPING * dt);
|
|
220
|
+
const reachDown = this.#allowReachDown?.() ?? true;
|
|
195
221
|
|
|
196
222
|
// 1. Sample the ground under each ANIMATED foot; smooth offset + normal.
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
223
|
+
// Two lift signals, merged:
|
|
224
|
+
// - TERRAIN-relative plant: the ground height under the foot vs the body
|
|
225
|
+
// root (the VRM origin = its floor/sole level), NOT vs the animated
|
|
226
|
+
// foot. Stride-phase independent — a lifted swing foot keeps its
|
|
227
|
+
// animation (no dragging/sinking while running) while a planted foot
|
|
228
|
+
// still lands on its step, and it self-corrects any residual
|
|
229
|
+
// capsule-float gap (feet reach true ground).
|
|
230
|
+
// - needLift: raise the ankle ONLY as much as needed so the sole clears
|
|
231
|
+
// the contact under it — the anti-dig-in floor for poses that dip the
|
|
232
|
+
// animated feet below the clip's own ground (weapon recoil etc. on
|
|
233
|
+
// differently-proportioned avatars).
|
|
234
|
+
// With reach-down gated off (full-body one-shots), ONLY needLift
|
|
235
|
+
// applies, capped at GATED_LIFT_CAP: choreography feet are left alone
|
|
236
|
+
// unless they'd clip into a step, and a foot the stance carries INTO a
|
|
237
|
+
// riser gets at most a small hop, not a knee-to-the-chest fold. The
|
|
238
|
+
// lift-only offsets also mean min(0,offset)=0 keeps the pelvis drop
|
|
239
|
+
// off. Smoothing eases every transition (one-shot start/end, a ray
|
|
240
|
+
// crossing a step edge) without pops.
|
|
203
241
|
for (const leg of this.#legs) {
|
|
204
242
|
leg.foot.getWorldPosition(leg.animFootPos);
|
|
205
243
|
const sample = active ? this.#query(leg.animFootPos) : null;
|
|
@@ -207,14 +245,30 @@ export class FootIK {
|
|
|
207
245
|
_normalTarget.copy(
|
|
208
246
|
sample !== null && typeof sample !== "number" && sample.normal ? sample.normal : UP,
|
|
209
247
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
248
|
+
let desired = 0;
|
|
249
|
+
if (groundY !== null) {
|
|
250
|
+
// Anti-dig-in signal: how much lift (if any) the ankle needs so the
|
|
251
|
+
// sole clears the contact under it. NEGATIVE when the foot is safely
|
|
252
|
+
// above the contact — and it must stay negative: flooring it at zero
|
|
253
|
+
// here would win every Math.max below and silently kill the downward
|
|
254
|
+
// reach (and with it the pelvis drop) for the whole planter.
|
|
255
|
+
const needLift =
|
|
256
|
+
groundY +
|
|
257
|
+
this.#soleClearance +
|
|
258
|
+
this.#restFootHeight -
|
|
259
|
+
PENETRATION_SLACK -
|
|
260
|
+
leg.animFootPos.y;
|
|
261
|
+
desired = reachDown
|
|
262
|
+
? Math.max(
|
|
263
|
+
THREE.MathUtils.clamp(
|
|
264
|
+
groundY + this.#soleClearance - _rootPos.y,
|
|
265
|
+
-this.#maxOffset,
|
|
266
|
+
this.#maxOffset,
|
|
267
|
+
),
|
|
268
|
+
Math.min(needLift, this.#maxOffset),
|
|
269
|
+
)
|
|
270
|
+
: THREE.MathUtils.clamp(needLift, 0, GATED_LIFT_CAP);
|
|
271
|
+
}
|
|
218
272
|
leg.offset += (desired - leg.offset) * k;
|
|
219
273
|
leg.normal.lerp(_normalTarget, kNormal).normalize();
|
|
220
274
|
}
|
|
@@ -229,6 +283,8 @@ export class FootIK {
|
|
|
229
283
|
}
|
|
230
284
|
|
|
231
285
|
// 3. Per-leg two-bone IK to the grounded target, then flatten planted feet.
|
|
286
|
+
// (Anti-dig-in is already folded into the smoothed offset — needLift in
|
|
287
|
+
// step 1 — so the target needs no extra instant clamp here.)
|
|
232
288
|
for (const leg of this.#legs) {
|
|
233
289
|
_target.copy(leg.animFootPos);
|
|
234
290
|
_target.y += leg.offset * this.#weight;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Retarget Quaternius Universal Animation Library clips
|
|
3
|
-
//
|
|
2
|
+
// Retarget Quaternius Universal Animation Library clips onto a three-vrm
|
|
3
|
+
// normalized humanoid rig (Genex AG-747/AG-775). Two source rigs are supported
|
|
4
|
+
// and auto-detected: the free UAL's Blender Rigify `DEF-*` skeleton and the UAL
|
|
5
|
+
// Pro's UE-mannequin-style skeleton (`pelvis`/`spine_01`/…) that the bundled
|
|
6
|
+
// core library and every CDN animation pack use. Adapted from the
|
|
4
7
|
// official three-vrm Mixamo retarget recipe (@pixiv/three-vrm examples, MIT):
|
|
5
8
|
// rewrite each bone track into the VRM's normalized-bone local space using the
|
|
6
9
|
// SOURCE rig's rest-pose world rotations, and scale the hips translation by the
|
|
@@ -56,26 +59,81 @@ const DEF_TO_VRM: Record<string, VrmBone> = {
|
|
|
56
59
|
"DEF-toe.R": VRMHumanBoneName.RightToes,
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
// Quaternius UAL Pro (UE-mannequin-style skeleton) -> VRM humanoid bone. Bone
|
|
63
|
+
// names verified against the UAL1 master GLB ("Head" really is capitalized).
|
|
64
|
+
// Fingers are omitted, same policy as the DEF map.
|
|
65
|
+
const UE_TO_VRM: Record<string, VrmBone> = {
|
|
66
|
+
pelvis: VRMHumanBoneName.Hips,
|
|
67
|
+
spine_01: VRMHumanBoneName.Spine,
|
|
68
|
+
spine_02: VRMHumanBoneName.Chest,
|
|
69
|
+
spine_03: VRMHumanBoneName.UpperChest,
|
|
70
|
+
neck_01: VRMHumanBoneName.Neck,
|
|
71
|
+
Head: VRMHumanBoneName.Head,
|
|
72
|
+
clavicle_l: VRMHumanBoneName.LeftShoulder,
|
|
73
|
+
upperarm_l: VRMHumanBoneName.LeftUpperArm,
|
|
74
|
+
lowerarm_l: VRMHumanBoneName.LeftLowerArm,
|
|
75
|
+
hand_l: VRMHumanBoneName.LeftHand,
|
|
76
|
+
clavicle_r: VRMHumanBoneName.RightShoulder,
|
|
77
|
+
upperarm_r: VRMHumanBoneName.RightUpperArm,
|
|
78
|
+
lowerarm_r: VRMHumanBoneName.RightLowerArm,
|
|
79
|
+
hand_r: VRMHumanBoneName.RightHand,
|
|
80
|
+
thigh_l: VRMHumanBoneName.LeftUpperLeg,
|
|
81
|
+
calf_l: VRMHumanBoneName.LeftLowerLeg,
|
|
82
|
+
foot_l: VRMHumanBoneName.LeftFoot,
|
|
83
|
+
ball_l: VRMHumanBoneName.LeftToes,
|
|
84
|
+
thigh_r: VRMHumanBoneName.RightUpperLeg,
|
|
85
|
+
calf_r: VRMHumanBoneName.RightLowerLeg,
|
|
86
|
+
foot_r: VRMHumanBoneName.RightFoot,
|
|
87
|
+
ball_r: VRMHumanBoneName.RightToes,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pick the source-rig bone map by looking for each rig's hips bone in the
|
|
92
|
+
* scene: `DEF-hips` -> Rigify (free UAL), `pelvis` -> UE-style (UAL Pro).
|
|
93
|
+
* Returns null for unrecognized rigs.
|
|
94
|
+
*/
|
|
95
|
+
export function detectBoneMap(sourceRoot: THREE.Object3D): Record<string, VrmBone> | null {
|
|
96
|
+
let map: Record<string, VrmBone> | null = null;
|
|
97
|
+
sourceRoot.traverse((object) => {
|
|
98
|
+
if (map !== null) return;
|
|
99
|
+
if (object.name === "DEF-hips") map = DEF_TO_VRM;
|
|
100
|
+
else if (object.name === "pelvis") map = UE_TO_VRM;
|
|
101
|
+
});
|
|
102
|
+
return map;
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
/**
|
|
60
106
|
* Retarget UAL clips onto `vrm`.
|
|
61
107
|
* @param vrm the loaded VRM (already through {@link loadVrm}).
|
|
62
|
-
* @param animationRoot the animation
|
|
63
|
-
*
|
|
64
|
-
* @param clips that GLB's animations
|
|
108
|
+
* @param animationRoot the animation GLB's scene — its bones in rest pose
|
|
109
|
+
* supply the source frame the tracks are relative to.
|
|
110
|
+
* @param clips that GLB's animations.
|
|
111
|
+
* @param boneMap source-bone-name -> VRM humanoid bone. Defaults to
|
|
112
|
+
* {@link detectBoneMap} on `animationRoot` (Rigify or
|
|
113
|
+
* UE-style rigs bind automatically).
|
|
65
114
|
* @returns new clips whose tracks target the VRM's normalized humanoid bones.
|
|
66
115
|
*/
|
|
67
116
|
export function retargetClips(
|
|
68
117
|
vrm: VRM,
|
|
69
118
|
animationRoot: THREE.Object3D,
|
|
70
119
|
clips: THREE.AnimationClip[],
|
|
120
|
+
boneMap?: Record<string, VrmBone>,
|
|
71
121
|
): THREE.AnimationClip[] {
|
|
122
|
+
const sourceToVrm = boneMap ?? detectBoneMap(animationRoot);
|
|
123
|
+
if (sourceToVrm === null) {
|
|
124
|
+
console.warn(
|
|
125
|
+
"[vrm-retarget] unrecognized animation rig (no DEF-hips or pelvis bone found) — returning no clips; pass an explicit boneMap to retargetClips.",
|
|
126
|
+
);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
72
130
|
animationRoot.updateWorldMatrix(true, true);
|
|
73
131
|
vrm.scene.updateWorldMatrix(true, true);
|
|
74
132
|
|
|
75
133
|
// three's GLTFLoader SANITIZES node names in animation track targets
|
|
76
134
|
// (PropertyBinding strips `[].:/ ` and turns spaces into `_`), so a Rigify bone
|
|
77
135
|
// "DEF-upper_arm.L" shows up in tracks as "DEF-upper_armL". Map those sanitized
|
|
78
|
-
// names back to the real bones, whose actual names carry the dots
|
|
136
|
+
// names back to the real bones, whose actual names carry the dots the map
|
|
79
137
|
// keys on. (Mixamo names are dotless, so the upstream recipe never needed this.)
|
|
80
138
|
const sanitize = (name: string): string => name.replace(/\s/g, "_").replace(/[[\]./:]/g, "");
|
|
81
139
|
const sourceByTrackName = new Map<string, THREE.Object3D>();
|
|
@@ -83,9 +141,9 @@ export function retargetClips(
|
|
|
83
141
|
if (o.name) sourceByTrackName.set(sanitize(o.name), o);
|
|
84
142
|
});
|
|
85
143
|
// glTF load sanitizes bone names too, so `source.name` is already dot-stripped —
|
|
86
|
-
// key the VRM-bone lookup by the sanitized
|
|
87
|
-
const
|
|
88
|
-
for (const [
|
|
144
|
+
// key the VRM-bone lookup by the sanitized source name, matching the track's nodeName.
|
|
145
|
+
const sanitizedSourceToVrm: Record<string, VrmBone> = {};
|
|
146
|
+
for (const [src, bone] of Object.entries(sourceToVrm)) sanitizedSourceToVrm[sanitize(src)] = bone;
|
|
89
147
|
|
|
90
148
|
// Reusable bind-pose quaternions. Both rigs are at rest here (nothing has
|
|
91
149
|
// animated them yet), so getWorldQuaternion reads the bind pose.
|
|
@@ -99,8 +157,10 @@ export function retargetClips(
|
|
|
99
157
|
// hips' vertical bob/crouch on the target hips as a rest-relative DELTA, so
|
|
100
158
|
// the physics controller still owns the whole-body base translation while the
|
|
101
159
|
// pose keeps the pelvis (and therefore the feet) at the right height.
|
|
102
|
-
const
|
|
103
|
-
|
|
160
|
+
const hipsSourceName = Object.entries(sanitizedSourceToVrm).find(
|
|
161
|
+
([, bone]) => bone === VRMHumanBoneName.Hips,
|
|
162
|
+
)?.[0];
|
|
163
|
+
const srcHips = (hipsSourceName ? sourceByTrackName.get(hipsSourceName) : undefined) ?? null;
|
|
104
164
|
const tgtHips = vrm.humanoid.getNormalizedBoneNode(VRMHumanBoneName.Hips);
|
|
105
165
|
const hipsSrcParentWorld = new THREE.Quaternion();
|
|
106
166
|
const hipsTgtParentWorldInv = new THREE.Quaternion();
|
|
@@ -128,7 +188,7 @@ export function retargetClips(
|
|
|
128
188
|
const nodeName = track.name.slice(0, lastDot);
|
|
129
189
|
const prop = track.name.slice(lastDot + 1);
|
|
130
190
|
const source = sourceByTrackName.get(nodeName);
|
|
131
|
-
const vrmBone =
|
|
191
|
+
const vrmBone = sanitizedSourceToVrm[nodeName];
|
|
132
192
|
if (!source || !vrmBone) continue;
|
|
133
193
|
|
|
134
194
|
const target = vrm.humanoid.getNormalizedBoneNode(vrmBone);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-character-controller
|
|
3
|
-
description: Add a tuned physics character controller to a Genex Three.js game with `npx genex controller character` — dynamic-capsule
|
|
3
|
+
description: Add a tuned physics character controller to a Genex Three.js game with `npx genex controller character` — dynamic-capsule walk/run/jump/crouch (slopes, stairs, moving platforms), follow camera, keyboard + touch input, animation binding. Animation packs (sword, pistol, magic, climb, swim, emotes…) install via `npx genex controller anims <tags>`. Use for any on-foot player or third-person movement, and whenever the user asks for ecctrl — this is that controller, ported to plain Three.js.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Character Controller
|
|
@@ -14,9 +14,13 @@ npm i @dimforge/rapier3d-compat @pixiv/three-vrm # three is already in the sca
|
|
|
14
14
|
|
|
15
15
|
The command vendors tested, tuned controller code into the game: TypeScript
|
|
16
16
|
modules into `src/controllers/` (including `character/vrm/` — VRM loading,
|
|
17
|
-
animation retargeting, capsule auto-fit, foot IK) and the
|
|
18
|
-
`animation-library.glb`
|
|
19
|
-
|
|
17
|
+
animation retargeting, capsule auto-fit, foot IK) and the 12-clip core
|
|
18
|
+
`animation-library.glb` (idle/walk/run/jump/crouch + hit/death/interact,
|
|
19
|
+
~1.3 MB) into `public/assets/`. Need more — swords, pistols, magic, climbing,
|
|
20
|
+
swimming, emotes? Install exactly what the game uses with
|
|
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` —
|
|
23
|
+
**your** avatar when you're signed in,
|
|
20
24
|
otherwise a bundled CC0 default (attribution in `src/controllers/NOTICE.md`).
|
|
21
25
|
The character plays as that VRM. The copied files are then owned by the game —
|
|
22
26
|
edit them freely; re-running skips existing files unless `--force`. Do not write
|
|
@@ -36,27 +40,28 @@ platforms, climbs stairs and slides on too-steep slopes out of the box.
|
|
|
36
40
|
| `character/keyboard-input.ts` | `KeyboardInput` | WASD/arrows/Shift/Space/F state, no per-frame polling setup |
|
|
37
41
|
| `character/touch-joystick.ts` | `TouchJoystick`, `VirtualButton` | mobile controls |
|
|
38
42
|
| `character/character-animations.ts` | `CharacterAnimations` | animation state machine + fuzzy clip binding + `playOneShot` + procedural fallback |
|
|
39
|
-
| `character/
|
|
43
|
+
| `character/animation-packs.ts` | `loadCharacterClips` | one call loads the core library + every installed `genex controller anims` pack, retargeted onto the VRM |
|
|
44
|
+
| `character/vrm/*` | `loadVrm`, `retargetClips`, `capsuleFromModel`, `FootIK` | load the VRM avatar, retarget library clips onto its humanoid rig, auto-fit the capsule, ground the feet |
|
|
40
45
|
|
|
41
46
|
## Minimal wiring
|
|
42
47
|
|
|
43
48
|
```ts
|
|
44
|
-
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
45
49
|
import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
|
|
46
50
|
import { CharacterController } from "./controllers/character/character-controller.ts";
|
|
47
51
|
import { CharacterAnimations } from "./controllers/character/character-animations.ts";
|
|
52
|
+
import { loadCharacterClips } from "./controllers/character/animation-packs.ts";
|
|
48
53
|
import { characterPresets } from "./controllers/character/presets.ts";
|
|
49
54
|
import { FollowCamera } from "./controllers/character/follow-camera.ts";
|
|
50
55
|
import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
|
|
51
56
|
import { loadVrm } from "./controllers/character/vrm/vrm-loader.ts";
|
|
52
|
-
import { retargetClips } from "./controllers/character/vrm/vrm-retarget.ts";
|
|
53
57
|
import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
|
|
54
58
|
|
|
55
59
|
const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run before this resolves
|
|
56
60
|
|
|
57
|
-
// Load the player's avatar
|
|
61
|
+
// Load the player's avatar, then every animation the game has (the bundled core
|
|
62
|
+
// library + any packs installed by `genex controller anims`), retargeted onto the VRM.
|
|
58
63
|
const { scene: avatar, vrm } = await loadVrm("./assets/avatar.vrm");
|
|
59
|
-
const
|
|
64
|
+
const clips = await loadCharacterClips(vrm);
|
|
60
65
|
|
|
61
66
|
const fit = capsuleFromModel(avatar); // collider fits THIS avatar's bounds
|
|
62
67
|
const character = new CharacterController(physics.world, camera, {
|
|
@@ -70,7 +75,7 @@ character.root.add(avatar); // parent the avatar under the c
|
|
|
70
75
|
avatar.position.y = fit.modelOffsetY; // root = capsule CENTER; drop the model so feet touch the floor
|
|
71
76
|
physics.registerBody(character.body, character.root); // root now follows the body, interpolated
|
|
72
77
|
|
|
73
|
-
const anims = new CharacterAnimations(avatar,
|
|
78
|
+
const anims = new CharacterAnimations(avatar, clips);
|
|
74
79
|
addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // punch on click
|
|
75
80
|
|
|
76
81
|
const kb = new KeyboardInput();
|
|
@@ -127,17 +132,39 @@ preset table with provenance, the density/spring scaling rule, and the
|
|
|
127
132
|
- The capsule ships with friction `-0.5` **on purpose** (grip is synthesized by
|
|
128
133
|
the controller). Do not "fix" it to a positive value.
|
|
129
134
|
|
|
130
|
-
## Animations
|
|
135
|
+
## Animations + animation packs
|
|
131
136
|
|
|
132
|
-
`CharacterAnimations` resolves
|
|
133
|
-
JUMP_START / JUMP_IDLE / JUMP_FALL / JUMP_LAND)
|
|
134
|
-
and crossfades mixer actions. Every OTHER
|
|
135
|
-
pistol fire, spells, sit, dance, hit reactions —
|
|
136
|
-
`anims.playOneShot("Punch_Jab")`, which layers over locomotion
|
|
137
|
-
when done (punch-on-click is the default).
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
`CharacterAnimations` resolves nine locomotion states (IDLE / WALK / RUN /
|
|
138
|
+
CROUCH_IDLE / CROUCH_MOVE / JUMP_START / JUMP_IDLE / JUMP_FALL / JUMP_LAND)
|
|
139
|
+
from the controller's live flags and crossfades mixer actions. Every OTHER
|
|
140
|
+
clip — punches, sword swings, pistol fire, spells, sit, dance, hit reactions —
|
|
141
|
+
plays through `anims.playOneShot("Punch_Jab")`, which layers over locomotion
|
|
142
|
+
and returns to it when done (punch-on-click is the default).
|
|
143
|
+
|
|
144
|
+
The bundled library carries only the 12 core clips. **Install what the game's
|
|
145
|
+
theme needs** from the 120-clip catalog, by tag or exact clip name:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx genex controller anims sword pistol # a sword+shooter game
|
|
149
|
+
npx genex controller anims stealth climb crawl # a ninja game
|
|
150
|
+
npx genex controller anims --list # browse tags; --list <tag> for per-clip details
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Clips land in `public/assets/anims/` and `loadCharacterClips(vrm)` picks them
|
|
154
|
+
up automatically — re-run with more tags any time (additive). Read
|
|
155
|
+
[references/animations.md](references/animations.md) for the tag catalog with
|
|
156
|
+
genre hints, `playOneShot` options, overrides, foot IK, and remote-player
|
|
157
|
+
animation.
|
|
158
|
+
|
|
159
|
+
## Crouch (built in)
|
|
160
|
+
|
|
161
|
+
`C` toggles crouch (capsule shrinks, speed drops to `crouchSpeedRatio ×
|
|
162
|
+
maxWalkVel`, CROUCH_IDLE/CROUCH_MOVE play). Standing back up is
|
|
163
|
+
ceiling-checked: under a low obstacle the character STAYS crouched and pops up
|
|
164
|
+
automatically once clear; jumping while crouched requests a stand instead of
|
|
165
|
+
jumping. `crouchMode: "hold"` makes it hold-to-crouch;
|
|
166
|
+
`character.setCrouch(bool)` and `character.crouchActive` are the programmatic
|
|
167
|
+
hooks (that's also how a touch button wires in — see Mobile below).
|
|
141
168
|
|
|
142
169
|
## Mobile: TouchJoystick + VirtualButton (wire by default)
|
|
143
170
|
|
|
@@ -151,6 +178,11 @@ import { TouchJoystick, VirtualButton } from "./controllers/character/touch-joys
|
|
|
151
178
|
|
|
152
179
|
const joy = new TouchJoystick({ wrapperStyle: { left: "20px", bottom: "20px" } }); // position is REQUIRED
|
|
153
180
|
const btnJump = new VirtualButton({ label: "Jump", wrapperStyle: { right: "30px", bottom: "30px" } });
|
|
181
|
+
const btnCrouch = new VirtualButton({
|
|
182
|
+
label: "Crouch",
|
|
183
|
+
wrapperStyle: { right: "100px", bottom: "30px" },
|
|
184
|
+
onPress: () => character.setCrouch(!character.crouchActive), // tap = toggle
|
|
185
|
+
});
|
|
154
186
|
|
|
155
187
|
physics.onBeforeStep(() => {
|
|
156
188
|
character.setMovement({
|
|
@@ -178,8 +210,9 @@ players' physics locally guarantees divergence — every client would compute a
|
|
|
178
210
|
different world.
|
|
179
211
|
|
|
180
212
|
- Publish your own `currPos` + yaw on the fixed 10–20 Hz tick, not per frame.
|
|
181
|
-
- To animate remotes, sync the
|
|
182
|
-
|
|
213
|
+
- To animate remotes, sync the six animation booleans (`isOnGround`,
|
|
214
|
+
`isFalling`, `isMoving`, `runActive`, `jumpActive`, `crouchActive`) and feed
|
|
215
|
+
them to a per-remote `CharacterAnimations` — see the animations reference.
|
|
183
216
|
- Load `$genex-threejs-multiplayer` before writing any networking code; it is
|
|
184
217
|
mandatory for any 2+ player game.
|
|
185
218
|
|