babylon.quarks 0.18.1 → 0.19.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.
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
1
  {
2
- "name": "babylon.quarks",
3
- "version": "0.18.1",
4
- "description": "A High-Performance Particle System for Babylon.js",
5
- "type": "module",
6
- "types": "./dist/types/index.d.ts",
7
- "main": "./dist/babylon.quarks.cjs",
8
- "module": "./dist/babylon.quarks.esm.js",
9
- "unpkg": "./dist/babylon.quarks.umd.min.js",
10
- "jsdelivr": "./dist/babylon.quarks.umd.min.js",
11
- "exports": {
12
- ".": {
13
- "import": "./dist/babylon.quarks.esm.js",
14
- "require": "./dist/babylon.quarks.cjs",
15
- "types": "./dist/types/index.d.ts"
2
+ "name": "babylon.quarks",
3
+ "version": "0.19.0",
4
+ "description": "A High-Performance Particle System for Babylon.js",
5
+ "type": "module",
6
+ "types": "./dist/types/index.d.ts",
7
+ "main": "./dist/babylon.quarks.cjs",
8
+ "module": "./dist/babylon.quarks.esm.js",
9
+ "unpkg": "./dist/babylon.quarks.umd.min.js",
10
+ "jsdelivr": "./dist/babylon.quarks.umd.min.js",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/babylon.quarks.esm.js",
14
+ "require": "./dist/babylon.quarks.cjs",
15
+ "types": "./dist/types/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "LICENSE",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "scripts": {
26
+ "clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
27
+ "build:types": "tsc --emitDeclarationOnly",
28
+ "build:js": "cross-env NODE_ENV=production rollup -c",
29
+ "build": "npm run clean && npm run build:types && npm run build:js",
30
+ "test": "jest",
31
+ "docs": "typedoc",
32
+ "test:coverage": "jest --coverage",
33
+ "prettier": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
38
+ },
39
+ "keywords": [
40
+ "babylonjs",
41
+ "babylon.js",
42
+ "particles",
43
+ "particle-system",
44
+ "particle",
45
+ "vfx",
46
+ "visual-effects",
47
+ "gpu-particles",
48
+ "webgl",
49
+ "webgpu",
50
+ "quarks",
51
+ "three.quarks",
52
+ "3d",
53
+ "game",
54
+ "performance"
55
+ ],
56
+ "author": "soullnik <soullnik96@gmail.com>",
57
+ "license": "MIT",
58
+ "bugs": {
59
+ "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
60
+ },
61
+ "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
62
+ "peerDependencies": {
63
+ "@babylonjs/core": ">=9.0.0"
64
+ },
65
+ "devDependencies": {
66
+ "@babylonjs/core": "^9.0.0",
67
+ "@types/jest": "^30.0.0",
68
+ "quarks.core": "*"
16
69
  }
17
- },
18
- "files": [
19
- "dist",
20
- "src",
21
- "LICENSE",
22
- "README.md",
23
- "CHANGELOG.md"
24
- ],
25
- "scripts": {
26
- "clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
27
- "build:types": "tsc --emitDeclarationOnly",
28
- "build:js": "cross-env NODE_ENV=production rollup -c",
29
- "build": "npm run clean && npm run build:types && npm run build:js",
30
- "test": "jest",
31
- "docs": "typedoc",
32
- "test:coverage": "jest --coverage",
33
- "prettier": "prettier --write --ignore-unknown **/*.ts"
34
- },
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
38
- },
39
- "keywords": [
40
- "babylonjs",
41
- "babylon.js",
42
- "particles",
43
- "particle-system",
44
- "particle",
45
- "vfx",
46
- "visual-effects",
47
- "gpu-particles",
48
- "webgl",
49
- "webgpu",
50
- "quarks",
51
- "three.quarks",
52
- "3d",
53
- "game",
54
- "performance"
55
- ],
56
- "author": "soullnik <soullnik96@gmail.com>",
57
- "license": "MIT",
58
- "bugs": {
59
- "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
60
- },
61
- "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
62
- "peerDependencies": {
63
- "@babylonjs/core": ">=9.0.0"
64
- },
65
- "devDependencies": {
66
- "@babylonjs/core": "^9.0.0",
67
- "@types/jest": "^30.0.0",
68
- "quarks.core": "*"
69
- }
70
70
  }
