@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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 +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The procedural humanoid BODY: entirely procedural three.js geometry
|
|
3
|
+
* (capsules + one pelvis box via `CapsuleGeometry`/`BoxGeometry`, merged with
|
|
4
|
+
* `BufferGeometryUtils.mergeGeometries` into ONE `SkinnedMesh`). No imported
|
|
5
|
+
* mesh anywhere. Promoted from the accepted Phase-1 PoC
|
|
6
|
+
* (`packages/engine/e2e/humanoid-skin-proof/procedural-body.ts`) and
|
|
7
|
+
* parameterized by body dimensions — with all-default dims the output is
|
|
8
|
+
* bit-identical to the PoC body.
|
|
9
|
+
*
|
|
10
|
+
* Skin binding is **analytic per-segment with smooth 2-bone joint blending**:
|
|
11
|
+
* every body part is generated along one bone segment (proximal joint →
|
|
12
|
+
* distal joint, both known a priori from the rest pose), so each vertex's
|
|
13
|
+
* normalized position `t` along the bone axis is exact — no nearest-bone
|
|
14
|
+
* search, no heuristic falloff radius. Vertices in the interior of a segment
|
|
15
|
+
* bind 100% to that segment's bone; vertices inside a blend zone at either
|
|
16
|
+
* end split weight smoothstep-wise with the adjacent bone, reaching 50/50 at
|
|
17
|
+
* (and past) the joint itself. This is the standard cure for the two classic
|
|
18
|
+
* rigid-binding artifacts: gaps/interpenetration at elbows/knees (fixed by
|
|
19
|
+
* the overshoot + 50/50 blend across the joint) and hard shading creases
|
|
20
|
+
* (fixed by the smoothstep ramp). Because the blend zone is derived from the
|
|
21
|
+
* ACTUAL joint positions of the (possibly rescaled) skeleton, weights stay
|
|
22
|
+
* correct as body proportions change. Two influences per vertex is enough
|
|
23
|
+
* for a capsule-limb body; the vertex format still uses the standard 4-wide
|
|
24
|
+
* skinIndex/skinWeight attributes, zero-padded, exactly as GLTF skins do.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as THREE from 'three';
|
|
28
|
+
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
29
|
+
import type { BuiltSkeleton } from './skeleton';
|
|
30
|
+
|
|
31
|
+
/** Which dimension multiplier a part's radius follows. `foot` parts scale by
|
|
32
|
+
* heightFactor ONLY, so ground clearance (set by the skeleton re-ground
|
|
33
|
+
* step, which knows nothing about radii) is never violated by a thick body. */
|
|
34
|
+
type RadiusGroup = 'torso' | 'limb' | 'head' | 'foot';
|
|
35
|
+
|
|
36
|
+
/** One procedural body part, generated along the bone segment `from`→`to`. */
|
|
37
|
+
interface SegmentSpec {
|
|
38
|
+
/** The bone this part binds to (owns the segment; usually === `from`). */
|
|
39
|
+
bone: string;
|
|
40
|
+
/** Proximal joint (t = 0). */
|
|
41
|
+
from: string;
|
|
42
|
+
/** Distal joint (t = 1) — an existing child bone name. */
|
|
43
|
+
to: string;
|
|
44
|
+
/** Capsule radius, metres at reference scale (multiplied by the part's
|
|
45
|
+
* radius-group dim before use). */
|
|
46
|
+
radius: number;
|
|
47
|
+
/** Radius-group multiplier this part follows. */
|
|
48
|
+
group: RadiusGroup;
|
|
49
|
+
/** Blend partner near t=0 (typically the parent bone). Omit = rigid end. */
|
|
50
|
+
proximalBlend?: string;
|
|
51
|
+
/** Blend partner near t=1 (typically the `to` bone). Omit = rigid end. */
|
|
52
|
+
distalBlend?: string;
|
|
53
|
+
/** Extend the capsule past the proximal / distal joint, metres at reference
|
|
54
|
+
* scale — overlaps the neighbouring part so blended joints never show gaps. */
|
|
55
|
+
overshootFrom?: number;
|
|
56
|
+
overshootTo?: number;
|
|
57
|
+
/** Per-part vertex color (visualizes the segment map + blend zones). */
|
|
58
|
+
color: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const C = {
|
|
62
|
+
torso: 0x4f8fba,
|
|
63
|
+
pelvis: 0x3a6ea5,
|
|
64
|
+
head: 0xd9a066,
|
|
65
|
+
neck: 0xc98b5e,
|
|
66
|
+
armL: 0xe07b39,
|
|
67
|
+
foreL: 0xf0a860,
|
|
68
|
+
armR: 0x4fae6a,
|
|
69
|
+
foreR: 0x7fd196,
|
|
70
|
+
hand: 0xd9c8b4,
|
|
71
|
+
thigh: 0x8a5fb0,
|
|
72
|
+
shin: 0xb08ad0,
|
|
73
|
+
foot: 0x555f6e,
|
|
74
|
+
toe: 0x6d7885,
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
/** The full body map. Radii/overshoots are metres at reference scale, tuned
|
|
78
|
+
* to the reference rig's T-pose proportions (~1.83 m tall). */
|
|
79
|
+
const SEGMENTS: readonly SegmentSpec[] = [
|
|
80
|
+
// Torso column — three short, fat, overlapping capsules following the
|
|
81
|
+
// spine chain, each blending into its neighbours.
|
|
82
|
+
{
|
|
83
|
+
bone: 'mixamorigSpine',
|
|
84
|
+
from: 'mixamorigSpine',
|
|
85
|
+
to: 'mixamorigSpine1',
|
|
86
|
+
radius: 0.135,
|
|
87
|
+
group: 'torso',
|
|
88
|
+
proximalBlend: 'mixamorigHips',
|
|
89
|
+
distalBlend: 'mixamorigSpine1',
|
|
90
|
+
overshootFrom: 0.03,
|
|
91
|
+
overshootTo: 0.02,
|
|
92
|
+
color: C.torso,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
bone: 'mixamorigSpine1',
|
|
96
|
+
from: 'mixamorigSpine1',
|
|
97
|
+
to: 'mixamorigSpine2',
|
|
98
|
+
radius: 0.145,
|
|
99
|
+
group: 'torso',
|
|
100
|
+
proximalBlend: 'mixamorigSpine',
|
|
101
|
+
distalBlend: 'mixamorigSpine2',
|
|
102
|
+
overshootFrom: 0.02,
|
|
103
|
+
overshootTo: 0.02,
|
|
104
|
+
color: C.torso,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
bone: 'mixamorigSpine2',
|
|
108
|
+
from: 'mixamorigSpine2',
|
|
109
|
+
to: 'mixamorigNeck',
|
|
110
|
+
radius: 0.14,
|
|
111
|
+
group: 'torso',
|
|
112
|
+
proximalBlend: 'mixamorigSpine1',
|
|
113
|
+
distalBlend: 'mixamorigNeck',
|
|
114
|
+
overshootFrom: 0.02,
|
|
115
|
+
overshootTo: 0.01,
|
|
116
|
+
color: C.torso,
|
|
117
|
+
},
|
|
118
|
+
// Neck + head.
|
|
119
|
+
{
|
|
120
|
+
bone: 'mixamorigNeck',
|
|
121
|
+
from: 'mixamorigNeck',
|
|
122
|
+
to: 'mixamorigHead',
|
|
123
|
+
radius: 0.048,
|
|
124
|
+
group: 'head',
|
|
125
|
+
proximalBlend: 'mixamorigSpine2',
|
|
126
|
+
distalBlend: 'mixamorigHead',
|
|
127
|
+
overshootFrom: 0.02,
|
|
128
|
+
overshootTo: 0.02,
|
|
129
|
+
color: C.neck,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
bone: 'mixamorigHead',
|
|
133
|
+
from: 'mixamorigHead',
|
|
134
|
+
to: 'mixamorigHeadTop_End',
|
|
135
|
+
radius: 0.105,
|
|
136
|
+
group: 'head',
|
|
137
|
+
proximalBlend: 'mixamorigNeck',
|
|
138
|
+
color: C.head,
|
|
139
|
+
},
|
|
140
|
+
// Left arm chain (shoulder → upper arm → forearm → hand).
|
|
141
|
+
{
|
|
142
|
+
bone: 'mixamorigLeftShoulder',
|
|
143
|
+
from: 'mixamorigLeftShoulder',
|
|
144
|
+
to: 'mixamorigLeftArm',
|
|
145
|
+
radius: 0.062,
|
|
146
|
+
group: 'torso',
|
|
147
|
+
proximalBlend: 'mixamorigSpine2',
|
|
148
|
+
distalBlend: 'mixamorigLeftArm',
|
|
149
|
+
overshootFrom: 0.02,
|
|
150
|
+
color: C.torso,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
bone: 'mixamorigLeftArm',
|
|
154
|
+
from: 'mixamorigLeftArm',
|
|
155
|
+
to: 'mixamorigLeftForeArm',
|
|
156
|
+
radius: 0.052,
|
|
157
|
+
group: 'limb',
|
|
158
|
+
proximalBlend: 'mixamorigLeftShoulder',
|
|
159
|
+
distalBlend: 'mixamorigLeftForeArm',
|
|
160
|
+
overshootFrom: 0.015,
|
|
161
|
+
overshootTo: 0.01,
|
|
162
|
+
color: C.armL,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
bone: 'mixamorigLeftForeArm',
|
|
166
|
+
from: 'mixamorigLeftForeArm',
|
|
167
|
+
to: 'mixamorigLeftHand',
|
|
168
|
+
radius: 0.044,
|
|
169
|
+
group: 'limb',
|
|
170
|
+
proximalBlend: 'mixamorigLeftArm',
|
|
171
|
+
distalBlend: 'mixamorigLeftHand',
|
|
172
|
+
overshootFrom: 0.01,
|
|
173
|
+
overshootTo: 0.01,
|
|
174
|
+
color: C.foreL,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
bone: 'mixamorigLeftHand',
|
|
178
|
+
from: 'mixamorigLeftHand',
|
|
179
|
+
to: 'mixamorigLeftHandMiddle1',
|
|
180
|
+
radius: 0.04,
|
|
181
|
+
group: 'limb',
|
|
182
|
+
proximalBlend: 'mixamorigLeftForeArm',
|
|
183
|
+
overshootTo: 0.05,
|
|
184
|
+
color: C.hand,
|
|
185
|
+
},
|
|
186
|
+
// Right arm chain.
|
|
187
|
+
{
|
|
188
|
+
bone: 'mixamorigRightShoulder',
|
|
189
|
+
from: 'mixamorigRightShoulder',
|
|
190
|
+
to: 'mixamorigRightArm',
|
|
191
|
+
radius: 0.062,
|
|
192
|
+
group: 'torso',
|
|
193
|
+
proximalBlend: 'mixamorigSpine2',
|
|
194
|
+
distalBlend: 'mixamorigRightArm',
|
|
195
|
+
overshootFrom: 0.02,
|
|
196
|
+
color: C.torso,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
bone: 'mixamorigRightArm',
|
|
200
|
+
from: 'mixamorigRightArm',
|
|
201
|
+
to: 'mixamorigRightForeArm',
|
|
202
|
+
radius: 0.052,
|
|
203
|
+
group: 'limb',
|
|
204
|
+
proximalBlend: 'mixamorigRightShoulder',
|
|
205
|
+
distalBlend: 'mixamorigRightForeArm',
|
|
206
|
+
overshootFrom: 0.015,
|
|
207
|
+
overshootTo: 0.01,
|
|
208
|
+
color: C.armR,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
bone: 'mixamorigRightForeArm',
|
|
212
|
+
from: 'mixamorigRightForeArm',
|
|
213
|
+
to: 'mixamorigRightHand',
|
|
214
|
+
radius: 0.044,
|
|
215
|
+
group: 'limb',
|
|
216
|
+
proximalBlend: 'mixamorigRightArm',
|
|
217
|
+
distalBlend: 'mixamorigRightHand',
|
|
218
|
+
overshootFrom: 0.01,
|
|
219
|
+
overshootTo: 0.01,
|
|
220
|
+
color: C.foreR,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
bone: 'mixamorigRightHand',
|
|
224
|
+
from: 'mixamorigRightHand',
|
|
225
|
+
to: 'mixamorigRightHandMiddle1',
|
|
226
|
+
radius: 0.04,
|
|
227
|
+
group: 'limb',
|
|
228
|
+
proximalBlend: 'mixamorigRightForeArm',
|
|
229
|
+
overshootTo: 0.05,
|
|
230
|
+
color: C.hand,
|
|
231
|
+
},
|
|
232
|
+
// Left leg chain (thigh → shin → foot → toes).
|
|
233
|
+
{
|
|
234
|
+
bone: 'mixamorigLeftUpLeg',
|
|
235
|
+
from: 'mixamorigLeftUpLeg',
|
|
236
|
+
to: 'mixamorigLeftLeg',
|
|
237
|
+
radius: 0.078,
|
|
238
|
+
group: 'limb',
|
|
239
|
+
proximalBlend: 'mixamorigHips',
|
|
240
|
+
distalBlend: 'mixamorigLeftLeg',
|
|
241
|
+
overshootFrom: 0.035,
|
|
242
|
+
overshootTo: 0.01,
|
|
243
|
+
color: C.thigh,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
bone: 'mixamorigLeftLeg',
|
|
247
|
+
from: 'mixamorigLeftLeg',
|
|
248
|
+
to: 'mixamorigLeftFoot',
|
|
249
|
+
radius: 0.058,
|
|
250
|
+
group: 'limb',
|
|
251
|
+
proximalBlend: 'mixamorigLeftUpLeg',
|
|
252
|
+
distalBlend: 'mixamorigLeftFoot',
|
|
253
|
+
overshootFrom: 0.01,
|
|
254
|
+
overshootTo: 0.01,
|
|
255
|
+
color: C.shin,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
bone: 'mixamorigLeftFoot',
|
|
259
|
+
from: 'mixamorigLeftFoot',
|
|
260
|
+
to: 'mixamorigLeftToeBase',
|
|
261
|
+
radius: 0.05,
|
|
262
|
+
group: 'foot',
|
|
263
|
+
proximalBlend: 'mixamorigLeftLeg',
|
|
264
|
+
distalBlend: 'mixamorigLeftToeBase',
|
|
265
|
+
overshootFrom: 0.01,
|
|
266
|
+
color: C.foot,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
bone: 'mixamorigLeftToeBase',
|
|
270
|
+
from: 'mixamorigLeftToeBase',
|
|
271
|
+
to: 'mixamorigLeftToe_End',
|
|
272
|
+
radius: 0.042,
|
|
273
|
+
group: 'foot',
|
|
274
|
+
proximalBlend: 'mixamorigLeftFoot',
|
|
275
|
+
color: C.toe,
|
|
276
|
+
},
|
|
277
|
+
// Right leg chain.
|
|
278
|
+
{
|
|
279
|
+
bone: 'mixamorigRightUpLeg',
|
|
280
|
+
from: 'mixamorigRightUpLeg',
|
|
281
|
+
to: 'mixamorigRightLeg',
|
|
282
|
+
radius: 0.078,
|
|
283
|
+
group: 'limb',
|
|
284
|
+
proximalBlend: 'mixamorigHips',
|
|
285
|
+
distalBlend: 'mixamorigRightLeg',
|
|
286
|
+
overshootFrom: 0.035,
|
|
287
|
+
overshootTo: 0.01,
|
|
288
|
+
color: C.thigh,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
bone: 'mixamorigRightLeg',
|
|
292
|
+
from: 'mixamorigRightLeg',
|
|
293
|
+
to: 'mixamorigRightFoot',
|
|
294
|
+
radius: 0.058,
|
|
295
|
+
group: 'limb',
|
|
296
|
+
proximalBlend: 'mixamorigRightUpLeg',
|
|
297
|
+
distalBlend: 'mixamorigRightFoot',
|
|
298
|
+
overshootFrom: 0.01,
|
|
299
|
+
overshootTo: 0.01,
|
|
300
|
+
color: C.shin,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
bone: 'mixamorigRightFoot',
|
|
304
|
+
from: 'mixamorigRightFoot',
|
|
305
|
+
to: 'mixamorigRightToeBase',
|
|
306
|
+
radius: 0.05,
|
|
307
|
+
group: 'foot',
|
|
308
|
+
proximalBlend: 'mixamorigRightLeg',
|
|
309
|
+
distalBlend: 'mixamorigRightToeBase',
|
|
310
|
+
overshootFrom: 0.01,
|
|
311
|
+
color: C.foot,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
bone: 'mixamorigRightToeBase',
|
|
315
|
+
from: 'mixamorigRightToeBase',
|
|
316
|
+
to: 'mixamorigRightToe_End',
|
|
317
|
+
radius: 0.042,
|
|
318
|
+
group: 'foot',
|
|
319
|
+
proximalBlend: 'mixamorigRightFoot',
|
|
320
|
+
color: C.toe,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
/** The head capsule's reference radius — `generate.ts` feeds this (× headSize)
|
|
325
|
+
* to the skeleton height normalization as the crown padding. */
|
|
326
|
+
export const HEAD_CAPSULE_RADIUS =
|
|
327
|
+
SEGMENTS.find((s) => s.bone === 'mixamorigHead')?.radius ?? 0.105;
|
|
328
|
+
|
|
329
|
+
/** Body dimension multipliers, resolved by `generate.ts` from the params. */
|
|
330
|
+
export interface HumanoidBodyDims {
|
|
331
|
+
/** The skeleton's uniform height factor — every base metre scales by this. */
|
|
332
|
+
heightFactor: number;
|
|
333
|
+
limbThickness: number;
|
|
334
|
+
torsoGirth: number;
|
|
335
|
+
headSize: number;
|
|
336
|
+
hipWidth: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export const smoothstep = (x: number): number => {
|
|
340
|
+
const t = Math.min(1, Math.max(0, x));
|
|
341
|
+
return t * t * (3 - 2 * t);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/** Blend-zone length as a fraction of a segment (≈10 cm at reference scale,
|
|
345
|
+
* clamped to 20–50% of the segment) — shared verbatim by the body generator
|
|
346
|
+
* and `skinToHumanoid` so clothing gets the SAME joint blend as the body. */
|
|
347
|
+
export const blendZone = (len: number, heightFactor: number): number =>
|
|
348
|
+
Math.min(0.5, Math.max(0.2, (0.1 * heightFactor) / len));
|
|
349
|
+
|
|
350
|
+
/** World rest position of a named bone (scene must have updated matrices). */
|
|
351
|
+
function jointPos(rig: BuiltSkeleton, name: string): THREE.Vector3 {
|
|
352
|
+
const bone = rig.byName.get(name);
|
|
353
|
+
if (!bone) throw new Error(`humanoid: unknown bone '${name}'`);
|
|
354
|
+
return new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** name → skinIndex row, cached per rig (reviewer note: the former O(n)
|
|
358
|
+
* findIndex per call was fine for 22 parts, not for per-prop/per-vertex use). */
|
|
359
|
+
const boneIndexCache = new WeakMap<BuiltSkeleton, Map<string, number>>();
|
|
360
|
+
|
|
361
|
+
function boneIndex(rig: BuiltSkeleton, name: string): number {
|
|
362
|
+
let map = boneIndexCache.get(rig);
|
|
363
|
+
if (!map) {
|
|
364
|
+
map = new Map(rig.bones.map((b, i) => [b.name, i]));
|
|
365
|
+
boneIndexCache.set(rig, map);
|
|
366
|
+
}
|
|
367
|
+
const i = map.get(name);
|
|
368
|
+
if (i === undefined) throw new Error(`humanoid: unknown bone '${name}'`);
|
|
369
|
+
return i;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Fill vertex colors with a flat per-part color. */
|
|
373
|
+
function paint(geo: THREE.BufferGeometry, color: number): void {
|
|
374
|
+
const n = geo.attributes['position']!.count;
|
|
375
|
+
const c = new THREE.Color(color);
|
|
376
|
+
const arr = new Float32Array(n * 3);
|
|
377
|
+
for (let i = 0; i < n; i++) arr.set([c.r, c.g, c.b], i * 3);
|
|
378
|
+
geo.setAttribute('color', new THREE.BufferAttribute(arr, 3));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Compute skinIndex/skinWeight for a segment geometry: per-vertex projection
|
|
383
|
+
* onto the joint axis → smoothstep 2-bone blend inside the end zones. The
|
|
384
|
+
* blend zone length adapts to the segment (≈10 cm at reference scale,
|
|
385
|
+
* clamped to 20–50% of the segment) so short torso links still blend and
|
|
386
|
+
* long thighs stay mostly rigid.
|
|
387
|
+
*/
|
|
388
|
+
function skinSegment(
|
|
389
|
+
geo: THREE.BufferGeometry,
|
|
390
|
+
rig: BuiltSkeleton,
|
|
391
|
+
spec: SegmentSpec,
|
|
392
|
+
p0: THREE.Vector3,
|
|
393
|
+
p1: THREE.Vector3,
|
|
394
|
+
heightFactor: number,
|
|
395
|
+
): void {
|
|
396
|
+
const primary = boneIndex(rig, spec.bone);
|
|
397
|
+
const proximal = spec.proximalBlend ? boneIndex(rig, spec.proximalBlend) : -1;
|
|
398
|
+
const distal = spec.distalBlend ? boneIndex(rig, spec.distalBlend) : -1;
|
|
399
|
+
const axis = p1.clone().sub(p0);
|
|
400
|
+
const len = axis.length();
|
|
401
|
+
const axisN = axis.clone().divideScalar(len);
|
|
402
|
+
const blend = blendZone(len, heightFactor);
|
|
403
|
+
|
|
404
|
+
const pos = geo.attributes['position']!;
|
|
405
|
+
const n = pos.count;
|
|
406
|
+
const skinIndex = new Uint16Array(n * 4);
|
|
407
|
+
const skinWeight = new Float32Array(n * 4);
|
|
408
|
+
const v = new THREE.Vector3();
|
|
409
|
+
for (let i = 0; i < n; i++) {
|
|
410
|
+
v.fromBufferAttribute(pos as THREE.BufferAttribute, i);
|
|
411
|
+
const t = v.sub(p0).dot(axisN) / len; // may exceed [0,1] via overshoot
|
|
412
|
+
let other = primary;
|
|
413
|
+
let w2 = 0;
|
|
414
|
+
if (proximal >= 0 && t < blend) {
|
|
415
|
+
// 50/50 exactly at (and beyond) the joint, ramping to 100% primary.
|
|
416
|
+
other = proximal;
|
|
417
|
+
w2 = 0.5 * (1 - smoothstep(t / blend));
|
|
418
|
+
} else if (distal >= 0 && t > 1 - blend) {
|
|
419
|
+
other = distal;
|
|
420
|
+
w2 = 0.5 * smoothstep((t - (1 - blend)) / blend);
|
|
421
|
+
}
|
|
422
|
+
skinIndex.set([primary, other, 0, 0], i * 4);
|
|
423
|
+
skinWeight.set([1 - w2, w2, 0, 0], i * 4);
|
|
424
|
+
}
|
|
425
|
+
geo.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndex, 4));
|
|
426
|
+
geo.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeight, 4));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Capsule spanning exactly `p0 - dir*overshootFrom` → `p1 + dir*overshootTo`. */
|
|
430
|
+
function capsuleBetween(
|
|
431
|
+
p0: THREE.Vector3,
|
|
432
|
+
p1: THREE.Vector3,
|
|
433
|
+
radius: number,
|
|
434
|
+
overshootFrom: number,
|
|
435
|
+
overshootTo: number,
|
|
436
|
+
): THREE.BufferGeometry {
|
|
437
|
+
const dir = p1.clone().sub(p0).normalize();
|
|
438
|
+
const a = p0.clone().addScaledVector(dir, -overshootFrom);
|
|
439
|
+
const b = p1.clone().addScaledVector(dir, overshootTo);
|
|
440
|
+
const span = a.distanceTo(b);
|
|
441
|
+
const cyl = Math.max(0.01, span - 2 * radius);
|
|
442
|
+
const geo = new THREE.CapsuleGeometry(radius, cyl, 6, 16);
|
|
443
|
+
geo.deleteAttribute('uv'); // untextured; keeps merged attribute sets identical
|
|
444
|
+
const quat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
445
|
+
const mid = a.clone().add(b).multiplyScalar(0.5);
|
|
446
|
+
geo.applyMatrix4(new THREE.Matrix4().compose(mid, quat, new THREE.Vector3(1, 1, 1)));
|
|
447
|
+
return geo;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** The pelvis is the one non-capsule part: a box centred between the hips
|
|
451
|
+
* and the upper-leg roots, rigidly bound to `mixamorigHips` (the thigh
|
|
452
|
+
* capsules overshoot up into it and carry the blending). */
|
|
453
|
+
function buildPelvis(rig: BuiltSkeleton, dims: HumanoidBodyDims): THREE.BufferGeometry {
|
|
454
|
+
const hips = jointPos(rig, 'mixamorigHips');
|
|
455
|
+
const upL = jointPos(rig, 'mixamorigLeftUpLeg');
|
|
456
|
+
const upR = jointPos(rig, 'mixamorigRightUpLeg');
|
|
457
|
+
const centerY = (hips.y + (upL.y + upR.y) / 2) / 2;
|
|
458
|
+
const f = dims.heightFactor;
|
|
459
|
+
const geo = new THREE.BoxGeometry(
|
|
460
|
+
0.3 * f * dims.hipWidth,
|
|
461
|
+
0.17 * f,
|
|
462
|
+
0.19 * f * dims.torsoGirth,
|
|
463
|
+
2,
|
|
464
|
+
2,
|
|
465
|
+
2,
|
|
466
|
+
);
|
|
467
|
+
geo.deleteAttribute('uv');
|
|
468
|
+
geo.translate(hips.x, centerY, hips.z);
|
|
469
|
+
paint(geo, C.pelvis);
|
|
470
|
+
const hipsIndex = boneIndex(rig, 'mixamorigHips');
|
|
471
|
+
const n = geo.attributes['position']!.count;
|
|
472
|
+
const skinIndex = new Uint16Array(n * 4);
|
|
473
|
+
const skinWeight = new Float32Array(n * 4);
|
|
474
|
+
for (let i = 0; i < n; i++) {
|
|
475
|
+
skinIndex[i * 4] = hipsIndex;
|
|
476
|
+
skinWeight[i * 4] = 1;
|
|
477
|
+
}
|
|
478
|
+
geo.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndex, 4));
|
|
479
|
+
geo.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeight, 4));
|
|
480
|
+
return geo;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** One merged-body part's vertex range + the bones its weights may reference —
|
|
484
|
+
* the segment-adjacency contract the unit tests verify. */
|
|
485
|
+
export interface HumanoidBodyPart {
|
|
486
|
+
name: string;
|
|
487
|
+
/** skinIndex values every non-zero-weight influence in the range must use. */
|
|
488
|
+
boneIndices: readonly number[];
|
|
489
|
+
vertexStart: number;
|
|
490
|
+
vertexCount: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface HumanoidBody {
|
|
494
|
+
mesh: THREE.SkinnedMesh;
|
|
495
|
+
skeleton: THREE.Skeleton;
|
|
496
|
+
vertexCount: number;
|
|
497
|
+
parts: readonly HumanoidBodyPart[];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function radiusMultiplier(group: RadiusGroup, dims: HumanoidBodyDims): number {
|
|
501
|
+
switch (group) {
|
|
502
|
+
case 'torso':
|
|
503
|
+
return dims.torsoGirth;
|
|
504
|
+
case 'limb':
|
|
505
|
+
return dims.limbThickness;
|
|
506
|
+
case 'head':
|
|
507
|
+
return dims.headSize;
|
|
508
|
+
case 'foot':
|
|
509
|
+
return 1;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Build the whole body as ONE merged `SkinnedMesh` in the rig root's space
|
|
515
|
+
* (the skeleton's rest pose), and bind it. Callers must have called
|
|
516
|
+
* `updateMatrixWorld(true)` on the armature's parent after adding the bone
|
|
517
|
+
* hierarchy, so every bone's `matrixWorld` is its rest pose —
|
|
518
|
+
* `new THREE.Skeleton(bones)` captures the inverse bind matrices from
|
|
519
|
+
* exactly those.
|
|
520
|
+
*/
|
|
521
|
+
export function buildHumanoidBody(
|
|
522
|
+
rig: BuiltSkeleton,
|
|
523
|
+
dims: HumanoidBodyDims,
|
|
524
|
+
material?: THREE.Material,
|
|
525
|
+
): HumanoidBody {
|
|
526
|
+
const geos: THREE.BufferGeometry[] = [buildPelvis(rig, dims)];
|
|
527
|
+
const parts: HumanoidBodyPart[] = [];
|
|
528
|
+
let vertexCursor = geos[0]!.attributes['position']!.count;
|
|
529
|
+
parts.push({
|
|
530
|
+
name: 'pelvis',
|
|
531
|
+
boneIndices: [boneIndex(rig, 'mixamorigHips')],
|
|
532
|
+
vertexStart: 0,
|
|
533
|
+
vertexCount: vertexCursor,
|
|
534
|
+
});
|
|
535
|
+
const f = dims.heightFactor;
|
|
536
|
+
for (const spec of SEGMENTS) {
|
|
537
|
+
const p0 = jointPos(rig, spec.from);
|
|
538
|
+
const p1 = jointPos(rig, spec.to);
|
|
539
|
+
const radius = spec.radius * f * radiusMultiplier(spec.group, dims);
|
|
540
|
+
const geo = capsuleBetween(
|
|
541
|
+
p0,
|
|
542
|
+
p1,
|
|
543
|
+
radius,
|
|
544
|
+
(spec.overshootFrom ?? 0) * f,
|
|
545
|
+
(spec.overshootTo ?? 0) * f,
|
|
546
|
+
);
|
|
547
|
+
paint(geo, spec.color);
|
|
548
|
+
skinSegment(geo, rig, spec, p0, p1, f);
|
|
549
|
+
const count = geo.attributes['position']!.count;
|
|
550
|
+
const boneIndices = [boneIndex(rig, spec.bone)];
|
|
551
|
+
if (spec.proximalBlend) boneIndices.push(boneIndex(rig, spec.proximalBlend));
|
|
552
|
+
if (spec.distalBlend) boneIndices.push(boneIndex(rig, spec.distalBlend));
|
|
553
|
+
parts.push({ name: spec.bone, boneIndices, vertexStart: vertexCursor, vertexCount: count });
|
|
554
|
+
vertexCursor += count;
|
|
555
|
+
geos.push(geo);
|
|
556
|
+
}
|
|
557
|
+
const merged = mergeGeometries(geos, false);
|
|
558
|
+
if (!merged) throw new Error('humanoid: mergeGeometries failed');
|
|
559
|
+
for (const g of geos) g.dispose();
|
|
560
|
+
|
|
561
|
+
const mat =
|
|
562
|
+
material ??
|
|
563
|
+
new THREE.MeshStandardMaterial({
|
|
564
|
+
vertexColors: true,
|
|
565
|
+
roughness: 0.75,
|
|
566
|
+
metalness: 0.05,
|
|
567
|
+
});
|
|
568
|
+
const mesh = new THREE.SkinnedMesh(merged, mat);
|
|
569
|
+
mesh.name = 'ProceduralHumanoidBody';
|
|
570
|
+
mesh.castShadow = true;
|
|
571
|
+
// An animated skinned mesh moves far outside its static rest-pose bounds.
|
|
572
|
+
mesh.frustumCulled = false;
|
|
573
|
+
|
|
574
|
+
const skeleton = new THREE.Skeleton(rig.bones);
|
|
575
|
+
mesh.updateMatrixWorld(true);
|
|
576
|
+
mesh.bind(skeleton, mesh.matrixWorld.clone());
|
|
577
|
+
return {
|
|
578
|
+
mesh,
|
|
579
|
+
skeleton,
|
|
580
|
+
vertexCount: merged.attributes['position']!.count,
|
|
581
|
+
parts,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// --- Segment-binding exposure (package-internal, consumed by compose.ts) ----
|
|
586
|
+
|
|
587
|
+
/** One skin-binding segment of the standard body, resolved against a concrete
|
|
588
|
+
* rest pose: the analytic per-segment data `skinToHumanoid` shares with the
|
|
589
|
+
* body generator so arbitrary clothing geometry deforms EXACTLY like the
|
|
590
|
+
* body part underneath it. Not part of the public module surface. */
|
|
591
|
+
export interface HumanoidBindingSegment {
|
|
592
|
+
/** Primary bone row (the skinIndex value). */
|
|
593
|
+
bone: number;
|
|
594
|
+
/** Blend-partner rows near t=0 / t=1 (-1 = rigid end). */
|
|
595
|
+
proximal: number;
|
|
596
|
+
distal: number;
|
|
597
|
+
/** Proximal joint rest position, model-space metres. */
|
|
598
|
+
p0: THREE.Vector3;
|
|
599
|
+
/** Unit axis p0→p1 (p1 = distal joint) and the joint-to-joint length. */
|
|
600
|
+
axis: THREE.Vector3;
|
|
601
|
+
len: number;
|
|
602
|
+
/** Normalized blend-zone length (the body's own `blendZone` value). */
|
|
603
|
+
blend: number;
|
|
604
|
+
/** The body part's surface radius, metres — nearest-segment selection
|
|
605
|
+
* measures distance to the SURFACE so loose clothing picks the part it
|
|
606
|
+
* visually covers, not merely the closest bone axis. */
|
|
607
|
+
radius: number;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Resolve the body's segment map (plus one synthetic rigid pelvis segment —
|
|
612
|
+
* the pelvis box binds 100% to Hips, and belts/skirts must get the same
|
|
613
|
+
* anchor to stay congruent with it) against a concrete rest pose.
|
|
614
|
+
* Parameterized on lookups rather than `BuiltSkeleton` because the compose
|
|
615
|
+
* path recovers rest joints from `Skeleton.boneInverses` (bones may be
|
|
616
|
+
* mid-animation by then), while the generator reads live rest matrices.
|
|
617
|
+
* `jointAt` must return a FRESH Vector3 per call (results are mutated here).
|
|
618
|
+
*/
|
|
619
|
+
export function computeBindingSegments(
|
|
620
|
+
jointAt: (name: string) => THREE.Vector3,
|
|
621
|
+
indexOf: (name: string) => number,
|
|
622
|
+
dims: HumanoidBodyDims,
|
|
623
|
+
): HumanoidBindingSegment[] {
|
|
624
|
+
const f = dims.heightFactor;
|
|
625
|
+
const segments: HumanoidBindingSegment[] = [];
|
|
626
|
+
|
|
627
|
+
// Axis LATERALLY along the hip-to-hip line (not hips→legs): a capsule
|
|
628
|
+
// there is the closest round proxy for the pelvis box, and — unlike a
|
|
629
|
+
// short vertical axis with a wide radius — it does not bulge spherically
|
|
630
|
+
// down between the thighs and steal inner-thigh vertices from the legs.
|
|
631
|
+
const upLegL = jointAt('mixamorigLeftUpLeg');
|
|
632
|
+
const upLegR = jointAt('mixamorigRightUpLeg');
|
|
633
|
+
const pelvisAxis = upLegR.clone().sub(upLegL);
|
|
634
|
+
const pelvisLen = Math.max(pelvisAxis.length(), 1e-6);
|
|
635
|
+
segments.push({
|
|
636
|
+
bone: indexOf('mixamorigHips'),
|
|
637
|
+
proximal: -1,
|
|
638
|
+
distal: -1,
|
|
639
|
+
p0: upLegL,
|
|
640
|
+
axis: pelvisAxis.divideScalar(pelvisLen),
|
|
641
|
+
len: pelvisLen,
|
|
642
|
+
blend: 0,
|
|
643
|
+
radius: 0.1 * f * dims.hipWidth,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
for (const spec of SEGMENTS) {
|
|
647
|
+
const p0 = jointAt(spec.from);
|
|
648
|
+
const p1 = jointAt(spec.to);
|
|
649
|
+
const axis = p1.sub(p0);
|
|
650
|
+
const len = Math.max(axis.length(), 1e-6);
|
|
651
|
+
segments.push({
|
|
652
|
+
bone: indexOf(spec.bone),
|
|
653
|
+
proximal: spec.proximalBlend ? indexOf(spec.proximalBlend) : -1,
|
|
654
|
+
distal: spec.distalBlend ? indexOf(spec.distalBlend) : -1,
|
|
655
|
+
p0,
|
|
656
|
+
axis: axis.divideScalar(len),
|
|
657
|
+
len,
|
|
658
|
+
blend: blendZone(len, f),
|
|
659
|
+
radius: spec.radius * f * radiusMultiplier(spec.group, dims),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return segments;
|
|
663
|
+
}
|