babylon.quarks 0.21.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/babylon.quarks.cjs +1 -1
  3. package/dist/babylon.quarks.cjs.map +1 -1
  4. package/dist/babylon.quarks.esm.js +1 -1
  5. package/dist/babylon.quarks.esm.js.map +1 -1
  6. package/dist/babylon.quarks.umd.min.js +1 -1
  7. package/dist/babylon.quarks.umd.min.js.map +1 -1
  8. package/dist/types/index.d.ts +936 -45
  9. package/package.json +2 -2
  10. package/src/BatchedRenderer.ts +205 -4
  11. package/src/EffectPool.ts +165 -0
  12. package/src/MeshSurfaceEmitter.ts +4 -4
  13. package/src/ParticleEmitter.ts +2 -1
  14. package/src/ParticleSystem.ts +975 -69
  15. package/src/QuarksLoader.ts +25 -4
  16. package/src/QuarksUtil.ts +55 -7
  17. package/src/SpriteBatch.ts +515 -114
  18. package/src/TrailBatch.ts +128 -75
  19. package/src/VFXBatch.ts +255 -3
  20. package/src/index.ts +5 -2
  21. package/src/instanceSort.ts +121 -0
  22. package/src/shaders/chunks/fog.ts +127 -0
  23. package/src/shaders/local_particle_physics_vert.glsl.ts +4 -0
  24. package/src/shaders/local_particle_physics_vert.wgsl.ts +4 -0
  25. package/src/shaders/particle_frag.glsl.ts +4 -0
  26. package/src/shaders/particle_frag.wgsl.ts +4 -0
  27. package/src/shaders/particle_physics_frag.glsl.ts +30 -1
  28. package/src/shaders/particle_physics_frag.wgsl.ts +30 -1
  29. package/src/shaders/particle_vert.glsl.ts +23 -4
  30. package/src/shaders/particle_vert.wgsl.ts +23 -4
  31. package/src/shaders/stretched_bb_particle_vert.glsl.ts +17 -0
  32. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +18 -1
  33. package/src/shaders/trail_frag.glsl.ts +4 -0
  34. package/src/shaders/trail_frag.wgsl.ts +4 -0
  35. package/src/shaders/trail_vert.glsl.ts +4 -0
  36. package/src/shaders/trail_vert.wgsl.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.21.0",
3
+ "version": "0.24.0",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -63,7 +63,7 @@
63
63
  "@babylonjs/core": ">=9.0.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@babylonjs/core": "^9.0.0",
66
+ "@babylonjs/core": "^9.31.0",
67
67
  "@types/jest": "^30.0.0",
68
68
  "quarks.core": "*"
69
69
  }
@@ -1,12 +1,25 @@
1
+ import {Camera} from '@babylonjs/core/Cameras/camera';
1
2
  import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
3
  import {Texture} from '@babylonjs/core/Materials/Textures/texture';
4
+ import {Frustum} from '@babylonjs/core/Maths/math.frustum';
5
+ import {Plane} from '@babylonjs/core/Maths/math.plane';
3
6
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
4
7
  import {Scene} from '@babylonjs/core/scene';
5
8
  import {EmitSubParticleSystem, IParticleSystem, Vector3} from 'quarks.core';
6
9
  import {ParticleSystem} from './ParticleSystem';
7
10
  import {SpriteBatch} from './SpriteBatch';
8
11
  import {TrailBatch} from './TrailBatch';
9
- import {RenderMode, StoredBatchSettings, VFXBatch} from './VFXBatch';
12
+ import {
13
+ defaultMeshLighting,
14
+ MeshLighting,
15
+ MeshLightingSettings,
16
+ MeshRim,
17
+ meshRimEqual,
18
+ RenderMode,
19
+ SortMode,
20
+ StoredBatchSettings,
21
+ VFXBatch,
22
+ } from './VFXBatch';
10
23
 
