lecodes-cli 0.17.1 → 0.17.2
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/dist/index.js +3 -3
- package/package.json +3 -3
- package/runtime/sdk/gl/CharacterController.ts +3 -0
- package/runtime/sdk/gl/Material.ts +12 -0
- package/runtime/sdk/gl/Particles.ts +676 -625
- package/runtime/sdk/gl/Physics.ts +126 -119
- package/runtime/sdk/gl/Scene.ts +299 -291
- package/runtime/sdk/gl/Shape.ts +37 -1
- package/runtime/sdk/gl/Trigger.ts +50 -46
- package/runtime/sdk-types.json +1 -1
|
@@ -1,625 +1,676 @@
|
|
|
1
|
-
// GPU particle system, as a Node. Draws with the default point-sprite material (Material.particles)
|
|
2
|
-
// unless a custom one is passed; properties drive the native emitter.
|
|
3
|
-
//
|
|
4
|
-
// All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
|
|
5
|
-
// records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
|
|
6
|
-
// call, live setters send a one-record buffer. The tag values mirror PsTag in
|
|
7
|
-
// creator-gl/src/particles.h (guarded by tests/particles-tags.test.ts).
|
|
8
|
-
//
|
|
9
|
-
// Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
|
|
10
|
-
// below. A builder's whole state is the flat `_data` array (the exact record payload), so the
|
|
11
|
-
// chisel compiler can constant-fold literal chains into the buffer at compile time.
|
|
12
|
-
|
|
13
|
-
import { Color, type ColorInput } from "../core/color"
|
|
14
|
-
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
15
|
-
import type { Geometry } from "./Geometry"
|
|
16
|
-
import { Material, _sheetColsRows, type ParticlesMaterialOptions } from "./Material"
|
|
17
|
-
import { Node } from "./Node"
|
|
18
|
-
|
|
19
|
-
type Range<T> = T | { min: T, max: T }
|
|
20
|
-
|
|
21
|
-
// --- config record tags (keep in sync with PsTag in creator-gl/src/particles.h) -----------------
|
|
22
|
-
|
|
23
|
-
const TAG_SHAPE = 1
|
|
24
|
-
const TAG_LIFETIME = 2
|
|
25
|
-
const TAG_RATE = 3
|
|
26
|
-
const TAG_VELOCITY = 4
|
|
27
|
-
const TAG_VELOCITY_RND = 5
|
|
28
|
-
const TAG_ACCELERATION = 6
|
|
29
|
-
const TAG_DRAG = 7
|
|
30
|
-
const TAG_NOISE = 8
|
|
31
|
-
const TAG_SEED = 9
|
|
32
|
-
const TAG_PARAM_CURVE = 10
|
|
33
|
-
const TAG_COLOR_CURVE = 11
|
|
34
|
-
const TAG_SPACE = 12
|
|
35
|
-
const TAG_INHERIT_VELOCITY = 13
|
|
36
|
-
const TAG_RATE_DISTANCE = 14
|
|
37
|
-
const TAG_RENDER_MODE = 15
|
|
38
|
-
const TAG_MESH_GEOMETRY = 16
|
|
39
|
-
const TAG_ANGULAR_VELOCITY = 17
|
|
40
|
-
const TAG_GROUND = 18
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// (
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
out
|
|
121
|
-
out[
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this._lo = Color.toRgba01(base)
|
|
145
|
-
this._hi =
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
lo =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
out
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
type
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
radius?: number
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
export type
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
*
|
|
283
|
-
frame
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
} else {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
out.push(
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
out.push(
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
set
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
set
|
|
519
|
-
set
|
|
520
|
-
|
|
521
|
-
set
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1
|
+
// GPU particle system, as a Node. Draws with the default point-sprite material (Material.particles)
|
|
2
|
+
// unless a custom one is passed; properties drive the native emitter.
|
|
3
|
+
//
|
|
4
|
+
// All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
|
|
5
|
+
// records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
|
|
6
|
+
// call, live setters send a one-record buffer. The tag values mirror PsTag in
|
|
7
|
+
// creator-gl/src/particles.h (guarded by tests/particles-tags.test.ts).
|
|
8
|
+
//
|
|
9
|
+
// Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
|
|
10
|
+
// below. A builder's whole state is the flat `_data` array (the exact record payload), so the
|
|
11
|
+
// chisel compiler can constant-fold literal chains into the buffer at compile time.
|
|
12
|
+
|
|
13
|
+
import { Color, type ColorInput } from "../core/color"
|
|
14
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
15
|
+
import type { Geometry } from "./Geometry"
|
|
16
|
+
import { Material, _sheetColsRows, type ParticlesMaterialOptions } from "./Material"
|
|
17
|
+
import { Node } from "./Node"
|
|
18
|
+
|
|
19
|
+
type Range<T> = T | { min: T, max: T }
|
|
20
|
+
|
|
21
|
+
// --- config record tags (keep in sync with PsTag in creator-gl/src/particles.h) -----------------
|
|
22
|
+
|
|
23
|
+
const TAG_SHAPE = 1
|
|
24
|
+
const TAG_LIFETIME = 2
|
|
25
|
+
const TAG_RATE = 3
|
|
26
|
+
const TAG_VELOCITY = 4
|
|
27
|
+
const TAG_VELOCITY_RND = 5
|
|
28
|
+
const TAG_ACCELERATION = 6
|
|
29
|
+
const TAG_DRAG = 7
|
|
30
|
+
const TAG_NOISE = 8
|
|
31
|
+
const TAG_SEED = 9
|
|
32
|
+
const TAG_PARAM_CURVE = 10
|
|
33
|
+
const TAG_COLOR_CURVE = 11
|
|
34
|
+
const TAG_SPACE = 12
|
|
35
|
+
const TAG_INHERIT_VELOCITY = 13
|
|
36
|
+
const TAG_RATE_DISTANCE = 14
|
|
37
|
+
const TAG_RENDER_MODE = 15
|
|
38
|
+
const TAG_MESH_GEOMETRY = 16
|
|
39
|
+
const TAG_ANGULAR_VELOCITY = 17
|
|
40
|
+
const TAG_GROUND = 18
|
|
41
|
+
const TAG_VELOCITY_LIFE = 19
|
|
42
|
+
const TAG_ORDER = 20
|
|
43
|
+
|
|
44
|
+
/** @internal — exported for the tag-sync test only. */
|
|
45
|
+
export const _particleTags = {
|
|
46
|
+
SHAPE: TAG_SHAPE, LIFETIME: TAG_LIFETIME, RATE: TAG_RATE, VELOCITY: TAG_VELOCITY,
|
|
47
|
+
VELOCITY_RND: TAG_VELOCITY_RND, ACCELERATION: TAG_ACCELERATION, DRAG: TAG_DRAG,
|
|
48
|
+
NOISE: TAG_NOISE, SEED: TAG_SEED, PARAM_CURVE: TAG_PARAM_CURVE, COLOR_CURVE: TAG_COLOR_CURVE,
|
|
49
|
+
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
50
|
+
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
51
|
+
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
52
|
+
ORDER: TAG_ORDER,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- curve builders -----------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
// A value curve reads as a journey over the particle's life:
|
|
58
|
+
// curve(0.3).from(0).via(0.15, 1).via(0.6, 1).to(0) // fade in, hold, fade out — or:
|
|
59
|
+
// curve(0.3).fade(0.15, 0.4)
|
|
60
|
+
// The curve multiplies the base value (or adds to it with mode 'add'); the base and any stop can be
|
|
61
|
+
// a { min, max } range, randomized per particle. With no `.from` the curve starts at the identity
|
|
62
|
+
// (1 for multiply, 0 for add); with no `.to` it holds its last value to end of life.
|
|
63
|
+
//
|
|
64
|
+
// chisel constant-folds a literal chain into its `_data` buffer at compile time and validates it
|
|
65
|
+
// (bad stop order, >8 stops, unparsable color → a build diagnostic): chisel-core/src/curve.rs mirrors
|
|
66
|
+
// these method bodies op-for-op, against a verbatim copy of them in chisel's fixtures/curve-sdk/. Keep
|
|
67
|
+
// the two in step — a change here means a change there (chisel's scripts/curve-exact.mjs compares the
|
|
68
|
+
// fused payload against the real builder, buffer for buffer).
|
|
69
|
+
|
|
70
|
+
const lohi = (v: Range<number>): [number, number] =>
|
|
71
|
+
typeof v === "number" ? [ v, v ] : [ v.min, v.max ]
|
|
72
|
+
|
|
73
|
+
export class CurveBuilder {
|
|
74
|
+
private _kind: 0 | 1
|
|
75
|
+
private _lo: number
|
|
76
|
+
private _hi: number
|
|
77
|
+
private _stops: number[] = [] // flat (t, lo, hi)
|
|
78
|
+
|
|
79
|
+
constructor(base: Range<number>, mode: "multiply" | "add") {
|
|
80
|
+
this._kind = mode === "add" ? 1 : 0
|
|
81
|
+
;[ this._lo, this._hi ] = lohi(base)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Curve value at birth (t = 0). Must come before any other stop. */
|
|
85
|
+
from(v: Range<number>): this {
|
|
86
|
+
if (this._stops.length > 0) throw new Error("curve: .from() must come first")
|
|
87
|
+
return this.via(0, v)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Curve value at `t` (0..1 of the particle's life). Stops must be added in ascending t order. */
|
|
91
|
+
via(t: number, v: Range<number>): this {
|
|
92
|
+
const [ lo, hi ] = lohi(v)
|
|
93
|
+
this._stops.push(t, lo, hi)
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Curve value at death (t = 1). */
|
|
98
|
+
to(v: Range<number>): this {
|
|
99
|
+
return this.via(1, v)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The classic opacity envelope: rise over the first `fadeIn`, fall over the last `fadeOut`. */
|
|
103
|
+
fade(fadeIn = 0.15, fadeOut = fadeIn): this {
|
|
104
|
+
const up = Math.min(Math.max(fadeIn, 0), 1)
|
|
105
|
+
const down = Math.min(Math.max(fadeOut, 0), 1 - up)
|
|
106
|
+
if (up > 0) this.via(0, 0).via(up, 1)
|
|
107
|
+
else this.via(0, 1)
|
|
108
|
+
if (down > 0) {
|
|
109
|
+
if (1 - down > up) this.via(1 - down, 1)
|
|
110
|
+
this.via(1, 0)
|
|
111
|
+
}
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The PARAM_CURVE record payload (sans slot): [kind, baseLo, baseHi, n, (t, lo, hi)×n]. */
|
|
116
|
+
get _data(): Float32Array {
|
|
117
|
+
const stops = this._stops
|
|
118
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
119
|
+
const n = stops.length / 3 + (prepend ? 1 : 0)
|
|
120
|
+
const out = new Float32Array(4 + n * 3)
|
|
121
|
+
out[0] = this._kind
|
|
122
|
+
out[1] = this._lo
|
|
123
|
+
out[2] = this._hi
|
|
124
|
+
out[3] = n
|
|
125
|
+
let o = 4
|
|
126
|
+
if (prepend) {
|
|
127
|
+
const identity = this._kind === 1 ? 0 : 1
|
|
128
|
+
out[o + 1] = identity
|
|
129
|
+
out[o + 2] = identity
|
|
130
|
+
o += 3
|
|
131
|
+
}
|
|
132
|
+
out.set(stops, o)
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class ColorCurveBuilder {
|
|
138
|
+
private _lo: [number, number, number, number]
|
|
139
|
+
private _hi: [number, number, number, number]
|
|
140
|
+
private _stops: number[] = [] // flat (t, lo rgba, hi rgba)
|
|
141
|
+
|
|
142
|
+
constructor(base: Range<ColorInput>) {
|
|
143
|
+
if (isColorRange(base)) {
|
|
144
|
+
this._lo = Color.toRgba01(base.min)
|
|
145
|
+
this._hi = Color.toRgba01(base.max)
|
|
146
|
+
} else {
|
|
147
|
+
this._lo = Color.toRgba01(base)
|
|
148
|
+
this._hi = this._lo
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Curve color at birth (t = 0). Must come first. The curve multiplies the base color. */
|
|
153
|
+
from(c: Range<ColorInput>): this {
|
|
154
|
+
if (this._stops.length > 0) throw new Error("colorCurve: .from() must come first")
|
|
155
|
+
return this.via(0, c)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Curve color at `t` (0..1 of the particle's life), in ascending t order. */
|
|
159
|
+
via(t: number, c: Range<ColorInput>): this {
|
|
160
|
+
let lo: number[], hi: number[]
|
|
161
|
+
if (isColorRange(c)) {
|
|
162
|
+
lo = Color.toRgba01(c.min)
|
|
163
|
+
hi = Color.toRgba01(c.max)
|
|
164
|
+
} else {
|
|
165
|
+
lo = hi = Color.toRgba01(c)
|
|
166
|
+
}
|
|
167
|
+
this._stops.push(t, ...lo, ...hi)
|
|
168
|
+
return this
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Curve color at death (t = 1). */
|
|
172
|
+
to(c: Range<ColorInput>): this {
|
|
173
|
+
return this.via(1, c)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The COLOR_CURVE record payload: [baseLo rgba, baseHi rgba, n, (t, lo rgba, hi rgba)×n]. */
|
|
177
|
+
get _data(): Float32Array {
|
|
178
|
+
const stops = this._stops
|
|
179
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
180
|
+
const n = stops.length / 9 + (prepend ? 1 : 0)
|
|
181
|
+
const out = new Float32Array(9 + n * 9)
|
|
182
|
+
out.set(this._lo, 0)
|
|
183
|
+
out.set(this._hi, 4)
|
|
184
|
+
out[8] = n
|
|
185
|
+
let o = 9
|
|
186
|
+
if (prepend) {
|
|
187
|
+
for (let i = 1; i < 9; i++) out[o + i] = 1 // identity multiplier: white, t = 0
|
|
188
|
+
o += 9
|
|
189
|
+
}
|
|
190
|
+
out.set(stops, o)
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A value curve over each particle's lifetime. Default mode `multiply` (the curve scales the
|
|
196
|
+
* base); `add` offsets it instead. */
|
|
197
|
+
export const curve = (base: Range<number> = 1, mode: "multiply" | "add" = "multiply"): CurveBuilder =>
|
|
198
|
+
new CurveBuilder(base, mode)
|
|
199
|
+
|
|
200
|
+
/** A color curve over each particle's lifetime — always multiplies the base color. */
|
|
201
|
+
export const colorCurve = (base: Range<ColorInput> = "#ffffff"): ColorCurveBuilder =>
|
|
202
|
+
new ColorCurveBuilder(base)
|
|
203
|
+
|
|
204
|
+
// --- option types -------------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export type ParticleValue = Range<number> | CurveBuilder | { _data: Float32Array }
|
|
207
|
+
export type ParticleColor = ColorInput | { min: ColorInput, max: ColorInput } | ColorCurveBuilder | { _data: Float32Array }
|
|
208
|
+
|
|
209
|
+
type VelocityValue = {
|
|
210
|
+
speed?: Range<number>
|
|
211
|
+
/** Cone half-angle in radians — jitter the launch direction uniformly within it. */
|
|
212
|
+
spread?: number
|
|
213
|
+
/** Asymmetric jitter: two angles (u, v — the x/y components) in radians. */
|
|
214
|
+
randomizeAngle?: { min: Vec3Like, max: Vec3Like }
|
|
215
|
+
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
|
|
216
|
+
|
|
217
|
+
/** Where particles are born, in emitter space. Sphere / hemisphere / cone have an axis of +Y (the
|
|
218
|
+
* hemisphere is the +Y half, the cone opens along +Y from its base disc at `center`) — aim the
|
|
219
|
+
* node with `lookAt(target, 'y')`. `thickness` 0 emits from the surface only, 1 (default) from the
|
|
220
|
+
* whole volume. A sphere or cone also sets the LAUNCH DIRECTION (outward / from the cone's apex)
|
|
221
|
+
* when `startVelocity` gives a speed but no direction, like Unity's shapes. */
|
|
222
|
+
type Shape =
|
|
223
|
+
| { type: "point", v: Vec3Like }
|
|
224
|
+
| { type: "box", min: Vec3Like, max: Vec3Like }
|
|
225
|
+
| { type: "circle", center: Vec3Like, radius: number }
|
|
226
|
+
| { type: "sphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
227
|
+
| { type: "hemisphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
228
|
+
| { type: "cone", center?: Vec3Like, radius: number, /** half-angle, radians */ angle?: number, /** emit this far up the axis too */ length?: number, thickness?: number }
|
|
229
|
+
/** Extra velocity over each particle's life — evaluated from the curves every frame and ADDED as a
|
|
230
|
+
* displacement (not accumulated like `acceleration`): `radial` pushes outward along the particle's
|
|
231
|
+
* birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
|
|
232
|
+
* space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
|
|
233
|
+
export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
|
|
234
|
+
type Noise = { strength?: number, frequency?: number, speed?: number }
|
|
235
|
+
|
|
236
|
+
/** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
|
|
237
|
+
* world y with `space: 'world'`, emitter-local y otherwise. */
|
|
238
|
+
export type ParticleGround = {
|
|
239
|
+
/** Plane height (default 0). */
|
|
240
|
+
height?: number
|
|
241
|
+
/** Contact radius — particles stop `radius` above the plane (a mesh's half-extent; default 0). */
|
|
242
|
+
radius?: number
|
|
243
|
+
/** Fraction of the vertical speed kept on impact, 0..1 (default 0.3). */
|
|
244
|
+
bounce?: number
|
|
245
|
+
/** Tangential damping while in contact, per second — like `drag` (default 8). */
|
|
246
|
+
friction?: number
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
250
|
+
/** Draw every particle as this mesh — lit, tumbling 3D debris (shell casings, rubble, leaves,
|
|
251
|
+
* shrapnel) in one draw call. The system switches to the mesh render mode; the default
|
|
252
|
+
* material becomes `Material.lit()` (pass `material` for color/roughness/metallic — the
|
|
253
|
+
* sprite look options are ignored). `size` scales the mesh (default 1); `rotation`/`opacity`/
|
|
254
|
+
* `frame` don't apply. See `angularVelocity` and `ground`. */
|
|
255
|
+
mesh?: Geometry
|
|
256
|
+
/** (mesh) Spin, radians per second about the particle's own axes — a vector, or a per-component
|
|
257
|
+
* `{ min, max }` range randomized at spawn. Particles are born in the emitter's orientation. */
|
|
258
|
+
angularVelocity?: Vec3Like | { min: Vec3Like, max: Vec3Like }
|
|
259
|
+
/** A ground plane the particles bounce on and come to rest on (any render mode); `null`
|
|
260
|
+
* disables it. */
|
|
261
|
+
ground?: ParticleGround | null
|
|
262
|
+
/** Custom draw material. Unset = the default point-sprite material, configured by the
|
|
263
|
+
* `ParticlesMaterialOptions` sugar (map/sheet/emissive/blend/soft) — which is ignored
|
|
264
|
+
* when an explicit material is passed. */
|
|
265
|
+
material?: Material
|
|
266
|
+
/** Particle pool capacity (default 1000). */
|
|
267
|
+
maxParticles?: number
|
|
268
|
+
/** Particles emitted per second. */
|
|
269
|
+
rate?: number
|
|
270
|
+
/** Simulation space. `'local'` (default) — particles ride the node's transform; `'world'` —
|
|
271
|
+
* each particle stays where it was born as the emitter moves on (trails: drift smoke, mud,
|
|
272
|
+
* wake). Switching at runtime resets live particles. */
|
|
273
|
+
space?: "local" | "world"
|
|
274
|
+
/** Fraction (usually 0..1) of the emitter's own velocity added to each particle at spawn —
|
|
275
|
+
* smoke "thrown" off a moving car. Takes effect with `space: 'world'`. */
|
|
276
|
+
inheritVelocity?: number
|
|
277
|
+
/** Particles emitted per world unit the emitter MOVES, on top of `rate`, with spawn points
|
|
278
|
+
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
279
|
+
* Meant for `space: 'world'`. */
|
|
280
|
+
rateOverDistance?: number
|
|
281
|
+
/** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
|
|
282
|
+
* sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
|
|
283
|
+
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
284
|
+
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
285
|
+
order?: number
|
|
286
|
+
shape?: Shape
|
|
287
|
+
/** Extra velocity over life (radial burst that decays, axis drift) — see `VelocityOverLife`. */
|
|
288
|
+
velocityOverLife?: VelocityOverLife
|
|
289
|
+
startVelocity?: Vec3Like | VelocityValue
|
|
290
|
+
/** Downward acceleration, world units/s² — positive pulls particles DOWN (sugar for
|
|
291
|
+
* `acceleration: [0, -g, 0]`). */
|
|
292
|
+
gravity?: number
|
|
293
|
+
/** Constant acceleration vector (gravity, wind, …). Wins over `gravity` when both are set. */
|
|
294
|
+
acceleration?: Vec3Like
|
|
295
|
+
drag?: number
|
|
296
|
+
lifetime?: Range<number>
|
|
297
|
+
color?: ParticleColor
|
|
298
|
+
/** World-unit diameter (alias of custom[0]). Default 1. */
|
|
299
|
+
size?: ParticleValue
|
|
300
|
+
/** Radians (alias of custom[1]). */
|
|
301
|
+
rotation?: ParticleValue
|
|
302
|
+
/** Per-particle opacity 0..1 (alias of custom[2]). Default 1. */
|
|
303
|
+
opacity?: ParticleValue
|
|
304
|
+
/** Flipbook frame index (alias of custom[3]). With `sheet` set and no explicit frame, the
|
|
305
|
+
* default is one full sheet cycle over each particle's lifetime, starting at `startFrame`. */
|
|
306
|
+
frame?: ParticleValue
|
|
307
|
+
/** Where the default flipbook cycle begins: `'random'` (default — desynchronized loops like
|
|
308
|
+
* fire/smoke) or a frame index (`0` plays the sheet in order — explosions, one-shot bursts).
|
|
309
|
+
* Only shapes the default; an explicit `frame` wins. */
|
|
310
|
+
startFrame?: number | "random"
|
|
311
|
+
noise?: Noise | null
|
|
312
|
+
/** Fixed random seed for deterministic tests. */
|
|
313
|
+
seed?: number
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- record assembly ----------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const isColorRange = (v: unknown): v is { min: ColorInput, max: ColorInput } =>
|
|
319
|
+
typeof v === "object" && v !== null && !Array.isArray(v) && "min" in v
|
|
320
|
+
|
|
321
|
+
const hasData = (v: unknown): v is { _data: Float32Array } =>
|
|
322
|
+
typeof v === "object" && v !== null && "_data" in v
|
|
323
|
+
|
|
324
|
+
const pushParam = (out: number[], slot: number, value: ParticleValue): void => {
|
|
325
|
+
if (typeof value === "number") {
|
|
326
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value, value, 0)
|
|
327
|
+
} else if (hasData(value)) {
|
|
328
|
+
const d = value._data
|
|
329
|
+
out.push(TAG_PARAM_CURVE, d.length + 1, slot)
|
|
330
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
331
|
+
} else {
|
|
332
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value.min, value.max, 0)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const pushColor = (out: number[], value: ParticleColor): void => {
|
|
337
|
+
let d: Float32Array
|
|
338
|
+
if (hasData(value)) {
|
|
339
|
+
d = value._data
|
|
340
|
+
} else if (isColorRange(value)) {
|
|
341
|
+
d = new ColorCurveBuilder(value)._data
|
|
342
|
+
} else {
|
|
343
|
+
d = new ColorCurveBuilder(value as ColorInput)._data
|
|
344
|
+
}
|
|
345
|
+
out.push(TAG_COLOR_CURVE, d.length)
|
|
346
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const pushMesh = (out: number[], g: Geometry): void => {
|
|
350
|
+
const nv = g.vertices.length / 3
|
|
351
|
+
const ni = g.indices.length
|
|
352
|
+
out.push(TAG_MESH_GEOMETRY, 2 + nv * 8 + ni, nv, ni)
|
|
353
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.vertices[i])
|
|
354
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.normals[i])
|
|
355
|
+
for (let i = 0; i < nv * 2; i++) out.push(g.uv[i])
|
|
356
|
+
for (let i = 0; i < ni; i++) out.push(g.indices[i])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const pushAngularVelocity = (out: number[], v: Vec3Like | { min: Vec3Like, max: Vec3Like }): void => {
|
|
360
|
+
const r = v as { min: Vec3Like, max: Vec3Like }
|
|
361
|
+
if (typeof r === "object" && r !== null && !Array.isArray(r) && "min" in r && !(v instanceof Float32Array) && !(v instanceof Vec3)) {
|
|
362
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(r.min), cy(r.min), cz(r.min), cx(r.max), cy(r.max), cz(r.max))
|
|
363
|
+
} else {
|
|
364
|
+
const a = v as Vec3Like
|
|
365
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(a), cy(a), cz(a), cx(a), cy(a), cz(a))
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const pushGround = (out: number[], g: ParticleGround | null): void => {
|
|
370
|
+
if (g === null) out.push(TAG_GROUND, 5, 0, 0, 0, 0, 0)
|
|
371
|
+
else out.push(TAG_GROUND, 5, 1, g.height ?? 0, g.radius ?? 0, g.bounce ?? 0.3, g.friction ?? 8)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const pushShape = (out: number[], shape: Shape): void => {
|
|
375
|
+
if (shape.type === "point") out.push(TAG_SHAPE, 4, 0, cx(shape.v), cy(shape.v), cz(shape.v))
|
|
376
|
+
else if (shape.type === "box") out.push(TAG_SHAPE, 7, 1, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
|
|
377
|
+
else if (shape.type === "circle") out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
|
|
378
|
+
else {
|
|
379
|
+
const c = shape.center ?? [ 0, 0, 0 ]
|
|
380
|
+
const thickness = shape.thickness ?? 1
|
|
381
|
+
if (shape.type === "cone") out.push(TAG_SHAPE, 8, 5, cx(c), cy(c), cz(c), shape.radius, shape.angle ?? 0, shape.length ?? 0, thickness)
|
|
382
|
+
else out.push(TAG_SHAPE, 6, shape.type === "sphere" ? 3 : 4, cx(c), cy(c), cz(c), shape.radius, thickness)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const pushVelocityLife = (out: number[], v: VelocityOverLife): void => {
|
|
387
|
+
const axes: Array<[number, ParticleValue | undefined]> = [ [ 0, v.radial ], [ 1, v.x ], [ 2, v.y ], [ 3, v.z ] ]
|
|
388
|
+
for (const [ axis, value ] of axes) {
|
|
389
|
+
if (value === undefined) continue
|
|
390
|
+
if (typeof value === "number") out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value, value, 0)
|
|
391
|
+
else if (hasData(value)) {
|
|
392
|
+
const d = value._data
|
|
393
|
+
out.push(TAG_VELOCITY_LIFE, d.length + 1, axis)
|
|
394
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
395
|
+
} else out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value.min, value.max, 0)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const pushVelocity = (out: number[], val: Vec3Like | VelocityValue): void => {
|
|
400
|
+
if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
|
|
401
|
+
// a plain vector: launch along it, its length is the speed (native derives it)
|
|
402
|
+
out.push(TAG_VELOCITY, 6, 1, cx(val), cy(val), cz(val), 0, 0)
|
|
403
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
const v = val as VelocityValue
|
|
407
|
+
let mode = 0
|
|
408
|
+
let p: Vec3Like = [ 0, 0, 0 ]
|
|
409
|
+
if ("dir" in v) { mode = 1; p = v.dir }
|
|
410
|
+
else if ("from" in v) { mode = 2; p = v.from }
|
|
411
|
+
else if ("to" in v) { mode = 3; p = v.to }
|
|
412
|
+
let lo = 0, hi = 0
|
|
413
|
+
if (v.speed !== undefined) {
|
|
414
|
+
if (typeof v.speed === "number") lo = hi = v.speed
|
|
415
|
+
else { lo = v.speed.min; hi = v.speed.max }
|
|
416
|
+
}
|
|
417
|
+
out.push(TAG_VELOCITY, 6, mode, cx(p), cy(p), cz(p), lo, hi)
|
|
418
|
+
if (v.spread !== undefined) {
|
|
419
|
+
out.push(TAG_VELOCITY_RND, 5, 1, -v.spread, -v.spread, v.spread, v.spread)
|
|
420
|
+
} else if (v.randomizeAngle) {
|
|
421
|
+
const a = v.randomizeAngle
|
|
422
|
+
out.push(TAG_VELOCITY_RND, 5, 1, cx(a.min), cy(a.min), cx(a.max), cy(a.max))
|
|
423
|
+
} else {
|
|
424
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// --- the node -----------------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
export class Particles extends Node {
|
|
431
|
+
private _material: Material
|
|
432
|
+
|
|
433
|
+
/** Custom curve-param slots (0..3); size/rotation/opacity/frame alias slots 0-3. */
|
|
434
|
+
readonly custom: ParticleValue[]
|
|
435
|
+
|
|
436
|
+
constructor(options: ParticlesOptions = {}) {
|
|
437
|
+
super()
|
|
438
|
+
this._material = options.material ?? (options.mesh ? Material.lit() : Material.particles(options))
|
|
439
|
+
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
|
|
440
|
+
|
|
441
|
+
const id = this.id
|
|
442
|
+
this.custom = new Proxy([] as ParticleValue[], {
|
|
443
|
+
set(target, prop: string, value: ParticleValue) {
|
|
444
|
+
const i = parseInt(prop)
|
|
445
|
+
if (Number.isNaN(i)) { (target as any)[prop] = value; return true }
|
|
446
|
+
target[i] = value
|
|
447
|
+
const out: number[] = []
|
|
448
|
+
pushParam(out, i, value)
|
|
449
|
+
_creator.setParticleSystemConfig(id, Float32Array.from(out))
|
|
450
|
+
return true
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const out: number[] = []
|
|
455
|
+
if (options.mesh) {
|
|
456
|
+
out.push(TAG_RENDER_MODE, 1, 3)
|
|
457
|
+
pushMesh(out, options.mesh)
|
|
458
|
+
} else if (options.render !== undefined && options.render !== "point") {
|
|
459
|
+
out.push(TAG_RENDER_MODE, 1, options.render === "ribbon" ? 2 : 1)
|
|
460
|
+
}
|
|
461
|
+
if (options.angularVelocity !== undefined) pushAngularVelocity(out, options.angularVelocity)
|
|
462
|
+
if (options.ground !== undefined) pushGround(out, options.ground)
|
|
463
|
+
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
464
|
+
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
465
|
+
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
466
|
+
if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
|
|
467
|
+
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
468
|
+
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
469
|
+
if (options.velocityOverLife !== undefined) pushVelocityLife(out, options.velocityOverLife)
|
|
470
|
+
if (options.startVelocity !== undefined) pushVelocity(out, options.startVelocity)
|
|
471
|
+
if (options.acceleration !== undefined) {
|
|
472
|
+
const a = options.acceleration
|
|
473
|
+
out.push(TAG_ACCELERATION, 3, cx(a), cy(a), cz(a))
|
|
474
|
+
} else if (options.gravity !== undefined) {
|
|
475
|
+
out.push(TAG_ACCELERATION, 3, 0, -options.gravity, 0)
|
|
476
|
+
}
|
|
477
|
+
if (options.drag !== undefined) out.push(TAG_DRAG, 1, options.drag)
|
|
478
|
+
if (options.lifetime !== undefined) {
|
|
479
|
+
const [ lo, hi ] = lohi(options.lifetime)
|
|
480
|
+
out.push(TAG_LIFETIME, 2, lo, hi)
|
|
481
|
+
}
|
|
482
|
+
if (options.color !== undefined) pushColor(out, options.color)
|
|
483
|
+
if (options.size !== undefined) pushParam(out, 0, options.size)
|
|
484
|
+
if (options.rotation !== undefined) pushParam(out, 1, options.rotation)
|
|
485
|
+
if (options.opacity !== undefined) pushParam(out, 2, options.opacity)
|
|
486
|
+
if (options.frame !== undefined) {
|
|
487
|
+
pushParam(out, 3, options.frame)
|
|
488
|
+
} else if (options.material === undefined && !options.mesh) {
|
|
489
|
+
const [ cols, rows ] = _sheetColsRows(options.sheet)
|
|
490
|
+
const frames = cols * rows
|
|
491
|
+
if (frames > 1) {
|
|
492
|
+
const start = options.startFrame ?? "random"
|
|
493
|
+
const base = start === "random" ? { min: 0, max: frames - 1 } : start
|
|
494
|
+
pushParam(out, 3, curve(base, "add").from(0).to(frames))
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (options.noise !== undefined) {
|
|
498
|
+
const n = options.noise
|
|
499
|
+
out.push(TAG_NOISE, 3, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0)
|
|
500
|
+
}
|
|
501
|
+
if (options.seed !== undefined) out.push(TAG_SEED, 1, options.seed)
|
|
502
|
+
|
|
503
|
+
if (out.length > 0) _creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Emit `count` particles immediately (a burst). */
|
|
507
|
+
spawn(count: number): this { _creator.spawnParticles(this.id, count); return this }
|
|
508
|
+
|
|
509
|
+
private _send(out: number[]): void {
|
|
510
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
set rate(val: number) { this._send([ TAG_RATE, 1, val ]) }
|
|
514
|
+
|
|
515
|
+
/** `'world'` leaves particles behind where they were born (trails). Switching resets live ones. */
|
|
516
|
+
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
517
|
+
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
518
|
+
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
519
|
+
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
520
|
+
|
|
521
|
+
set velocityOverLife(v: VelocityOverLife) {
|
|
522
|
+
const out: number[] = []
|
|
523
|
+
pushVelocityLife(out, v)
|
|
524
|
+
this._send(out)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
set shape(shape: Shape) {
|
|
528
|
+
const out: number[] = []
|
|
529
|
+
pushShape(out, shape)
|
|
530
|
+
this._send(out)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** (mesh mode) Replace the particle mesh; live particles keep flying as the new shape. */
|
|
534
|
+
set mesh(g: Geometry) {
|
|
535
|
+
const out: number[] = [ TAG_RENDER_MODE, 1, 3 ]
|
|
536
|
+
pushMesh(out, g)
|
|
537
|
+
this._send(out)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
set angularVelocity(v: Vec3Like | { min: Vec3Like, max: Vec3Like }) {
|
|
541
|
+
const out: number[] = []
|
|
542
|
+
pushAngularVelocity(out, v)
|
|
543
|
+
this._send(out)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
set ground(g: ParticleGround | null) {
|
|
547
|
+
const out: number[] = []
|
|
548
|
+
pushGround(out, g)
|
|
549
|
+
this._send(out)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
set startVelocity(val: Vec3Like | VelocityValue) {
|
|
553
|
+
const out: number[] = []
|
|
554
|
+
pushVelocity(out, val)
|
|
555
|
+
this._send(out)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Positive pulls particles down — sugar for `acceleration = [0, -g, 0]`. */
|
|
559
|
+
set gravity(val: number) { this._send([ TAG_ACCELERATION, 3, 0, -val, 0 ]) }
|
|
560
|
+
set acceleration(a: Vec3Like) { this._send([ TAG_ACCELERATION, 3, cx(a), cy(a), cz(a) ]) }
|
|
561
|
+
set drag(val: number) { this._send([ TAG_DRAG, 1, val ]) }
|
|
562
|
+
set seed(val: number) { this._send([ TAG_SEED, 1, val ]) }
|
|
563
|
+
|
|
564
|
+
set lifetime(val: Range<number>) {
|
|
565
|
+
const [ lo, hi ] = lohi(val)
|
|
566
|
+
this._send([ TAG_LIFETIME, 2, lo, hi ])
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
set size(val: ParticleValue) { this.custom[0] = val }
|
|
570
|
+
set rotation(val: ParticleValue) { this.custom[1] = val }
|
|
571
|
+
set opacity(val: ParticleValue) { this.custom[2] = val }
|
|
572
|
+
set frame(val: ParticleValue) { this.custom[3] = val }
|
|
573
|
+
|
|
574
|
+
set color(value: ParticleColor) {
|
|
575
|
+
const out: number[] = []
|
|
576
|
+
pushColor(out, value)
|
|
577
|
+
this._send(out)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
get material(): Material { return this._material }
|
|
581
|
+
set material(material: Material) {
|
|
582
|
+
this._material = material
|
|
583
|
+
_creator.setMaterial(this.id, Material.idOf(material), 0)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
set noise(noise: Noise | null) {
|
|
587
|
+
this._send(noise
|
|
588
|
+
? [ TAG_NOISE, 3, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1 ]
|
|
589
|
+
: [ TAG_NOISE, 3, 0, 0, 0 ])
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// --- ribbon trail -------------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
/** `Trail` options. The look options (map/sheet/emissive/blend/soft) come from
|
|
596
|
+
* `ParticlesMaterialOptions`, same as `Particles`. */
|
|
597
|
+
export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch"> & {
|
|
598
|
+
/** Seconds each trail point lives — the trail's length in time. Default 0.5. */
|
|
599
|
+
time?: number
|
|
600
|
+
/** Strip width in world units; a curve tapers it over each point's life (head → tail).
|
|
601
|
+
* Default 0.1. */
|
|
602
|
+
width?: ParticleValue
|
|
603
|
+
/** Minimum emitter movement (world units) between recorded points. Default 0.05. */
|
|
604
|
+
minDistance?: number
|
|
605
|
+
/** Point capacity (default 128). */
|
|
606
|
+
maxPoints?: number
|
|
607
|
+
color?: ParticleColor
|
|
608
|
+
/** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
|
|
609
|
+
opacity?: ParticleValue
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** A ribbon strip that follows this node through the world — sword swings, skid marks, missile
|
|
613
|
+
* trails. Attach it to (or under) the moving node and just move; points are laid down per
|
|
614
|
+
* `minDistance` of movement, live for `time` seconds, and the strip stays glued to the node at
|
|
615
|
+
* its head. Width/opacity/color accept the same curves as `Particles`, evaluated over each
|
|
616
|
+
* point's life — i.e. along the trail from head (fresh) to tail (dying). */
|
|
617
|
+
export class Trail extends Node {
|
|
618
|
+
private _material: Material
|
|
619
|
+
private _rateDistance: number
|
|
620
|
+
|
|
621
|
+
constructor(options: TrailOptions = {}) {
|
|
622
|
+
super()
|
|
623
|
+
this._material = Material.particles({ ...options, render: "ribbon" })
|
|
624
|
+
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxPoints ?? 128)
|
|
625
|
+
|
|
626
|
+
const time = options.time ?? 0.5
|
|
627
|
+
this._rateDistance = 1 / Math.max(options.minDistance ?? 0.05, 1e-3)
|
|
628
|
+
const out: number[] = [
|
|
629
|
+
TAG_RENDER_MODE, 1, 2,
|
|
630
|
+
TAG_SPACE, 1, 1, // a trail is only meaningful in world space
|
|
631
|
+
TAG_RATE, 1, 0, // points come from movement, never from time
|
|
632
|
+
TAG_RATE_DISTANCE, 1, this._rateDistance,
|
|
633
|
+
TAG_LIFETIME, 2, time, time,
|
|
634
|
+
]
|
|
635
|
+
pushParam(out, 0, options.width ?? 0.1)
|
|
636
|
+
pushParam(out, 2, options.opacity ?? curve().to(0))
|
|
637
|
+
if (options.color !== undefined) pushColor(out, options.color)
|
|
638
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private _send(out: number[]): void {
|
|
642
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Trail length in seconds — how long each laid-down point lives. */
|
|
646
|
+
set time(val: number) { this._send([ TAG_LIFETIME, 2, val, val ]) }
|
|
647
|
+
|
|
648
|
+
set width(val: ParticleValue) {
|
|
649
|
+
const out: number[] = []
|
|
650
|
+
pushParam(out, 0, val)
|
|
651
|
+
this._send(out)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
set opacity(val: ParticleValue) {
|
|
655
|
+
const out: number[] = []
|
|
656
|
+
pushParam(out, 2, val)
|
|
657
|
+
this._send(out)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
set color(value: ParticleColor) {
|
|
661
|
+
const out: number[] = []
|
|
662
|
+
pushColor(out, value)
|
|
663
|
+
this._send(out)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
set minDistance(val: number) {
|
|
667
|
+
this._rateDistance = 1 / Math.max(val, 1e-3)
|
|
668
|
+
this._send([ TAG_RATE_DISTANCE, 1, this._rateDistance ])
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
|
|
672
|
+
* after e.g. a sword swing ends. */
|
|
673
|
+
set emitting(val: boolean) { this._send([ TAG_RATE_DISTANCE, 1, val ? this._rateDistance : 0 ]) }
|
|
674
|
+
|
|
675
|
+
get material(): Material { return this._material }
|
|
676
|
+
}
|