@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.
Files changed (147) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/package.json +55 -0
  4. package/src/adapter/authoring.ts +402 -0
  5. package/src/adapter/colyseus-networking-adapter.ts +72 -0
  6. package/src/adapter/first-party-systems.ts +103 -0
  7. package/src/adapter/game-adapter.ts +151 -0
  8. package/src/adapter/host-context.ts +77 -0
  9. package/src/adapter/index.ts +85 -0
  10. package/src/adapter/ingest/game-contract.ts +59 -0
  11. package/src/adapter/ingest/overlay-applier.ts +207 -0
  12. package/src/adapter/ingest/overlay-apply.ts +124 -0
  13. package/src/adapter/ingest/overlay-file.ts +126 -0
  14. package/src/adapter/ingest/overlay-report.ts +176 -0
  15. package/src/adapter/ingest/scene-capture.ts +307 -0
  16. package/src/adapter/ingest/upstream-pin.ts +52 -0
  17. package/src/adapter/loop-gate-report.ts +54 -0
  18. package/src/adapter/rapier-physics-adapter.ts +56 -0
  19. package/src/adapter/system-adapter.ts +154 -0
  20. package/src/adapter/transform.ts +18 -0
  21. package/src/adapter/vgai-scene-game-adapter.ts +886 -0
  22. package/src/adapter/world-kind.ts +34 -0
  23. package/src/ai/navigation.ts +164 -0
  24. package/src/animation/anim-graph-types.ts +56 -0
  25. package/src/animation/anim-graph.ts +406 -0
  26. package/src/animation/anim-system.ts +28 -0
  27. package/src/animation/blend-node.ts +119 -0
  28. package/src/animation/property-track.ts +178 -0
  29. package/src/animation/schema.ts +204 -0
  30. package/src/assets.ts +80 -0
  31. package/src/audio/ambient.ts +300 -0
  32. package/src/audio/impacts.ts +212 -0
  33. package/src/audio/index.ts +7 -0
  34. package/src/audio/movement.ts +140 -0
  35. package/src/audio/musical.ts +200 -0
  36. package/src/audio/ui-sounds.ts +171 -0
  37. package/src/audio/vehicle.ts +235 -0
  38. package/src/audio/weapons.ts +152 -0
  39. package/src/core/game-loop.ts +127 -0
  40. package/src/core/system-runner.ts +298 -0
  41. package/src/core/types.ts +58 -0
  42. package/src/dev/console-bridge.ts +83 -0
  43. package/src/dev/debug-draw.ts +80 -0
  44. package/src/dev/logger.ts +119 -0
  45. package/src/ecs/component-manager.ts +748 -0
  46. package/src/ecs/game-component.ts +147 -0
  47. package/src/ecs/hmr-swap-report.ts +65 -0
  48. package/src/input/input-manager.ts +439 -0
  49. package/src/input/input-types.ts +19 -0
  50. package/src/input/schema.ts +129 -0
  51. package/src/loader.ts +70 -0
  52. package/src/manifest/index.ts +24 -0
  53. package/src/manifest/load-file.ts +16 -0
  54. package/src/manifest/load.ts +378 -0
  55. package/src/manifest/schema.ts +375 -0
  56. package/src/physics/collision-system.ts +76 -0
  57. package/src/physics/physics-registry.ts +83 -0
  58. package/src/physics/transform-writer.ts +41 -0
  59. package/src/physics/trigger-dispatch.ts +97 -0
  60. package/src/react/game-state.tsx +172 -0
  61. package/src/render/auto-batcher.ts +169 -0
  62. package/src/render/render-batch-system.ts +268 -0
  63. package/src/render/render-features.ts +146 -0
  64. package/src/render/render-settings.ts +72 -0
  65. package/src/runtime/create-runtime.ts +1152 -0
  66. package/src/runtime/frame-selector-cache.ts +81 -0
  67. package/src/runtime/game.ts +1003 -0
  68. package/src/runtime/input-router.ts +213 -0
  69. package/src/runtime/mount-game.ts +269 -0
  70. package/src/runtime/mount-manifest.ts +361 -0
  71. package/src/runtime/scene-ui-bridge.ts +86 -0
  72. package/src/runtime/scene-ui-data.ts +119 -0
  73. package/src/runtime/state-bridge.ts +79 -0
  74. package/src/runtime/types.ts +196 -0
  75. package/src/scene/asset-loaders.ts +195 -0
  76. package/src/scene/asset-paths.ts +123 -0
  77. package/src/scene/asset-registry.ts +67 -0
  78. package/src/scene/collider-dimensions.ts +125 -0
  79. package/src/scene/component-registry.ts +40 -0
  80. package/src/scene/defaults.ts +164 -0
  81. package/src/scene/geometries/index.ts +7 -0
  82. package/src/scene/geometries/terrain.ts +42 -0
  83. package/src/scene/geometry-registry.ts +42 -0
  84. package/src/scene/instance-registry.ts +84 -0
  85. package/src/scene/instancers/grid.ts +38 -0
  86. package/src/scene/instancers/index.ts +7 -0
  87. package/src/scene/light-camera-factory.ts +97 -0
  88. package/src/scene/material-factory.ts +211 -0
  89. package/src/scene/material-registry.ts +73 -0
  90. package/src/scene/materials/index.ts +7 -0
  91. package/src/scene/materials/water.ts +56 -0
  92. package/src/scene/parse.ts +71 -0
  93. package/src/scene/particles-factory.ts +383 -0
  94. package/src/scene/scene-apply.ts +356 -0
  95. package/src/scene/scene-diff-schema.ts +115 -0
  96. package/src/scene/scene-diff-types.ts +29 -0
  97. package/src/scene/scene-loader.ts +1533 -0
  98. package/src/scene/scene-query.ts +63 -0
  99. package/src/scene/scene-types.ts +34 -0
  100. package/src/scene/scene-version.ts +40 -0
  101. package/src/scene/schema/animation.ts +95 -0
  102. package/src/scene/schema/audio.ts +25 -0
  103. package/src/scene/schema/camera.ts +21 -0
  104. package/src/scene/schema/collider.ts +69 -0
  105. package/src/scene/schema/entity-ref.ts +78 -0
  106. package/src/scene/schema/entity.ts +169 -0
  107. package/src/scene/schema/environment.ts +384 -0
  108. package/src/scene/schema/index.ts +95 -0
  109. package/src/scene/schema/instances.ts +35 -0
  110. package/src/scene/schema/joint.ts +26 -0
  111. package/src/scene/schema/light.ts +38 -0
  112. package/src/scene/schema/material.ts +113 -0
  113. package/src/scene/schema/mesh.ts +108 -0
  114. package/src/scene/schema/particles.ts +398 -0
  115. package/src/scene/schema/physics.ts +49 -0
  116. package/src/scene/schema/scene-file.ts +299 -0
  117. package/src/scene/schema/shadow.ts +24 -0
  118. package/src/scene/schema/spline.ts +21 -0
  119. package/src/scene/schema/tuples.ts +21 -0
  120. package/src/scene/schema/ui.ts +602 -0
  121. package/src/scene/user-data.ts +203 -0
  122. package/src/setup/setup-audio.ts +60 -0
  123. package/src/setup/setup-particles.ts +23 -0
  124. package/src/setup/setup-physics.ts +67 -0
  125. package/src/setup/setup-renderer.ts +529 -0
  126. package/src/types-n8ao.d.ts +37 -0
  127. package/src/types-realism-effects.d.ts +61 -0
  128. package/src/world2d/authoring-2d.ts +208 -0
  129. package/src/world2d/capture-to-scene2d.ts +52 -0
  130. package/src/world2d/collision-2d.ts +106 -0
  131. package/src/world2d/components-2d.ts +86 -0
  132. package/src/world2d/index.ts +66 -0
  133. package/src/world2d/ingest-iframe-2d.ts +255 -0
  134. package/src/world2d/ingest2d.ts +131 -0
  135. package/src/world2d/physics2d-registry.ts +49 -0
  136. package/src/world2d/pixi-game-adapter.ts +325 -0
  137. package/src/world2d/pixi-surface.ts +78 -0
  138. package/src/world2d/scene-capture-2d.ts +117 -0
  139. package/src/world2d/scene2d-loader.ts +308 -0
  140. package/src/world2d/schema/entity2d.ts +145 -0
  141. package/src/world2d/schema/physics2d.ts +53 -0
  142. package/src/world2d/schema/sprite.ts +71 -0
  143. package/src/world2d/schema/tilemap.ts +22 -0
  144. package/src/world2d/schema/tuples2d.ts +25 -0
  145. package/src/world2d/system-adapters-2d.ts +49 -0
  146. package/src/world2d/transform-writer-2d.ts +24 -0
  147. package/src/world2d/types.ts +55 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared world-scale collider dimension math (T1.2).