11
24
  export interface VFXBatchSettings {
12
25
  instancingGeometry: Float32Array;
@@ -47,6 +60,17 @@ export interface VFXBatchSettings {
47
60
  */
48
61
  reflectionAtlas: BaseTexture | null;
49
62
  layerMask: number;
63
+ /**
64
+ * Babylon rendering group (`mesh.renderingGroupId`) the batch draws in,
65
+ * 0 by default. Systems with different groups are batched apart.
66
+ */
67
+ renderingGroupId: number;
68
+ /**
69
+ * Whether the scene's fog applies to the particles, as it does to any other
70
+ * mesh; true by default. Additive particles fade toward black rather than
71
+ * the fog colour. Systems that differ are batched apart.
72
+ */
73
+ applyFog: boolean;
50
74
  /**
51
75
  * Offset of billboard quads from their particle, in particle-size units
52
76
  * (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
@@ -55,6 +79,27 @@ export interface VFXBatchSettings {
55
79
  * y across, and z is unused. Absent or zero: no offset. Meshes ignore it.
56
80
  */
57
81
  pivot?: Vector3;
82
+ /** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
83
+ sortMode?: SortMode;
84
+ /**
85
+ * Smallest size a billboard or stretched particle may have on screen, as a
86
+ * fraction of the viewport height (Unity's Min Particle Size); 0 by default.
87
+ * Meshes and trails ignore it. Systems that differ are batched apart.
88
+ */
89
+ minParticleSize?: number;
90
+ /**
91
+ * Largest size a billboard or stretched particle may have on screen, as a
92
+ * fraction of the viewport height (Unity's Max Particle Size); unlimited by
93
+ * default. Meshes and trails ignore it. Systems that differ are batched apart.
94
+ */
95
+ maxParticleSize?: number;
96
+ /**
97
+ * How mesh particles are lit (`'unlit'`, `'fixed'` or `'scene'`); unset is
98
+ * `'fixed'`. See {@link MeshLighting}. Other render modes ignore it.
99
+ */
100
+ lighting?: MeshLighting;
101
+ /** Rim (Fresnel) term on mesh particles; unset draws none. See {@link MeshRim}. */
102
+ rim?: MeshRim;
58
103
  }
59
104
 
60
105
  export interface AdaptivePerformanceOptions {
@@ -130,11 +175,109 @@ export class BatchedRenderer extends TransformNode {
130
175
  lastFrameCpuMs: 0,
131
176
  };
132
177
  private lastAppliedQuality = Number.NaN;
178
+ private meshLighting: MeshLightingSettings = defaultMeshLighting();
179
+
180
+ /**
181
+ * Whether systems whose {@link ParticleSystem.cullingMode} is not
182
+ * `'always'` are tested against the active cameras each frame and paused
183
+ * while unseen. Off, every system simulates every frame; the batches' own
184
+ * frustum culling of the draw stays on either way.
185
+ */
186
+ cullingEnabled = true;
187
+
188
+ /**
189
+ * Time per frame, in milliseconds, that systems coming back into view may
190
+ * spend together on the steps they missed while culled (`'pauseAndCatchUp'`).
191
+ * Steps that do not fit are dropped, not deferred, so a crowd of effects
192
+ * reappearing at once costs at most this. 2 by default.
193
+ */
194
+ catchUpBudgetMs = 2;
195
+
196
+ /**
197
+ * Time per frame, in milliseconds, after which the systems still to be
198
+ * advanced run one step instead of catching up on a long frame. The time
199
+ * the skipped steps would have covered is dropped, so a frame that already
200
+ * ran long cannot make the next one longer still. Unlimited by default;
201
+ * {@link configureAdaptivePerformance} does not set it.
202
+ */
203
+ simulationBudgetMs = Infinity;
204
+
205
+ /** Frustum planes of the active cameras, six per camera, rebuilt each update. */
206
+ private frustumPlanes: Plane[] = [];
207
+ private readonly boundsMin = new Vector3();
208
+ private readonly boundsMax = new Vector3();
133
209
 
134
210
  constructor(name: string, scene: Scene) {
135
211
  super(name, scene);
136
212
  }
137
213
 
214
+ /**
215
+ * Gathers the frustum planes of the cameras the scene renders with, six per
216
+ * camera. Empty when there is no camera yet; systems are then never culled.
217
+ */
218
+ private collectFrustumPlanes(): Plane[] {
219
+ const scene = this.getScene();
220
+ const planes = this.frustumPlanes;
221
+ const activeCameras = scene.activeCameras;
222
+ const activeCamera = scene.activeCamera;
223
+ const cameras: Camera[] =
224
+ activeCameras && activeCameras.length > 0 ? activeCameras : activeCamera ? [activeCamera] : [];
225
+ const needed = cameras.length * 6;
226
+ while (planes.length < needed) {
227
+ planes.push(new Plane(0, 0, 0, 0));
228
+ }
229
+ planes.length = needed;
230
+ for (let c = 0; c < cameras.length; c++) {
231
+ const camera = cameras[c];
232
+ // Both matrices are recomputed only on demand: a camera moved since
233
+ // the last render would otherwise be tested where it was, and one
234
+ // that has not rendered yet has no projection at all.
235
+ camera.getViewMatrix();
236
+ camera.getProjectionMatrix();
237
+ const transform = camera.getTransformationMatrix();
238
+ const slice = c === 0 ? planes : planes.slice(c * 6, c * 6 + 6);
239
+ Frustum.GetPlanesToRef(transform, slice);
240
+ if (c !== 0) {
241
+ for (let p = 0; p < 6; p++) {
242
+ planes[c * 6 + p] = slice[p];
243
+ }
244
+ }
245
+ }
246
+ return planes;
247
+ }
248
+
249
+ /**
250
+ * True when `system`'s world bounds reach into at least one camera's
251
+ * frustum, or when it has no bounds to test (empty, or not yet measured).
252
+ */
253
+ private isSystemInView(system: ParticleSystem, planes: Plane[]): boolean {
254
+ const min = this.boundsMin;
255
+ const max = this.boundsMax;
256
+ if (!system.getWorldBounds(min, max)) {
257
+ return true;
258
+ }
259
+ for (let c = 0; c < planes.length; c += 6) {
260
+ let inside = true;
261
+ for (let p = c; p < c + 6; p++) {
262
+ const plane = planes[p];
263
+ const n = plane.normal;
264
+ // The box corner farthest along the plane's normal: if even it
265
+ // is behind the plane, the whole box is.
266
+ const x = n.x >= 0 ? max.x : min.x;
267
+ const y = n.y >= 0 ? max.y : min.y;
268
+ const z = n.z >= 0 ? max.z : min.z;
269
+ if (n.x * x + n.y * y + n.z * z + plane.d < 0) {
270
+ inside = false;
271
+ break;
272
+ }
273
+ }
274
+ if (inside) {
275
+ return true;
276
+ }
277
+ }
278
+ return false;
279
+ }
280
+
138
281
  private static clamp(value: number, min: number, max: number): number {
139
282
  return Math.max(min, Math.min(max, value));
140
283
  }
@@ -161,7 +304,14 @@ export class BatchedRenderer extends TransformNode {
161
304
  a.instancingGeometry === b.instancingGeometry &&
162
305
  a.renderOrder === b.renderOrder &&
163
306
  a.layerMask === b.layerMask &&
164
- pivotEqual(a.pivot, b.pivot)
307
+ a.renderingGroupId === b.renderingGroupId &&
308
+ a.applyFog === b.applyFog &&
309
+ pivotEqual(a.pivot, b.pivot) &&
310
+ a.sortMode === (b.sortMode ?? 'none') &&
311
+ a.minParticleSize === (b.minParticleSize ?? 0) &&
312
+ a.maxParticleSize === (b.maxParticleSize ?? Infinity) &&
313
+ (a.lighting ?? 'fixed') === (b.lighting ?? 'fixed') &&
314
+ meshRimEqual(a.rim, b.rim)
165
315
  );
166
316
  }
167
317
 
@@ -209,6 +359,7 @@ export class BatchedRenderer extends TransformNode {
209
359
  if (this.depthTexture) {
210
360
  batch.applyDepthTexture(this.depthTexture);
211
361
  }
362
+ batch.applyMeshLighting(this.meshLighting);
212
363
  batch.addSystem(system);
213
364
  this.batches.push(batch);
214
365
  this.batchEmptyFrames.push(0);
@@ -259,6 +410,43 @@ export class BatchedRenderer extends TransformNode {
259
410
  }
260
411
  }
261
412
 
413
+ /**
414
+ * Sets the light mesh particles with `lighting: 'fixed'` (the default) are
415
+ * lit by, for every mesh batch of this renderer, including ones created
416
+ * later. Fields left out keep their current value. Also the fallback for
417
+ * `'scene'` lighting when the scene has no directional or hemispheric light.
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * renderer.setMeshLighting({direction: new Vector3(-0.3, -1, 0.2), color: new Color3(1, 0.9, 0.75)});
422
+ * ```
423
+ */
424
+ setMeshLighting(lighting: Partial<MeshLightingSettings>): void {
425
+ const current = this.meshLighting;
426
+ if (lighting.direction) {
427
+ current.direction = lighting.direction.clone();
428
+ }
429
+ if (lighting.color) {
430
+ current.color = lighting.color.clone();
431
+ }
432
+ if (lighting.ambient) {
433
+ current.ambient = lighting.ambient.clone();
434
+ }
435
+ for (const batch of this.batches) {
436
+ batch.applyMeshLighting(current);
437
+ }
438
+ }
439
+
440
+ /** A copy of the light set with {@link setMeshLighting}. */
441
+ getMeshLighting(): MeshLightingSettings {
442
+ const current = this.meshLighting;
443
+ return {
444
+ direction: current.direction.clone(),
445
+ color: current.color.clone(),
446
+ ambient: current.ambient.clone(),
447
+ };
448
+ }
449
+
262
450
  configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}) {
263
451
  const state = this.adaptivePerformanceState;
264
452
  const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
@@ -361,13 +549,26 @@ export class BatchedRenderer extends TransformNode {
361
549
  this.applyAdaptiveQuality();
362
550
  }
363
551
  const systems = this.orderSystems();
552
+ const planes = this.cullingEnabled ? this.collectFrustumPlanes() : null;
553
+ const budgetStart =
554
+ this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
555
+ const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
556
+ const catchUpDeadline = this.catchUpBudgetMs === Infinity ? Infinity : budgetStart + this.catchUpBudgetMs;
364
557
  for (let i = 0; i < systems.length; i++) {
365
- const system = systems[i];
558
+ const system = systems[i] as ParticleSystem;
366
559
  // A disabled emitter holds its particles frozen instead of simulating
367
560
  // them unseen, matching how Unity treats a disabled ParticleSystem.
368
561
  // Pooled effects parked with setEnabled(false) then cost nothing.
369
562
  if (system.emitter.visible) {
370
- (system as ParticleSystem).update(delta);
563
+ // A system no camera sees pauses, by its own choice. One that
564
+ // only serves as a sub emitter target follows its parent.
565
+ system.culled =
566
+ planes !== null &&
567
+ planes.length > 0 &&
568
+ system.cullingMode !== 'always' &&
569
+ system.onlyUsedByOther !== true &&
570
+ !this.isSystemInView(system, planes);
571
+ system.update(delta, stepDeadline, catchUpDeadline);
371
572
  }
372
573
  // An autoDestroy system disposes itself from inside update and drops
373
574
  // out of the list; stay on the slot it vacated.
@@ -0,0 +1,165 @@
1
+ import {Vector3} from '@babylonjs/core/Maths/math.vector';
2
+ import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
+ import {Node} from '@babylonjs/core/node';
4
+ import {BatchedRenderer} from './BatchedRenderer';
5
+ import {ParticleSystem} from './ParticleSystem';
6
+ import {QuarksUtil} from './QuarksUtil';
7
+
8
+ export interface EffectPoolOptions {
9
+ /** The renderer every copy the pool makes is added to. */
10
+ renderer: BatchedRenderer;
11
+ /** Copies made up front, so the first plays do not pay for cloning. Default 0. */
12
+ prewarm?: number;
13
+ /** Most copies the pool will ever hold; {@link EffectPool.acquire} returns null past it. Default unlimited. */
14
+ maxSize?: number;
15
+ }
16
+
17
+ /**
18
+ * A pool of copies of one effect, for effects played many times over: hits,
19
+ * footsteps, muzzle flashes, pickups.
20
+ *
21
+ * {@link acquire} hands out an idle copy (or makes one), places it and plays
22
+ * it; a copy whose every system has finished goes back to the pool by itself,
23
+ * so a one-shot effect needs nothing more. A looping effect is ended with
24
+ * {@link stop}, which lets its particles die out before the copy returns, or
25
+ * dropped at once with {@link release}.
26
+ *
27
+ * Idle copies are parked disabled, which costs the renderer nothing, and keep
28
+ * their place in its batches, so acquiring one allocates nothing.
29
+ *
30
+ * The template itself is never played by the pool; it is only cloned. Disable
31
+ * it, or keep it out of the renderer, if it is not meant to be seen.
32
+ */
33
+ export class EffectPool {
34
+ private readonly idle: TransformNode[] = [];
35
+ private readonly playing = new Set<TransformNode>();
36
+ private readonly systemsOf = new Map<TransformNode, ParticleSystem[]>();
37
+ private disposed = false;
38
+
39
+ constructor(
40
+ private readonly template: TransformNode,
41
+ private readonly options: EffectPoolOptions
42
+ ) {
43
+ for (let i = 0; i < (options.prewarm ?? 0); i++) {
44
+ const copy = this.makeCopy();
45
+ if (copy === null) {
46
+ break;
47
+ }
48
+ this.idle.push(copy);
49
+ }
50
+ }
51
+
52
+ /** Copies currently playing. */
53
+ get activeCount(): number {
54
+ return this.playing.size;
55
+ }
56
+
57
+ /** Copies parked and ready. */
58
+ get idleCount(): number {
59
+ return this.idle.length;
60
+ }
61
+
62
+ /**
63
+ * Takes an idle copy (making one when none is idle and the pool is under
64
+ * its size), parents it, places it and starts it from the beginning.
65
+ * Null when the pool is full or disposed.
66
+ */
67
+ acquire(position?: Vector3, parent: Node | null = null): TransformNode | null {
68
+ if (this.disposed) {
69
+ return null;
70
+ }
71
+ const copy = this.idle.pop() ?? this.makeCopy();
72
+ if (copy === null) {
73
+ return null;
74
+ }
75
+ copy.parent = parent;
76
+ if (position !== undefined) {
77
+ copy.position.copyFrom(position);
78
+ }
79
+ copy.setEnabled(true);
80
+ this.playing.add(copy);
81
+ QuarksUtil.restart(copy);
82
+ QuarksUtil.play(copy);
83
+ return copy;
84
+ }
85
+
86
+ /**
87
+ * Ends the emission of a playing copy; it returns to the pool on its own
88
+ * once its particles have died, looping systems included.
89
+ */
90
+ stop(copy: TransformNode): void {
91
+ if (this.playing.has(copy)) {
92
+ QuarksUtil.endEmit(copy);
93
+ }
94
+ }
95
+
96
+ /** Puts a copy back at once, particles and all, parked and disabled. */
97
+ release(copy: TransformNode): void {
98
+ if (!this.playing.delete(copy)) {
99
+ return;
100
+ }
101
+ QuarksUtil.stop(copy);
102
+ copy.setEnabled(false);
103
+ copy.parent = null;
104
+ if (!this.disposed) {
105
+ this.idle.push(copy);
106
+ }
107
+ }
108
+
109
+ /** Disposes every copy, playing or idle. The template is left alone. */
110
+ dispose(): void {
111
+ this.disposed = true;
112
+ for (const copy of [...this.playing, ...this.idle]) {
113
+ this.playing.delete(copy);
114
+ this.disposeCopy(copy);
115
+ }
116
+ this.idle.length = 0;
117
+ }
118
+
119
+ private makeCopy(): TransformNode | null {
120
+ const made = this.idle.length + this.playing.size;
121
+ if (made >= (this.options.maxSize ?? Infinity)) {
122
+ return null;
123
+ }
124
+ const copy = QuarksUtil.instantiate(this.template);
125
+ const systems: ParticleSystem[] = [];
126
+ QuarksUtil.runOnAllParticleEmitters(copy, (emitter) => {
127
+ const system = emitter.system as unknown as ParticleSystem;
128
+ // The pool owns the copy's life; a system disposing itself would
129
+ // pull one emitter out of an effect that is meant to play again.
130
+ system.autoDestroy = false;
131
+ system.addEventListener('finished', this.onSystemFinished);
132
+ systems.push(system);
133
+ });
134
+ this.systemsOf.set(copy, systems);
135
+ QuarksUtil.addToBatchRenderer(copy, this.options.renderer);
136
+ copy.setEnabled(false);
137
+ copy.parent = null;
138
+ return copy;
139
+ }
140
+
141
+ private disposeCopy(copy: TransformNode): void {
142
+ const systems = this.systemsOf.get(copy) ?? [];
143
+ for (const system of systems) {
144
+ system.removeEventListener('finished', this.onSystemFinished);
145
+ this.options.renderer.deleteSystem(system);
146
+ system.dispose();
147
+ }
148
+ this.systemsOf.delete(copy);
149
+ copy.dispose();
150
+ }
151
+
152
+ /** The copy a system belongs to is done when all of its systems are. */
153
+ private readonly onSystemFinished = (event: {particleSystem: unknown}): void => {
154
+ const system = event.particleSystem as ParticleSystem;
155
+ for (const copy of this.playing) {
156
+ const systems = this.systemsOf.get(copy);
157
+ if (systems !== undefined && systems.includes(system)) {
158
+ if (QuarksUtil.isEffectFinished(copy)) {
159
+ this.release(copy);
160
+ }
161
+ return;
162
+ }
163
+ }
164
+ };
165
+ }
@@ -1,6 +1,6 @@
1
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
2
2
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
3
- import {EmitterShape, IParticleSystem, Particle, Plugin, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
3
+ import {EmitterShape, IParticleSystem, Particle, Plugin, random, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
4
4
 
5
5
  export class MeshSurfaceEmitter implements EmitterShape {
6
6
  type = 'mesh_surface';
@@ -72,7 +72,7 @@ export class MeshSurfaceEmitter implements EmitterShape {
72
72
  const triCount = this._triangleIndexToArea.length - 1;
73
73
  let left = 0,
74
74
  right = triCount;
75
- const target = Math.random() * this._triangleIndexToArea[triCount];
75
+ const target = random() * this._triangleIndexToArea[triCount];
76
76
 
77
77
  while (left + 1 < right) {
78
78
  const mid = Math.floor((left + right) / 2);
@@ -83,8 +83,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
83
83
  }
84
84
  }
85
85
 
86
- let u1 = Math.random();
87
- let u2 = Math.random();
86
+ let u1 = random();
87
+ let u2 = random();
88
88
  if (u1 + u2 > 1) {
89
89
  u1 = 1 - u1;
90
90
  u2 = 1 - u2;
@@ -83,7 +83,8 @@ export class ParticleEmitter extends TransformNode {
83
83
  if (this.rotationQuaternion) {
84
84
  clonedEmitter.rotationQuaternion = this.rotationQuaternion.clone();
85
85
  }
86
- clonedEmitter.setEnabled(this.isEnabled());
86
+ // Its own flag only: a clone of an emitter under a disabled parent is not itself disabled.
87
+ clonedEmitter.setEnabled(this.isEnabled(false));
87
88
  return clonedEmitter;
88
89
  }
89
90