@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,78 @@
1
+ import { z } from 'zod';
2
+
3
+ const MeshDescriptorBase = 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
+ lod: z
34
+ .array(
35
+ z.object({
36
+ distance: z
37
+ .number()
38
+ .describe(
39
+ 'Camera distance in world units at/beyond which this level shows (THREE.LOD ' +
40
+ 'threshold). Use 0 for the closest/highest-detail level.',
41
+ ),
42
+ node: z
43
+ .string()
44
+ .describe(
45
+ 'Name of a node inside the referenced glTF (mesh.src) to show at this distance — ' +
46
+ 'resolved via getObjectByName, geometry shared (F4-style). A skinned node throws ' +
47
+ '(atomic-subtree rule).',
48
+ ),
49
+ }),
50
+ )
51
+ .optional()
52
+ .describe(
53
+ 'Per-entity Level-of-Detail: distance-keyed glTF sub-nodes swapped by camera distance ' +
54
+ '(THREE.LOD). gltf-only. Gated by the environment.rendering.lod master toggle — when ' +
55
+ 'off, only the closest level renders. Cannot combine with node.',
56
+ ),
57
+ })
58
+ .strict();
59
+
60
+ export const MeshDescriptorSchema = MeshDescriptorBase.refine(
61
+ (m) => (m.type !== 'gltf' && m.type !== 'splat') || m.src != null,
62
+ {
63
+ message: "External render asset requires 'src' field",
64
+ },
65
+ )
66
+ .refine((m) => m.node == null || m.type === 'gltf', {
67
+ message:
68
+ "Mesh 'node' is only meaningful with type: 'gltf' (names a sub-node of the referenced glTF)",
69
+ })
70
+ .refine((m) => m.lod == null || m.type === 'gltf', {
71
+ message: "Mesh 'lod' is only meaningful with type: 'gltf'",
72
+ })
73
+ .refine((m) => !(m.lod && m.node), {
74
+ message: "Mesh 'lod' cannot combine with 'node' — LOD is the multi-level form of a node ref",
75
+ })
76
+ .describe('Mesh geometry definition');
77
+
78
+ export type MeshDescriptor = z.infer<typeof MeshDescriptorSchema>;
@@ -0,0 +1,426 @@
1
+ import { z } from 'zod';
2
+ import { Vec3Schema } from './tuples';
3
+
4
+ // --- Value Generators (mirrors quarks.core FunctionJSON) ---
5
+
6
+ const ConstantValueSchema = z.object({
7
+ type: z.literal('ConstantValue'),
8
+ value: z.number(),
9
+ });
10
+
11
+ const IntervalValueSchema = z.object({
12
+ type: z.literal('IntervalValue'),
13
+ a: z.number(),
14
+ b: z.number(),
15
+ });
16
+
17
+ const BezierSchema = z.object({
18
+ p0: z.number(),
19
+ p1: z.number(),
20
+ p2: z.number(),
21
+ p3: z.number(),
22
+ });
23
+
24
+ const PiecewiseBezierSchema = z.object({
25
+ type: z.literal('PiecewiseBezier'),
26
+ functions: z.array(
27
+ z.object({
28
+ function: BezierSchema,
29
+ start: z.number(),
30
+ }),
31
+ ),
32
+ });
33
+
34
+ export const ValueGeneratorSchema = z
35
+ .discriminatedUnion('type', [ConstantValueSchema, IntervalValueSchema, PiecewiseBezierSchema])
36
+ .describe('Numeric value: constant, random interval, or bezier curve');
37
+
38
+ export type ValueGeneratorJSON = z.infer<typeof ValueGeneratorSchema>;
39
+
40
+ // --- Color Generators (mirrors quarks.core ColorGenerator JSON) ---
41
+
42
+ const ColorRGBA = z.object({
43
+ r: z.number(),
44
+ g: z.number(),
45
+ b: z.number(),
46
+ a: z.number(),
47
+ });
48
+
49
+ const ColorRGB = z.object({
50
+ r: z.number(),
51
+ g: z.number(),
52
+ b: z.number(),
53
+ });
54
+
55
+ /** ContinuousLinearFunction JSON format used by Gradient internally */
56
+ const CLinearColorSchema = z.object({
57
+ type: z.literal('CLinearFunction'),
58
+ subType: z.literal('Color'),
59
+ keys: z.array(z.object({ value: ColorRGB, pos: z.number() })),
60
+ });
61
+
62
+ const CLinearNumberSchema = z.object({
63
+ type: z.literal('CLinearFunction'),
64
+ subType: z.literal('Number'),
65
+ keys: z.array(z.object({ value: z.number(), pos: z.number() })),
66
+ });
67
+
68
+ const ConstantColorSchema = z.object({
69
+ type: z.literal('ConstantColor'),
70
+ color: ColorRGBA,
71
+ });
72
+
73
+ const ColorRangeSchema = z.object({
74
+ type: z.literal('ColorRange'),
75
+ a: ColorRGBA,
76
+ b: ColorRGBA,
77
+ });
78
+
79
+ const GradientSchema = z.object({
80
+ type: z.literal('Gradient'),
81
+ color: CLinearColorSchema,
82
+ alpha: CLinearNumberSchema,
83
+ });
84
+
85
+ const RandomColorBetweenGradientSchema = z.object({
86
+ type: z.literal('RandomColorBetweenGradient'),
87
+ gradient1: GradientSchema,
88
+ gradient2: GradientSchema,
89
+ });
90
+
91
+ export const ColorGeneratorSchema = z
92
+ .discriminatedUnion('type', [
93
+ ConstantColorSchema,
94
+ ColorRangeSchema,
95
+ GradientSchema,
96
+ RandomColorBetweenGradientSchema,
97
+ ])
98
+ .describe('Color generator: constant, range, gradient, or random between gradients');
99
+
100
+ export type ColorGeneratorJSON = z.infer<typeof ColorGeneratorSchema>;
101
+
102
+ // --- Emitter Shapes (mirrors quarks.core shape constructor params) ---
103
+
104
+ const PointEmitterSchema = z.object({ type: z.literal('point') });
105
+
106
+ const SphereEmitterSchema = z.object({
107
+ type: z.literal('sphere'),
108
+ radius: z.number().optional().describe('Sphere radius'),
109
+ arc: z.number().optional().describe('Arc angle in radians (2π = full sphere)'),
110
+ thickness: z.number().optional().describe('Shell thickness 0–1 (1 = solid, 0 = surface only)'),
111
+ });
112
+
113
+ const HemisphereEmitterSchema = z.object({
114
+ type: z.literal('hemisphere'),
115
+ radius: z.number().optional().describe('Hemisphere radius'),
116
+ arc: z.number().optional().describe('Arc angle in radians'),
117
+ thickness: z.number().optional().describe('Shell thickness 0–1'),
118
+ });
119
+
120
+ const ConeEmitterSchema = z.object({
121
+ type: z.literal('cone'),
122
+ radius: z.number().optional().describe('Cone base radius'),
123
+ arc: z.number().optional().describe('Arc angle in radians'),
124
+ thickness: z.number().optional().describe('Shell thickness 0–1'),
125
+ angle: z.number().optional().describe('Cone opening angle in radians'),
126
+ });
127
+
128
+ const CircleEmitterSchema = z.object({
129
+ type: z.literal('circle'),
130
+ radius: z.number().optional().describe('Circle radius'),
131
+ arc: z.number().optional().describe('Arc angle in radians'),
132
+ thickness: z.number().optional().describe('Ring thickness 0–1'),
133
+ });
134
+
135
+ const DonutEmitterSchema = z.object({
136
+ type: z.literal('donut'),
137
+ radius: z.number().optional().describe('Donut major radius'),
138
+ arc: z.number().optional().describe('Arc angle in radians'),
139
+ thickness: z.number().optional().describe('Ring thickness 0–1'),
140
+ donutRadius: z.number().optional().describe('Donut tube (minor) radius'),
141
+ });
142
+
143
+ const RectangleEmitterSchema = z.object({
144
+ type: z.literal('rectangle'),
145
+ width: z.number().optional().describe('Rectangle width'),
146
+ height: z.number().optional().describe('Rectangle height'),
147
+ thickness: z.number().optional().describe('Extrusion depth 0–1'),
148
+ });
149
+
150
+ const GridEmitterSchema = z.object({
151
+ type: z.literal('grid'),
152
+ width: z.number().optional().describe('Grid total width'),
153
+ height: z.number().optional().describe('Grid total height'),
154
+ column: z.number().int().optional().describe('Number of columns'),
155
+ row: z.number().int().optional().describe('Number of rows'),
156
+ });
157
+
158
+ export const EmitterShapeSchema = z
159
+ .discriminatedUnion('type', [
160
+ PointEmitterSchema,
161
+ SphereEmitterSchema,
162
+ HemisphereEmitterSchema,
163
+ ConeEmitterSchema,
164
+ CircleEmitterSchema,
165
+ DonutEmitterSchema,
166
+ RectangleEmitterSchema,
167
+ GridEmitterSchema,
168
+ ])
169
+ .describe('Particle emitter shape');
170
+
171
+ export type EmitterShapeJSON = z.infer<typeof EmitterShapeSchema>;
172
+
173
+ // --- Behaviors (mirrors quarks.core behavior toJSON) ---
174
+
175
+ const ApplyForceBehaviorSchema = z.object({
176
+ type: z.literal('ApplyForce'),
177
+ direction: Vec3Schema.describe('Force direction vector'),
178
+ magnitude: ValueGeneratorSchema.describe('Force magnitude'),
179
+ });
180
+
181
+ const ColorOverLifeBehaviorSchema = z.object({
182
+ type: z.literal('ColorOverLife'),
183
+ color: ColorGeneratorSchema.describe('Color curve over particle lifetime'),
184
+ });
185
+
186
+ const SizeOverLifeBehaviorSchema = z.object({
187
+ type: z.literal('SizeOverLife'),
188
+ size: ValueGeneratorSchema.describe('Size curve over particle lifetime'),
189
+ });
190
+
191
+ const SpeedOverLifeBehaviorSchema = z.object({
192
+ type: z.literal('SpeedOverLife'),
193
+ speed: ValueGeneratorSchema.describe('Speed multiplier curve over lifetime'),
194
+ });
195
+
196
+ const RotationOverLifeBehaviorSchema = z.object({
197
+ type: z.literal('RotationOverLife'),
198
+ angularVelocity: ValueGeneratorSchema.describe('Angular velocity in radians/sec'),
199
+ });
200
+
201
+ const ForceOverLifeBehaviorSchema = z.object({
202
+ type: z.literal('ForceOverLife'),
203
+ x: ValueGeneratorSchema.describe('Force along X axis over lifetime'),
204
+ y: ValueGeneratorSchema.describe('Force along Y axis over lifetime'),
205
+ z: ValueGeneratorSchema.describe('Force along Z axis over lifetime'),
206
+ });
207
+
208
+ const OrbitOverLifeBehaviorSchema = z.object({
209
+ type: z.literal('OrbitOverLife'),
210
+ orbitSpeed: ValueGeneratorSchema.describe('Orbit speed over lifetime'),
211
+ axis: Vec3Schema.optional().describe('Orbit axis. Defaults to [0, 1, 0]'),
212
+ });
213
+
214
+ const NoiseBehaviorSchema = z.object({
215
+ type: z.literal('Noise'),
216
+ frequency: ValueGeneratorSchema.describe('Noise frequency'),
217
+ power: ValueGeneratorSchema.describe('Noise amplitude'),
218
+ positionAmount: ValueGeneratorSchema.optional().describe('Position displacement amount'),
219
+ rotationAmount: ValueGeneratorSchema.optional().describe('Rotation displacement amount'),
220
+ });
221
+
222
+ const TurbulenceFieldBehaviorSchema = z.object({
223
+ type: z.literal('TurbulenceField'),
224
+ scale: Vec3Schema.describe('Turbulence field scale'),
225
+ octaves: z.number().int().describe('Noise octave count'),
226
+ velocityMultiplier: Vec3Schema.describe('Velocity multiplier per axis'),
227
+ timeScale: Vec3Schema.describe('Time scale per axis'),
228
+ });
229
+
230
+ const FrameOverLifeBehaviorSchema = z.object({
231
+ type: z.literal('FrameOverLife'),
232
+ frame: ValueGeneratorSchema.describe('Sprite sheet frame index over lifetime'),
233
+ });
234
+
235
+ const LimitSpeedOverLifeBehaviorSchema = z.object({
236
+ type: z.literal('LimitSpeedOverLife'),
237
+ speed: ValueGeneratorSchema.describe('Maximum speed over lifetime'),
238
+ dampen: z.number().describe('Damping factor when speed exceeds limit (0–1)'),
239
+ });
240
+
241
+ const ChangeEmitDirectionBehaviorSchema = z.object({
242
+ type: z.literal('ChangeEmitDirection'),
243
+ angle: ValueGeneratorSchema.describe('Angle deviation from original emit direction'),
244
+ });
245
+
246
+ const GravityForceBehaviorSchema = z.object({
247
+ type: z.literal('GravityForce'),
248
+ center: Vec3Schema.describe('Gravity center point'),
249
+ magnitude: z.number().describe('Gravity force strength'),
250
+ });
251
+
252
+ const WidthOverLengthBehaviorSchema = z.object({
253
+ type: z.literal('WidthOverLength'),
254
+ width: ValueGeneratorSchema.describe('Trail width over trail length'),
255
+ });
256
+
257
+ export const BehaviorSchema = z
258
+ .discriminatedUnion('type', [
259
+ ApplyForceBehaviorSchema,
260
+ ColorOverLifeBehaviorSchema,
261
+ SizeOverLifeBehaviorSchema,
262
+ SpeedOverLifeBehaviorSchema,
263
+ RotationOverLifeBehaviorSchema,
264
+ ForceOverLifeBehaviorSchema,
265
+ OrbitOverLifeBehaviorSchema,
266
+ NoiseBehaviorSchema,
267
+ TurbulenceFieldBehaviorSchema,
268
+ FrameOverLifeBehaviorSchema,
269
+ LimitSpeedOverLifeBehaviorSchema,
270
+ ChangeEmitDirectionBehaviorSchema,
271
+ GravityForceBehaviorSchema,
272
+ WidthOverLengthBehaviorSchema,
273
+ ])
274
+ .describe('Particle behavior applied over lifetime');
275
+
276
+ export type BehaviorJSON = z.infer<typeof BehaviorSchema>;
277
+
278
+ // --- Render Mode ---
279
+
280
+ export const RenderModeSchema = z
281
+ .enum([
282
+ 'billboard',
283
+ 'stretchedBillboard',
284
+ 'mesh',
285
+ 'trail',
286
+ 'horizontalBillboard',
287
+ 'verticalBillboard',
288
+ ])
289
+ .describe('How particles are rendered');
290
+
291
+ // --- Particle Material (our format — asset paths, not Three.js UUIDs) ---
292
+
293
+ /**
294
+ * The sprite `map`'s SAMPLER — how the image is addressed outside [0,1], interpolated, and
295
+ * mip-chained. Separate from the URL because two emitters can name one image and want different
296
+ * sampling of it; the factory caches per URL *and* sampler for exactly that reason.
297
+ *
298
+ * An absent field leaves three's own default alone (clamp-to-edge, linear, mipmapped, anisotropy
299
+ * 1) rather than restating it — the same "absent means the loader's default" contract the rest of
300
+ * this descriptor keeps. `map`'s COLOUR SPACE is deliberately not here: a `map` is always a colour
301
+ * texture, so the factory states sRGB unconditionally (see `particles-factory.ts`).
302
+ */
303
+ const ParticleMapSamplerSchema = z
304
+ .object({
305
+ wrap: z
306
+ .enum(['clamp', 'repeat', 'mirror'])
307
+ .optional()
308
+ .describe('How UVs outside [0,1] address the sprite image (three wrapS/wrapT)'),
309
+ filter: z
310
+ .enum(['linear', 'nearest'])
311
+ .optional()
312
+ .describe('Texel interpolation — nearest keeps a pixel-art sprite crisp'),
313
+ mipmaps: z.boolean().optional().describe('Generate and sample a mip chain'),
314
+ anisotropy: z
315
+ .number()
316
+ .optional()
317
+ .describe('Anisotropic filtering samples, for sprites viewed at grazing angles'),
318
+ })
319
+ .describe('Sprite texture sampler state');
320
+
321
+ const ParticleMaterialSchema = z
322
+ .object({
323
+ type: z.enum(['basic', 'standard']).optional().describe('Particle material shading model'),
324
+ color: z.string().optional().describe('Particle color as CSS hex string'),
325
+ map: z.string().optional().describe('Path to particle sprite texture'),
326
+ mapSampler: ParticleMapSamplerSchema.optional().describe(
327
+ 'Sampler state applied to the sprite texture named by `map`',
328
+ ),
329
+ blending: z
330
+ .enum(['normal', 'additive'])
331
+ .optional()
332
+ .describe('Blending mode (additive for glowing/fire effects)'),
333
+ transparent: z.boolean().optional().describe('Enable transparency'),
334
+ depthWrite: z
335
+ .boolean()
336
+ .optional()
337
+ .describe('Write to depth buffer (disable for additive effects)'),
338
+ side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
339
+ })
340
+ .describe('Particle material settings');
341
+
342
+ // --- Burst ---
343
+
344
+ const BurstSchema = z
345
+ .object({
346
+ time: z.number().describe('Time offset in seconds within the emission cycle'),
347
+ count: ValueGeneratorSchema.describe('Number of particles to emit'),
348
+ cycle: z.number().optional().describe('Burst repeat interval in seconds'),
349
+ interval: z.number().optional().describe('Delay between burst repetitions'),
350
+ probability: z.number().optional().describe('Chance of burst firing (0–1)'),
351
+ })
352
+ .describe('Timed particle burst');
353
+
354
+ // --- Top-level ParticlesDescriptor ---
355
+
356
+ export const ParticlesDescriptorSchema = z
357
+ .object({
358
+ // Lifecycle
359
+ autoDestroy: z
360
+ .boolean()
361
+ .optional()
362
+ .describe('Destroy the particle system when all particles have died'),
363
+ looping: z.boolean().optional().describe('Continuously emit particles'),
364
+ prewarm: z
365
+ .boolean()
366
+ .optional()
367
+ .describe('Simulate one full cycle on start so particles appear immediately'),
368
+ duration: z.number().optional().describe('Total emission duration in seconds'),
369
+
370
+ // Shape
371
+ shape: EmitterShapeSchema.optional().describe(
372
+ 'Emitter shape that defines where particles spawn',
373
+ ),
374
+
375
+ // Per-particle initial values
376
+ startLife: ValueGeneratorSchema.optional().describe('Initial particle lifetime in seconds'),
377
+ startSpeed: ValueGeneratorSchema.optional().describe('Initial particle speed'),
378
+ startSize: ValueGeneratorSchema.optional().describe('Initial particle size'),
379
+ startRotation: ValueGeneratorSchema.optional().describe('Initial particle rotation in radians'),
380
+ startColor: ColorGeneratorSchema.optional().describe('Initial particle color'),
381
+ startLength: ValueGeneratorSchema.optional().describe(
382
+ 'Initial trail length (trail render mode only)',
383
+ ),
384
+ startTileIndex: ValueGeneratorSchema.optional().describe('Initial sprite sheet tile index'),
385
+
386
+ // Emission
387
+ emissionOverTime: ValueGeneratorSchema.optional().describe('Particles emitted per second'),
388
+ emissionOverDistance: ValueGeneratorSchema.optional().describe(
389
+ 'Particles emitted per unit of emitter movement',
390
+ ),
391
+ emissionBursts: z.array(BurstSchema).optional().describe('Timed burst emissions'),
392
+
393
+ // Behaviors
394
+ behaviors: z
395
+ .array(BehaviorSchema)
396
+ .optional()
397
+ .describe('Behaviors applied to particles over their lifetime'),
398
+
399
+ // Rendering
400
+ renderMode: RenderModeSchema.optional().describe(
401
+ 'How particles are rendered (billboard, trail, mesh, etc.)',
402
+ ),
403
+ worldSpace: z
404
+ .boolean()
405
+ .optional()
406
+ .describe('Simulate particles in world space instead of local space'),
407
+ renderOrder: z
408
+ .number()
409
+ .optional()
410
+ .describe('Render order for sorting (higher = rendered later)'),
411
+ uTileCount: z.number().int().optional().describe('Horizontal tile count in sprite sheet'),
412
+ vTileCount: z.number().int().optional().describe('Vertical tile count in sprite sheet'),
413
+ blendTiles: z
414
+ .boolean()
415
+ .optional()
416
+ .describe('Blend between sprite sheet tiles for smooth animation'),
417
+ softParticles: z.boolean().optional().describe('Enable soft particle depth fading'),
418
+ softFarFade: z.number().optional().describe('Far fade distance for soft particles'),
419
+ softNearFade: z.number().optional().describe('Near fade distance for soft particles'),
420
+
421
+ // Material
422
+ material: ParticleMaterialSchema.describe('Particle material settings'),
423
+ })
424
+ .describe('Particle system configuration');
425
+
426
+ export type ParticlesDescriptor = z.infer<typeof ParticlesDescriptorSchema>;