@@ -1,12 +1,12 @@
1
+ import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
+ import {Texture} from '@babylonjs/core/Materials/Textures/texture';
1
3
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
4
  import {Scene} from '@babylonjs/core/scene';
3
- import {Texture} from '@babylonjs/core/Materials/Textures/texture';
4
- import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
5
- import {IParticleSystem} from 'quarks.core';
6
- import {VFXBatch, RenderMode, StoredBatchSettings} from './VFXBatch';
5
+ import {EmitSubParticleSystem, IParticleSystem} from 'quarks.core';
6
+ import {ParticleSystem} from './ParticleSystem';
7
7
  import {SpriteBatch} from './SpriteBatch';
8
8
  import {TrailBatch} from './TrailBatch';
9
- import {ParticleSystem} from './ParticleSystem';
9
+ import {RenderMode, StoredBatchSettings, VFXBatch} from './VFXBatch';
10
10
 
11
11
  export interface VFXBatchSettings {
12
12
  instancingGeometry: Float32Array;
@@ -27,6 +27,25 @@ export interface VFXBatchSettings {
27
27
  materialDepthWrite: boolean;
28
28
  materialAlphaTest: number;
29
29
  texture: Texture | null;
30
+ /**
31
+ * Cubemap (or other reflection texture) harvested from a Babylon material's
32
+ * `reflectionTexture`. Mesh batches sample it like StandardMaterial cubic
33
+ * reflection; other render modes ignore it.
34
+ */
35
+ reflectionTexture: BaseTexture | null;
36
+ /** Multiplier for the reflection sample — mirrors `texture.level`. */
37
+ reflectionLevel: number;
38
+ /**
39
+ * Optional six cube-face 2D textures (px,py,pz,nx,ny,nz). Prefer
40
+ * `reflectionAtlas` on iOS — many simultaneous face samplers still trip
41
+ * GL_INVALID_OPERATION on WebKit.
42
+ */
43
+ reflectionFaces: BaseTexture[] | null;
44
+ /**
45
+ * Single 3×2 atlas of cube faces (px py pz / nx ny nz). One sampler2D —
46
+ * the path that stays valid on iOS WebKit ShaderMaterial draws.
47
+ */
48
+ reflectionAtlas: BaseTexture | null;
30
49
  layerMask: number;
31
50
  }
32
51
 
@@ -38,6 +57,22 @@ export interface AdaptivePerformanceOptions {
38
57
  increaseStep: number;
39
58
  }
40
59
 
60
+ /** True when both face lists are the same six texture references (or both empty). */
61
+ function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
62
+ if (a === b) {
63
+ return true;
64
+ }
65
+ if (!a || !b || a.length !== b.length) {
66
+ return false;
67
+ }
68
+ for (let i = 0; i < a.length; i++) {
69
+ if (a[i] !== b[i]) {
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+
41
76
  export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
42
77
  enabled: boolean;
43
78
  currentQuality: number;
@@ -48,6 +83,29 @@ export class BatchedRenderer extends TransformNode {
48
83
  batches: Array<VFXBatch> = [];
49
84
  systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
50
85
  depthTexture: BaseTexture | null = null;
86
+ /** Systems in insertion order — iterated every frame, unlike the map. */
87
+ private systems: Array<IParticleSystem> = [];
88
+ /**
89
+ * The same systems, ordered so that one emitting into another comes first.
90
+ *
91
+ * A sub emitter's particles are created by its parent's behaviors, part way
92
+ * through the parent's step. A sub system updated before its parent has
93
+ * already fixed the list of particles it will run behaviors over, so those
94
+ * new particles are drawn that frame with nothing but their start values —
95
+ * white where a colour curve would have coloured them, at their start size,
96
+ * sitting at the point they were born. Rebuilt only when the set of systems
97
+ * or their behaviors changes, not per frame.
98
+ */
99
+ private orderedSystems: Array<IParticleSystem> = [];
100
+ private orderedSystemsStale = true;
101
+ /** Consecutive frames each batch has held no system, parallel to `batches`. */
102
+ private batchEmptyFrames: Array<number> = [];
103
+ /**
104
+ * How long an empty batch is kept before it is disposed. Systems that toggle
105
+ * a setting back and forth reuse the batch within this window instead of
106
+ * rebuilding a mesh and material every time.
107
+ */
108
+ private static readonly EMPTY_BATCH_GRACE_FRAMES = 120;
51
109
  private adaptivePerformanceState: AdaptivePerformanceState = {
52
110
  enabled: false,
53
111
  targetFrameMs: 16.7,
@@ -76,6 +134,10 @@ export class BatchedRenderer extends TransformNode {
76
134
  a.materialDepthWrite === b.materialDepthWrite &&
77
135
  a.materialAlphaTest === b.materialAlphaTest &&
78
136
  a.texture === b.texture &&
137
+ a.reflectionTexture === b.reflectionTexture &&
138
+ a.reflectionLevel === b.reflectionLevel &&
139
+ reflectionFacesEqual(a.reflectionFaces, b.reflectionFaces) &&
140
+ a.reflectionAtlas === b.reflectionAtlas &&
79
141
  a.renderMode === b.renderMode &&
80
142
  a.blendTiles === b.blendTiles &&
81
143
  a.softParticles === b.softParticles &&
@@ -92,9 +154,19 @@ export class BatchedRenderer extends TransformNode {
92
154
  addSystem(system: IParticleSystem) {
93
155
  const particleSystem = system as ParticleSystem;
94
156
  particleSystem._renderer = this;
157
+ if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
158
+ this.systems.push(system);
159
+ this.orderedSystemsStale = true;
160
+ }
95
161
  if (this.adaptivePerformanceState.enabled) {
96
162
  particleSystem.setQualityFactor(this.adaptivePerformanceState.currentQuality);
97
163
  }
164
+ this.assignToBatch(particleSystem);
165
+ }
166
+
167
+ /** Places a system into a compatible batch, creating one when none matches. */
168
+ private assignToBatch(particleSystem: ParticleSystem) {
169
+ const system = particleSystem as IParticleSystem;
98
170
  const settings = particleSystem.getRendererSettings();
99
171
  for (let i = 0; i < this.batches.length; i++) {
100
172
  if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
@@ -125,19 +197,44 @@ export class BatchedRenderer extends TransformNode {
125
197
  }
126
198
  batch.addSystem(system);
127
199
  this.batches.push(batch);
200
+ this.batchEmptyFrames.push(0);
128
201
  this.systemToBatchIndex.set(system, this.batches.length - 1);
129
202
  }
130
203
 
131
204
  deleteSystem(system: IParticleSystem) {
205
+ const removed = this.removeFromBatch(system);
206
+ const index = this.systems.indexOf(system);
207
+ if (index !== -1) {
208
+ this.systems.splice(index, 1);
209
+ this.orderedSystemsStale = true;
210
+ }
211
+ // Also out of the ordered copy, which update() may be iterating right
212
+ // now: a system that disposes itself from inside its own update leaves
213
+ // the loop expecting the slot it vacated to hold the next one.
214
+ const ordered = this.orderedSystems.indexOf(system);
215
+ if (ordered !== -1) {
216
+ this.orderedSystems.splice(ordered, 1);
217
+ }
218
+ return removed;
219
+ }
220
+
221
+ /** Detaches a system from its batch, leaving the renderer's system list alone. */
222
+ private removeFromBatch(system: IParticleSystem): boolean {
132
223
  const batchIndex = this.systemToBatchIndex.get(system);
133
- if (batchIndex !== undefined) {
134
- this.batches[batchIndex].removeSystem(system);
135
- this.systemToBatchIndex.delete(system);
224
+ if (batchIndex === undefined) {
225
+ return false;
136
226
  }
227
+ this.batches[batchIndex].removeSystem(system);
228
+ this.systemToBatchIndex.delete(system);
229
+ return true;
137
230
  }
138
231
 
232
+ /**
233
+ * Re-buckets a system whose render settings changed. The system keeps its
234
+ * place in the update order, so this is safe to call mid-frame.
235
+ */
139
236
  updateSystem(system: IParticleSystem) {
140
- this.deleteSystem(system);
237
+ this.removeFromBatch(system);
141
238
  this.addSystem(system);
142
239
  }
143
240
 
@@ -179,20 +276,90 @@ export class BatchedRenderer extends TransformNode {
179
276
  if (quality === this.lastAppliedQuality) {
180
277
  return;
181
278
  }
182
- for (const ps of this.systemToBatchIndex.keys()) {
183
- (ps as ParticleSystem).setQualityFactor(quality);
279
+ for (let i = 0; i < this.systems.length; i++) {
280
+ (this.systems[i] as ParticleSystem).setQualityFactor(quality);
184
281
  }
185
282
  this.lastAppliedQuality = quality;
186
283
  }
187
284
 
285
+ /** @internal Called when a system's behaviors change, which can add a sub emitter. */
286
+ _invalidateUpdateOrder(): void {
287
+ this.orderedSystemsStale = true;
288
+ }
289
+
290
+ /**
291
+ * Orders the systems so a system emitting into another runs first.
292
+ *
293
+ * Depth first over "emits into" edges, recording each system once all the
294
+ * systems it feeds have been recorded, then reversed — a system therefore
295
+ * lands ahead of everything it emits into, however deep the chain. A cycle
296
+ * cannot be satisfied by any order; it is left as found rather than dropped.
297
+ */
298
+ private orderSystems(): Array<IParticleSystem> {
299
+ if (!this.orderedSystemsStale) {
300
+ return this.orderedSystems;
301
+ }
302
+ const ordered: Array<IParticleSystem> = [];
303
+ const visiting = new Set<IParticleSystem>();
304
+ const done = new Set<IParticleSystem>();
305
+ const known = new Set<IParticleSystem>(this.systems);
306
+
307
+ const subTargetsOf = (system: IParticleSystem): Array<IParticleSystem> => {
308
+ const targets: Array<IParticleSystem> = [];
309
+ for (const behavior of (system as ParticleSystem).behaviors ?? []) {
310
+ if (behavior.type !== 'EmitSubParticleSystem') {
311
+ continue;
312
+ }
313
+ const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
314
+ if (target !== undefined && known.has(target)) {
315
+ targets.push(target);
316
+ }
317
+ }
318
+ return targets;
319
+ };
320
+
321
+ const visit = (system: IParticleSystem): void => {
322
+ if (done.has(system) || visiting.has(system)) {
323
+ return;
324
+ }
325
+ visiting.add(system);
326
+ for (const target of subTargetsOf(system)) {
327
+ visit(target);
328
+ }
329
+ visiting.delete(system);
330
+ done.add(system);
331
+ ordered.push(system);
332
+ };
333
+
334
+ for (const system of this.systems) {
335
+ visit(system);
336
+ }
337
+ ordered.reverse();
338
+ this.orderedSystems = ordered;
339
+ this.orderedSystemsStale = false;
340
+ return ordered;
341
+ }
342
+
188
343
  update(delta: number) {
189
344
  const adaptiveState = this.adaptivePerformanceState;
190
345
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
191
346
  if (adaptiveState.enabled) {
192
347
  this.applyAdaptiveQuality();
193
348
  }
194
- for (const ps of this.systemToBatchIndex.keys()) {
195
- (ps as ParticleSystem).update(delta);
349
+ const systems = this.orderSystems();
350
+ for (let i = 0; i < systems.length; i++) {
351
+ const system = systems[i];
352
+ // A disabled emitter holds its particles frozen instead of simulating
353
+ // them unseen, matching how Unity treats a disabled ParticleSystem.
354
+ // Pooled effects parked with setEnabled(false) then cost nothing.
355
+ if (system.emitter.visible) {
356
+ (system as ParticleSystem).update(delta);
357
+ }
358
+ // An autoDestroy system disposes itself from inside update and drops
359
+ // out of the list; stay on the slot it vacated.
360
+ if (systems[i] !== system) {
361
+ i--;
362
+ }
196
363
  }
197
364
  this.refreshBatches();
198
365
  if (!adaptiveState.enabled) {
@@ -217,8 +384,38 @@ export class BatchedRenderer extends TransformNode {
217
384
 
218
385
  /** Rebuild instance buffers from current particle state without advancing simulation. */
219
386
  refreshBatches(): void {
387
+ const emptyFrames = this.batchEmptyFrames;
220
388
  for (let i = 0; i < this.batches.length; i++) {
221
- this.batches[i].update();
389
+ const batch = this.batches[i];
390
+ if (batch.systems.size > 0) {
391
+ emptyFrames[i] = 0;
392
+ batch.update();
393
+ continue;
394
+ }
395
+ const framesEmpty = (emptyFrames[i] ?? 0) + 1;
396
+ emptyFrames[i] = framesEmpty;
397
+ if (framesEmpty === 1) {
398
+ // One last update clears the instance data left on screen.
399
+ batch.update();
400
+ } else if (framesEmpty > BatchedRenderer.EMPTY_BATCH_GRACE_FRAMES) {
401
+ // Nothing has used these settings for a while: give the mesh,
402
+ // material and GPU buffers back instead of re-uploading empty
403
+ // instance data forever.
404
+ this.removeBatch(i);
405
+ i--;
406
+ }
407
+ }
408
+ }
409
+
410
+ /** Disposes the batch at `index` and repairs the system→batch mapping. */
411
+ private removeBatch(index: number): void {
412
+ this.batches[index].dispose();
413
+ this.batches.splice(index, 1);
414
+ this.batchEmptyFrames.splice(index, 1);
415
+ for (const [system, batchIndex] of this.systemToBatchIndex) {
416
+ if (batchIndex > index) {
417
+ this.systemToBatchIndex.set(system, batchIndex - 1);
418
+ }
222
419
  }
223
420
  }
224
421
 
@@ -253,6 +450,8 @@ export class BatchedRenderer extends TransformNode {
253
450
  batch.dispose();
254
451
  }
255
452
  this.batches = [];
453
+ this.batchEmptyFrames.length = 0;
454
+ this.systems.length = 0;
256
455
  this.systemToBatchIndex.clear();
257
456
  super.dispose();
258
457
  }
@@ -1,13 +1,6 @@
1
- import {Mesh} from '@babylonjs/core/Meshes/mesh';
2
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
3
- import {
4
- Particle,
5
- EmitterShape,
6
- ShapeJSON,
7
- IParticleSystem,
8
- Vector3,
9
- Plugin,
10
- } from 'quarks.core';
2
+ import {Mesh} from '@babylonjs/core/Meshes/mesh';
3
+ import {EmitterShape, IParticleSystem, Particle, Plugin, ShapeJSON, Vector3} from 'quarks.core';
11
4
 
12
5
  export class MeshSurfaceEmitter implements EmitterShape {
13
6
  type = 'mesh_surface';
@@ -73,7 +66,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
73
66
  }
74
67
 
75
68
  const triCount = this._triangleIndexToArea.length - 1;
76
- let left = 0, right = triCount;
69
+ let left = 0,
70
+ right = triCount;
77
71
  const target = Math.random() * this._triangleIndexToArea[triCount];
78
72
 
79
73
  while (left + 1 < right) {
@@ -96,9 +90,15 @@ export class MeshSurfaceEmitter implements EmitterShape {
96
90
  const idx1 = this._indices[left * 3 + 1];
97
91
  const idx2 = this._indices[left * 3 + 2];
98
92
 
99
- const ax = this._positions[idx0 * 3], ay = this._positions[idx0 * 3 + 1], az = this._positions[idx0 * 3 + 2];
100
- const bx = this._positions[idx1 * 3] - ax, by = this._positions[idx1 * 3 + 1] - ay, bz = this._positions[idx1 * 3 + 2] - az;
101
- const cx = this._positions[idx2 * 3] - ax, cy = this._positions[idx2 * 3 + 1] - ay, cz = this._positions[idx2 * 3 + 2] - az;
93
+ const ax = this._positions[idx0 * 3],
94
+ ay = this._positions[idx0 * 3 + 1],
95
+ az = this._positions[idx0 * 3 + 2];
96
+ const bx = this._positions[idx1 * 3] - ax,
97
+ by = this._positions[idx1 * 3 + 1] - ay,
98
+ bz = this._positions[idx1 * 3 + 2] - az;
99
+ const cx = this._positions[idx2 * 3] - ax,
100
+ cy = this._positions[idx2 * 3 + 1] - ay,
101
+ cz = this._positions[idx2 * 3 + 2] - az;
102
102
 
103
103
  p.position.set(ax + bx * u1 + cx * u2, ay + by * u1 + cy * u2, az + bz * u1 + cz * u2);
104
104
 
@@ -110,7 +110,10 @@ export class MeshSurfaceEmitter implements EmitterShape {
110
110
  }
111
111
 
112
112
  toJSON(): ShapeJSON {
113
- return {type: 'mesh_surface', mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : this._meshReferenceId ?? ''};
113
+ return {
114
+ type: 'mesh_surface',
115
+ mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
116
+ };
114
117
  }
115
118
 
116
119
  static fromJSON(json: any): MeshSurfaceEmitter {
@@ -127,11 +130,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
127
130
  export const MeshSurfaceEmitterPlugin: Plugin = {
128
131
  id: 'babylon.quarks',
129
132
  initialize: () => {},
130
- emitterShapes: [{
131
- type: 'mesh_surface',
132
- params: [['mesh', ['mesh']]],
133
- constructor: MeshSurfaceEmitter,
134
- loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
135
- }],
133
+ emitterShapes: [
134
+ {
135
+ type: 'mesh_surface',
136
+ params: [['mesh', ['mesh']]],
137
+ constructor: MeshSurfaceEmitter,
138
+ loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
139
+ },
140
+ ],
136
141
  behaviors: [],
137
142
  };
@@ -1,18 +1,42 @@
1
1
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
2
  import {Scene} from '@babylonjs/core/scene';
3
- import {Matrix4, IParticleSystem, SerializationOptions} from 'quarks.core';
3
+ import {IParticleSystem, Matrix4, SerializationOptions} from 'quarks.core';
4
4
 
5
5
  export class ParticleEmitter extends TransformNode {
6
6
  system: IParticleSystem;
7
7
  private _matrixWorld: Matrix4 = new Matrix4();
8
+ private _matrixWorldFlag = -1;
8
9
 
10
+ /**
11
+ * The node's world matrix in quarks.core form. Mirrored from Babylon's
12
+ * matrix only when that matrix actually changed (tracked by its
13
+ * `updateFlag`), so repeated reads inside a frame are free.
14
+ */
9
15
  get matrixWorld(): Matrix4 {
10
16
  const m = this.getWorldMatrix();
17
+ const flag = m.updateFlag;
18
+ if (flag === this._matrixWorldFlag) {
19
+ return this._matrixWorld;
20
+ }
21
+ this._matrixWorldFlag = flag;
11
22
  const e = this._matrixWorld.elements;
12
- e[0] = m.m[0]; e[1] = m.m[1]; e[2] = m.m[2]; e[3] = m.m[3];
13
- e[4] = m.m[4]; e[5] = m.m[5]; e[6] = m.m[6]; e[7] = m.m[7];
14
- e[8] = m.m[8]; e[9] = m.m[9]; e[10] = m.m[10]; e[11] = m.m[11];
15
- e[12] = m.m[12]; e[13] = m.m[13]; e[14] = m.m[14]; e[15] = m.m[15];
23
+ const s = m.m;
24
+ e[0] = s[0];
25
+ e[1] = s[1];
26
+ e[2] = s[2];
27
+ e[3] = s[3];
28
+ e[4] = s[4];
29
+ e[5] = s[5];
30
+ e[6] = s[6];
31
+ e[7] = s[7];
32
+ e[8] = s[8];
33
+ e[9] = s[9];
34
+ e[10] = s[10];
35
+ e[11] = s[11];
36
+ e[12] = s[12];
37
+ e[13] = s[13];
38
+ e[14] = s[14];
39
+ e[15] = s[15];
16
40
  return this._matrixWorld;
17
41
  }
18
42
 
@@ -72,11 +96,11 @@ export class ParticleEmitter extends TransformNode {
72
96
  rotation: [this.rotation.x, this.rotation.y, this.rotation.z],
73
97
  quaternion: this.rotationQuaternion
74
98
  ? [
75
- this.rotationQuaternion.x,
76
- this.rotationQuaternion.y,
77
- this.rotationQuaternion.z,
78
- this.rotationQuaternion.w,
79
- ]
99
+ this.rotationQuaternion.x,
100
+ this.rotationQuaternion.y,
101
+ this.rotationQuaternion.z,
102
+ this.rotationQuaternion.w,
103
+ ]
80
104
  : undefined,
81
105
  scale: [this.scaling.x, this.scaling.y, this.scaling.z],
82
106
  visible: this.isEnabled(),