babylon.quarks 0.18.1 → 0.20.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 (35) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +38 -16
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.ts +2121 -1157
  10. package/package.json +67 -67
  11. package/src/BatchedRenderer.ts +228 -15
  12. package/src/MeshSurfaceEmitter.ts +34 -23
  13. package/src/ParticleEmitter.ts +34 -10
  14. package/src/ParticleSystem.ts +558 -197
  15. package/src/QuarksLoader.ts +204 -18
  16. package/src/QuarksPrefab.ts +8 -11
  17. package/src/QuarksUtil.ts +1 -3
  18. package/src/SpriteBatch.ts +520 -98
  19. package/src/TrailBatch.ts +374 -166
  20. package/src/VFXBatch.ts +24 -5
  21. package/src/envAtlas.ts +136 -0
  22. package/src/index.ts +34 -29
  23. package/src/materials/ParticleMaterials.ts +1 -1
  24. package/src/shaders/index.ts +4 -4
  25. package/src/shaders/local_particle_physics_vert.wgsl.ts +81 -0
  26. package/src/shaders/particle_frag.wgsl.ts +64 -0
  27. package/src/shaders/particle_physics_frag.glsl.ts +56 -1
  28. package/src/shaders/particle_physics_frag.wgsl.ts +125 -0
  29. package/src/shaders/particle_vert.glsl.ts +20 -0
  30. package/src/shaders/particle_vert.wgsl.ts +107 -0
  31. package/src/shaders/shaderLanguageSupport.ts +54 -0
  32. package/src/shaders/stretched_bb_particle_vert.glsl.ts +18 -5
  33. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +101 -0
  34. package/src/shaders/trail_frag.wgsl.ts +23 -0
  35. package/src/shaders/trail_vert.wgsl.ts +63 -0
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.20.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, Vector3} 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,7 +27,34 @@ 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;
50
+ /**
51
+ * Offset of billboard quads from their particle, in particle-size units
52
+ * (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
53
+ * before it is rotated and z moves it toward the camera (up for horizontal
54
+ * billboards). On stretched billboards x runs from the head to the tail and
55
+ * y across, and z is unused. Absent or zero: no offset. Meshes ignore it.
56
+ */
57
+ pivot?: Vector3;
31
58
  }
32
59
 
33
60
  export interface AdaptivePerformanceOptions {
@@ -38,6 +65,27 @@ export interface AdaptivePerformanceOptions {
38
65
  increaseStep: number;
39
66
  }
40
67
 
68
+ /** True when both pivots put the quad in the same place; a missing pivot is zero. */
69
+ function pivotEqual(a: Vector3 | undefined, b: Vector3 | undefined): boolean {
70
+ return (a?.x ?? 0) === (b?.x ?? 0) && (a?.y ?? 0) === (b?.y ?? 0) && (a?.z ?? 0) === (b?.z ?? 0);
71
+ }
72
+
73
+ /** True when both face lists are the same six texture references (or both empty). */
74
+ function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
75
+ if (a === b) {
76
+ return true;
77
+ }
78
+ if (!a || !b || a.length !== b.length) {
79
+ return false;
80
+ }
81
+ for (let i = 0; i < a.length; i++) {
82
+ if (a[i] !== b[i]) {
83
+ return false;
84
+ }
85
+ }
86
+ return true;
87
+ }
88
+
41
89
  export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
42
90
  enabled: boolean;
43
91
  currentQuality: number;
