@volter/threejs-runtime 0.5.65

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 (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/package.json +62 -0
  4. package/src/adapter/body-marks.ts +69 -0
  5. package/src/adapter/constraint.ts +78 -0
  6. package/src/adapter/first-party-navigation-system.ts +48 -0
  7. package/src/adapter/hierarchy-marks.ts +156 -0
  8. package/src/adapter/ingest/scene-capture.ts +865 -0
  9. package/src/adapter/ingest/structural-ids.ts +139 -0
  10. package/src/adapter/ingest/visible-capture-window.ts +302 -0
  11. package/src/adapter/object3d-authoring-subject.ts +50 -0
  12. package/src/adapter/rapier-physics-adapter.ts +226 -0
  13. package/src/adapter/reflection-probe.ts +75 -0
  14. package/src/adapter/renderer-config.ts +116 -0
  15. package/src/adapter/trigger-volume.ts +29 -0
  16. package/src/ai/navigation.ts +627 -0
  17. package/src/animation/animation-clock.ts +479 -0
  18. package/src/animation/runtime-inspection.ts +45 -0
  19. package/src/animation/xstate-animation-binding.ts +655 -0
  20. package/src/animation/xstate-animation-meta.ts +360 -0
  21. package/src/asset-formats/camera.ts +18 -0
  22. package/src/asset-formats/collider.ts +66 -0
  23. package/src/asset-formats/index.ts +45 -0
  24. package/src/asset-formats/light.ts +50 -0
  25. package/src/asset-formats/material.ts +93 -0
  26. package/src/asset-formats/mesh.ts +78 -0
  27. package/src/asset-formats/particles.ts +426 -0
  28. package/src/asset-formats/render-env.ts +318 -0
  29. package/src/asset-formats/tuples.ts +21 -0
  30. package/src/asset-loaders.ts +242 -0
  31. package/src/asset-parse-error.ts +29 -0
  32. package/src/assets.ts +68 -0
  33. package/src/behavior/xstate-inspection.ts +51 -0
  34. package/src/defaults.ts +135 -0
  35. package/src/ecs/object-marks.ts +75 -0
  36. package/src/ecs/scene-index.ts +439 -0
  37. package/src/ecs/scene-query.ts +43 -0
  38. package/src/ecs/user-data.ts +272 -0
  39. package/src/loader.ts +134 -0
  40. package/src/physics/collider-dimensions.ts +121 -0
  41. package/src/physics/collision-system.ts +76 -0
  42. package/src/physics/physics-registry.ts +83 -0
  43. package/src/physics/transform-writer.ts +41 -0
  44. package/src/render/directional-shadow-fit.ts +158 -0
  45. package/src/render/environment-capture.ts +133 -0
  46. package/src/render/ibl-override-material.ts +202 -0
  47. package/src/render/light-camera-factory.ts +103 -0
  48. package/src/render/matcap-texture.ts +92 -0
  49. package/src/render/material-factory.ts +201 -0
  50. package/src/render/mesh-shadow.ts +18 -0
  51. package/src/render/particles-factory.ts +589 -0
  52. package/src/render/render-batch-system.ts +260 -0
  53. package/src/render/render-features.ts +128 -0
  54. package/src/render/render-settings.ts +71 -0
  55. package/src/render/soft-particle-depth.ts +360 -0
  56. package/src/render/spark-renderer-lifecycle.ts +64 -0
  57. package/src/render/structural-signature.ts +231 -0
  58. package/src/render/viewport-shading.ts +163 -0
  59. package/src/render/voxel-surface.ts +358 -0
  60. package/src/setup/setup-particles.ts +32 -0
  61. package/src/setup/setup-physics.ts +67 -0
  62. package/src/setup/setup-renderer.ts +524 -0
  63. package/src/types-n8ao.d.ts +37 -0
  64. package/src/types-realism-effects.d.ts +61 -0
  65. package/vendor/realism-effects/LICENSE.md +21 -0
  66. package/vendor/realism-effects/UPSTREAM.md +19 -0
  67. package/vendor/realism-effects/dist/index.cjs +3447 -0
  68. package/vendor/realism-effects/dist/index.d.ts +59 -0
  69. package/vendor/realism-effects/dist/index.js +3434 -0
  70. package/vendor/realism-effects/package.json +23 -0
@@ -0,0 +1,360 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * E1 — Zod/TypeScript metadata attached to XState state `meta.animation`
5
+ * (spec §11 E1, §5.6, §3.3).
6
+ *
7
+ * This module defines ONLY animation data: which clip(s) a state plays, how
8
+ * they loop/blend/crossfade, and (for locomotion-style states) how a 1D/2D/
9
+ * direct blend tree distributes weight across several clips from a live
10
+ * parameter. It intentionally contains **no transition vocabulary** — no
11
+ * `on`/`target`/`guard`/`always`/`after`/`entry`/`exit`. XState alone owns
12
+ * states, transitions, events, and guards (§3.3); this metadata is read-only
13
+ * data a binding (`xstate-animation-binding.ts`, E2) consumes to drive a
14
+ * `THREE.AnimationMixer` when XState enters/exits a state. Every schema below
15
+ * is `.strict()` specifically so that smuggling a transition-shaped key into
16
+ * `meta.animation` is a loud Zod "unrecognized key" error, not a silently
17
+ * ignored field (E1 AC: "Metadata contains no transition language").
18
+ *
19
+ * ## Native layered composition
20
+ *
21
+ * Parallel XState regions may drive independent named layers. Each layer can
22
+ * filter a clip to selected bone subtrees, scale its weight, and use normal
23
+ * override or Three's native additive blend mode. XState still owns all
24
+ * state/transition semantics; these fields only describe how the active
25
+ * state's native `AnimationAction`s are composed by the binding.
26
+ *
27
+ * ## Blend tree shape
28
+ *
29
+ * Deliberately structurally IDENTICAL to {@link BlendTreeDef} below (same
30
+ * `type`/`parameter`/`parameterY`/`children[].clip`/`.threshold`/
31
+ * `.thresholdY`/`.weight` fields) so the binding can hand a parsed
32
+ * `AnimationBlendTree` straight to `evaluateBlendTree`
33
+ * (`xstate-animation-binding.ts`) with zero conversion.
34
+ */
35
+
36
+ /**
37
+ * The blend-tree shape the binding's blend math reads. It lived in
38
+ * `anim-graph-types.ts` while a second, removed AnimGraph runtime shared it;
39
+ * that runtime is gone and the file was folded in here on 2026-09-20, so the
40
+ * AnimGraph name no longer exists in the estate.
41
+ *
42
+ * The optional fields spell `| undefined` explicitly because the repo compiles
43
+ * with `exactOptionalPropertyTypes`, and the one producer of these values is
44
+ * the Zod schema below, whose `.optional()` infers `?: T | undefined`. Without
45
+ * the explicit union the parsed value is not assignable here and the call site
46
+ * needs a cast — which is exactly what used to hide field drift between the
47
+ * two declarations (see `asBlendTreeDef`).
48
+ */
49
+ export interface BlendTreeDef {
50
+ type: '1D' | '2D' | 'direct';
51
+ parameter: string;
52
+ parameterY?: string | undefined; // for 2D
53
+ children: BlendChild[];
54
+ }
55
+
56
+ export interface BlendChild {
57
+ clip: string;
58
+ threshold: number;
59
+ thresholdY?: number | undefined; // for 2D
60
+ weight?: number | undefined; // for direct
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Blend tree children
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const AnimationBlendChildSchema = z
68
+ .object({
69
+ clip: z.string().min(1).describe('Animation clip name this child plays'),
70
+ threshold: z.number().describe('1D/2D blend position on the primary parameter axis'),
71
+ thresholdY: z
72
+ .number()
73
+ .optional()
74
+ .describe('2D blend position on the secondary parameter axis (2D blend trees only)'),
75
+ weight: z
76
+ .number()
77
+ .min(0)
78
+ .optional()
79
+ .describe('Explicit weight for "direct" blend trees (ignored by 1D/2D)'),
80
+ })
81
+ .strict();
82
+
83
+ export type AnimationBlendChild = z.infer<typeof AnimationBlendChildSchema>;
84
+
85
+ const AnimationBlendTree1DSchema = z
86
+ .object({
87
+ type: z.literal('1D').describe('Interpolates linearly between sorted children by threshold'),
88
+ parameter: z.string().min(1).describe('Context/selector value driving the blend'),
89
+ children: z
90
+ .array(AnimationBlendChildSchema)
91
+ .min(2, '1D blend tree needs at least 2 children to interpolate between'),
92
+ })
93
+ .strict();
94
+
95
+ const AnimationBlendTree2DSchema = z
96
+ .object({
97
+ type: z
98
+ .literal('2D')
99
+ .describe('Inverse-distance-weights children by (parameter, parameterY) position'),
100
+ parameter: z.string().min(1).describe('Primary axis context/selector value'),
101
+ parameterY: z.string().min(1).describe('Secondary axis context/selector value'),
102
+ children: z
103
+ .array(AnimationBlendChildSchema.extend({ thresholdY: z.number() }))
104
+ .min(1, '2D blend tree needs at least 1 child'),
105
+ })
106
+ .strict();
107
+
108
+ const AnimationBlendTreeDirectSchema = z
109
+ .object({
110
+ type: z.literal('direct').describe('Each child has an explicit weight, normalized to sum to 1'),
111
+ // `parameter` is unused by direct blending (evaluateBlendTree never reads
112
+ // it for `type: 'direct'`) but kept required+defaulted so the parsed
113
+ // value stays structurally assignable to `BlendTreeDef` above, which
114
+ // declares `parameter: string` for every
115
+ // blend mode — the same convention that type already uses.
116
+ parameter: z.string().default(''),
117
+ children: z
118
+ .array(AnimationBlendChildSchema.extend({ weight: z.number().min(0) }))
119
+ .min(1, 'direct blend tree needs at least 1 child'),
120
+ })
121
+ .strict();
122
+
123
+ export const AnimationBlendTreeSchema = z.discriminatedUnion('type', [
124
+ AnimationBlendTree1DSchema,
125
+ AnimationBlendTree2DSchema,
126
+ AnimationBlendTreeDirectSchema,
127
+ ]);
128
+
129
+ export type AnimationBlendTree = z.infer<typeof AnimationBlendTreeSchema>;
130
+
131
+ /**
132
+ * The structural-identity claim in this file's header, made COMPILER-CHECKED.
133
+ *
134
+ * `evaluateBlendTree` takes {@link BlendTreeDef} above, and the
135
+ * whole reason this schema mirrors that shape field-for-field is so a parsed
136
+ * value can be handed over with zero conversion. The binding used to spell that
137
+ * hand-over `as unknown as BlendTreeDef` — a double cast that would have gone on
138
+ * compiling if either declaration drifted, silently feeding the blend math a
139
+ * shape it does not describe. This identity function is the assignability
140
+ * assertion instead: drift here is a type error at the seam that claims it.
141
+ */
142
+ export function asBlendTreeDef(tree: AnimationBlendTree): BlendTreeDef {
143
+ return tree;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Crossfade / blend-in-out metadata (shared by clip and blend-tree states)
148
+ // ---------------------------------------------------------------------------
149
+
150
+ const AnimationCrossfadeMetaSchema = z
151
+ .object({
152
+ duration: z
153
+ .number()
154
+ .min(0)
155
+ .default(0)
156
+ .describe(
157
+ 'Seconds to blend from the previously active clip into this state on entry, applied via ' +
158
+ 'native `AnimationAction.crossFadeTo`/`fadeIn` (E2). 0 = instant activation, no fade.',
159
+ ),
160
+ warp: z
161
+ .boolean()
162
+ .default(false)
163
+ .describe(
164
+ "Passed through to AnimationAction.crossFadeTo's `warp` param — linearly warps timeScale " +
165
+ 'across the two clips during the blend so differing clip lengths line up. Default false.',
166
+ ),
167
+ })
168
+ .strict()
169
+ .describe('Blend-in/out/crossfade timing for entering this state');
170
+
171
+ export type AnimationCrossfadeMeta = z.infer<typeof AnimationCrossfadeMetaSchema>;
172
+
173
+ const AnimationBoneMaskSchema = z
174
+ .object({
175
+ include: z
176
+ .array(z.string().min(1))
177
+ .min(1)
178
+ .optional()
179
+ .describe('Bone/object names whose complete descendant subtrees are retained'),
180
+ exclude: z
181
+ .array(z.string().min(1))
182
+ .min(1)
183
+ .optional()
184
+ .describe('Bone/object names whose complete descendant subtrees are removed'),
185
+ })
186
+ .strict()
187
+ .refine((mask) => Boolean(mask.include?.length || mask.exclude?.length), {
188
+ message: 'boneMask needs at least one include or exclude bone name',
189
+ })
190
+ .describe('Filters animation tracks by named Object3D/Bone subtrees');
191
+
192
+ export type AnimationBoneMask = z.infer<typeof AnimationBoneMaskSchema>;
193
+
194
+ const AnimationLayerFields = {
195
+ layer: z
196
+ .string()
197
+ .min(1)
198
+ .optional()
199
+ .describe('Composition layer name; omitted means the single base layer'),
200
+ weight: z
201
+ .number()
202
+ .min(0)
203
+ .max(1)
204
+ .optional()
205
+ .describe('Layer weight multiplier from 0 to 1; defaults to 1'),
206
+ blendMode: z
207
+ .enum(['override', 'additive'])
208
+ .optional()
209
+ .describe('Native Three normal/override or additive action blending; defaults to override'),
210
+ boneMask: AnimationBoneMaskSchema.optional().describe(
211
+ 'Optional include/exclude filter applied to every clip track in this state',
212
+ ),
213
+ };
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Per-state animation metadata: single clip OR blend tree, plus crossfade
217
+ // ---------------------------------------------------------------------------
218
+
219
+ const ClipAnimationMetaSchema = z
220
+ .object({
221
+ clip: z.string().min(1).describe('Single clip to play while this XState state is active'),
222
+ loop: z
223
+ .boolean()
224
+ .default(true)
225
+ .describe(
226
+ 'Loop the clip (THREE.LoopRepeat) vs play once (THREE.LoopOnce, clamped on last frame)',
227
+ ),
228
+ speed: z
229
+ .number()
230
+ .positive()
231
+ .default(1)
232
+ .describe('Playback speed multiplier (AnimationAction.timeScale)'),
233
+ crossfade: AnimationCrossfadeMetaSchema.optional(),
234
+ ...AnimationLayerFields,
235
+ })
236
+ .strict();
237
+
238
+ export type ClipAnimationMeta = z.infer<typeof ClipAnimationMetaSchema>;
239
+
240
+ const BlendTreeAnimationMetaSchema = z
241
+ .object({
242
+ blendTree: AnimationBlendTreeSchema.describe(
243
+ 'Multiple clips blended by a live context/selector-driven parameter',
244
+ ),
245
+ crossfade: AnimationCrossfadeMetaSchema.optional(),
246
+ ...AnimationLayerFields,
247
+ })
248
+ .strict();
249
+
250
+ export type BlendTreeAnimationMeta = z.infer<typeof BlendTreeAnimationMetaSchema>;
251
+
252
+ /**
253
+ * The full shape of `state.meta.animation` for one XState state (spec §5.6):
254
+ * exactly one of a single clip or a blend tree, plus optional crossfade
255
+ * timing. `.strict()` on both branches is what makes authoring a transition
256
+ * keyword (`on`, `target`, `guard`, …) inside `meta.animation` a validation
257
+ * error instead of a silently-accepted second transition dialect.
258
+ */
259
+ export const StateAnimationMetaSchema = z.union([
260
+ ClipAnimationMetaSchema,
261
+ BlendTreeAnimationMetaSchema,
262
+ ]);
263
+
264
+ export type StateAnimationMeta = z.infer<typeof StateAnimationMetaSchema>;
265
+
266
+ /** True iff a validated meta declares a single clip rather than a blend tree. */
267
+ export function isClipAnimationMeta(meta: StateAnimationMeta): meta is ClipAnimationMeta {
268
+ return 'clip' in meta;
269
+ }
270
+
271
+ /** True iff a validated meta declares a blend tree rather than a single clip. */
272
+ export function isBlendTreeAnimationMeta(meta: StateAnimationMeta): meta is BlendTreeAnimationMeta {
273
+ return 'blendTree' in meta;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Validation entry point
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /** Thrown by `parseStateAnimationMeta` — always names the offending state id. */
281
+ export class AnimationMetaValidationError extends Error {
282
+ constructor(
283
+ message: string,
284
+ readonly stateId: string,
285
+ readonly issues: z.ZodIssue[],
286
+ ) {
287
+ super(message);
288
+ this.name = 'AnimationMetaValidationError';
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Validate one state's `meta.animation` value. Called by the E2 binding for
294
+ * every state a machine declares (both at bind time, up front, and — for
295
+ * belt-and-suspenders — again whenever a state is entered).
296
+ *
297
+ * @throws {AnimationMetaValidationError} naming the state id and every Zod
298
+ * issue (path + message) — e.g. an unrecognized `on`/`target` key, a
299
+ * missing `clip`, or a 1D blend tree with only one child.
300
+ */
301
+ export function parseStateAnimationMeta(stateId: string, raw: unknown): StateAnimationMeta {
302
+ const result = StateAnimationMetaSchema.safeParse(raw);
303
+ if (!result.success) {
304
+ const detail = result.error.issues
305
+ .map((issue) => `${issue.path.length ? issue.path.join('.') : '(root)'}: ${issue.message}`)
306
+ .join('; ');
307
+ throw new AnimationMetaValidationError(
308
+ `[xstate-animation] invalid meta.animation for state "${stateId}": ${detail}`,
309
+ stateId,
310
+ result.error.issues,
311
+ );
312
+ }
313
+ return result.data;
314
+ }
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // Machine-wide collection (used by E2 to validate + resolve clips up front)
318
+ // ---------------------------------------------------------------------------
319
+
320
+ /** The minimal shape of an XState `StateNode` this module needs to walk. */
321
+ export interface AnimationMetaStateNodeLike {
322
+ id: string;
323
+ meta?: { animation?: unknown } & Record<string, unknown>;
324
+ states?: Record<string, AnimationMetaStateNodeLike>;
325
+ }
326
+
327
+ export interface MachineAnimationMetaEntry {
328
+ stateId: string;
329
+ meta: StateAnimationMeta;
330
+ }
331
+
332
+ /**
333
+ * Walk every state node in a machine (from its root `StateNode`, e.g.
334
+ * `actor.logic.root`) and validate every `meta.animation` value found.
335
+ * States with no `meta.animation` are skipped (not every XState state needs
336
+ * to drive animation). Returns one entry per animated state, in document
337
+ * order.
338
+ *
339
+ * @throws {AnimationMetaValidationError} on the FIRST invalid meta found —
340
+ * naming the specific state id, so an agent authoring a machine gets an
341
+ * actionable pointer straight to the bad state instead of a generic error.
342
+ */
343
+ export function collectMachineAnimationMeta(
344
+ root: AnimationMetaStateNodeLike,
345
+ ): MachineAnimationMetaEntry[] {
346
+ const out: MachineAnimationMetaEntry[] = [];
347
+ const visit = (node: AnimationMetaStateNodeLike): void => {
348
+ if (node.meta && 'animation' in node.meta && node.meta.animation !== undefined) {
349
+ out.push({ stateId: node.id, meta: parseStateAnimationMeta(node.id, node.meta.animation) });
350
+ }
351
+ for (const child of Object.values(node.states ?? {})) visit(child);
352
+ };
353
+ visit(root);
354
+ return out;
355
+ }
356
+
357
+ /** Every clip name referenced anywhere in a validated meta (1 for clip states, N for blend trees). */
358
+ export function clipNamesOf(meta: StateAnimationMeta): string[] {
359
+ return isClipAnimationMeta(meta) ? [meta.clip] : meta.blendTree.children.map((c) => c.clip);
360
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CameraDescriptorSchema = z
4
+ .object({
5
+ type: z.enum(['perspective', 'orthographic']).describe('Camera projection type'),
6
+ fov: z.number().optional().describe('Vertical field of view in degrees (perspective only)'),
7
+ near: z.number().optional().describe('Near clipping plane distance'),
8
+ far: z.number().optional().describe('Far clipping plane distance'),
9
+ orthoSize: z
10
+ .number()
11
+ .optional()
12
+ .describe('Half-height of the orthographic frustum (orthographic only)'),
13
+ width: z.number().optional().describe('Render width in pixels. Defaults to viewport width'),
14
+ height: z.number().optional().describe('Render height in pixels. Defaults to viewport height'),
15
+ })
16
+ .describe('Camera configuration');
17
+
18
+ export type CameraDescriptor = z.infer<typeof CameraDescriptorSchema>;
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { Vec3Schema } from './tuples';
3
+
4
+ export const ColliderDescriptorSchema = z
5
+ .object({
6
+ type: z
7
+ .enum(['cuboid', 'ball', 'capsule', 'trimesh'])
8
+ .describe(
9
+ "Collider shape type. T1.2: every shape scales with the entity's composed world scale " +
10
+ "(self × ancestors) — for trimesh this means the entity's own mesh geometry vertices " +
11
+ 'are pre-scaled per-axis before building the Rapier trimesh, so it matches whatever ' +
12
+ "size ends up on screen; it does not require the collider's own dimension fields.",
13
+ ),
14
+ halfExtents: Vec3Schema.optional().describe(
15
+ "Half-size [x, y, z] for cuboid colliders, in the ENTITY'S OWN LOCAL units. T1.2: " +
16
+ "scales with the entity — the actual Rapier collider is this value times the entity's " +
17
+ 'composed world scale (self × every transformed ancestor), per axis, baked in once at ' +
18
+ "load time (Rapier has no live notion of an Object3D's scale).",
19
+ ),
20
+ radius: z
21
+ .number()
22
+ .optional()
23
+ .describe(
24
+ "Radius for ball and capsule colliders, in the ENTITY'S OWN LOCAL units. T1.2: scales " +
25
+ "with the entity's composed world scale, which MUST be uniform on the collider's round " +
26
+ 'axes (ball: all of x/y/z; capsule: x and z) — a non-uniform scale there throws a loud ' +
27
+ 'load error (Rapier balls/capsules cannot represent an ellipse).',
28
+ ),
29
+ halfHeight: z
30
+ .number()
31
+ .optional()
32
+ .describe(
33
+ "Half-height for capsule colliders, in the entity's own local units, along the capsule's " +
34
+ "axis (Y). T1.2: scales by the entity's composed world scale on that axis.",
35
+ ),
36
+ friction: z.number().optional().describe('Surface friction coefficient'),
37
+ restitution: z
38
+ .number()
39
+ .optional()
40
+ .describe('Bounciness coefficient (0 = no bounce, 1 = perfect bounce)'),
41
+ density: z
42
+ .number()
43
+ .optional()
44
+ .describe('Mass density. Used to compute mass from collider volume'),
45
+ offset: Vec3Schema.optional().describe(
46
+ "Translation offset relative to body origin, in the ENTITY'S OWN LOCAL units. T1.2/R1c: " +
47
+ "scales with the entity's composed world scale (self × every transformed ancestor), " +
48
+ 'per axis, the same way halfExtents/radius/halfHeight do — an unscaled offset would ' +
49
+ "silently drift away from the entity's visual anchor point under any non-1 scale.",
50
+ ),
51
+ isSensor: z
52
+ .boolean()
53
+ .optional()
54
+ .describe('When true, detects overlaps without physical blocking (trigger volume)'),
55
+ collisionGroups: z
56
+ .number()
57
+ .optional()
58
+ .describe('Bitmask: collision layers this collider belongs to (max 16 bits)'),
59
+ collisionFilter: z
60
+ .number()
61
+ .optional()
62
+ .describe('Bitmask: collision layers this collider interacts with (max 16 bits)'),
63
+ })
64
+ .describe('Physics collider shape and properties');
65
+
66
+ export type ColliderDescriptor = z.infer<typeof ColliderDescriptorSchema>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The engine's runtime descriptor schemas.
3
+ *
4
+ * Each module is here because a real runtime READER consumes it — nothing in
5
+ * this directory exists as documentation of a format:
6
+ *
7
+ * - `material.ts` — the inline material descriptor `render/material-factory.ts` reads
8
+ * - `mesh.ts` — the geometry descriptor `render/material-factory.ts`'s `createGeometry` reads
9
+ * - `light.ts` / `camera.ts` — the descriptors `render/light-camera-factory.ts` reads
10
+ * - `collider.ts` — the descriptor `physics/collider-dimensions.ts` and the editor's
11
+ * collider gizmos read
12
+ * - `particles.ts` — the descriptor `render/particles-factory.ts` reads
13
+ * - `render-settings.ts` — the tone-mapping / post-processing / render-feature scopes
14
+ * `setup/setup-renderer.ts` reads
15
+ * - `tuples.ts` — the shared Vec3/Quat/Transform tuples the above build on
16
+ *
17
+ * These are DESCRIPTORS, not documents: each names a slice of authored data a
18
+ * render/physics factory reads, and none of them is a scene — which is why
19
+ * every one of them is named `*Descriptor`.
20
+ */
21
+
22
+ export type { CameraDescriptor } from './camera';
23
+ export { CameraDescriptorSchema } from './camera';
24
+ export type { ColliderDescriptor } from './collider';
25
+ export { ColliderDescriptorSchema } from './collider';
26
+ export type { LightDescriptor } from './light';
27
+ export { LightDescriptorSchema } from './light';
28
+ export type { MaterialDescriptor } from './material';
29
+ export { MaterialDescriptorSchema } from './material';
30
+ export type { MeshDescriptor } from './mesh';
31
+ export { MeshDescriptorSchema } from './mesh';
32
+ export type { ParticlesDescriptor } from './particles';
33
+ export { ParticlesDescriptorSchema } from './particles';
34
+ export type {
35
+ PostProcessingDescriptor,
36
+ RenderEnvironment,
37
+ ToneMappingDescriptor,
38
+ } from './render-env';
39
+ export {
40
+ PostProcessingDescriptorSchema,
41
+ RenderEnvironmentSchema,
42
+ ToneMappingDescriptorSchema,
43
+ } from './render-env';
44
+ export type { Quat, Transform, Vec3 } from './tuples';
45
+ export { QuatSchema, TransformSchema, Vec3Schema } from './tuples';
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+
3
+ const LightDescriptorBase = z.object({
4
+ type: z.enum(['directional', 'point', 'spot', 'hemisphere', 'area']).describe('Light type'),
5
+ color: z.string().optional().describe('Light color as CSS hex string'),
6
+ intensity: z
7
+ .number()
8
+ .optional()
9
+ .describe(
10
+ 'Native Three.js light intensity. Point and spot lights use candela with inverse-square ' +
11
+ 'falloff (a value near 1 is candle-scale); area lights use nit; directional and ' +
12
+ 'hemisphere lights use Three.js scene-light intensity units.',
13
+ ),
14
+ groundColor: z.string().optional().describe('Ground color for hemisphere lights'),
15
+ distance: z
16
+ .number()
17
+ .optional()
18
+ .describe('Maximum range of the light. 0 = infinite (point/spot only)'),
19
+ decay: z
20
+ .number()
21
+ .nonnegative()
22
+ .optional()
23
+ .describe(
24
+ 'Native Three.js distance-decay exponent for point/spot lights. Default 2 gives ' +
25
+ 'physically correct inverse-square falloff.',
26
+ ),
27
+ angle: z.number().optional().describe('Spotlight cone angle in radians (spot only)'),
28
+ penumbra: z.number().optional().describe('Spotlight penumbra softness 0–1 (spot only)'),
29
+ width: z
30
+ .number()
31
+ .optional()
32
+ .describe(
33
+ 'Area light width (area only). RectAreaLight has NO shadow support and only affects ' +
34
+ 'Standard/Physical materials — degrades loudly, not silently.',
35
+ ),
36
+ height: z
37
+ .number()
38
+ .optional()
39
+ .describe(
40
+ 'Area light height (area only). RectAreaLight has NO shadow support and only affects ' +
41
+ 'Standard/Physical materials — degrades loudly, not silently.',
42
+ ),
43
+ });
44
+
45
+ export const LightDescriptorSchema = LightDescriptorBase.refine(
46
+ (l) => l.type !== 'spot' || l.angle === undefined || l.angle > 0,
47
+ { message: 'Spot light angle must be positive' },
48
+ ).describe('Light source configuration');
49
+
50
+ export type LightDescriptor = z.infer<typeof LightDescriptorSchema>;
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+
3
+ const MaterialDescriptorObjectSchema = 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'),
28
+
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)'),
39
+
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)'),
54
+
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)'),
65
+
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)'),
82
+
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();
88
+
89
+ export const MaterialDescriptorSchema = MaterialDescriptorObjectSchema.describe(
90
+ 'Material appearance properties',
91
+ );
92
+
93
+ export type MaterialDescriptor = z.infer<typeof MaterialDescriptorSchema>;