@vgai/engine 0.2.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/LICENSE +202 -0
- package/README.md +35 -0
- package/package.json +55 -0
- package/src/adapter/authoring.ts +402 -0
- package/src/adapter/colyseus-networking-adapter.ts +72 -0
- package/src/adapter/first-party-systems.ts +103 -0
- package/src/adapter/game-adapter.ts +151 -0
- package/src/adapter/host-context.ts +77 -0
- package/src/adapter/index.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +59 -0
- package/src/adapter/ingest/overlay-applier.ts +207 -0
- package/src/adapter/ingest/overlay-apply.ts +124 -0
- package/src/adapter/ingest/overlay-file.ts +126 -0
- package/src/adapter/ingest/overlay-report.ts +176 -0
- package/src/adapter/ingest/scene-capture.ts +307 -0
- package/src/adapter/ingest/upstream-pin.ts +52 -0
- package/src/adapter/loop-gate-report.ts +54 -0
- package/src/adapter/rapier-physics-adapter.ts +56 -0
- package/src/adapter/system-adapter.ts +154 -0
- package/src/adapter/transform.ts +18 -0
- package/src/adapter/vgai-scene-game-adapter.ts +886 -0
- package/src/adapter/world-kind.ts +34 -0
- package/src/ai/navigation.ts +164 -0
- package/src/animation/anim-graph-types.ts +56 -0
- package/src/animation/anim-graph.ts +406 -0
- package/src/animation/anim-system.ts +28 -0
- package/src/animation/blend-node.ts +119 -0
- package/src/animation/property-track.ts +178 -0
- package/src/animation/schema.ts +204 -0
- package/src/assets.ts +80 -0
- package/src/audio/ambient.ts +300 -0
- package/src/audio/impacts.ts +212 -0
- package/src/audio/index.ts +7 -0
- package/src/audio/movement.ts +140 -0
- package/src/audio/musical.ts +200 -0
- package/src/audio/ui-sounds.ts +171 -0
- package/src/audio/vehicle.ts +235 -0
- package/src/audio/weapons.ts +152 -0
- package/src/core/game-loop.ts +127 -0
- package/src/core/system-runner.ts +298 -0
- package/src/core/types.ts +58 -0
- package/src/dev/console-bridge.ts +83 -0
- package/src/dev/debug-draw.ts +80 -0
- package/src/dev/logger.ts +119 -0
- package/src/ecs/component-manager.ts +748 -0
- package/src/ecs/game-component.ts +147 -0
- package/src/ecs/hmr-swap-report.ts +65 -0
- package/src/input/input-manager.ts +439 -0
- package/src/input/input-types.ts +19 -0
- package/src/input/schema.ts +129 -0
- package/src/loader.ts +70 -0
- package/src/manifest/index.ts +24 -0
- package/src/manifest/load-file.ts +16 -0
- package/src/manifest/load.ts +378 -0
- package/src/manifest/schema.ts +375 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/physics/trigger-dispatch.ts +97 -0
- package/src/react/game-state.tsx +172 -0
- package/src/render/auto-batcher.ts +169 -0
- package/src/render/render-batch-system.ts +268 -0
- package/src/render/render-features.ts +146 -0
- package/src/render/render-settings.ts +72 -0
- package/src/runtime/create-runtime.ts +1152 -0
- package/src/runtime/frame-selector-cache.ts +81 -0
- package/src/runtime/game.ts +1003 -0
- package/src/runtime/input-router.ts +213 -0
- package/src/runtime/mount-game.ts +269 -0
- package/src/runtime/mount-manifest.ts +361 -0
- package/src/runtime/scene-ui-bridge.ts +86 -0
- package/src/runtime/scene-ui-data.ts +119 -0
- package/src/runtime/state-bridge.ts +79 -0
- package/src/runtime/types.ts +196 -0
- package/src/scene/asset-loaders.ts +195 -0
- package/src/scene/asset-paths.ts +123 -0
- package/src/scene/asset-registry.ts +67 -0
- package/src/scene/collider-dimensions.ts +125 -0
- package/src/scene/component-registry.ts +40 -0
- package/src/scene/defaults.ts +164 -0
- package/src/scene/geometries/index.ts +7 -0
- package/src/scene/geometries/terrain.ts +42 -0
- package/src/scene/geometry-registry.ts +42 -0
- package/src/scene/instance-registry.ts +84 -0
- package/src/scene/instancers/grid.ts +38 -0
- package/src/scene/instancers/index.ts +7 -0
- package/src/scene/light-camera-factory.ts +97 -0
- package/src/scene/material-factory.ts +211 -0
- package/src/scene/material-registry.ts +73 -0
- package/src/scene/materials/index.ts +7 -0
- package/src/scene/materials/water.ts +56 -0
- package/src/scene/parse.ts +71 -0
- package/src/scene/particles-factory.ts +383 -0
- package/src/scene/scene-apply.ts +356 -0
- package/src/scene/scene-diff-schema.ts +115 -0
- package/src/scene/scene-diff-types.ts +29 -0
- package/src/scene/scene-loader.ts +1533 -0
- package/src/scene/scene-query.ts +63 -0
- package/src/scene/scene-types.ts +34 -0
- package/src/scene/scene-version.ts +40 -0
- package/src/scene/schema/animation.ts +95 -0
- package/src/scene/schema/audio.ts +25 -0
- package/src/scene/schema/camera.ts +21 -0
- package/src/scene/schema/collider.ts +69 -0
- package/src/scene/schema/entity-ref.ts +78 -0
- package/src/scene/schema/entity.ts +169 -0
- package/src/scene/schema/environment.ts +384 -0
- package/src/scene/schema/index.ts +95 -0
- package/src/scene/schema/instances.ts +35 -0
- package/src/scene/schema/joint.ts +26 -0
- package/src/scene/schema/light.ts +38 -0
- package/src/scene/schema/material.ts +113 -0
- package/src/scene/schema/mesh.ts +108 -0
- package/src/scene/schema/particles.ts +398 -0
- package/src/scene/schema/physics.ts +49 -0
- package/src/scene/schema/scene-file.ts +299 -0
- package/src/scene/schema/shadow.ts +24 -0
- package/src/scene/schema/spline.ts +21 -0
- package/src/scene/schema/tuples.ts +21 -0
- package/src/scene/schema/ui.ts +602 -0
- package/src/scene/user-data.ts +203 -0
- package/src/setup/setup-audio.ts +60 -0
- package/src/setup/setup-particles.ts +23 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +529 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/src/world2d/authoring-2d.ts +208 -0
- package/src/world2d/capture-to-scene2d.ts +52 -0
- package/src/world2d/collision-2d.ts +106 -0
- package/src/world2d/components-2d.ts +86 -0
- package/src/world2d/index.ts +66 -0
- package/src/world2d/ingest-iframe-2d.ts +255 -0
- package/src/world2d/ingest2d.ts +131 -0
- package/src/world2d/physics2d-registry.ts +49 -0
- package/src/world2d/pixi-game-adapter.ts +325 -0
- package/src/world2d/pixi-surface.ts +78 -0
- package/src/world2d/scene-capture-2d.ts +117 -0
- package/src/world2d/scene2d-loader.ts +308 -0
- package/src/world2d/schema/entity2d.ts +145 -0
- package/src/world2d/schema/physics2d.ts +53 -0
- package/src/world2d/schema/sprite.ts +71 -0
- package/src/world2d/schema/tilemap.ts +22 -0
- package/src/world2d/schema/tuples2d.ts +25 -0
- package/src/world2d/system-adapters-2d.ts +49 -0
- package/src/world2d/transform-writer-2d.ts +24 -0
- package/src/world2d/types.ts +55 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { loadTexture } from './asset-loaders';
|
|
3
|
+
import { DEFAULTS } from './defaults';
|
|
4
|
+
import { buildGeneratedGeometry } from './geometry-registry';
|
|
5
|
+
import { buildCustomMaterial } from './material-registry';
|
|
6
|
+
import type { SceneMaterial, SceneMesh } from './scene-types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single source of truth for turning a `SceneMesh` / `SceneMaterial` descriptor
|
|
10
|
+
* into Three.js geometry/material. Both the runtime scene-loader and the editor's
|
|
11
|
+
* entity-factory import these, so the editor preview renders exactly what the game
|
|
12
|
+
* renders (the WYSIWYG guarantee in ARCHITECTURE.md). Previously these were
|
|
13
|
+
* copy-pasted in two places and had to be hand-synced — any new material feature
|
|
14
|
+
* added to one and not the other silently broke that guarantee.
|
|
15
|
+
*
|
|
16
|
+
* Callers must have imported the side-effect registration modules
|
|
17
|
+
* (`./geometries`, `./materials`, plus any custom defs) so `buildGeneratedGeometry`
|
|
18
|
+
* and `buildCustomMaterial` resolve their registries.
|
|
19
|
+
*/
|
|
20
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward type-based switch with fallback args
|
|
21
|
+
export function createGeometry(mesh: SceneMesh): THREE.BufferGeometry {
|
|
22
|
+
if (mesh.generator) {
|
|
23
|
+
return buildGeneratedGeometry(mesh.generator, mesh.generatorParams ?? {});
|
|
24
|
+
}
|
|
25
|
+
const args = mesh.args ?? [];
|
|
26
|
+
switch (mesh.type) {
|
|
27
|
+
case 'box':
|
|
28
|
+
return new THREE.BoxGeometry(args[0] ?? 1, args[1] ?? 1, args[2] ?? 1);
|
|
29
|
+
case 'sphere':
|
|
30
|
+
return new THREE.SphereGeometry(args[0] ?? 0.5, args[1] ?? 32, args[2] ?? 16);
|
|
31
|
+
case 'plane':
|
|
32
|
+
return new THREE.PlaneGeometry(args[0] ?? 1, args[1] ?? 1);
|
|
33
|
+
case 'cylinder':
|
|
34
|
+
return new THREE.CylinderGeometry(
|
|
35
|
+
args[0] ?? 0.5,
|
|
36
|
+
args[1] ?? 0.5,
|
|
37
|
+
args[2] ?? 1,
|
|
38
|
+
args[3] ?? 32,
|
|
39
|
+
);
|
|
40
|
+
case 'capsule':
|
|
41
|
+
return new THREE.CapsuleGeometry(args[0] ?? 0.5, args[1] ?? 1, args[2] ?? 4, args[3] ?? 16);
|
|
42
|
+
default:
|
|
43
|
+
return new THREE.BoxGeometry(1, 1, 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: flat type-based switch building Three materials
|
|
48
|
+
export function createMaterial(mat?: SceneMaterial): THREE.Material {
|
|
49
|
+
if (!mat) return new THREE.MeshStandardMaterial({ color: DEFAULTS.material.color });
|
|
50
|
+
|
|
51
|
+
// Only include texture props when actually defined (Three.js warns on undefined values)
|
|
52
|
+
const textures: {
|
|
53
|
+
map?: THREE.Texture;
|
|
54
|
+
normalMap?: THREE.Texture;
|
|
55
|
+
emissiveMap?: THREE.Texture;
|
|
56
|
+
aoMap?: THREE.Texture;
|
|
57
|
+
lightMap?: THREE.Texture;
|
|
58
|
+
roughnessMap?: THREE.Texture;
|
|
59
|
+
metalnessMap?: THREE.Texture;
|
|
60
|
+
} = {};
|
|
61
|
+
if (mat.map) textures.map = loadTexture(mat.map);
|
|
62
|
+
if (mat.normalMap) textures.normalMap = loadTexture(mat.normalMap);
|
|
63
|
+
if (mat.emissiveMap) textures.emissiveMap = loadTexture(mat.emissiveMap);
|
|
64
|
+
if (mat.aoMap) textures.aoMap = loadTexture(mat.aoMap);
|
|
65
|
+
if (mat.lightMap) textures.lightMap = loadTexture(mat.lightMap);
|
|
66
|
+
if (mat.roughnessMap) textures.roughnessMap = loadTexture(mat.roughnessMap);
|
|
67
|
+
if (mat.metalnessMap) textures.metalnessMap = loadTexture(mat.metalnessMap);
|
|
68
|
+
|
|
69
|
+
// R1c — every fallback below reads DEFAULTS.material.* instead of
|
|
70
|
+
// duplicating its value as a bare literal, so DEFAULTS stays the single
|
|
71
|
+
// source of truth for what an omitted field renders as (see
|
|
72
|
+
// defaults-out-of-scope-consistency.test.ts / material-factory.test.ts's
|
|
73
|
+
// "omitted-field" locks, which fail if either copy drifts from the other).
|
|
74
|
+
const dMat = DEFAULTS.material;
|
|
75
|
+
const sideValue = mat.side ?? dMat.side;
|
|
76
|
+
const side =
|
|
77
|
+
sideValue === 'back'
|
|
78
|
+
? THREE.BackSide
|
|
79
|
+
: sideValue === 'double'
|
|
80
|
+
? THREE.DoubleSide
|
|
81
|
+
: THREE.FrontSide;
|
|
82
|
+
const opacity = mat.opacity ?? dMat.opacity;
|
|
83
|
+
const transparent = mat.transparent ?? dMat.transparent;
|
|
84
|
+
|
|
85
|
+
switch (mat.type) {
|
|
86
|
+
case 'basic':
|
|
87
|
+
return new THREE.MeshBasicMaterial({
|
|
88
|
+
color: mat.color ?? dMat.color,
|
|
89
|
+
...textures,
|
|
90
|
+
lightMapIntensity: mat.lightMapIntensity ?? 1,
|
|
91
|
+
opacity,
|
|
92
|
+
transparent,
|
|
93
|
+
side,
|
|
94
|
+
});
|
|
95
|
+
case 'toon':
|
|
96
|
+
return new THREE.MeshToonMaterial({
|
|
97
|
+
color: mat.color ?? dMat.color,
|
|
98
|
+
...textures,
|
|
99
|
+
lightMapIntensity: mat.lightMapIntensity ?? 1,
|
|
100
|
+
emissive: mat.emissive ?? dMat.emissive,
|
|
101
|
+
emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
|
|
102
|
+
opacity,
|
|
103
|
+
transparent,
|
|
104
|
+
side,
|
|
105
|
+
});
|
|
106
|
+
case 'physical':
|
|
107
|
+
return new THREE.MeshPhysicalMaterial({
|
|
108
|
+
color: mat.color ?? dMat.color,
|
|
109
|
+
metalness: mat.metalness ?? dMat.metalness,
|
|
110
|
+
roughness: mat.roughness ?? dMat.roughness,
|
|
111
|
+
...textures,
|
|
112
|
+
lightMapIntensity: mat.lightMapIntensity ?? 1,
|
|
113
|
+
emissive: mat.emissive ?? dMat.emissive,
|
|
114
|
+
emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
|
|
115
|
+
opacity,
|
|
116
|
+
transparent,
|
|
117
|
+
side,
|
|
118
|
+
flatShading: mat.flatShading ?? dMat.flatShading,
|
|
119
|
+
// Clearcoat
|
|
120
|
+
...(mat.clearcoat
|
|
121
|
+
? {
|
|
122
|
+
clearcoat: mat.clearcoat.clearcoat,
|
|
123
|
+
clearcoatRoughness:
|
|
124
|
+
mat.clearcoat.clearcoatRoughness ?? dMat.clearcoat.clearcoatRoughness,
|
|
125
|
+
...(mat.clearcoat.clearcoatMap
|
|
126
|
+
? { clearcoatMap: loadTexture(mat.clearcoat.clearcoatMap) }
|
|
127
|
+
: {}),
|
|
128
|
+
...(mat.clearcoat.clearcoatRoughnessMap
|
|
129
|
+
? { clearcoatRoughnessMap: loadTexture(mat.clearcoat.clearcoatRoughnessMap) }
|
|
130
|
+
: {}),
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
// Transmission — `thickness` used to hardcode a literal `0` here,
|
|
134
|
+
// silently diverging from DEFAULTS.material.transmission.thickness
|
|
135
|
+
// (0.5, per the editor's Transmission inspector default). Fixed:
|
|
136
|
+
// read DEFAULTS directly, like every other fallback in this file.
|
|
137
|
+
...(mat.transmission
|
|
138
|
+
? {
|
|
139
|
+
transmission: mat.transmission.transmission,
|
|
140
|
+
ior: mat.transmission.ior ?? dMat.transmission.ior,
|
|
141
|
+
thickness: mat.transmission.thickness ?? dMat.transmission.thickness,
|
|
142
|
+
...(mat.transmission.attenuationColor
|
|
143
|
+
? { attenuationColor: new THREE.Color(mat.transmission.attenuationColor) }
|
|
144
|
+
: {}),
|
|
145
|
+
attenuationDistance: mat.transmission.attenuationDistance ?? Infinity,
|
|
146
|
+
...(mat.transmission.transmissionMap
|
|
147
|
+
? { transmissionMap: loadTexture(mat.transmission.transmissionMap) }
|
|
148
|
+
: {}),
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
151
|
+
// Sheen
|
|
152
|
+
...(mat.sheen
|
|
153
|
+
? {
|
|
154
|
+
sheen: mat.sheen.sheen,
|
|
155
|
+
sheenColor: new THREE.Color(mat.sheen.sheenColor ?? dMat.sheen.sheenColor),
|
|
156
|
+
sheenRoughness: mat.sheen.sheenRoughness ?? dMat.sheen.sheenRoughness,
|
|
157
|
+
...(mat.sheen.sheenColorMap
|
|
158
|
+
? { sheenColorMap: loadTexture(mat.sheen.sheenColorMap) }
|
|
159
|
+
: {}),
|
|
160
|
+
...(mat.sheen.sheenRoughnessMap
|
|
161
|
+
? { sheenRoughnessMap: loadTexture(mat.sheen.sheenRoughnessMap) }
|
|
162
|
+
: {}),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
// Iridescence
|
|
166
|
+
...(mat.iridescence
|
|
167
|
+
? {
|
|
168
|
+
iridescence: mat.iridescence.iridescence,
|
|
169
|
+
iridescenceIOR: mat.iridescence.iridescenceIOR ?? dMat.iridescence.iridescenceIOR,
|
|
170
|
+
iridescenceThicknessRange:
|
|
171
|
+
mat.iridescence.iridescenceThicknessRange ??
|
|
172
|
+
dMat.iridescence.iridescenceThicknessRange,
|
|
173
|
+
...(mat.iridescence.iridescenceMap
|
|
174
|
+
? { iridescenceMap: loadTexture(mat.iridescence.iridescenceMap) }
|
|
175
|
+
: {}),
|
|
176
|
+
...(mat.iridescence.iridescenceThicknessMap
|
|
177
|
+
? {
|
|
178
|
+
iridescenceThicknessMap: loadTexture(mat.iridescence.iridescenceThicknessMap),
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
// Displacement
|
|
184
|
+
...(mat.displacementMap ? { displacementMap: loadTexture(mat.displacementMap) } : {}),
|
|
185
|
+
...(mat.displacementScale !== undefined
|
|
186
|
+
? { displacementScale: mat.displacementScale }
|
|
187
|
+
: {}),
|
|
188
|
+
...(mat.displacementBias !== undefined ? { displacementBias: mat.displacementBias } : {}),
|
|
189
|
+
});
|
|
190
|
+
case 'custom': {
|
|
191
|
+
if (!mat.def) {
|
|
192
|
+
throw new Error('Custom material requires a "def" (registered material name)');
|
|
193
|
+
}
|
|
194
|
+
return buildCustomMaterial(mat.def, mat.uniforms ?? {});
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
return new THREE.MeshStandardMaterial({
|
|
198
|
+
color: mat.color ?? dMat.color,
|
|
199
|
+
metalness: mat.metalness ?? dMat.metalness,
|
|
200
|
+
roughness: mat.roughness ?? dMat.roughness,
|
|
201
|
+
...textures,
|
|
202
|
+
lightMapIntensity: mat.lightMapIntensity ?? 1,
|
|
203
|
+
emissive: mat.emissive ?? dMat.emissive,
|
|
204
|
+
emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
|
|
205
|
+
opacity,
|
|
206
|
+
transparent,
|
|
207
|
+
side,
|
|
208
|
+
flatShading: mat.flatShading ?? dMat.flatShading,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A custom-material definition. Registered by name and resolved when a scene
|
|
5
|
+
* material declares `{ type: 'custom', def: <name> }`.
|
|
6
|
+
*
|
|
7
|
+
* The GLSL program lives here in code (the "behavior" half); the per-entity
|
|
8
|
+
* authored uniform values arrive as data on the scene material's `uniforms`
|
|
9
|
+
* (the "data" half) and are passed to `build`. A def may optionally tick its
|
|
10
|
+
* own uniforms each frame (e.g. uTime) via `update`.
|
|
11
|
+
*
|
|
12
|
+
* This is the native extension point of the material system — not a wrapper:
|
|
13
|
+
* `build` returns a real THREE.Material (typically a ShaderMaterial, or a stock
|
|
14
|
+
* material patched via onBeforeCompile), and the engine renders it directly.
|
|
15
|
+
*/
|
|
16
|
+
export interface MaterialDef {
|
|
17
|
+
/** Build a real THREE material from authored uniform values. */
|
|
18
|
+
build(uniforms: Record<string, unknown>): THREE.Material;
|
|
19
|
+
/** Optional per-frame uniform update for self-animating materials. */
|
|
20
|
+
update?(dt: number, material: THREE.Material, uniforms: Record<string, unknown>): void;
|
|
21
|
+
/** Representative flat color (CSS hex) for the editor's "unlit" view mode. */
|
|
22
|
+
previewColor?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const registry = new Map<string, MaterialDef>();
|
|
26
|
+
|
|
27
|
+
/** Register a custom-material definition under `name`. */
|
|
28
|
+
export function registerMaterial(name: string, def: MaterialDef): void {
|
|
29
|
+
registry.set(name, def);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Look up a registered material definition. */
|
|
33
|
+
export function getMaterialDef(name: string): MaterialDef | undefined {
|
|
34
|
+
return registry.get(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a custom material by registered def name. Tags the resulting material's
|
|
39
|
+
* `userData` so the per-frame tick and the editor view-mode override can find it.
|
|
40
|
+
* Throws if the def is unknown (fail loud — same posture as the component registry).
|
|
41
|
+
*/
|
|
42
|
+
export function buildCustomMaterial(
|
|
43
|
+
name: string,
|
|
44
|
+
uniforms: Record<string, unknown>,
|
|
45
|
+
): THREE.Material {
|
|
46
|
+
const def = registry.get(name);
|
|
47
|
+
if (!def) {
|
|
48
|
+
throw new Error(`Custom material def "${name}" not found in material registry`);
|
|
49
|
+
}
|
|
50
|
+
const material = def.build(uniforms ?? {});
|
|
51
|
+
material.userData['__matDef'] = name;
|
|
52
|
+
material.userData['__uniforms'] = uniforms ?? {};
|
|
53
|
+
return material;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tick every custom material under `root` (self-animating uniforms). Called from
|
|
58
|
+
* the runtime `preRender` phase and from the editor's edit-mode RAF — one code
|
|
59
|
+
* path, both modes. Walks the scene graph; materials without a `__matDef` tag are
|
|
60
|
+
* skipped.
|
|
61
|
+
*/
|
|
62
|
+
export function tickCustomMaterials(root: THREE.Object3D, dt: number): void {
|
|
63
|
+
root.traverse((obj) => {
|
|
64
|
+
const material = (obj as THREE.Mesh).material;
|
|
65
|
+
if (!material) return;
|
|
66
|
+
const list = Array.isArray(material) ? material : [material];
|
|
67
|
+
for (const mat of list) {
|
|
68
|
+
const defName = mat.userData?.['__matDef'] as string | undefined;
|
|
69
|
+
if (!defName) continue;
|
|
70
|
+
registry.get(defName)?.update?.(dt, mat, mat.userData['__uniforms'] ?? {});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in custom-material definitions. Importing this module for its side
|
|
3
|
+
* effects registers the built-ins into the material registry. Both the runtime
|
|
4
|
+
* factory (scene-loader) and the editor factory (entity-factory) import it so
|
|
5
|
+
* custom materials resolve on whichever side builds first.
|
|
6
|
+
*/
|
|
7
|
+
import './water';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { registerMaterial } from '../material-registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in "Water" custom material — the proof-of-concept for first-class custom
|
|
6
|
+
* shaders (roadmap item #1). Vertex shader displaces a plane with travelling sine
|
|
7
|
+
* waves driven by `uTime`; fragment tints by `uColor` with a little wave-crest
|
|
8
|
+
* lightening. Demonstrates: authored uniforms (color, waveHeight), a real
|
|
9
|
+
* ShaderMaterial built from scene JSON, and a self-animating uniform (uTime).
|
|
10
|
+
*/
|
|
11
|
+
const vertexShader = /* glsl */ `
|
|
12
|
+
uniform float uTime;
|
|
13
|
+
uniform float uWaveHeight;
|
|
14
|
+
varying float vWave;
|
|
15
|
+
void main() {
|
|
16
|
+
vec3 p = position;
|
|
17
|
+
// PlaneGeometry lies in local XY with normal +Z; displace along the normal so
|
|
18
|
+
// a plane rotated flat (XZ) ripples upward in world Y.
|
|
19
|
+
float wave =
|
|
20
|
+
sin(p.x * 1.5 + uTime) * 0.6 +
|
|
21
|
+
cos(p.y * 1.2 + uTime * 0.8) * 0.4;
|
|
22
|
+
p.z += wave * uWaveHeight;
|
|
23
|
+
vWave = wave;
|
|
24
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const fragmentShader = /* glsl */ `
|
|
29
|
+
uniform vec3 uColor;
|
|
30
|
+
varying float vWave;
|
|
31
|
+
void main() {
|
|
32
|
+
vec3 c = uColor + vWave * 0.12;
|
|
33
|
+
gl_FragColor = vec4(c, 1.0);
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
registerMaterial('Water', {
|
|
38
|
+
previewColor: '#0a5fa5',
|
|
39
|
+
build(uniforms) {
|
|
40
|
+
const color = (uniforms['color'] as string) ?? '#0a5fa5';
|
|
41
|
+
const waveHeight = (uniforms['waveHeight'] as number) ?? 0.4;
|
|
42
|
+
return new THREE.ShaderMaterial({
|
|
43
|
+
uniforms: {
|
|
44
|
+
uTime: { value: 0 },
|
|
45
|
+
uColor: { value: new THREE.Color(color) },
|
|
46
|
+
uWaveHeight: { value: waveHeight },
|
|
47
|
+
},
|
|
48
|
+
vertexShader,
|
|
49
|
+
fragmentShader,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
update(dt, material) {
|
|
53
|
+
const uTime = (material as THREE.ShaderMaterial).uniforms['uTime'];
|
|
54
|
+
if (uTime) uTime.value += dt;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ZodIssue } from 'zod';
|
|
2
|
+
import { checkSceneVersionInRange } from './scene-version';
|
|
3
|
+
import type { InstancesFile, MaterialFile, PrefabFile, SceneFile } from './schema';
|
|
4
|
+
import {
|
|
5
|
+
InstancesFileSchema,
|
|
6
|
+
MaterialFileSchema,
|
|
7
|
+
PrefabFileSchema,
|
|
8
|
+
SceneFileSchema,
|
|
9
|
+
} from './schema';
|
|
10
|
+
|
|
11
|
+
export class SceneParseError extends Error {
|
|
12
|
+
readonly issues: ZodIssue[];
|
|
13
|
+
/** The offending file's path/URL, when the caller knows it (T4.6). */
|
|
14
|
+
readonly filePath?: string | undefined;
|
|
15
|
+
|
|
16
|
+
constructor(issues: ZodIssue[], filePath?: string) {
|
|
17
|
+
const msg = issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
18
|
+
const header = filePath ? `Scene validation failed (${filePath}):` : 'Scene validation failed:';
|
|
19
|
+
super(`${header}\n${msg}`);
|
|
20
|
+
this.name = 'SceneParseError';
|
|
21
|
+
this.issues = issues;
|
|
22
|
+
this.filePath = filePath;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseSceneFile(json: unknown, filePath?: string): SceneFile {
|
|
27
|
+
const result = SceneFileSchema.safeParse(json);
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
throw new SceneParseError(result.error.issues, filePath);
|
|
30
|
+
}
|
|
31
|
+
// Post-Zod-parse range check (T2.3/D3 §1.E) — the Zod shape stays a bare
|
|
32
|
+
// `z.number()`; this is where the file-format `version` axis is actually
|
|
33
|
+
// read/enforced.
|
|
34
|
+
checkSceneVersionInRange(result.data.version, 'scene');
|
|
35
|
+
return result.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parsePrefabFile(json: unknown, filePath?: string): PrefabFile {
|
|
39
|
+
const result = PrefabFileSchema.safeParse(json);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
throw new SceneParseError(result.error.issues, filePath);
|
|
42
|
+
}
|
|
43
|
+
checkSceneVersionInRange(result.data.version, 'prefab');
|
|
44
|
+
return result.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a `.mat.json` shared material asset (T4.6). Unlike scene/prefab files,
|
|
49
|
+
* there is no version-range gate yet for this format — `MaterialFileSchema`'s
|
|
50
|
+
* `version` field is RESERVED (see `schema-consumption-map.ts`).
|
|
51
|
+
*/
|
|
52
|
+
export function parseMaterialFile(json: unknown, filePath?: string): MaterialFile {
|
|
53
|
+
const result = MaterialFileSchema.safeParse(json);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
throw new SceneParseError(result.error.issues, filePath);
|
|
56
|
+
}
|
|
57
|
+
return result.data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a `.instances.json` instance-transform asset (F3,
|
|
62
|
+
* docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Mirrors `parseMaterialFile` exactly —
|
|
63
|
+
* no version-range gate (the format is a bare tuple array, no `version` field).
|
|
64
|
+
*/
|
|
65
|
+
export function parseInstancesFile(json: unknown, filePath?: string): InstancesFile {
|
|
66
|
+
const result = InstancesFileSchema.safeParse(json);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
throw new SceneParseError(result.error.issues, filePath);
|
|
69
|
+
}
|
|
70
|
+
return result.data;
|
|
71
|
+
}
|