@@ -48,6 +96,29 @@ export class BatchedRenderer extends TransformNode {
48
96
  batches: Array<VFXBatch> = [];
49
97
  systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
50
98
  depthTexture: BaseTexture | null = null;
99
+ /** Systems in insertion order — iterated every frame, unlike the map. */
100
+ private systems: Array<IParticleSystem> = [];
101
+ /**
102
+ * The same systems, ordered so that one emitting into another comes first.
103
+ *
104
+ * A sub emitter's particles are created by its parent's behaviors, part way
105
+ * through the parent's step. A sub system updated before its parent has
106
+ * already fixed the list of particles it will run behaviors over, so those
107
+ * new particles are drawn that frame with nothing but their start values —
108
+ * white where a colour curve would have coloured them, at their start size,
109
+ * sitting at the point they were born. Rebuilt only when the set of systems
110
+ * or their behaviors changes, not per frame.
111
+ */
112
+ private orderedSystems: Array<IParticleSystem> = [];
113
+ private orderedSystemsStale = true;
114
+ /** Consecutive frames each batch has held no system, parallel to `batches`. */
115
+ private batchEmptyFrames: Array<number> = [];
116
+ /**
117
+ * How long an empty batch is kept before it is disposed. Systems that toggle
118
+ * a setting back and forth reuse the batch within this window instead of
119
+ * rebuilding a mesh and material every time.
120
+ */
121
+ private static readonly EMPTY_BATCH_GRACE_FRAMES = 120;
51
122
  private adaptivePerformanceState: AdaptivePerformanceState = {
52
123
  enabled: false,
53
124
  targetFrameMs: 16.7,
@@ -76,6 +147,10 @@ export class BatchedRenderer extends TransformNode {
76
147
  a.materialDepthWrite === b.materialDepthWrite &&
77
148
  a.materialAlphaTest === b.materialAlphaTest &&
78
149
  a.texture === b.texture &&
150
+ a.reflectionTexture === b.reflectionTexture &&
151
+ a.reflectionLevel === b.reflectionLevel &&
152
+ reflectionFacesEqual(a.reflectionFaces, b.reflectionFaces) &&
153
+ a.reflectionAtlas === b.reflectionAtlas &&
79
154
  a.renderMode === b.renderMode &&
80
155
  a.blendTiles === b.blendTiles &&
81
156
  a.softParticles === b.softParticles &&
@@ -85,16 +160,27 @@ export class BatchedRenderer extends TransformNode {
85
160
  a.vTileCount === b.vTileCount &&
86
161
  a.instancingGeometry === b.instancingGeometry &&
87
162
  a.renderOrder === b.renderOrder &&
88
- a.layerMask === b.layerMask
163
+ a.layerMask === b.layerMask &&
164
+ pivotEqual(a.pivot, b.pivot)
89
165
  );
90
166
  }
91
167
 
92
168
  addSystem(system: IParticleSystem) {
93
169
  const particleSystem = system as ParticleSystem;
94
170
  particleSystem._renderer = this;
171
+ if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
172
+ this.systems.push(system);
173
+ this.orderedSystemsStale = true;
174
+ }
95
175
  if (this.adaptivePerformanceState.enabled) {
96
176
  particleSystem.setQualityFactor(this.adaptivePerformanceState.currentQuality);
97
177
  }
178
+ this.assignToBatch(particleSystem);
179
+ }
180
+
181
+ /** Places a system into a compatible batch, creating one when none matches. */
182
+ private assignToBatch(particleSystem: ParticleSystem) {
183
+ const system = particleSystem as IParticleSystem;
98
184
  const settings = particleSystem.getRendererSettings();
99
185
  for (let i = 0; i < this.batches.length; i++) {
100
186
  if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
@@ -125,19 +211,44 @@ export class BatchedRenderer extends TransformNode {
125
211
  }
126
212
  batch.addSystem(system);
127
213
  this.batches.push(batch);
214
+ this.batchEmptyFrames.push(0);
128
215
  this.systemToBatchIndex.set(system, this.batches.length - 1);
129
216
  }
130
217
 
131
218
  deleteSystem(system: IParticleSystem) {
219
+ const removed = this.removeFromBatch(system);
220
+ const index = this.systems.indexOf(system);
221
+ if (index !== -1) {
222
+ this.systems.splice(index, 1);
223
+ this.orderedSystemsStale = true;
224
+ }
225
+ // Also out of the ordered copy, which update() may be iterating right
226
+ // now: a system that disposes itself from inside its own update leaves
227
+ // the loop expecting the slot it vacated to hold the next one.
228
+ const ordered = this.orderedSystems.indexOf(system);
229
+ if (ordered !== -1) {
230
+ this.orderedSystems.splice(ordered, 1);
231
+ }
232
+ return removed;
233
+ }
234
+
235
+ /** Detaches a system from its batch, leaving the renderer's system list alone. */
236
+ private removeFromBatch(system: IParticleSystem): boolean {
132
237
  const batchIndex = this.systemToBatchIndex.get(system);
133
- if (batchIndex !== undefined) {
134
- this.batches[batchIndex].removeSystem(system);
135
- this.systemToBatchIndex.delete(system);
238
+ if (batchIndex === undefined) {
239
+ return false;
136
240
  }
241
+ this.batches[batchIndex].removeSystem(system);
242
+ this.systemToBatchIndex.delete(system);
243
+ return true;
137
244
  }
138
245
 
246
+ /**
247
+ * Re-buckets a system whose render settings changed. The system keeps its
248
+ * place in the update order, so this is safe to call mid-frame.
249
+ */
139
250
  updateSystem(system: IParticleSystem) {
140
- this.deleteSystem(system);
251
+ this.removeFromBatch(system);
141
252
  this.addSystem(system);
142
253
  }
143
254
 
@@ -179,20 +290,90 @@ export class BatchedRenderer extends TransformNode {
179
290
  if (quality === this.lastAppliedQuality) {
180
291
  return;
181
292
  }
182
- for (const ps of this.systemToBatchIndex.keys()) {
183
- (ps as ParticleSystem).setQualityFactor(quality);
293
+ for (let i = 0; i < this.systems.length; i++) {
294
+ (this.systems[i] as ParticleSystem).setQualityFactor(quality);
184
295
  }
185
296
  this.lastAppliedQuality = quality;
186
297
  }
187
298
 
299
+ /** @internal Called when a system's behaviors change, which can add a sub emitter. */
300
+ _invalidateUpdateOrder(): void {
301
+ this.orderedSystemsStale = true;
302
+ }
303
+
304
+ /**
305
+ * Orders the systems so a system emitting into another runs first.
306
+ *
307
+ * Depth first over "emits into" edges, recording each system once all the
308
+ * systems it feeds have been recorded, then reversed — a system therefore
309
+ * lands ahead of everything it emits into, however deep the chain. A cycle
310
+ * cannot be satisfied by any order; it is left as found rather than dropped.
311
+ */
312
+ private orderSystems(): Array<IParticleSystem> {
313
+ if (!this.orderedSystemsStale) {
314
+ return this.orderedSystems;
315
+ }
316
+ const ordered: Array<IParticleSystem> = [];
317
+ const visiting = new Set<IParticleSystem>();
318
+ const done = new Set<IParticleSystem>();
319
+ const known = new Set<IParticleSystem>(this.systems);
320
+
321
+ const subTargetsOf = (system: IParticleSystem): Array<IParticleSystem> => {
322
+ const targets: Array<IParticleSystem> = [];
323
+ for (const behavior of (system as ParticleSystem).behaviors ?? []) {
324
+ if (behavior.type !== 'EmitSubParticleSystem') {
325
+ continue;
326
+ }
327
+ const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
328
+ if (target !== undefined && known.has(target)) {
329
+ targets.push(target);
330
+ }
331
+ }
332
+ return targets;
333
+ };
334
+
335
+ const visit = (system: IParticleSystem): void => {
336
+ if (done.has(system) || visiting.has(system)) {
337
+ return;
338
+ }
339
+ visiting.add(system);
340
+ for (const target of subTargetsOf(system)) {
341
+ visit(target);
342
+ }
343
+ visiting.delete(system);
344
+ done.add(system);
345
+ ordered.push(system);
346
+ };
347
+
348
+ for (const system of this.systems) {
349
+ visit(system);
350
+ }
351
+ ordered.reverse();
352
+ this.orderedSystems = ordered;
353
+ this.orderedSystemsStale = false;
354
+ return ordered;
355
+ }
356
+
188
357
  update(delta: number) {
189
358
  const adaptiveState = this.adaptivePerformanceState;
190
359
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
191
360
  if (adaptiveState.enabled) {
192
361
  this.applyAdaptiveQuality();
193
362
  }
194
- for (const ps of this.systemToBatchIndex.keys()) {
195
- (ps as ParticleSystem).update(delta);
363
+ const systems = this.orderSystems();
364
+ for (let i = 0; i < systems.length; i++) {
365
+ const system = systems[i];
366
+ // A disabled emitter holds its particles frozen instead of simulating
367
+ // them unseen, matching how Unity treats a disabled ParticleSystem.
368
+ // Pooled effects parked with setEnabled(false) then cost nothing.
369
+ if (system.emitter.visible) {
370
+ (system as ParticleSystem).update(delta);
371
+ }
372
+ // An autoDestroy system disposes itself from inside update and drops
373
+ // out of the list; stay on the slot it vacated.
374
+ if (systems[i] !== system) {
375
+ i--;
376
+ }
196
377
  }
197
378
  this.refreshBatches();
198
379
  if (!adaptiveState.enabled) {
@@ -217,8 +398,38 @@ export class BatchedRenderer extends TransformNode {
217
398
 
218
399
  /** Rebuild instance buffers from current particle state without advancing simulation. */
219
400
  refreshBatches(): void {
401
+ const emptyFrames = this.batchEmptyFrames;
220
402
  for (let i = 0; i < this.batches.length; i++) {
221
- this.batches[i].update();
403
+ const batch = this.batches[i];
404
+ if (batch.systems.size > 0) {
405
+ emptyFrames[i] = 0;
406
+ batch.update();
407
+ continue;
408
+ }
409
+ const framesEmpty = (emptyFrames[i] ?? 0) + 1;
410
+ emptyFrames[i] = framesEmpty;
411
+ if (framesEmpty === 1) {
412
+ // One last update clears the instance data left on screen.
413
+ batch.update();
414
+ } else if (framesEmpty > BatchedRenderer.EMPTY_BATCH_GRACE_FRAMES) {
415
+ // Nothing has used these settings for a while: give the mesh,
416
+ // material and GPU buffers back instead of re-uploading empty
417
+ // instance data forever.
418
+ this.removeBatch(i);
419
+ i--;
420
+ }
421
+ }
422
+ }
423
+
424
+ /** Disposes the batch at `index` and repairs the system→batch mapping. */
425
+ private removeBatch(index: number): void {
426
+ this.batches[index].dispose();
427
+ this.batches.splice(index, 1);
428
+ this.batchEmptyFrames.splice(index, 1);
429
+ for (const [system, batchIndex] of this.systemToBatchIndex) {
430
+ if (batchIndex > index) {
431
+ this.systemToBatchIndex.set(system, batchIndex - 1);
432
+ }
222
433
  }
223
434
  }
224
435
 
@@ -253,6 +464,8 @@ export class BatchedRenderer extends TransformNode {
253
464
  batch.dispose();
254
465
  }
255
466
  this.batches = [];
467
+ this.batchEmptyFrames.length = 0;
468
+ this.systems.length = 0;
256
469
  this.systemToBatchIndex.clear();
257
470
  super.dispose();
258
471
  }
@@ -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, ShapeTransform, Vector3} from 'quarks.core';
11
4
 
12
5
  export class MeshSurfaceEmitter implements EmitterShape {
13
6
  type = 'mesh_surface';
@@ -17,6 +10,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
17
10
  private _positions?: Float32Array;
18
11
  private _indices?: Uint32Array | Int32Array;
19
12
  private _meshReferenceId?: string;
13
+ /** Offset, rotation and scale of the mesh within the particle system. */
14
+ transform?: ShapeTransform;
20
15
 
21
16
  get mesh() {
22
17
  return this._mesh;
@@ -60,8 +55,9 @@ export class MeshSurfaceEmitter implements EmitterShape {
60
55
  }
61
56
  }
62
57
 
63
- constructor(mesh?: Mesh, meshReferenceId?: string) {
58
+ constructor(mesh?: Mesh, meshReferenceId?: string, transform?: ShapeTransform) {
64
59
  this._meshReferenceId = meshReferenceId;
60
+ this.transform = transform;
65
61
  if (mesh) this.mesh = mesh;
66
62
  }
67
63
 
@@ -69,11 +65,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
69
65
  if (!this._positions || !this._indices) {
70
66
  p.position.set(0, 0, 0);
71
67
  p.velocity.set(0, 0, 1).multiplyScalar(p.startSpeed);
68
+ this.transform?.apply(p);
72
69
  return;
73
70
  }
74
71
 
75
72
  const triCount = this._triangleIndexToArea.length - 1;
76
- let left = 0, right = triCount;
73
+ let left = 0,
74
+ right = triCount;
77
75
  const target = Math.random() * this._triangleIndexToArea[triCount];
78
76
 
79
77
  while (left + 1 < right) {
@@ -96,9 +94,15 @@ export class MeshSurfaceEmitter implements EmitterShape {
96
94
  const idx1 = this._indices[left * 3 + 1];
97
95
  const idx2 = this._indices[left * 3 + 2];
98
96
 
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;
97
+ const ax = this._positions[idx0 * 3],
98
+ ay = this._positions[idx0 * 3 + 1],
99
+ az = this._positions[idx0 * 3 + 2];
100
+ const bx = this._positions[idx1 * 3] - ax,
101
+ by = this._positions[idx1 * 3 + 1] - ay,
102
+ bz = this._positions[idx1 * 3 + 2] - az;
103
+ const cx = this._positions[idx2 * 3] - ax,
104
+ cy = this._positions[idx2 * 3 + 1] - ay,
105
+ cz = this._positions[idx2 * 3 + 2] - az;
102
106
 
103
107
  p.position.set(ax + bx * u1 + cx * u2, ay + by * u1 + cy * u2, az + bz * u1 + cz * u2);
104
108
 
@@ -107,18 +111,23 @@ export class MeshSurfaceEmitter implements EmitterShape {
107
111
  const nz = bx * cy - by * cx;
108
112
  const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
109
113
  p.velocity.set(nx / len, ny / len, nz / len).multiplyScalar(p.startSpeed);
114
+ this.transform?.apply(p);
110
115
  }
111
116
 
112
117
  toJSON(): ShapeJSON {
113
- return {type: 'mesh_surface', mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : this._meshReferenceId ?? ''};
118
+ return {
119
+ type: 'mesh_surface',
120
+ mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
121
+ ...ShapeTransform.toShapeJSON(this.transform),
122
+ };
114
123
  }
115
124
 
116
125
  static fromJSON(json: any): MeshSurfaceEmitter {
117
- return new MeshSurfaceEmitter(undefined, json?.mesh);
126
+ return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
118
127
  }
119
128
 
120
129
  clone(): EmitterShape {
121
- return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId);
130
+ return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId, this.transform?.clone());
122
131
  }
123
132
 
124
133
  update(_system: IParticleSystem, _delta: number): void {}
@@ -127,11 +136,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
127
136
  export const MeshSurfaceEmitterPlugin: Plugin = {
128
137
  id: 'babylon.quarks',
129
138
  initialize: () => {},
130
- emitterShapes: [{
131
- type: 'mesh_surface',
132
- params: [['mesh', ['mesh']]],
133
- constructor: MeshSurfaceEmitter,
134
- loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
135
- }],
139
+ emitterShapes: [
140
+ {
141
+ type: 'mesh_surface',
142
+ params: [['mesh', ['mesh']]],
143
+ constructor: MeshSurfaceEmitter,
144
+ loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
145
+ },
146
+ ],
136
147
  behaviors: [],
137
148
  };
@@ -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(),