@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
|
@@ -4,30 +4,30 @@ import type * as PIXI from 'pixi.js';
|
|
|
4
4
|
import type * as THREE from 'three';
|
|
5
5
|
import type { z } from 'zod';
|
|
6
6
|
import type { SystemPhaseName } from '../core/types';
|
|
7
|
-
import type {
|
|
7
|
+
import type { AdapterSurface, WorldInstance } from '../runtime/game';
|
|
8
8
|
import type { GameContext } from '../runtime/types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Map a {@link
|
|
11
|
+
* Map a {@link AdapterSurface} to its native world node type (T7.2, D8 —
|
|
12
12
|
* `docs/GAME-COMPONENT-GENERALIZATION.md` §2). `react` maps to `never`: react
|
|
13
13
|
* entities host no GameComponents (they render from game state via the T7.4
|
|
14
14
|
* state bridge instead).
|
|
15
15
|
*/
|
|
16
|
-
export type NodeOf<K extends
|
|
16
|
+
export type NodeOf<K extends AdapterSurface> = K extends 'threejs'
|
|
17
17
|
? THREE.Object3D
|
|
18
18
|
: K extends 'pixijs'
|
|
19
19
|
? PIXI.Container
|
|
20
20
|
: never;
|
|
21
21
|
|
|
22
|
-
/** Map a {@link
|
|
23
|
-
export type BodyOf<K extends
|
|
22
|
+
/** Map a {@link AdapterSurface} to its native Rapier rigid-body type. */
|
|
23
|
+
export type BodyOf<K extends AdapterSurface> = K extends 'threejs'
|
|
24
24
|
? RAPIER3D.RigidBody
|
|
25
25
|
: K extends 'pixijs'
|
|
26
26
|
? RAPIER2D.RigidBody
|
|
27
27
|
: never;
|
|
28
28
|
|
|
29
|
-
/** Map a {@link
|
|
30
|
-
export type ColliderOf<K extends
|
|
29
|
+
/** Map a {@link AdapterSurface} to its native Rapier collider type. */
|
|
30
|
+
export type ColliderOf<K extends AdapterSurface> = K extends 'threejs'
|
|
31
31
|
? RAPIER3D.Collider
|
|
32
32
|
: K extends 'pixijs'
|
|
33
33
|
? RAPIER2D.Collider
|
|
@@ -54,13 +54,97 @@ export type ColliderOf<K extends WorldKind> = K extends 'threejs'
|
|
|
54
54
|
* State lives on `this` (instance properties). On HMR, the engine swaps the
|
|
55
55
|
* prototype via Object.setPrototypeOf — instance state survives, method bodies
|
|
56
56
|
* update to the new code.
|
|
57
|
+
*
|
|
58
|
+
* `update` is deliberately mandatory, not optional like `init`/`dispose`
|
|
59
|
+
* (owner decision, issue #101): components are for BEHAVIOR. If your
|
|
60
|
+
* subclass's `update(dt, ctx)` body is empty, that is a signal the class may
|
|
61
|
+
* not need to be a component at all — a plain state-holder with only
|
|
62
|
+
* `init`/event methods (score/hits counters, etc.) usually belongs as a
|
|
63
|
+
* plain object or a static-scenery/UI object instead (see the "GameComponents
|
|
64
|
+
* are for things with behavior only" rule).
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* Cross-copy brand (`Symbol.for` is process-global, so every physical copy of
|
|
68
|
+
* the engine shares ONE symbol): `x.prototype instanceof GameComponent` is
|
|
69
|
+
* FALSE when a project resolves TWO copies of the engine at once — the
|
|
70
|
+
* published `@vgai/engine/*` subpaths next to the `@engine/*` source alias,
|
|
71
|
+
* exactly what a scaffolded project's node-side scripts do (meteor-dodge
|
|
72
|
+
* dry-run friction: `validate-scenes --components` reported every field of a
|
|
73
|
+
* pristine scaffold's `SceneCamera` as unknown). Structural checks
|
|
74
|
+
* (`isGameComponentClass`) test this brand as well as `instanceof`.
|
|
57
75
|
*/
|
|
58
|
-
export
|
|
76
|
+
export const GAME_COMPONENT_BRAND = Symbol.for('vgai.GameComponent');
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* HMR class lineage. A true component hot-swap deliberately replaces a live
|
|
80
|
+
* instance's prototype with the newly imported class, but other already-live
|
|
81
|
+
* project modules can still hold the previous class object. Native
|
|
82
|
+
* `instanceof PreviousClass` would then become false even though the object is
|
|
83
|
+
* still the same component under the same hot-reload lineage. That broke both
|
|
84
|
+
* ordinary trigger guards (`component instanceof PlayerController`) and the
|
|
85
|
+
* canonical React-state selector (`game.queryByComponent(PlayerController)`).
|
|
86
|
+
*
|
|
87
|
+
* Keep this as class-to-class lineage rather than a string/name registry:
|
|
88
|
+
* minified classes, aliases, and two unrelated components with the same name
|
|
89
|
+
* remain distinct. The WeakMap owns no game/entity state and introduces no
|
|
90
|
+
* second identity model; it records only the exact constructor pairs the
|
|
91
|
+
* existing HMR boundary has actually swapped.
|
|
92
|
+
*/
|
|
93
|
+
type HmrComponentConstructor = abstract new (...args: never[]) => object;
|
|
94
|
+
|
|
95
|
+
const hmrLineages = new WeakMap<HmrComponentConstructor, Set<HmrComponentConstructor>>();
|
|
96
|
+
|
|
97
|
+
/** @internal Called only by ComponentManager's successful HMR swap path. */
|
|
98
|
+
export function linkGameComponentHmrClasses(
|
|
99
|
+
previous: HmrComponentConstructor,
|
|
100
|
+
next: HmrComponentConstructor,
|
|
101
|
+
): void {
|
|
102
|
+
const previousLineage = hmrLineages.get(previous);
|
|
103
|
+
const nextLineage = hmrLineages.get(next);
|
|
104
|
+
const merged = new Set<HmrComponentConstructor>([previous, next]);
|
|
105
|
+
if (previousLineage) {
|
|
106
|
+
for (const member of previousLineage) merged.add(member);
|
|
107
|
+
}
|
|
108
|
+
if (nextLineage) {
|
|
109
|
+
for (const member of nextLineage) merged.add(member);
|
|
110
|
+
}
|
|
111
|
+
for (const member of merged) hmrLineages.set(member, merged);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function classesShareHmrLineage(a: HmrComponentConstructor, b: HmrComponentConstructor): boolean {
|
|
115
|
+
return a === b || hmrLineages.get(a)?.has(b) === true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export abstract class GameComponent<K extends AdapterSurface = 'threejs'> {
|
|
59
119
|
/** Which phase this component's update runs in. Override in subclasses. */
|
|
60
120
|
static phase: SystemPhaseName = 'gameLogic';
|
|
61
121
|
|
|
122
|
+
/** See {@link GAME_COMPONENT_BRAND} — inherited by every subclass. */
|
|
123
|
+
static readonly [GAME_COMPONENT_BRAND] = true;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Preserve ordinary JavaScript `instanceof` across a true HMR prototype
|
|
127
|
+
* swap. Subclasses inherit this static hook, so `value instanceof
|
|
128
|
+
* PlayerController` first performs the native prototype-chain check and,
|
|
129
|
+
* only when that fails, accepts an exact constructor lineage previously
|
|
130
|
+
* linked by ComponentManager.hotSwap(). Non-HMR values retain byte-for-byte
|
|
131
|
+
* native semantics.
|
|
132
|
+
*/
|
|
133
|
+
static [Symbol.hasInstance](value: unknown): boolean {
|
|
134
|
+
// biome-ignore lint/complexity/noThisInStatic: Symbol.hasInstance is inherited; `this` must be the queried subclass, not the GameComponent base.
|
|
135
|
+
const expected = this as HmrComponentConstructor;
|
|
136
|
+
const nativeMatch = Function.prototype[Symbol.hasInstance].call(expected, value) as boolean;
|
|
137
|
+
if (nativeMatch) return true;
|
|
138
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false;
|
|
139
|
+
const actual = (value as { constructor?: unknown }).constructor;
|
|
140
|
+
return (
|
|
141
|
+
typeof actual === 'function' &&
|
|
142
|
+
classesShareHmrLineage(expected, actual as HmrComponentConstructor)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
62
146
|
/**
|
|
63
|
-
* Runtime-checkable declaration of which {@link
|
|
147
|
+
* Runtime-checkable declaration of which {@link AdapterSurface} this component
|
|
64
148
|
* is written for (T7.2, D8 §3 — `docs/GAME-COMPONENT-GENERALIZATION.md`).
|
|
65
149
|
* TS's `K` type parameter is erased at runtime, so this static is what the
|
|
66
150
|
* ComponentManager checks at attach time: a node whose world's kind
|
|
@@ -71,7 +155,7 @@ export abstract class GameComponent<K extends WorldKind = 'threejs'> {
|
|
|
71
155
|
* any world components are legal in (every world except `'react'`, which
|
|
72
156
|
* always throws regardless of `declaredKind` — see the ComponentManager).
|
|
73
157
|
*/
|
|
74
|
-
static declaredKind:
|
|
158
|
+
static declaredKind: AdapterSurface | 'any' = 'threejs';
|
|
75
159
|
|
|
76
160
|
/**
|
|
77
161
|
* Optional Zod object schema for this component's authored fields. When set,
|
|
@@ -88,7 +172,7 @@ export abstract class GameComponent<K extends WorldKind = 'threejs'> {
|
|
|
88
172
|
world!: WorldInstance;
|
|
89
173
|
|
|
90
174
|
/**
|
|
91
|
-
* Typed accessor, threejs
|
|
175
|
+
* Typed accessor, threejs roots only — kept for compatibility. On a
|
|
92
176
|
* `GameComponent` (default `K`) this types as `THREE.Object3D`, so every
|
|
93
177
|
* existing component compiles unchanged. On a non-threejs instance it
|
|
94
178
|
* THROWS a descriptive error rather than ever returning a wrong-kind node —
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-contributed Project Command for baking the procedural humanoid into
|
|
3
|
+
* ordinary project assets. The editor discovers this module through
|
|
4
|
+
* @vgai/engine's package.json; the editor itself contains no humanoid branch.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
9
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
10
|
+
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
|
|
11
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { createHumanoidClipLibrary, retargetClipToHumanoid } from './clips';
|
|
14
|
+
import { generateHumanoid } from './generate';
|
|
15
|
+
import { HumanoidParamsSchema } from './schema';
|
|
16
|
+
|
|
17
|
+
const InputSchema = z.object({
|
|
18
|
+
name: z
|
|
19
|
+
.string()
|
|
20
|
+
.regex(/^[a-z0-9][a-z0-9-_]{0,63}$/)
|
|
21
|
+
.default('humanoid')
|
|
22
|
+
.describe(
|
|
23
|
+
'Stable lowercase asset name used for the generated GLB, prefab, and provenance files.',
|
|
24
|
+
),
|
|
25
|
+
params: HumanoidParamsSchema.default(HumanoidParamsSchema.parse({})).describe(
|
|
26
|
+
'Procedural body proportions.',
|
|
27
|
+
),
|
|
28
|
+
clipSource: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe(
|
|
32
|
+
'Optional project-public GLB path containing Mixamo-named clips. When supplied, Idle, Walk, and Run are retargeted and baked into the generated humanoid GLB.',
|
|
33
|
+
),
|
|
34
|
+
clipNames: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.min(1)
|
|
37
|
+
.default(['Idle', 'Walk', 'Run'])
|
|
38
|
+
.describe('Exact standard clip names to retarget from clipSource.'),
|
|
39
|
+
dryRun: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(false)
|
|
42
|
+
.describe('Generate and validate the complete output batch without writing project files.'),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const OutputFileSchema = z.object({
|
|
46
|
+
path: z.string(),
|
|
47
|
+
bytes: z.number(),
|
|
48
|
+
mediaType: z.string().optional(),
|
|
49
|
+
role: z.enum(['asset', 'prefab', 'provenance', 'other']).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ResultSchema = z.object({
|
|
53
|
+
model: z.string(),
|
|
54
|
+
prefab: z.string(),
|
|
55
|
+
provenance: z.string(),
|
|
56
|
+
vertexCount: z.number(),
|
|
57
|
+
clips: z.array(z.string()),
|
|
58
|
+
files: z.array(OutputFileSchema),
|
|
59
|
+
totalBytes: z.number(),
|
|
60
|
+
dryRun: z.boolean(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
interface OutputWriter {
|
|
64
|
+
write(
|
|
65
|
+
files: ReadonlyArray<{
|
|
66
|
+
path: string;
|
|
67
|
+
content: string | Uint8Array;
|
|
68
|
+
mediaType?: string;
|
|
69
|
+
role?: 'asset' | 'prefab' | 'provenance' | 'other';
|
|
70
|
+
}>,
|
|
71
|
+
options?: { dryRun?: boolean },
|
|
72
|
+
): Promise<{
|
|
73
|
+
files: z.infer<typeof OutputFileSchema>[];
|
|
74
|
+
totalBytes: number;
|
|
75
|
+
dryRun: boolean;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ProjectOperationContext {
|
|
80
|
+
projectRoot?: string;
|
|
81
|
+
projectOutputs?: OutputWriter;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const NODE_THREE_POLYFILL_KEYS = [
|
|
86
|
+
'self',
|
|
87
|
+
'ProgressEvent',
|
|
88
|
+
'createImageBitmap',
|
|
89
|
+
'FileReader',
|
|
90
|
+
] as const;
|
|
91
|
+
let nodeThreePolyfillDepth = 0;
|
|
92
|
+
let nodeThreePreviousDescriptors: Record<
|
|
93
|
+
(typeof NODE_THREE_POLYFILL_KEYS)[number],
|
|
94
|
+
PropertyDescriptor | undefined
|
|
95
|
+
> | null = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Three's official GLTFLoader/GLTFExporter addons expect a small browser API
|
|
99
|
+
* surface even when invoked by this declared Node-only project operation.
|
|
100
|
+
* Install that surface for the duration of overlapping bakes and restore the
|
|
101
|
+
* editor-server process exactly when the last caller releases it.
|
|
102
|
+
*/
|
|
103
|
+
function installNodeThreePolyfills(): () => void {
|
|
104
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
105
|
+
if (nodeThreePolyfillDepth === 0) {
|
|
106
|
+
nodeThreePreviousDescriptors = Object.fromEntries(
|
|
107
|
+
NODE_THREE_POLYFILL_KEYS.map((key) => [key, Object.getOwnPropertyDescriptor(target, key)]),
|
|
108
|
+
) as NonNullable<typeof nodeThreePreviousDescriptors>;
|
|
109
|
+
if (!target['self']) target['self'] = globalThis;
|
|
110
|
+
if (!target['ProgressEvent']) {
|
|
111
|
+
target['ProgressEvent'] = class {
|
|
112
|
+
readonly type: string;
|
|
113
|
+
constructor(type: string, init: Record<string, unknown> = {}) {
|
|
114
|
+
this.type = type;
|
|
115
|
+
Object.assign(this, init);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!target['createImageBitmap']) {
|
|
120
|
+
target['createImageBitmap'] = async () => ({ close() {} });
|
|
121
|
+
}
|
|
122
|
+
if (!target['FileReader']) {
|
|
123
|
+
target['FileReader'] = class {
|
|
124
|
+
result: string | ArrayBuffer | null = null;
|
|
125
|
+
onloadend: (() => void) | null = null;
|
|
126
|
+
readAsArrayBuffer(blob: Blob): void {
|
|
127
|
+
void blob.arrayBuffer().then((value) => {
|
|
128
|
+
this.result = value;
|
|
129
|
+
this.onloadend?.();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
readAsDataURL(blob: Blob): void {
|
|
133
|
+
void blob.arrayBuffer().then((value) => {
|
|
134
|
+
this.result = `data:${blob.type || 'application/octet-stream'};base64,${Buffer.from(value).toString('base64')}`;
|
|
135
|
+
this.onloadend?.();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
nodeThreePolyfillDepth++;
|
|
142
|
+
let released = false;
|
|
143
|
+
return () => {
|
|
144
|
+
if (released) return;
|
|
145
|
+
released = true;
|
|
146
|
+
nodeThreePolyfillDepth--;
|
|
147
|
+
if (nodeThreePolyfillDepth !== 0 || !nodeThreePreviousDescriptors) return;
|
|
148
|
+
for (const key of NODE_THREE_POLYFILL_KEYS) {
|
|
149
|
+
const descriptor = nodeThreePreviousDescriptors[key];
|
|
150
|
+
if (descriptor) Object.defineProperty(target, key, descriptor);
|
|
151
|
+
else delete target[key];
|
|
152
|
+
}
|
|
153
|
+
nodeThreePreviousDescriptors = null;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolvePublicFile(projectRoot: string, path: string): Promise<string> {
|
|
158
|
+
const relativePath = path.replace(/^\//, '');
|
|
159
|
+
const publicRoot = await realpath(resolve(projectRoot, 'public'));
|
|
160
|
+
const candidate = await realpath(resolve(publicRoot, relativePath));
|
|
161
|
+
if (candidate !== publicRoot && !candidate.startsWith(`${publicRoot}${sep}`)) {
|
|
162
|
+
throw new Error(`clipSource must resolve inside the project's public/ directory: ${path}`);
|
|
163
|
+
}
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadRetargetedClips(
|
|
168
|
+
projectRoot: string,
|
|
169
|
+
sourcePath: string,
|
|
170
|
+
names: string[],
|
|
171
|
+
rig: ReturnType<typeof generateHumanoid>,
|
|
172
|
+
) {
|
|
173
|
+
const releasePolyfills = installNodeThreePolyfills();
|
|
174
|
+
try {
|
|
175
|
+
const absolute = await resolvePublicFile(projectRoot, sourcePath);
|
|
176
|
+
const bytes = await readFile(absolute);
|
|
177
|
+
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
178
|
+
const gltf = await new GLTFLoader().parseAsync(arrayBuffer, `${dirname(absolute)}${sep}`);
|
|
179
|
+
const library = createHumanoidClipLibrary(gltf.animations);
|
|
180
|
+
const clips = names.map((name) => {
|
|
181
|
+
const clip = retargetClipToHumanoid(library.get(name), rig);
|
|
182
|
+
clip.name = name;
|
|
183
|
+
return clip;
|
|
184
|
+
});
|
|
185
|
+
gltf.scene.traverse((object) => {
|
|
186
|
+
const mesh = object as typeof object & {
|
|
187
|
+
geometry?: { dispose(): void };
|
|
188
|
+
material?: { dispose(): void } | Array<{ dispose(): void }>;
|
|
189
|
+
};
|
|
190
|
+
mesh.geometry?.dispose();
|
|
191
|
+
for (const material of Array.isArray(mesh.material)
|
|
192
|
+
? mesh.material
|
|
193
|
+
: mesh.material
|
|
194
|
+
? [mesh.material]
|
|
195
|
+
: []) {
|
|
196
|
+
material.dispose();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return { clips, sourceSha256: createHash('sha256').update(bytes).digest('hex') };
|
|
200
|
+
} finally {
|
|
201
|
+
releasePolyfills();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function exportGlb(
|
|
206
|
+
root: ReturnType<typeof generateHumanoid>['root'],
|
|
207
|
+
animations: Awaited<ReturnType<typeof loadRetargetedClips>>['clips'],
|
|
208
|
+
): Promise<Uint8Array> {
|
|
209
|
+
const releasePolyfills = installNodeThreePolyfills();
|
|
210
|
+
try {
|
|
211
|
+
const result = await new Promise<ArrayBuffer>((resolveResult, reject) => {
|
|
212
|
+
new GLTFExporter().parse(
|
|
213
|
+
root,
|
|
214
|
+
(value) => {
|
|
215
|
+
if (value instanceof ArrayBuffer) resolveResult(value);
|
|
216
|
+
else reject(new Error('GLTFExporter returned JSON while binary output was requested.'));
|
|
217
|
+
},
|
|
218
|
+
reject,
|
|
219
|
+
{
|
|
220
|
+
binary: true,
|
|
221
|
+
animations,
|
|
222
|
+
onlyVisible: false,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
return new Uint8Array(result);
|
|
227
|
+
} finally {
|
|
228
|
+
releasePolyfills();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const operation = {
|
|
233
|
+
name: 'project.humanoid.bake',
|
|
234
|
+
summary: 'Bake a procedural humanoid into an ordinary GLB asset and prefab.',
|
|
235
|
+
description:
|
|
236
|
+
'Generates VGAI’s skinned procedural humanoid from body parameters. Optionally retargets ' +
|
|
237
|
+
'Mixamo-named clips from a project-public GLB, then atomically writes the generated GLB, ' +
|
|
238
|
+
'a normal .prefab.json referencing it, and reproducibility provenance. Use the prefab as ' +
|
|
239
|
+
'the authored scene object; do not regenerate the visible body every gameplay run. The ' +
|
|
240
|
+
'baked body keeps the multicolored engineering-reference material: it is not a styled ' +
|
|
241
|
+
'production character. A prominent player character should receive an intentional, ' +
|
|
242
|
+
'project-owned material/composition treatment unless that reference look is deliberate.',
|
|
243
|
+
input: InputSchema,
|
|
244
|
+
result: ResultSchema,
|
|
245
|
+
errors: [],
|
|
246
|
+
requires: { project: true },
|
|
247
|
+
host: 'node' as const,
|
|
248
|
+
mutates: true,
|
|
249
|
+
supportsDryRun: true,
|
|
250
|
+
longRunning: true,
|
|
251
|
+
permission: {
|
|
252
|
+
risk: 'write' as const,
|
|
253
|
+
summary: 'Writes or replaces generated model, prefab, and provenance files under public/.',
|
|
254
|
+
},
|
|
255
|
+
async impl(input: z.infer<typeof InputSchema>, ctx: ProjectOperationContext) {
|
|
256
|
+
if (!ctx.projectRoot || !ctx.projectOutputs) {
|
|
257
|
+
throw new Error('project.humanoid.bake requires a project root and generated-output writer.');
|
|
258
|
+
}
|
|
259
|
+
if (ctx.signal?.aborted) throw new Error('project.humanoid.bake was cancelled.');
|
|
260
|
+
|
|
261
|
+
const rig = generateHumanoid(input.params);
|
|
262
|
+
try {
|
|
263
|
+
const loaded = input.clipSource
|
|
264
|
+
? await loadRetargetedClips(ctx.projectRoot, input.clipSource, input.clipNames, rig)
|
|
265
|
+
: { clips: [], sourceSha256: null };
|
|
266
|
+
const glb = await exportGlb(rig.root, loaded.clips);
|
|
267
|
+
const base = input.name;
|
|
268
|
+
const model = `public/models/generated/${base}.glb`;
|
|
269
|
+
const prefab = `public/prefabs/generated/${base}.prefab.json`;
|
|
270
|
+
const provenance = `public/models/generated/${base}.provenance.json`;
|
|
271
|
+
const modelUrl = `/models/generated/${base}.glb`;
|
|
272
|
+
const prefabDocument = {
|
|
273
|
+
version: 1,
|
|
274
|
+
name: base,
|
|
275
|
+
root: {
|
|
276
|
+
name: base,
|
|
277
|
+
mesh: { type: 'gltf', src: modelUrl },
|
|
278
|
+
...(loaded.clips.length > 0 ? { animation: {} } : {}),
|
|
279
|
+
shadow: { enabled: true },
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
const provenanceDocument = {
|
|
283
|
+
version: 1,
|
|
284
|
+
generator: '@vgai/engine:project.humanoid.bake',
|
|
285
|
+
params: rig.params,
|
|
286
|
+
clipSource: input.clipSource ?? null,
|
|
287
|
+
clipSourceSha256: loaded.sourceSha256,
|
|
288
|
+
clips: loaded.clips.map((clip) => clip.name),
|
|
289
|
+
modelSha256: createHash('sha256').update(glb).digest('hex'),
|
|
290
|
+
};
|
|
291
|
+
const output = await ctx.projectOutputs.write(
|
|
292
|
+
[
|
|
293
|
+
{
|
|
294
|
+
path: model,
|
|
295
|
+
content: glb,
|
|
296
|
+
mediaType: 'model/gltf-binary',
|
|
297
|
+
role: 'asset',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
path: prefab,
|
|
301
|
+
content: `${JSON.stringify(prefabDocument, null, 2)}\n`,
|
|
302
|
+
mediaType: 'application/json',
|
|
303
|
+
role: 'prefab',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
path: provenance,
|
|
307
|
+
content: `${JSON.stringify(provenanceDocument, null, 2)}\n`,
|
|
308
|
+
mediaType: 'application/json',
|
|
309
|
+
role: 'provenance',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
{ dryRun: input.dryRun },
|
|
313
|
+
);
|
|
314
|
+
return {
|
|
315
|
+
model,
|
|
316
|
+
prefab,
|
|
317
|
+
provenance,
|
|
318
|
+
vertexCount: rig.vertexCount,
|
|
319
|
+
clips: loaded.clips.map((clip) => clip.name),
|
|
320
|
+
...output,
|
|
321
|
+
};
|
|
322
|
+
} finally {
|
|
323
|
+
rig.dispose();
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
};
|