3
+ *
4
+ * Rapier colliders have no notion of a parent Object3D's `scale` — a body's
5
+ * shape is defined in absolute world units at creation time. Three.js meshes,
6
+ * by contrast, scale visually for free via the normal Object3D hierarchy. To
7
+ * keep the physical collider matching what's on screen, the LOADER must bake
8
+ * the entity's composed WORLD scale (self × every transformed ancestor — see
9
+ * T1.1's world-transform composition) into the collider's dimensions at
10
+ * creation time.
11
+ *
12
+ * This is the ONE place that does that math, so both the runtime loader
13
+ * (scene-loader.ts `createPhysicsBody`) and the editor's collider gizmo
14
+ * (gizmo-registry.ts) agree on exactly the same numbers — previously the
15
+ * editor gizmo only rendered the entity's own live Object3D scale (via normal
16
+ * Three.js parenting) while the Rapier body ignored scale entirely, so the
17
+ * two silently agreed by coincidence for uniform scale and disagreed for
18
+ * everything else.
19
+ *
20
+ * Round colliders (ball/capsule) can only represent a uniform radius — Rapier
21
+ * has no ellipsoid/elliptical-capsule shape. A non-uniform world scale on a
22
+ * round collider's round axes throws a loud error naming the entity, rather
23
+ * than silently simulating (or rendering) the wrong shape.
24
+ */
25
+
26
+ import { DEFAULTS } from './defaults';
27
+ import type { SceneCollider } from './scene-types';
28
+
29
+ /** Axes are considered "uniform enough" within this tolerance (float error only). */
30
+ const UNIFORM_SCALE_EPSILON = 1e-4;
31
+
32
+ export interface ColliderWorldDimensions {
33
+ /** cuboid only: per-axis half-extents, world scale applied. */
34
+ halfExtents?: [number, number, number];
35
+ /** ball/capsule only: radius, world scale applied (uniform axes required). */
36
+ radius?: number;
37
+ /** capsule only: half-height along the capsule's axis, world scale applied. */
38
+ halfHeight?: number;
39
+ }
40
+
41
+ /**
42
+ * Compute a collider's WORLD-space dimensions given the entity's composed
43
+ * world scale (self × ancestors, per-axis, in the entity's own local frame —
44
+ * X/Y/Z order matches `SceneEntity.transform.scale`).
45
+ *
46
+ * @param collider - the entity's authored (unscaled) collider definition.
47
+ * @param worldScale - composed world scale `[x, y, z]`.
48
+ * @param entityLabel - human-readable entity identifier, used only in thrown
49
+ * error messages (loud-failure naming, never silently wrong).
50
+ * @throws if a ball/capsule collider's round cross-section axes have a
51
+ * non-uniform world scale (beyond {@link UNIFORM_SCALE_EPSILON}).
52
+ */
53
+ export function computeColliderWorldDimensions(
54
+ collider: SceneCollider,
55
+ worldScale: readonly [number, number, number],
56
+ entityLabel: string,
57
+ ): ColliderWorldDimensions {
58
+ const [sx, sy, sz] = worldScale;
59
+
60
+ switch (collider.type) {
61
+ case 'cuboid': {
62
+ const he = collider.halfExtents ?? DEFAULTS.collider.cuboid.halfExtents;
63
+ return { halfExtents: [he[0]! * sx, he[1]! * sy, he[2]! * sz] };
64
+ }
65
+ case 'ball': {
66
+ if (
67
+ Math.abs(sx - sy) > UNIFORM_SCALE_EPSILON ||
68
+ Math.abs(sy - sz) > UNIFORM_SCALE_EPSILON ||
69
+ Math.abs(sx - sz) > UNIFORM_SCALE_EPSILON
70
+ ) {
71
+ throw new Error(
72
+ `Entity "${entityLabel}" has a "ball" collider under a non-uniform world scale ` +
73
+ `[${sx}, ${sy}, ${sz}]. Rapier ball colliders are always perfect spheres — a ` +
74
+ `non-uniform scale cannot be represented and would silently simulate the wrong ` +
75
+ `shape. Use a uniform scale on this entity (and every transformed ancestor), or ` +
76
+ `switch to a "cuboid" collider.`,
77
+ );
78
+ }
79
+ const radius = collider.radius ?? DEFAULTS.collider.ball.radius;
80
+ return { radius: radius * sx };
81
+ }
82
+ case 'capsule': {
83
+ // Rapier capsules run their half-height along the local Y axis, with a
84
+ // circular cross-section in the XZ plane — so only X/Z need to match.
85
+ if (Math.abs(sx - sz) > UNIFORM_SCALE_EPSILON) {
86
+ throw new Error(
87
+ `Entity "${entityLabel}" has a "capsule" collider whose X/Z world scale differs ` +
88
+ `(x=${sx}, z=${sz}). Rapier capsule colliders have a circular cross-section — a ` +
89
+ `non-uniform X/Z scale cannot be represented and would silently simulate the ` +
90
+ `wrong shape. Use a uniform X/Z scale on this entity (and every transformed ` +
91
+ `ancestor), or switch to a "cuboid" collider.`,
92
+ );
93
+ }
94
+ const radius = collider.radius ?? DEFAULTS.collider.capsule.radius;
95
+ const halfHeight = collider.halfHeight ?? DEFAULTS.collider.capsule.halfHeight;
96
+ return { radius: radius * sx, halfHeight: halfHeight * sy };
97
+ }
98
+ default:
99
+ return {};
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Compute a collider's WORLD-scaled translation offset (sibling to
105
+ * {@link computeColliderWorldDimensions}, same input contract).
106
+ *
107
+ * Rapier's `ColliderDesc.setTranslation` is a raw physics-engine offset with no
108
+ * notion of a parent Object3D's scale — like dimensions, an authored `offset`
109
+ * (in the entity's own local units) must be scaled per-axis by the entity's
110
+ * composed world scale before being handed to Rapier, or the collider's center
111
+ * silently drifts away from its visual anchor point under any non-1 scale.
112
+ * Applies to every collider type (including trimesh, which is otherwise not
113
+ * handled by {@link computeColliderWorldDimensions}).
114
+ *
115
+ * Returns `undefined` when no offset is authored, mirroring the `if (col.offset)`
116
+ * guard call sites already use.
117
+ */
118
+ export function computeColliderWorldOffset(
119
+ offset: readonly [number, number, number] | undefined,
120
+ worldScale: readonly [number, number, number],
121
+ ): [number, number, number] | undefined {
122
+ if (!offset) return undefined;
123
+ const [sx, sy, sz] = worldScale;
124
+ return [offset[0] * sx, offset[1] * sy, offset[2] * sz];
125
+ }
@@ -0,0 +1,40 @@
1
+ import type * as THREE from 'three';
2
+ import type { ComponentManager } from '../ecs/component-manager';
3
+ import { GameComponent, type GameComponentClass } from '../ecs/game-component';
4
+
5
+ /** A registry mapping component names to GameComponent classes. */
6
+ export type ComponentRegistry = Record<string, GameComponentClass>;
7
+
8
+ /** Type guard: is this value a GameComponent class? */
9
+ export function isGameComponentClass(entry: unknown): entry is GameComponentClass {
10
+ return typeof entry === 'function' && entry.prototype instanceof GameComponent;
11
+ }
12
+
13
+ /**
14
+ * Apply components from scene JSON data onto an entity (an Object3D).
15
+ *
16
+ * For each named component: look up its class in the registry, validate +
17
+ * default the authored data through the optional `static schema` (Zod), assign
18
+ * onto a fresh instance, and attach it via the ComponentManager (which wires the
19
+ * Object3D + physics refs and queues init()).
20
+ */
21
+ export function applyComponents(
22
+ registry: ComponentRegistry,
23
+ object3D: THREE.Object3D,
24
+ components: Record<string, Record<string, unknown>>,
25
+ manager: ComponentManager,
26
+ ): void {
27
+ for (const [name, data] of Object.entries(components)) {
28
+ const Klass = registry[name];
29
+ if (!Klass) {
30
+ throw new Error(`Component "${name}" not found in registry`);
31
+ }
32
+ const parsed = Klass.schema ? Klass.schema.parse(data) : data;
33
+ const inst = new Klass();
34
+ Object.assign(inst, parsed);
35
+ // T5.4: record the registry name + raw authored data so a later `hotSwap`
36
+ // keys on this stable identity (not the live `constructor.name`) and can
37
+ // re-validate `data` against a NEW class version's schema.
38
+ manager.attach(object3D, inst, { key: name, props: data });
39
+ }
40
+ }
@@ -0,0 +1,164 @@
1
+ /** Single source of truth for all scene default values. */
2
+ export const DEFAULTS = {
3
+ transform: {
4
+ position: [0, 0, 0] as [number, number, number],
5
+ rotation: [0, 0, 0, 1] as [number, number, number, number],
6
+ scale: [1, 1, 1] as [number, number, number],
7
+ },
8
+
9
+ entity: {
10
+ visible: true,
11
+ locked: false,
12
+ pivot: [0, 0, 0] as [number, number, number],
13
+ },
14
+
15
+ material: {
16
+ type: 'standard' as const,
17
+ color: '#888888',
18
+ metalness: 0,
19
+ roughness: 0.5,
20
+ emissive: '#000000',
21
+ emissiveIntensity: 0,
22
+ opacity: 1,
23
+ transparent: false,
24
+ side: 'front' as const,
25
+ flatShading: false,
26
+ clearcoat: { clearcoat: 1, clearcoatRoughness: 0 },
27
+ transmission: { transmission: 1, ior: 1.5, thickness: 0.5 },
28
+ sheen: { sheen: 1, sheenColor: '#ffffff', sheenRoughness: 0.5 },
29
+ iridescence: {
30
+ iridescence: 1,
31
+ iridescenceIOR: 1.3,
32
+ iridescenceThicknessRange: [100, 400] as [number, number],
33
+ },
34
+ displacementScale: 1,
35
+ displacementBias: 0,
36
+ },
37
+
38
+ light: {
39
+ color: '#ffffff',
40
+ intensity: 1,
41
+ groundColor: '#000000',
42
+ distance: 0,
43
+ angle: Math.PI / 6,
44
+ penumbra: 0,
45
+ // three.js's own RectAreaLight constructor defaults (area lights only).
46
+ width: 10,
47
+ height: 10,
48
+ },
49
+
50
+ camera: {
51
+ fov: 60,
52
+ near: 0.1,
53
+ far: 1000,
54
+ orthoSize: 5,
55
+ width: 1920,
56
+ height: 1080,
57
+ },
58
+
59
+ audio: {
60
+ spatial: true,
61
+ volume: 1,
62
+ refDistance: 1,
63
+ rolloffFactor: 1,
64
+ maxDistance: 10000,
65
+ loop: false,
66
+ autoplay: false,
67
+ },
68
+
69
+ collider: {
70
+ cuboid: { halfExtents: [0.5, 0.5, 0.5] as [number, number, number] },
71
+ ball: { radius: 0.5 },
72
+ capsule: { radius: 0.25, halfHeight: 0.5 },
73
+ },
74
+
75
+ shadow: {
76
+ mapSize: 1024,
77
+ },
78
+
79
+ animation: {
80
+ loop: true,
81
+ },
82
+
83
+ fog: {
84
+ linear: { near: 1, far: 100 },
85
+ exponential: { density: 0.01 },
86
+ },
87
+
88
+ environment: {
89
+ envMapIntensity: 1.0,
90
+ },
91
+
92
+ toneMapping: {
93
+ // T1.13: the effective renderer default is ACES (see
94
+ // setup-renderer.ts's `createHostRenderer` + `applyScenePostProcessing`,
95
+ // which both hardcode `THREE.ACESFilmicToneMapping`/'aces' as the
96
+ // fallback when a scene omits `environment.toneMapping`) — NOT 'none'.
97
+ // This used to say 'none', silently disagreeing with the runtime and
98
+ // with the editor's Tone Mapping inspector dropdown.
99
+ mode: 'aces' as const,
100
+ exposure: 1.0,
101
+ },
102
+
103
+ postProcessing: {
104
+ bloom: { intensity: 1.0, luminanceThreshold: 0.9, luminanceSmoothing: 0.025 },
105
+ vignette: { darkness: 0.5, offset: 0.5 },
106
+ brightnessContrast: { brightness: 0, contrast: 0 },
107
+ hueSaturation: { hue: 0, saturation: 0 },
108
+ sepia: { intensity: 1.0 },
109
+ colorDepth: { bits: 16 },
110
+ chromaticAberration: { offsetX: 0.002, offsetY: 0.002, modulationOffset: 0.15 },
111
+ lensDistortion: { distortionX: 0, distortionY: 0, focalLengthX: 1, focalLengthY: 1, skew: 0 },
112
+ depthOfField: { worldFocusDistance: 10, worldFocusRange: 5, bokehScale: 3, focalLength: 0.04 },
113
+ tiltShift: { offset: 0, rotation: 0, focusArea: 0.4, feather: 0.3 },
114
+ noise: { premultiply: false },
115
+ scanline: { density: 1.25 },
116
+ dotScreen: { angle: 1.57, scale: 1.0 },
117
+ grid: { scale: 1.0, lineWidth: 0.0 },
118
+ pixelation: { granularity: 6 },
119
+ glitch: {
120
+ delayX: 1.5,
121
+ delayY: 3.5,
122
+ durationX: 0.6,
123
+ durationY: 1.0,
124
+ strengthX: 0.3,
125
+ strengthY: 1.0,
126
+ columns: 0.05,
127
+ ratio: 0.85,
128
+ },
129
+ shockWave: { speed: 2, maxRadius: 1, waveSize: 0.2, amplitude: 0.05 },
130
+ smaa: { preset: 'high' as const },
131
+ ssao: { radius: 0.05, intensity: 2.0, bias: 0.025, samples: 16, rings: 7, fade: 0.01 },
132
+ n8ao: {
133
+ aoSamples: 16,
134
+ aoRadius: 5.0,
135
+ intensity: 5,
136
+ denoiseSamples: 8,
137
+ denoiseRadius: 12,
138
+ distanceFalloff: 1.0,
139
+ },
140
+ godRays: { density: 0.96, decay: 0.93, weight: 0.4, exposure: 0.6, samples: 60 },
141
+ outline: {
142
+ edgeStrength: 3,
143
+ pulseSpeed: 0,
144
+ visibleEdgeColor: '#ffffff',
145
+ hiddenEdgeColor: '#22090a',
146
+ },
147
+ ssr: { intensity: 1, exponent: 1, distance: 10, thickness: 10, maxRoughness: 1 },
148
+ ssgi: { intensity: 1, distance: 10, thickness: 10, maxRoughness: 1 },
149
+ motionBlur: { intensity: 1, jitter: 1, samples: 16 },
150
+ },
151
+
152
+ navigation: {
153
+ cellSize: 0.3,
154
+ cellHeight: 0.2,
155
+ walkableSlopeAngle: 45,
156
+ walkableHeight: 2,
157
+ walkableClimb: 1,
158
+ walkableRadius: 0.5,
159
+ maxEdgeLen: 12,
160
+ maxSimplificationError: 1.3,
161
+ minRegionArea: 8,
162
+ mergeRegionArea: 20,
163
+ },
164
+ } as const;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Built-in geometry generators. Importing this module for its side effects
3
+ * registers the built-ins into the geometry registry. Both the runtime factory
4
+ * (scene-loader) and the editor factory (entity-factory) import it so generated
5
+ * meshes resolve on whichever side builds first.
6
+ */
7
+ import './terrain';
@@ -0,0 +1,42 @@
1
+ import * as THREE from 'three';
2
+ import { registerGeometryGenerator } from '../geometry-registry';
3
+
4
+ /**
5
+ * Built-in "Terrain" geometry generator — a horizontal heightfield (XZ plane,
6
+ * height in +Y) displaced by a deterministic sum-of-sines, so it pairs with a
7
+ * `trimesh` collider for matching terrain physics. Proof material for roadmap #3.
8
+ *
9
+ * Params: width, depth (world size), segments (grid resolution per axis),
10
+ * amplitude (max height), frequency (wave scale). Deterministic — no Math.random.
11
+ */
12
+ function heightAt(x: number, z: number, amplitude: number, frequency: number): number {
13
+ return (
14
+ amplitude *
15
+ (Math.sin(x * frequency) * 0.5 +
16
+ Math.cos(z * frequency * 1.3) * 0.3 +
17
+ Math.sin((x + z) * frequency * 0.5) * 0.2)
18
+ );
19
+ }
20
+
21
+ registerGeometryGenerator('Terrain', {
22
+ build(params) {
23
+ const width = (params['width'] as number) ?? 40;
24
+ const depth = (params['depth'] as number) ?? 40;
25
+ const segments = (params['segments'] as number) ?? 64;
26
+ const amplitude = (params['amplitude'] as number) ?? 3;
27
+ const frequency = (params['frequency'] as number) ?? 0.25;
28
+
29
+ const geo = new THREE.PlaneGeometry(width, depth, segments, segments);
30
+ // Lay the plane flat (XZ) with normal +Y, then displace Y by the heightfield.
31
+ geo.rotateX(-Math.PI / 2);
32
+ const pos = geo.attributes['position'] as THREE.BufferAttribute;
33
+ for (let i = 0; i < pos.count; i++) {
34
+ const x = pos.getX(i);
35
+ const z = pos.getZ(i);
36
+ pos.setY(i, heightAt(x, z, amplitude, frequency));
37
+ }
38
+ pos.needsUpdate = true;
39
+ geo.computeVertexNormals();
40
+ return geo;
41
+ },
42
+ });
@@ -0,0 +1,42 @@
1
+ import type * as THREE from 'three';
2
+
3
+ /**
4
+ * A geometry-generator definition. Registered by name and resolved when a scene
5
+ * mesh declares `{ generator: <name> }`. Mirrors the material/instancer registry
6
+ * pattern: the generation logic lives here in code, the authored params arrive as
7
+ * scene data.
8
+ *
9
+ * `build` returns a real THREE.BufferGeometry. Paired with a `trimesh` collider,
10
+ * the generated geometry also drives physics (e.g. terrain), so the visual and
11
+ * collision surfaces match exactly.
12
+ */
13
+ export interface GeometryGeneratorDef {
14
+ build(params: Record<string, unknown>): THREE.BufferGeometry;
15
+ }
16
+
17
+ const registry = new Map<string, GeometryGeneratorDef>();
18
+
19
+ /** Register a geometry generator under `name`. */
20
+ export function registerGeometryGenerator(name: string, def: GeometryGeneratorDef): void {
21
+ registry.set(name, def);
22
+ }
23
+
24
+ /** Look up a registered geometry generator. */
25
+ export function getGeometryGenerator(name: string): GeometryGeneratorDef | undefined {
26
+ return registry.get(name);
27
+ }
28
+
29
+ /**
30
+ * Build a procedural geometry by registered generator name. Throws if the
31
+ * generator is unknown (fail loud — same posture as the other registries).
32
+ */
33
+ export function buildGeneratedGeometry(
34
+ name: string,
35
+ params: Record<string, unknown>,
36
+ ): THREE.BufferGeometry {
37
+ const def = registry.get(name);
38
+ if (!def) {
39
+ throw new Error(`Geometry generator "${name}" not found in geometry registry`);
40
+ }
41
+ return def.build(params ?? {});
42
+ }
@@ -0,0 +1,84 @@
1
+ import * as THREE from 'three';
2
+ import type { InstancesFile } from './schema/instances';
3
+
4
+ /**
5
+ * An instancer definition. Registered by name and resolved when a scene mesh
6
+ * declares `{ instancer: <name> }`. Mirrors the material-registry pattern: the
7
+ * generation logic lives here in code (the "behavior" half), the authored params
8
+ * arrive as scene data (the "data" half).
9
+ *
10
+ * `build` returns per-instance LOCAL transforms; the factory packs them into a
11
+ * single THREE.InstancedMesh, so N instances cost one draw call.
12
+ */
13
+ export interface InstancerDef {
14
+ /** Produce per-instance local matrices from authored params. */
15
+ build(params: Record<string, unknown>): THREE.Matrix4[];
16
+ }
17
+
18
+ const registry = new Map<string, InstancerDef>();
19
+
20
+ /** Register an instancer definition under `name`. */
21
+ export function registerInstancer(name: string, def: InstancerDef): void {
22
+ registry.set(name, def);
23
+ }
24
+
25
+ /** Look up a registered instancer. */
26
+ export function getInstancer(name: string): InstancerDef | undefined {
27
+ return registry.get(name);
28
+ }
29
+
30
+ /**
31
+ * Build a THREE.InstancedMesh from a registered instancer. Throws if the
32
+ * instancer is unknown (fail loud — same posture as the component/material
33
+ * registries). One InstancedMesh = one draw call for all instances.
34
+ */
35
+ export function buildInstancedMesh(
36
+ geometry: THREE.BufferGeometry,
37
+ material: THREE.Material,
38
+ name: string,
39
+ params: Record<string, unknown>,
40
+ ): THREE.InstancedMesh {
41
+ const def = registry.get(name);
42
+ if (!def) {
43
+ throw new Error(`Instancer "${name}" not found in instancer registry`);
44
+ }
45
+ const matrices = def.build(params ?? {});
46
+ const mesh = new THREE.InstancedMesh(geometry, material, matrices.length);
47
+ for (let i = 0; i < matrices.length; i++) {
48
+ mesh.setMatrixAt(i, matrices[i]!);
49
+ }
50
+ mesh.instanceMatrix.needsUpdate = true;
51
+ mesh.userData['__instancer'] = name;
52
+ return mesh;
53
+ }
54
+
55
+ /**
56
+ * Build a THREE.InstancedMesh directly from an already-parsed `.instances.json`
57
+ * tuple array (F3, docs/VSCN-STRUCTURAL-GAPS-DESIGN.md — the declarative,
58
+ * authored-data counterpart to `buildInstancedMesh`'s code-registered
59
+ * instancers). Each tuple is
60
+ * `[posX,posY,posZ, quatX,quatY,quatZ,quatW, scaleX,scaleY,scaleZ]`. Pure and
61
+ * synchronous (no fetch) so it's exercisable headlessly by both the scene
62
+ * loader (after it fetches+parses the asset) and unit tests.
63
+ */
64
+ export function buildInstancedMeshFromTuples(
65
+ geometry: THREE.BufferGeometry,
66
+ material: THREE.Material,
67
+ tuples: InstancesFile,
68
+ ): THREE.InstancedMesh {
69
+ const mesh = new THREE.InstancedMesh(geometry, material, tuples.length);
70
+ const pos = new THREE.Vector3();
71
+ const quat = new THREE.Quaternion();
72
+ const scale = new THREE.Vector3();
73
+ const matrix = new THREE.Matrix4();
74
+ for (let i = 0; i < tuples.length; i++) {
75
+ const t = tuples[i]!;
76
+ pos.set(t[0], t[1], t[2]);
77
+ quat.set(t[3], t[4], t[5], t[6]);
78
+ scale.set(t[7], t[8], t[9]);
79
+ matrix.compose(pos, quat, scale);
80
+ mesh.setMatrixAt(i, matrix);
81
+ }
82
+ mesh.instanceMatrix.needsUpdate = true;
83
+ return mesh;
84
+ }
@@ -0,0 +1,38 @@
1
+ import * as THREE from 'three';
2
+ import { registerInstancer } from '../instance-registry';
3
+
4
+ /**
5
+ * Built-in "Grid" instancer — a countX × countZ grid of instances on the XZ
6
+ * plane, centred on the entity origin, with optional deterministic y-jitter.
7
+ * Proof material for roadmap #2 (first-class instancing): a 100×100 grid is
8
+ * 10,000 instances in one draw call.
9
+ *
10
+ * Determinism: uses a seeded LCG (not Math.random) so a scene renders the same
11
+ * layout every load — important for reproducible scenes and tests.
12
+ */
13
+ registerInstancer('Grid', {
14
+ build(params) {
15
+ const countX = (params['countX'] as number) ?? 100;
16
+ const countZ = (params['countZ'] as number) ?? 100;
17
+ const spacing = (params['spacing'] as number) ?? 2;
18
+ const jitter = (params['jitter'] as number) ?? 0;
19
+ let seed = (params['seed'] as number) ?? 1;
20
+ const rand = () => {
21
+ seed = (seed * 1664525 + 1013904223) % 4294967296;
22
+ return seed / 4294967296;
23
+ };
24
+
25
+ const offX = ((countX - 1) * spacing) / 2;
26
+ const offZ = ((countZ - 1) * spacing) / 2;
27
+ const matrices: THREE.Matrix4[] = [];
28
+ const m = new THREE.Matrix4();
29
+ for (let x = 0; x < countX; x++) {
30
+ for (let z = 0; z < countZ; z++) {
31
+ const y = jitter ? (rand() - 0.5) * jitter : 0;
32
+ m.makeTranslation(x * spacing - offX, y, z * spacing - offZ);
33
+ matrices.push(m.clone());
34
+ }
35
+ }
36
+ return matrices;
37
+ },
38
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Built-in instancer definitions. Importing this module for its side effects
3
+ * registers the built-ins into the instancer registry. Both the runtime factory
4
+ * (scene-loader) and the editor factory (entity-factory) import it so instanced
5
+ * meshes resolve on whichever side builds first.
6
+ */
7
+ import './grid';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared light + camera factory (T1.13 — DEFAULTS truth).
3
+ *
4
+ * `createLight`/`createCamera` used to be duplicated verbatim between the
5
+ * runtime loader (`scene-loader.ts`) and the editor preview
6
+ * (`entity-factory.ts`) — both hardcoded the same fallback literals (color
7
+ * '#ffffff', fov 60, near 0.1, far 1000, etc.) that happened to agree with
8
+ * `DEFAULTS.light`/`DEFAULTS.camera` by luck rather than by reading them. Any
9
+ * edit to one copy (or to `DEFAULTS`) could silently diverge from the other.
10
+ *
11
+ * This module is now the ONE place that builds a `THREE.Light`/`THREE.Camera`
12
+ * from a `SceneLight`/`SceneCamera` descriptor, reading its fallbacks from
13
+ * `DEFAULTS` (the single source of truth for scene default values — see
14
+ * `scene/defaults.ts`). Both the loader and the editor import it, so they
15
+ * can no longer disagree.
16
+ */
17
+
18
+ import * as THREE from 'three';
19
+ // F2 (docs/VSCN-STRUCTURAL-GAPS-DESIGN.md) — area lights. `RectAreaLight`
20
+ // requires its LTC lookup-texture uniforms initialized once before the first
21
+ // instance is constructed; three.js ships that setup as an addon, not part
22
+ // of core. Imported via the `three/addons/*` alias (package.json `exports`:
23
+ // `"./addons/*": "./examples/jsm/*"` — the identical file as
24
+ // `three/examples/jsm/lights/RectAreaLightUniformsLib.js`) because a literal
25
+ // `/examples/` specifier trips `engine-host-no-game-imports.test.ts`'s
26
+ // game/editor-example guard (a same-substring false positive against this
27
+ // repo's OWN `packages/editor/template/src/examples/` convention, unrelated
28
+ // to three.js's addon folder naming).
29
+ import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
30
+ import { DEFAULTS } from './defaults';
31
+ import type { SceneCamera, SceneLight } from './scene-types';
32
+
33
+ let rectAreaLightUniformsInitialized = false;
34
+
35
+ /** Build a Three.js light from a `SceneLight` descriptor. */
36
+ export function createLight(def: SceneLight): THREE.Light {
37
+ const d = DEFAULTS.light;
38
+ const color = def.color ?? d.color;
39
+ const intensity = def.intensity ?? d.intensity;
40
+
41
+ switch (def.type) {
42
+ case 'directional':
43
+ return new THREE.DirectionalLight(color, intensity);
44
+ case 'point':
45
+ return new THREE.PointLight(color, intensity, def.distance ?? d.distance);
46
+ case 'spot':
47
+ return new THREE.SpotLight(
48
+ color,
49
+ intensity,
50
+ def.distance ?? d.distance,
51
+ def.angle ?? d.angle,
52
+ def.penumbra ?? d.penumbra,
53
+ );
54
+ case 'hemisphere':
55
+ return new THREE.HemisphereLight(color, def.groundColor ?? d.groundColor, intensity);
56
+ case 'area': {
57
+ // No shadow map is ever wired for this light (RectAreaLight does not
58
+ // support shadows in three.js — see the honesty note on the schema
59
+ // field's .describe(); degrade loudly, not silently).
60
+ if (!rectAreaLightUniformsInitialized) {
61
+ RectAreaLightUniformsLib.init();
62
+ rectAreaLightUniformsInitialized = true;
63
+ }
64
+ return new THREE.RectAreaLight(
65
+ color,
66
+ intensity,
67
+ def.width ?? d.width,
68
+ def.height ?? d.height,
69
+ );
70
+ }
71
+ default:
72
+ return new THREE.DirectionalLight(color, intensity);
73
+ }
74
+ }
75
+
76
+ /** Build a Three.js camera from a `SceneCamera` descriptor. */
77
+ export function createCamera(def: SceneCamera): THREE.Camera {
78
+ const d = DEFAULTS.camera;
79
+ const aspect = (def.width ?? d.width) / (def.height ?? d.height);
80
+ if (def.type === 'orthographic') {
81
+ const size = def.orthoSize ?? d.orthoSize;
82
+ return new THREE.OrthographicCamera(
83
+ -size * aspect,
84
+ size * aspect,
85
+ size,
86
+ -size,
87
+ def.near ?? d.near,
88
+ def.far ?? d.far,
89
+ );
90
+ }
91
+ return new THREE.PerspectiveCamera(
92
+ def.fov ?? d.fov,
93
+ aspect,
94
+ def.near ?? d.near,
95
+ def.far ?? d.far,
96
+ );
97
+ }