@vgai/engine 0.4.1 → 0.5.0-canary.20260719.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 +48 -15
- package/package.json +11 -25
- package/schemas/engine-capabilities.json +10 -10
- package/schemas/entity2d.schema.json +468 -0
- package/schemas/mat.schema.json +2 -33
- package/schemas/prefab.schema.json +16 -172
- package/schemas/scn2d.schema.json +42 -23
- package/schemas/vgai-game.schema.json +34 -0
- package/schemas/vscn.schema.json +16 -172
- package/src/adapter/authoring.ts +152 -2
- package/src/adapter/colyseus-networking-adapter.ts +35 -1
- package/src/adapter/first-party-systems.ts +7 -1
- package/src/adapter/game-adapter.ts +13 -0
- package/src/adapter/index.ts +25 -0
- package/src/adapter/rapier-physics-adapter.ts +55 -2
- package/src/adapter/system-adapter.ts +249 -2
- package/src/adapter/vgai-scene-game-adapter.ts +149 -25
- package/src/ai/navigation.ts +28 -0
- package/src/animation/clip-map.ts +1 -8
- package/src/animation/theatre-director.ts +50 -0
- package/src/animation/xstate-animation-binding.ts +6 -0
- package/src/audio/audio-introspection.ts +290 -0
- package/src/audio/tone-context.ts +46 -0
- package/src/dev/chrome-trace.ts +153 -0
- package/src/dev/performance-profiler.ts +93 -6
- package/src/dev/render-debug-adapter.ts +199 -0
- package/src/dev/render-memory.ts +243 -0
- package/src/dev/webgl-frame-capture.ts +424 -0
- package/src/ecs/component-manager.ts +43 -10
- package/src/ecs/game-component.ts +39 -10
- package/src/input/input-manager.ts +24 -19
- package/src/input/input-types.ts +1 -1
- package/src/loader.ts +7 -0
- package/src/manifest/load.ts +14 -0
- package/src/manifest/schema.ts +65 -0
- package/src/react/game-state.tsx +1 -1
- package/src/render/render-batch-system.ts +26 -12
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/runtime/create-runtime.ts +33 -5
- package/src/runtime/debug-bridge.ts +5 -5
- package/src/runtime/game.ts +102 -20
- package/src/runtime/mount-manifest.ts +1 -1
- package/src/runtime/render-control.ts +121 -0
- package/src/runtime/types.ts +1 -1
- package/src/scene/asset-loaders.ts +77 -3
- package/src/scene/instance-mesh.ts +25 -0
- package/src/scene/material-factory.ts +4 -15
- package/src/scene/mesh-shadow.ts +18 -0
- package/src/scene/particles-factory.ts +59 -0
- package/src/scene/scene-loader.ts +41 -16
- package/src/scene/schema/instances.ts +1 -2
- package/src/scene/schema/material.ts +83 -94
- package/src/scene/schema/mesh.ts +76 -90
- package/src/scene/schema/scene-file.ts +1 -2
- package/src/scene/user-data.ts +30 -14
- package/src/setup/setup-renderer.ts +6 -1
- package/src/world2d/asset-paths2d.ts +44 -0
- package/src/world2d/collision-2d.ts +7 -14
- package/src/world2d/entity2d-asset.ts +22 -0
- package/src/world2d/index.ts +27 -2
- package/src/world2d/physics2d-transform.ts +173 -0
- package/src/world2d/physics2d-units.ts +10 -0
- package/src/world2d/pixi-game-adapter.ts +148 -36
- package/src/world2d/scene2d-identity.ts +49 -0
- package/src/world2d/scene2d-loader.ts +243 -119
- package/src/world2d/schema/entity2d.ts +51 -33
- package/src/world2d/schema/physics2d.ts +14 -3
- package/src/world2d/schema/sprite.ts +32 -4
- package/src/world2d/schema/tilemap.ts +26 -9
- package/src/world2d/transform-writer-2d.ts +29 -11
- package/src/world2d/types.ts +21 -8
- package/src/world3d-react/behavior.tsx +138 -0
- package/src/world3d-react/engine-bridge.ts +48 -0
- package/src/world3d-react/index.ts +44 -0
- package/src/world3d-react/r3f-adapter.tsx +303 -0
- package/src/world3d-react/world-context.ts +294 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
- package/src/character/cloth-sim.ts +0 -533
- package/src/character/spring-chain.ts +0 -307
- package/src/humanoid/body.ts +0 -663
- package/src/humanoid/clips.ts +0 -149
- package/src/humanoid/compose.ts +0 -209
- package/src/humanoid/generate.ts +0 -189
- package/src/humanoid/index.ts +0 -36
- package/src/humanoid/schema.ts +0 -108
- package/src/humanoid/skeleton.ts +0 -345
- package/src/react/humanoid-bake.document.tsx +0 -337
- package/src/scene/geometries/index.ts +0 -7
- package/src/scene/geometries/terrain.ts +0 -42
- package/src/scene/geometry-registry.ts +0 -42
- package/src/scene/instance-registry.ts +0 -84
- package/src/scene/instancers/grid.ts +0 -38
- package/src/scene/instancers/index.ts +0 -7
- package/src/scene/material-registry.ts +0 -73
- package/src/scene/materials/index.ts +0 -7
- package/src/scene/materials/water.ts +0 -56
- package/src/world2d/components-2d.ts +0 -86
- package/tools/humanoid-bake.tool.ts +0 -274
|
@@ -14,18 +14,17 @@ import {
|
|
|
14
14
|
assetCacheSizes,
|
|
15
15
|
clearAssetCaches as clearSharedAssetCaches,
|
|
16
16
|
loadGLTF,
|
|
17
|
+
loadSplat,
|
|
17
18
|
resolveGltfNode,
|
|
18
19
|
} from './asset-loaders';
|
|
19
20
|
import { applyImportCorrection, getAssetMeta, loadRegistry } from './asset-registry';
|
|
20
21
|
import { computeColliderWorldDimensions, computeColliderWorldOffset } from './collider-dimensions';
|
|
21
22
|
import { applyComponents, type ComponentRegistry } from './component-registry';
|
|
22
23
|
import { DEFAULTS } from './defaults';
|
|
23
|
-
import './
|
|
24
|
-
import './instancers';
|
|
25
|
-
import { buildInstancedMesh, buildInstancedMeshFromTuples } from './instance-registry';
|
|
24
|
+
import { buildInstancedMeshFromTuples } from './instance-mesh';
|
|
26
25
|
import { createCamera, createLight } from './light-camera-factory';
|
|
27
26
|
import { createGeometry, createMaterial } from './material-factory';
|
|
28
|
-
import './
|
|
27
|
+
import { setMeshShadowEnabled } from './mesh-shadow';
|
|
29
28
|
import { parseInstancesFile, parseMaterialFile, parsePrefabFile, parseSceneFile } from './parse';
|
|
30
29
|
import { createParticleSystemFromData } from './particles-factory';
|
|
31
30
|
import {
|
|
@@ -78,9 +77,8 @@ export function clearAssetCaches(): void {
|
|
|
78
77
|
/**
|
|
79
78
|
* F7 — per-frame LOD tick. THREE.LOD only swaps its active level when
|
|
80
79
|
* `.update(camera)` runs, so every `THREE.LOD` in the scene graph (built by
|
|
81
|
-
* the `mesh.lod` spawn branch above) needs this called once a frame.
|
|
82
|
-
*
|
|
83
|
-
* `preRender` system in `vgai-scene-game-adapter.ts`.
|
|
80
|
+
* the `mesh.lod` spawn branch above) needs this called once a frame. Wired
|
|
81
|
+
* into the runtime's `preRender` system in `vgai-scene-game-adapter.ts`.
|
|
84
82
|
*/
|
|
85
83
|
export function updateSceneLODs(scene: THREE.Scene, camera: THREE.Camera): void {
|
|
86
84
|
scene.traverse((o) => {
|
|
@@ -96,12 +94,14 @@ export function updateSceneLODs(scene: THREE.Scene, camera: THREE.Camera): void
|
|
|
96
94
|
export function __assetCacheSizesForTest(): {
|
|
97
95
|
textures: number;
|
|
98
96
|
gltf: number;
|
|
97
|
+
splat: number;
|
|
99
98
|
env: number;
|
|
100
99
|
} {
|
|
101
100
|
const sizes = assetCacheSizes();
|
|
102
101
|
return {
|
|
103
102
|
textures: sizes.textures,
|
|
104
103
|
gltf: sizes.gltf,
|
|
104
|
+
splat: sizes.splat,
|
|
105
105
|
env: envTextureCache.size,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
@@ -222,6 +222,13 @@ export interface SceneInstance {
|
|
|
222
222
|
* `instantiateScene` return site, so this is populated identically for both.
|
|
223
223
|
*/
|
|
224
224
|
entities: ReadonlyMap<string, THREE.Object3D>;
|
|
225
|
+
/** Number of native Gaussian-splat Object3Ds owned by this scene. The render
|
|
226
|
+
* host uses this to install exactly one SparkRenderer for the scene. */
|
|
227
|
+
splatCount: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface DisposableSplat extends THREE.Object3D {
|
|
231
|
+
dispose(): void;
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
/**
|
|
@@ -405,6 +412,7 @@ async function instantiateScene(
|
|
|
405
412
|
): Promise<SceneInstance> {
|
|
406
413
|
const tickFns: ((dt: number) => void)[] = [];
|
|
407
414
|
const mixers: THREE.AnimationMixer[] = [];
|
|
415
|
+
const splats: DisposableSplat[] = [];
|
|
408
416
|
|
|
409
417
|
// Ensure asset registry is loaded before spawning entities
|
|
410
418
|
await loadRegistry();
|
|
@@ -462,7 +470,7 @@ async function instantiateScene(
|
|
|
462
470
|
const idMap = new Map<string, THREE.Object3D>();
|
|
463
471
|
|
|
464
472
|
for (const entityDef of data.entities) {
|
|
465
|
-
const obj = await spawnEntity(entityDef, ctx, tickFns, mixers, idMap);
|
|
473
|
+
const obj = await spawnEntity(entityDef, ctx, tickFns, mixers, splats, idMap);
|
|
466
474
|
ctx.scene.add(obj);
|
|
467
475
|
}
|
|
468
476
|
|
|
@@ -516,6 +524,7 @@ async function instantiateScene(
|
|
|
516
524
|
cameras,
|
|
517
525
|
environment: data.environment,
|
|
518
526
|
entities: idMap,
|
|
527
|
+
splatCount: splats.length,
|
|
519
528
|
update(dt: number) {
|
|
520
529
|
for (const fn of tickFns) fn(dt);
|
|
521
530
|
},
|
|
@@ -523,6 +532,8 @@ async function instantiateScene(
|
|
|
523
532
|
for (const mixer of mixers) mixer.stopAllAction();
|
|
524
533
|
tickFns.length = 0;
|
|
525
534
|
mixers.length = 0;
|
|
535
|
+
for (const splat of splats) splat.dispose();
|
|
536
|
+
splats.length = 0;
|
|
526
537
|
},
|
|
527
538
|
};
|
|
528
539
|
}
|
|
@@ -532,6 +543,7 @@ async function spawnEntity(
|
|
|
532
543
|
ctx: SceneLoadContext,
|
|
533
544
|
tickFns: ((dt: number) => void)[],
|
|
534
545
|
mixers: THREE.AnimationMixer[],
|
|
546
|
+
splats: DisposableSplat[],
|
|
535
547
|
idMap?: Map<string, THREE.Object3D>,
|
|
536
548
|
ancestorCtx: AncestorPhysicsContext = ROOT_ANCESTOR_CONTEXT,
|
|
537
549
|
audioTracker?: AudioTracker,
|
|
@@ -578,7 +590,7 @@ async function spawnEntity(
|
|
|
578
590
|
if (def.prefab) {
|
|
579
591
|
const prefabDef = await loadPrefabFile(def.prefab, ctx);
|
|
580
592
|
const merged = mergePrefabInstance(prefabDef.root, def);
|
|
581
|
-
return spawnEntity(merged, ctx, tickFns, mixers, idMap, ancestorCtx, audioTracker);
|
|
593
|
+
return spawnEntity(merged, ctx, tickFns, mixers, splats, idMap, ancestorCtx, audioTracker);
|
|
582
594
|
}
|
|
583
595
|
|
|
584
596
|
// Transform
|
|
@@ -664,6 +676,11 @@ async function spawnEntity(
|
|
|
664
676
|
// Apply import correction from asset registry
|
|
665
677
|
applyImportCorrection(content, getAssetMeta(def.mesh.src)?.importCorrection);
|
|
666
678
|
|
|
679
|
+
// A glTF entity is a hierarchy, not one Mesh. Propagate the same
|
|
680
|
+
// authored shadow contract primitive entities already honor to every
|
|
681
|
+
// Mesh/SkinnedMesh descendant before wrapping the content.
|
|
682
|
+
setMeshShadowEnabled(content, def.shadow?.enabled);
|
|
683
|
+
|
|
667
684
|
// Wrap in a group so entity transform and import correction are separated.
|
|
668
685
|
// `content` carries import correction; wrapper carries entity transform.
|
|
669
686
|
const wrapper = new THREE.Group();
|
|
@@ -700,6 +717,16 @@ async function spawnEntity(
|
|
|
700
717
|
}
|
|
701
718
|
}
|
|
702
719
|
}
|
|
720
|
+
} else if (def.mesh.type === 'splat' && def.mesh.src) {
|
|
721
|
+
const wrapper = new THREE.Group();
|
|
722
|
+
// A headless game still needs the authored entity, transform, components,
|
|
723
|
+
// and hierarchy for bots/CI; only the visual payload is GPU-bound.
|
|
724
|
+
if (ctx.renderer) {
|
|
725
|
+
const content = await loadSplat(def.mesh.src);
|
|
726
|
+
splats.push(content);
|
|
727
|
+
wrapper.add(content);
|
|
728
|
+
}
|
|
729
|
+
object3d = wrapper;
|
|
703
730
|
} else {
|
|
704
731
|
const geometry = createGeometry(def.mesh);
|
|
705
732
|
const resolvedMat = await resolveMaterial(def, ctx);
|
|
@@ -710,13 +737,6 @@ async function spawnEntity(
|
|
|
710
737
|
// .instances.json and build one InstancedMesh from its tuples.
|
|
711
738
|
const tuples = await loadInstancesFile(def.mesh.instances, ctx);
|
|
712
739
|
mesh = buildInstancedMeshFromTuples(geometry, material, tuples);
|
|
713
|
-
} else if (def.mesh.instancer) {
|
|
714
|
-
mesh = buildInstancedMesh(
|
|
715
|
-
geometry,
|
|
716
|
-
material,
|
|
717
|
-
def.mesh.instancer,
|
|
718
|
-
def.mesh.instancerParams ?? {},
|
|
719
|
-
);
|
|
720
740
|
} else {
|
|
721
741
|
mesh = new THREE.Mesh(geometry, material);
|
|
722
742
|
}
|
|
@@ -930,6 +950,7 @@ async function spawnEntity(
|
|
|
930
950
|
ctx,
|
|
931
951
|
tickFns,
|
|
932
952
|
mixers,
|
|
953
|
+
splats,
|
|
933
954
|
idMap,
|
|
934
955
|
childAncestorCtx,
|
|
935
956
|
audioTracker,
|
|
@@ -1391,12 +1412,14 @@ export async function spawnPrefab(
|
|
|
1391
1412
|
};
|
|
1392
1413
|
const tickFns: ((dt: number) => void)[] = [];
|
|
1393
1414
|
const mixers: THREE.AnimationMixer[] = [];
|
|
1415
|
+
const splats: DisposableSplat[] = [];
|
|
1394
1416
|
const audioTracker: AudioTracker = { audios: [], disposed: { value: false } };
|
|
1395
1417
|
const object3D = await spawnEntity(
|
|
1396
1418
|
instance,
|
|
1397
1419
|
ctx,
|
|
1398
1420
|
tickFns,
|
|
1399
1421
|
mixers,
|
|
1422
|
+
splats,
|
|
1400
1423
|
undefined,
|
|
1401
1424
|
ROOT_ANCESTOR_CONTEXT,
|
|
1402
1425
|
audioTracker,
|
|
@@ -1494,8 +1517,10 @@ export async function spawnPrefab(
|
|
|
1494
1517
|
|
|
1495
1518
|
ctx.scene.remove(object3D);
|
|
1496
1519
|
for (const mixer of mixers) mixer.stopAllAction();
|
|
1520
|
+
for (const splat of splats) splat.dispose();
|
|
1497
1521
|
tickFns.length = 0;
|
|
1498
1522
|
mixers.length = 0;
|
|
1523
|
+
splats.length = 0;
|
|
1499
1524
|
},
|
|
1500
1525
|
};
|
|
1501
1526
|
}
|
|
@@ -3,8 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
/**
|
|
4
4
|
* `.instances.json` — a declarative, authored-data instance-transform asset
|
|
5
5
|
* (F3, docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Referenced by `mesh.instances`
|
|
6
|
-
* as
|
|
7
|
-
* a bare array of 10-number tuples, one per instance, each
|
|
6
|
+
* as a bare array of 10-number tuples, one per instance, each
|
|
8
7
|
* `[posX,posY,posZ, quatX,quatY,quatZ,quatW, scaleX,scaleY,scaleZ]`. The
|
|
9
8
|
* scene loader fetches+parses this file and renders it as one
|
|
10
9
|
* `THREE.InstancedMesh` (one draw call) — bulk transform data is referenced
|
|
@@ -1,105 +1,94 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
const SceneMaterialObjectSchema = z
|
|
4
|
-
|
|
5
|
-
.enum(['standard', 'physical', 'basic', 'toon'
|
|
6
|
-
.describe('
|
|
7
|
-
|
|
8
|
-
.
|
|
9
|
-
.optional()
|
|
10
|
-
.describe('
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.optional()
|
|
14
|
-
.describe('
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.optional()
|
|
27
|
-
.describe(
|
|
28
|
-
"Path to a baked lightmap texture (uses the mesh's second UV set). Applies the pre-baked GI/shadow result on top of the material.",
|
|
29
|
-
),
|
|
30
|
-
lightMapIntensity: z.number().optional().describe('Lightmap intensity multiplier (default 1).'),
|
|
31
|
-
roughnessMap: z.string().optional().describe('Path to roughness map texture image'),
|
|
32
|
-
metalnessMap: z.string().optional().describe('Path to metalness map texture image'),
|
|
33
|
-
opacity: z.number().optional().describe('Opacity 0–1. Set transparent: true to enable'),
|
|
34
|
-
transparent: z.boolean().optional().describe('Enable alpha blending for this material'),
|
|
35
|
-
side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
|
|
36
|
-
flatShading: z.boolean().optional().describe('Use flat (faceted) shading instead of smooth'),
|
|
3
|
+
const SceneMaterialObjectSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
type: z.enum(['standard', 'physical', 'basic', 'toon']).describe('Material shading model'),
|
|
6
|
+
color: z.string().optional().describe('Base color as CSS hex string (e.g. "#ff0000")'),
|
|
7
|
+
metalness: z.number().optional().describe('Metalness factor 0–1 (standard/physical only)'),
|
|
8
|
+
roughness: z.number().optional().describe('Roughness factor 0–1 (standard/physical only)'),
|
|
9
|
+
map: z.string().optional().describe('Path to albedo/diffuse texture image'),
|
|
10
|
+
normalMap: z.string().optional().describe('Path to normal map texture image'),
|
|
11
|
+
emissive: z.string().optional().describe('Emissive color as CSS hex string'),
|
|
12
|
+
emissiveIntensity: z.number().optional().describe('Emissive light intensity multiplier'),
|
|
13
|
+
emissiveMap: z.string().optional().describe('Path to emissive map texture image'),
|
|
14
|
+
aoMap: z.string().optional().describe('Path to ambient occlusion map texture image'),
|
|
15
|
+
lightMap: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe(
|
|
19
|
+
"Path to a baked lightmap texture (uses the mesh's second UV set). Applies the pre-baked GI/shadow result on top of the material.",
|
|
20
|
+
),
|
|
21
|
+
lightMapIntensity: z.number().optional().describe('Lightmap intensity multiplier (default 1).'),
|
|
22
|
+
roughnessMap: z.string().optional().describe('Path to roughness map texture image'),
|
|
23
|
+
metalnessMap: z.string().optional().describe('Path to metalness map texture image'),
|
|
24
|
+
opacity: z.number().optional().describe('Opacity 0–1. Set transparent: true to enable'),
|
|
25
|
+
transparent: z.boolean().optional().describe('Enable alpha blending for this material'),
|
|
26
|
+
side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
|
|
27
|
+
flatShading: z.boolean().optional().describe('Use flat (faceted) shading instead of smooth'),
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
29
|
+
// Physical material feature groups (physical only, all optional)
|
|
30
|
+
clearcoat: z
|
|
31
|
+
.object({
|
|
32
|
+
clearcoat: z.number().describe('Clearcoat layer intensity 0–1'),
|
|
33
|
+
clearcoatRoughness: z.number().optional().describe('Clearcoat roughness 0–1'),
|
|
34
|
+
clearcoatMap: z.string().optional().describe('Path to clearcoat intensity map'),
|
|
35
|
+
clearcoatRoughnessMap: z.string().optional().describe('Path to clearcoat roughness map'),
|
|
36
|
+
})
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Clearcoat layer (physical only)'),
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
transmission: z
|
|
41
|
+
.object({
|
|
42
|
+
transmission: z.number().describe('Transmission intensity 0–1 (glass, liquid)'),
|
|
43
|
+
ior: z.number().optional().describe('Index of refraction (default 1.5)'),
|
|
44
|
+
thickness: z.number().optional().describe('Volume thickness for refraction'),
|
|
45
|
+
attenuationColor: z.string().optional().describe('Color absorbed over distance'),
|
|
46
|
+
attenuationDistance: z
|
|
47
|
+
.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Distance at which attenuation color takes full effect'),
|
|
50
|
+
transmissionMap: z.string().optional().describe('Path to transmission map'),
|
|
51
|
+
})
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Light transmission (physical only)'),
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
sheen: z
|
|
56
|
+
.object({
|
|
57
|
+
sheen: z.number().describe('Sheen intensity 0–1 (fabric, velvet)'),
|
|
58
|
+
sheenColor: z.string().optional().describe('Sheen tint color'),
|
|
59
|
+
sheenRoughness: z.number().optional().describe('Sheen roughness 0–1'),
|
|
60
|
+
sheenColorMap: z.string().optional().describe('Path to sheen color map'),
|
|
61
|
+
sheenRoughnessMap: z.string().optional().describe('Path to sheen roughness map'),
|
|
62
|
+
})
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Sheen layer (physical only)'),
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
iridescence: z
|
|
67
|
+
.object({
|
|
68
|
+
iridescence: z.number().describe('Iridescence intensity 0–1 (soap bubbles, oil slick)'),
|
|
69
|
+
iridescenceIOR: z.number().optional().describe('Thin-film IOR (default 1.3)'),
|
|
70
|
+
iridescenceThicknessRange: z
|
|
71
|
+
.tuple([z.number(), z.number()])
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Min/max thin-film thickness in nm'),
|
|
74
|
+
iridescenceMap: z.string().optional().describe('Path to iridescence intensity map'),
|
|
75
|
+
iridescenceThicknessMap: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Path to iridescence thickness map'),
|
|
79
|
+
})
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Iridescence thin-film (physical only)'),
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
83
|
+
displacementMap: z.string().optional().describe('Path to displacement/height map'),
|
|
84
|
+
displacementScale: z.number().optional().describe('Displacement height multiplier'),
|
|
85
|
+
displacementBias: z.number().optional().describe('Displacement offset'),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
93
88
|
|
|
94
|
-
export const SceneMaterialSchema = SceneMaterialObjectSchema.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
code: 'custom',
|
|
98
|
-
path: ['def'],
|
|
99
|
-
message: 'Custom material requires a non-empty registered definition name',
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}).describe('Material appearance properties');
|
|
89
|
+
export const SceneMaterialSchema = SceneMaterialObjectSchema.describe(
|
|
90
|
+
'Material appearance properties',
|
|
91
|
+
);
|
|
103
92
|
|
|
104
93
|
/** Partial schema for prefab instance overrides (type not required). */
|
|
105
94
|
export const SceneMaterialOverrideSchema = SceneMaterialObjectSchema.partial();
|
package/src/scene/schema/mesh.ts
CHANGED
|
@@ -1,104 +1,90 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
const SceneMeshBase = z
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.describe(
|
|
69
|
-
'Name of a node inside the referenced glTF (mesh.src) to show at this distance — ' +
|
|
70
|
-
'resolved via getObjectByName, geometry shared (F4-style). A skinned node throws ' +
|
|
71
|
-
'(atomic-subtree rule).',
|
|
72
|
-
),
|
|
73
|
-
}),
|
|
74
|
-
)
|
|
75
|
-
.optional()
|
|
76
|
-
.describe(
|
|
77
|
-
'Per-entity Level-of-Detail: distance-keyed glTF sub-nodes swapped by camera distance ' +
|
|
78
|
-
'(THREE.LOD). gltf-only. Gated by the environment.rendering.lod master toggle — when ' +
|
|
79
|
-
'off, only the closest level renders. Cannot combine with node/instances/instancer.',
|
|
80
|
-
),
|
|
81
|
-
});
|
|
3
|
+
const SceneMeshBase = z
|
|
4
|
+
.object({
|
|
5
|
+
type: z
|
|
6
|
+
.enum(['box', 'sphere', 'plane', 'cylinder', 'capsule', 'gltf', 'splat'])
|
|
7
|
+
.describe(
|
|
8
|
+
'Renderable type: a built-in mesh primitive, "gltf" for an external 3D model, or "splat" for a native Gaussian-splat asset',
|
|
9
|
+
),
|
|
10
|
+
args: z
|
|
11
|
+
.array(z.number())
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
'Geometry constructor args — box: [w,h,d], sphere: [radius], cylinder: [rTop,rBot,h], capsule: [radius,length], plane: [w,h,widthSegments,heightSegments]',
|
|
15
|
+
),
|
|
16
|
+
src: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe(
|
|
20
|
+
'Path to the external render asset. Required for "gltf" (GLTF/GLB) and "splat" (SPZ)',
|
|
21
|
+
),
|
|
22
|
+
node: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(
|
|
26
|
+
'Name (or path) of a single node inside the referenced glTF (resolved via ' +
|
|
27
|
+
"getObjectByName) to instantiate as this entity's geometry, instead of the whole " +
|
|
28
|
+
'file. The geometry stays SHARED from the cached glTF — nothing is copied out. ' +
|
|
29
|
+
'Only meaningful with type: "gltf". CONSTRAINT: a skinned/animated node cannot be ' +
|
|
30
|
+
'lifted out of its armature — if the named node is a SkinnedMesh (or contains one), ' +
|
|
31
|
+
'resolving it throws (degrade loudly, not silently).',
|
|
32
|
+
),
|
|
33
|
+
instances: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
'Path to an instance-transform asset (.instances.json: an array of ' +
|
|
38
|
+
'[posX,posY,posZ, quatX,quatY,quatZ,quatW, scaleX,scaleY,scaleZ] tuples). Renders one ' +
|
|
39
|
+
'THREE.InstancedMesh (one draw call). Bulk transform data is referenced as an asset, ' +
|
|
40
|
+
'never inlined — keeps vscn thin.',
|
|
41
|
+
),
|
|
42
|
+
lod: z
|
|
43
|
+
.array(
|
|
44
|
+
z.object({
|
|
45
|
+
distance: z
|
|
46
|
+
.number()
|
|
47
|
+
.describe(
|
|
48
|
+
'Camera distance in world units at/beyond which this level shows (THREE.LOD ' +
|
|
49
|
+
'threshold). Use 0 for the closest/highest-detail level.',
|
|
50
|
+
),
|
|
51
|
+
node: z
|
|
52
|
+
.string()
|
|
53
|
+
.describe(
|
|
54
|
+
'Name of a node inside the referenced glTF (mesh.src) to show at this distance — ' +
|
|
55
|
+
'resolved via getObjectByName, geometry shared (F4-style). A skinned node throws ' +
|
|
56
|
+
'(atomic-subtree rule).',
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
.optional()
|
|
61
|
+
.describe(
|
|
62
|
+
'Per-entity Level-of-Detail: distance-keyed glTF sub-nodes swapped by camera distance ' +
|
|
63
|
+
'(THREE.LOD). gltf-only. Gated by the environment.rendering.lod master toggle — when ' +
|
|
64
|
+
'off, only the closest level renders. Cannot combine with node/instances.',
|
|
65
|
+
),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
82
68
|
|
|
83
|
-
export const SceneMeshSchema = SceneMeshBase.refine(
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
export const SceneMeshSchema = SceneMeshBase.refine(
|
|
70
|
+
(m) => (m.type !== 'gltf' && m.type !== 'splat') || m.src != null,
|
|
71
|
+
{
|
|
72
|
+
message: "External render asset requires 'src' field",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
86
75
|
.refine((m) => m.node == null || m.type === 'gltf', {
|
|
87
76
|
message:
|
|
88
77
|
"Mesh 'node' is only meaningful with type: 'gltf' (names a sub-node of the referenced glTF)",
|
|
89
78
|
})
|
|
90
|
-
.refine((m) => !(m.instances && m.instancer), {
|
|
91
|
-
message:
|
|
92
|
-
"Mesh cannot set both 'instances' (authored) and 'instancer' (code-registered) — " +
|
|
93
|
-
'pick one canonical instancing source per mesh',
|
|
94
|
-
})
|
|
95
79
|
.refine((m) => m.lod == null || m.type === 'gltf', {
|
|
96
80
|
message: "Mesh 'lod' is only meaningful with type: 'gltf'",
|
|
97
81
|
})
|
|
98
|
-
.refine((m) =>
|
|
82
|
+
.refine((m) => m.instances == null || m.type !== 'splat', {
|
|
83
|
+
message: "Mesh 'instances' is not supported with type: 'splat'",
|
|
84
|
+
})
|
|
85
|
+
.refine((m) => !(m.lod && (m.node || m.instances)), {
|
|
99
86
|
message:
|
|
100
|
-
"Mesh 'lod' cannot combine with 'node'/'instances'
|
|
101
|
-
'form of a node ref',
|
|
87
|
+
"Mesh 'lod' cannot combine with 'node'/'instances' — LOD is the multi-level form of a node ref",
|
|
102
88
|
})
|
|
103
89
|
.describe('Mesh geometry definition');
|
|
104
90
|
|
|
@@ -105,8 +105,7 @@ function validateMergedEntity(merged: SceneEntity, instance: SceneEntity): Scene
|
|
|
105
105
|
* is now included — an instance's per-entity render overrides are honored
|
|
106
106
|
* and merged the same way as `mesh`/`material`.)
|
|
107
107
|
*
|
|
108
|
-
* NOTE (T1.10 audit): `
|
|
109
|
-
* `animation.parameters`/`animation.clipAliases` are keyed dict maps nested
|
|
108
|
+
* NOTE (T1.10 audit): `animation.parameters`/`animation.clipAliases` are keyed dict maps nested
|
|
110
109
|
* one level down, and `particles` nests discriminated-union shapes/behaviors
|
|
111
110
|
* arrays. These have the same "shallow replace" defect as `material`/
|
|
112
111
|
* `physics` did, but no single well-defined field-merge semantics (dict keys
|