babylon.quarks 0.26.0 → 0.27.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 (79) hide show
  1. package/CHANGELOG.md +769 -8
  2. package/README.md +10 -8
  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.cts +6683 -0
  10. package/dist/types/index.d.ts +1959 -588
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +204 -101
  13. package/src/EffectPool.ts +4 -1
  14. package/src/MeshSurfaceEmitter.ts +29 -4
  15. package/src/ParticleEmitter.ts +45 -15
  16. package/src/ParticleSystem.ts +909 -2061
  17. package/src/QuarksLoader.ts +421 -67
  18. package/src/QuarksPrefab.ts +91 -4
  19. package/src/QuarksSerializer.ts +484 -0
  20. package/src/QuarksUtil.ts +15 -0
  21. package/src/SpriteBatch.ts +106 -246
  22. package/src/TrailBatch.ts +533 -342
  23. package/src/VFXBatch.ts +236 -136
  24. package/src/batchKey.ts +237 -0
  25. package/src/effectResources.ts +79 -0
  26. package/src/envAtlas.ts +119 -17
  27. package/src/index.ts +32 -33
  28. package/src/nodeId.ts +13 -0
  29. package/src/shaderProgram.ts +325 -0
  30. package/src/shaderVariants.ts +225 -0
  31. package/src/shaders/chunks/fog.ts +4 -2
  32. package/src/shaders/chunks/sheet.ts +111 -0
  33. package/src/shaders/chunks/soft.ts +84 -0
  34. package/src/shaders/chunks/surface.ts +10 -0
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  37. package/src/shaders/particle_frag.glsl.ts +3 -22
  38. package/src/shaders/particle_frag.wgsl.ts +3 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +12 -25
  40. package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
  41. package/src/shaders/particle_vert.glsl.ts +5 -27
  42. package/src/shaders/particle_vert.wgsl.ts +5 -26
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  45. package/src/shaders/trail_frag.glsl.ts +11 -1
  46. package/src/shaders/trail_frag.wgsl.ts +11 -0
  47. package/src/shaders/trail_vert.glsl.ts +45 -36
  48. package/src/shaders/trail_vert.wgsl.ts +46 -40
  49. package/src/softDepth.ts +136 -0
  50. package/src/system/BehaviorPlan.ts +85 -0
  51. package/src/system/BoundsTracker.ts +181 -0
  52. package/src/system/EmissionController.ts +360 -0
  53. package/src/system/EventDispatcher.ts +44 -0
  54. package/src/system/ParticleSpawner.ts +274 -0
  55. package/src/system/SimulationClock.ts +130 -0
  56. package/src/system/geometry.ts +111 -0
  57. package/src/system/integrate.ts +428 -0
  58. package/src/system/materialResolve.ts +116 -0
  59. package/src/system/normalize.ts +95 -0
  60. package/src/system/serialization.ts +401 -0
  61. package/src/system/settingsSchema.ts +317 -0
  62. package/src/system/types.ts +410 -0
  63. package/src/textureSource.ts +30 -0
  64. package/src/trailSettings.ts +51 -0
  65. package/src/BatchedParticleRenderer.ts +0 -3
  66. package/src/materials/ParticleMaterials.ts +0 -23
  67. package/src/materials/index.ts +0 -1
  68. package/src/shaders/chunks/index.ts +0 -1
  69. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  70. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  71. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  72. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  73. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  74. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  75. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  76. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  77. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  78. package/src/shaders/index.ts +0 -9
  79. package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -10,10 +10,16 @@
10
10
  "jsdelivr": "./dist/babylon.quarks.umd.min.js",
11
11
  "exports": {
12
12
  ".": {
13
- "import": "./dist/babylon.quarks.esm.js",
14
- "require": "./dist/babylon.quarks.cjs",
15
- "types": "./dist/types/index.d.ts"
16
- }
13
+ "import": {
14
+ "types": "./dist/types/index.d.ts",
15
+ "default": "./dist/babylon.quarks.esm.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/types/index.d.cts",
19
+ "default": "./dist/babylon.quarks.cjs"
20
+ }
21
+ },
22
+ "./package.json": "./package.json"
17
23
  },
18
24
  "files": [
19
25
  "dist",
@@ -29,7 +35,7 @@
29
35
  "build": "npm run clean && npm run build:types && npm run build:js",
30
36
  "test": "jest",
31
37
  "docs": "typedoc",
32
- "test:coverage": "jest --coverage",
38
+ "docs:check": "typedoc --out node_modules/.cache/typedoc-check",
33
39
  "prettier": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
34
40
  },
35
41
  "repository": {
@@ -4,72 +4,102 @@ import {Texture} from '@babylonjs/core/Materials/Textures/texture';
4
4
  import {Frustum} from '@babylonjs/core/Maths/math.frustum';
5
5
  import {Plane} from '@babylonjs/core/Maths/math.plane';
6
6
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
7
+ import type {DepthRenderer} from '@babylonjs/core/Rendering/depthRenderer';
7
8
  import {Scene} from '@babylonjs/core/scene';
8
9
  import {EmitSubParticleSystem, IParticleSystem, Vector3} from 'quarks.core';
10
+ import {batchSettingsEqual, shaderKeyEqual, storedBatchSettings} from './batchKey';
9
11
  import {ParticleSystem} from './ParticleSystem';
12
+ import {depthBinding, DepthBinding, DepthTextureOptions, SoftDepthEncoding} from './softDepth';
10
13
  import {SpriteBatch} from './SpriteBatch';
11
14
  import {TrailBatch} from './TrailBatch';
12
15
  import {
13
16
  defaultMeshLighting,
14
- erosionEqual,
15
- gradientMapEqual,
16
- mapUVEqual,
17
17
  MeshLighting,
18
18
  MeshLightingSettings,
19
19
  MeshRim,
20
- meshRimEqual,
21
20
  ParticleErosion,
22
21
  ParticleGradientMap,
23
22
  ParticleUVDistortion,
24
- readsUV1,
25
23
  RenderMode,
26
24
  SortMode,
27
- StoredBatchSettings,
28
25
  TextureUV,
29
- uvDistortionEqual,
30
26
  VFXBatch,
31
27
  } from './VFXBatch';
32
28
 
29
+ /**
30
+ * Everything about a system that decides how it is drawn: {@link ParticleSystem.getRendererSettings}
31
+ * returns it, and the renderer draws systems whose settings match from one batch. Only the
32
+ * settings a render mode reads count: two billboards that differ in a mesh-only setting, such as
33
+ * `rim`, share a batch.
34
+ *
35
+ * A batch is one draw with one material, so its settings fall into two groups, as a Unity
36
+ * material's keywords and texture slots against its MaterialPropertyBlock values, or an Unreal
37
+ * material against a dynamic material instance's parameters. The shader key (render mode,
38
+ * geometry, textures, blend and depth state, and which effects are on) decides the material.
39
+ * The live parameters (the fade distances, alpha cutoff, pivot, size limits, reflection level,
40
+ * rim colour, each effect's numbers, and each texture's tiling, offset and scroll) are uniforms:
41
+ * changing one on a system that is alone in its batch writes the material in place, with no new
42
+ * batch or material. A texture's UV channel is part of the shader key, so changing it rebuilds.
43
+ *
44
+ * Prefer the system's own setters, which also move it to a matching batch; after changing a
45
+ * field directly, set {@link ParticleSystem.neededToUpdateRender}.
46
+ */
33
47
  export interface VFXBatchSettings {
48
+ /** Mesh particles' vertex positions, see {@link ParticleSystem.instancingGeometry}. A quad for other modes. */
34
49
  instancingGeometry: Float32Array;
50
+ /** Triangle indices into {@link instancingGeometry}. */
35
51
  instancingIndices: Uint32Array | Uint16Array;
52
+ /** The mesh's first UV set, two values per vertex. */
36
53
  instancingUVs?: Float32Array;
37
54
  /** The mesh's second UV set, see {@link UVChannel}. */
38
55
  instancingUVs2?: Float32Array;
56
+ /** The mesh's vertex normals; mesh particles fall back to +Z. */
39
57
  instancingNormals?: Float32Array;
58
+ /** See {@link ParticleSystem.renderMode}. */
40
59
  renderMode: RenderMode;
60
+ /** See {@link ParticleSystem.renderOrder}. */
41
61
  renderOrder: number;
62
+ /** See {@link ParticleSystem.uTileCount}. */
42
63
  uTileCount: number;
64
+ /** See {@link ParticleSystem.vTileCount}. */
43
65
  vTileCount: number;
66
+ /** See {@link ParticleSystem.blendTiles}; read with a sheet, and not by trails. */
44
67
  blendTiles: boolean;
68
+ /** See {@link ParticleSystem.softParticles}. */
45
69
  softParticles: boolean;
70
+ /** See {@link ParticleSystem.softNearFade}. */
46
71
  softNearFade: number;
72
+ /** See {@link ParticleSystem.softFarFade}. */
47
73
  softFarFade: number;
74
+ /** Babylon alpha mode, see {@link ParticleSystem.blending}. */
48
75
  materialBlendMode: number;
76
+ /** Whether the batch draws in the transparent pass. */
49
77
  materialTransparent: boolean;
78
+ /** Whether particles are hidden behind nearer geometry. */
50
79
  materialDepthTest: boolean;
80
+ /** Whether particles write depth. */
51
81
  materialDepthWrite: boolean;
82
+ /** Alpha below which a fragment is discarded; 0 discards nothing. */
52
83
  materialAlphaTest: number;
84
+ /** See {@link ParticleSystem.texture}. */
53
85
  texture: Texture | null;
54
86
  /**
55
- * Cubemap (or other reflection texture) harvested from a Babylon material's
56
- * `reflectionTexture`. Mesh batches sample it like StandardMaterial cubic
57
- * reflection; other render modes ignore it.
87
+ * The reflection a mesh particle's material carries (a Babylon material's `reflectionTexture`
88
+ * or `environmentTexture`). It is not sampled itself: a cube texture is unfolded into
89
+ * {@link reflectionAtlas} once it has loaded and sampled from there; anything else is kept
90
+ * only to tell materials apart. Other render modes ignore it.
58
91
  */
59
92
  reflectionTexture: BaseTexture | null;
60
93
  /** Multiplier for the reflection sample — mirrors `texture.level`. */
61
94
  reflectionLevel: number;
62
95
  /**
63
- * Optional six cube-face 2D textures (px,py,pz,nx,ny,nz). Prefer
64
- * `reflectionAtlas` on iOS — many simultaneous face samplers still trip
65
- * GL_INVALID_OPERATION on WebKit.
66
- */
67
- reflectionFaces: BaseTexture[] | null;
68
- /**
69
- * Single 3×2 atlas of cube faces (px py pz / nx ny nz). One sampler2D —
70
- * the path that stays valid on iOS WebKit ShaderMaterial draws.
96
+ * A 3×2 atlas of cube faces (+X +Y +Z over -X -Y -Z) that mesh particles reflect, through one
97
+ * `sampler2D`: the path that stays valid on iOS WebKit ShaderMaterial draws, where a
98
+ * `samplerCube` or six face samplers raise GL_INVALID_OPERATION. Built from
99
+ * {@link reflectionTexture} when that is a cube texture. Other render modes ignore it.
71
100
  */
72
101
  reflectionAtlas: BaseTexture | null;
102
+ /** Camera layer mask of the batch mesh, see {@link ParticleSystem.layers}. */
73
103
  layerMask: number;
74
104
  /**
75
105
  * Babylon rendering group (`mesh.renderingGroupId`) the batch draws in,
@@ -78,8 +108,11 @@ export interface VFXBatchSettings {
78
108
  renderingGroupId: number;
79
109
  /**
80
110
  * Whether the scene's fog applies to the particles, as it does to any other
81
- * mesh; true by default. Additive particles fade toward black rather than
82
- * the fog colour. Systems that differ are batched apart.
111
+ * mesh; true by default. Particles that add to the frame (`ALPHA_ADD`,
112
+ * `ALPHA_ONEONE`) fade toward black rather than the fog colour, which would
113
+ * fog the whole screen; every other blend mode mixes toward the fog colour,
114
+ * which is right for alpha blending and only approximate for a multiply or
115
+ * subtract. Systems that differ are batched apart.
83
116
  */
84
117
  applyFog: boolean;
85
118
  /**
@@ -121,47 +154,63 @@ export interface VFXBatchSettings {
121
154
  erosion?: ParticleErosion;
122
155
  }
123
156
 
157
+ /**
158
+ * How {@link BatchedRenderer.configureAdaptivePerformance} trades particles for frame time: each
159
+ * update that runs over `targetFrameMs` lowers every system's quality factor (see
160
+ * {@link ParticleSystem.setQualityFactor}) by `decreaseStep`, each one under it raises the factor
161
+ * by `increaseStep`, within `minQuality` and `maxQuality`.
162
+ */
124
163
  export interface AdaptivePerformanceOptions {
164
+ /** CPU time in milliseconds the renderer's update may take before quality drops. Default 16.7. */
125
165
  targetFrameMs: number;
166
+ /** Lowest quality factor, 0.1 to 1 ({@link ParticleSystem.setQualityFactor} clamps lower values). Default 0.35. */
126
167
  minQuality: number;
168
+ /** Highest quality factor, `minQuality` to 1. Default 1. */
127
169
  maxQuality: number;
170
+ /** Quality lost per update over budget. Default 0.08. */
128
171
  decreaseStep: number;
172
+ /** Quality regained per update within budget. Default 0.02. */
129
173
  increaseStep: number;
130
174
  }
131
175
 
132
- /** True when both pivots put the quad in the same place; a missing pivot is zero. */
133
- function pivotEqual(a: Vector3 | undefined, b: Vector3 | undefined): boolean {
134
- return (a?.x ?? 0) === (b?.x ?? 0) && (a?.y ?? 0) === (b?.y ?? 0) && (a?.z ?? 0) === (b?.z ?? 0);
135
- }
136
-
137
- /** True when both face lists are the same six texture references (or both empty). */
138
- function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
139
- if (a === b) {
140
- return true;
141
- }
142
- if (!a || !b || a.length !== b.length) {
143
- return false;
144
- }
145
- for (let i = 0; i < a.length; i++) {
146
- if (a[i] !== b[i]) {
147
- return false;
148
- }
149
- }
150
- return true;
151
- }
152
-
176
+ /** A snapshot of the renderer's adaptive performance, from {@link BatchedRenderer.getAdaptivePerformanceState}. */
153
177
  export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
178
+ /** Whether adaptive performance is on. */
154
179
  enabled: boolean;
180
+ /** The quality factor the systems run at now. */
155
181
  currentQuality: number;
182
+ /** CPU time the last measured update took, in milliseconds; kept from the last update while off. */
156
183
  lastFrameCpuMs: number;
157
184
  }
158
185
 
186
+ /**
187
+ * Draws particle systems, batching the ones that draw alike into one instanced mesh each, and
188
+ * advances them every frame. It is a node in the scene; make one per scene, add each system with
189
+ * {@link addSystem}, and call {@link update} once per frame:
190
+ *
191
+ * ```ts
192
+ * const renderer = new BatchedRenderer('particles', scene);
193
+ * renderer.addSystem(system);
194
+ * scene.onBeforeRenderObservable.add(() => renderer.update(scene.getEngine().getDeltaTime() / 1000));
195
+ * ```
196
+ *
197
+ * Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
198
+ *
199
+ * The renderer is a node so the batches have a parent to live under; leave it at the origin,
200
+ * unturned and unscaled. Particles are placed in the world by their emitters, and a transform on
201
+ * the renderer would move every one of them again (it warns once when it finds one). A system
202
+ * belongs to one renderer at a time: adding it to another takes it out of the first.
203
+ */
159
204
  export class BatchedRenderer extends TransformNode {
205
+ /** Whether the renderer has warned about its own transform; see the class description. */
206
+ private warnedAboutTransform = false;
207
+ /** The batches, one draw each. Read to inspect what is drawn; the renderer owns them. */
160
208
  batches: Array<VFXBatch> = [];
161
209
  /** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
162
210
  private elapsed = 0;
211
+ /** Index into {@link batches} of each system's batch. The renderer owns it. */
163
212
  systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
164
- depthTexture: BaseTexture | null = null;
213
+ private depth: DepthBinding | null = null;
165
214
  /** Systems in insertion order — iterated every frame, unlike the map. */
166
215
  private systems: Array<IParticleSystem> = [];
167
216
  /**
@@ -303,52 +352,25 @@ export class BatchedRenderer extends TransformNode {
303
352
  return Math.max(min, Math.min(max, value));
304
353
  }
305
354
 
306
- private static equals(a: StoredBatchSettings, b: VFXBatchSettings): boolean {
307
- return (
308
- a.materialBlendMode === b.materialBlendMode &&
309
- a.materialTransparent === b.materialTransparent &&
310
- a.materialDepthTest === b.materialDepthTest &&
311
- a.materialDepthWrite === b.materialDepthWrite &&
312
- a.materialAlphaTest === b.materialAlphaTest &&
313
- a.texture === b.texture &&
314
- a.reflectionTexture === b.reflectionTexture &&
315
- a.reflectionLevel === b.reflectionLevel &&
316
- reflectionFacesEqual(a.reflectionFaces, b.reflectionFaces) &&
317
- a.reflectionAtlas === b.reflectionAtlas &&
318
- a.renderMode === b.renderMode &&
319
- a.blendTiles === b.blendTiles &&
320
- a.softParticles === b.softParticles &&
321
- a.softFarFade === b.softFarFade &&
322
- a.softNearFade === b.softNearFade &&
323
- a.uTileCount === b.uTileCount &&
324
- a.vTileCount === b.vTileCount &&
325
- a.instancingGeometry === b.instancingGeometry &&
326
- // Trails draw their own strip and read none of the instanced geometry's UVs.
327
- (a.renderMode === RenderMode.Trail || a.instancingUVs === b.instancingUVs) &&
328
- // Normals, and the second UV set when read, only matter to meshes.
329
- (a.renderMode !== RenderMode.Mesh || a.instancingNormals === b.instancingNormals) &&
330
- (a.renderMode !== RenderMode.Mesh || !readsUV1(a) || a.instancingUVs2 === b.instancingUVs2) &&
331
- a.renderOrder === b.renderOrder &&
332
- a.layerMask === b.layerMask &&
333
- a.renderingGroupId === b.renderingGroupId &&
334
- a.applyFog === b.applyFog &&
335
- pivotEqual(a.pivot, b.pivot) &&
336
- a.sortMode === (b.sortMode ?? 'none') &&
337
- a.minParticleSize === (b.minParticleSize ?? 0) &&
338
- a.maxParticleSize === (b.maxParticleSize ?? Infinity) &&
339
- (a.lighting ?? 'fixed') === (b.lighting ?? 'fixed') &&
340
- meshRimEqual(a.rim, b.rim) &&
341
- mapUVEqual(a.mapUV, b.mapUV) &&
342
- uvDistortionEqual(a.uvDistortion, b.uvDistortion) &&
343
- gradientMapEqual(a.gradientMap, b.gradientMap) &&
344
- erosionEqual(a.erosion, b.erosion)
345
- );
346
- }
347
-
348
- addSystem(system: IParticleSystem) {
355
+ /**
356
+ * Starts drawing and updating `system`, in the batch whose settings match its own (a new one if
357
+ * none does). Add each system of an effect, or use `QuarksUtil.addToBatchRenderer` on its root.
358
+ */
359
+ addSystem(system: IParticleSystem): void {
360
+ if (this.isDisposed()) {
361
+ throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
362
+ }
349
363
  const particleSystem = system as ParticleSystem;
364
+ const previous = particleSystem._renderer;
365
+ if (previous !== undefined && previous !== this) {
366
+ // One renderer at a time, or two would step and draw it.
367
+ previous.deleteSystem(system);
368
+ }
350
369
  particleSystem._renderer = this;
351
- if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
370
+ // Added again after its settings changed: out of the old batch first, so it lands in the
371
+ // one that matches now rather than in both.
372
+ this.removeFromBatch(system);
373
+ if (this.systems.indexOf(system) === -1) {
352
374
  this.systems.push(system);
353
375
  this.orderedSystemsStale = true;
354
376
  }
@@ -362,8 +384,9 @@ export class BatchedRenderer extends TransformNode {
362
384
  private assignToBatch(particleSystem: ParticleSystem) {
363
385
  const system = particleSystem as IParticleSystem;
364
386
  const settings = particleSystem.getRendererSettings();
387
+ const stored = storedBatchSettings(settings);
365
388
  for (let i = 0; i < this.batches.length; i++) {
366
- if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
389
+ if (batchSettingsEqual(this.batches[i].settings, stored)) {
367
390
  this.batches[i].addSystem(system);
368
391
  this.systemToBatchIndex.set(system, i);
369
392
  return;
@@ -373,7 +396,7 @@ export class BatchedRenderer extends TransformNode {
373
396
  const scene = this.getScene();
374
397
  switch (settings.renderMode) {
375
398
  case RenderMode.Trail:
376
- batch = new TrailBatch(settings, scene);
399
+ batch = new TrailBatch(settings, scene, this.depth);
377
400
  break;
378
401
  case RenderMode.Mesh:
379
402
  case RenderMode.BillBoard:
@@ -381,15 +404,12 @@ export class BatchedRenderer extends TransformNode {
381
404
  case RenderMode.HorizontalBillBoard:
382
405
  case RenderMode.StretchedBillBoard:
383
406
  case RenderMode.Beam:
384
- batch = new SpriteBatch(settings, scene);
407
+ batch = new SpriteBatch(settings, scene, this.depth);
385
408
  break;
386
409
  default:
387
410
  throw new Error(`Unsupported render mode: ${settings.renderMode}`);
388
411
  }
389
412
  batch.mesh.parent = this;
390
- if (this.depthTexture) {
391
- batch.applyDepthTexture(this.depthTexture);
392
- }
393
413
  batch.applyMeshLighting(this.meshLighting);
394
414
  batch.addSystem(system);
395
415
  this.batches.push(batch);
@@ -397,7 +417,11 @@ export class BatchedRenderer extends TransformNode {
397
417
  this.systemToBatchIndex.set(system, this.batches.length - 1);
398
418
  }
399
419
 
400
- deleteSystem(system: IParticleSystem) {
420
+ /**
421
+ * Stops drawing and updating `system`; disposing the system calls it. Its particles are left as they are.
422
+ * @returns Whether the system was in one of this renderer's batches.
423
+ */
424
+ deleteSystem(system: IParticleSystem): boolean {
401
425
  const removed = this.removeFromBatch(system);
402
426
  const index = this.systems.indexOf(system);
403
427
  if (index !== -1) {
@@ -426,21 +450,66 @@ export class BatchedRenderer extends TransformNode {
426
450
  }
427
451
 
428
452
  /**
429
- * Re-buckets a system whose render settings changed. The system keeps its
430
- * place in the update order, so this is safe to call mid-frame.
453
+ * Takes a system's changed render settings. When only live parameters changed (see
454
+ * {@link VFXBatchSettings}) and the system is alone in its batch, the batch's material is
455
+ * written in place. Otherwise the system moves to the batch its settings now match. The
456
+ * system keeps its place in the update order, so this is safe to call mid-frame.
431
457
  */
432
458
  updateSystem(system: IParticleSystem) {
459
+ const batchIndex = this.systemToBatchIndex.get(system);
460
+ if (batchIndex !== undefined) {
461
+ const batch = this.batches[batchIndex];
462
+ const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
463
+ if (
464
+ batch.systems.size === 1 &&
465
+ shaderKeyEqual(batch.settings, settings) &&
466
+ // A batch that already draws with these values takes the system instead, so two
467
+ // batches never hold the same settings.
468
+ !this.batches.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
469
+ ) {
470
+ batch.applyLiveSettings(settings);
471
+ return;
472
+ }
473
+ }
433
474
  this.removeFromBatch(system);
434
475
  this.addSystem(system);
435
476
  }
436
477
 
437
- setDepthTexture(depthTexture: BaseTexture | null) {
438
- this.depthTexture = depthTexture;
478
+ /**
479
+ * Sets the scene depth that soft particles fade against. Without one, soft particles are not
480
+ * compiled in and draw hard, as Unity does for a camera with no depth texture.
481
+ *
482
+ * Pass the `DepthRenderer` itself to have its encoding read off it, for example
483
+ * `renderer.setDepthTexture(scene.enableDepthRenderer(camera))`. A bare texture is read as
484
+ * Babylon's default `'linear'` metric unless `options.encoding` says otherwise; see
485
+ * {@link SoftDepthEncoding}. The depth should be drawn by the camera that draws the particles.
486
+ * The texture is held, not the renderer: set it again if the `DepthRenderer` is disposed or
487
+ * replaced.
488
+ *
489
+ * @param depth - A depth texture, a Babylon `DepthRenderer`, or null to turn soft particles off.
490
+ * @param options - What the texture stores, when it cannot be read off a `DepthRenderer`.
491
+ */
492
+ setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
493
+ this.depth = depth ? depthBinding(depth, options) : null;
439
494
  for (const batch of this.batches) {
440
- batch.applyDepthTexture(depthTexture);
495
+ batch.applyDepthTexture(this.depth);
441
496
  }
442
497
  }
443
498
 
499
+ /** The depth texture soft particles fade against, see {@link setDepthTexture}. */
500
+ get depthTexture(): BaseTexture | null {
501
+ return this.depth?.texture ?? null;
502
+ }
503
+
504
+ set depthTexture(texture: BaseTexture | null) {
505
+ this.setDepthTexture(texture);
506
+ }
507
+
508
+ /** What the depth texture stores, see {@link setDepthTexture}. */
509
+ get depthEncoding(): SoftDepthEncoding | null {
510
+ return this.depth?.encoding ?? null;
511
+ }
512
+
444
513
  /**
445
514
  * Sets the light mesh particles with `lighting: 'fixed'` (the default) are
446
515
  * lit by, for every mesh batch of this renderer, including ones created
@@ -478,7 +547,12 @@ export class BatchedRenderer extends TransformNode {
478
547
  };
479
548
  }
480
549
 
481
- configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}) {
550
+ /**
551
+ * Turns on adaptive performance: the renderer measures its own update and thins every system's
552
+ * emission while it runs over budget, see {@link AdaptivePerformanceOptions}. Options left out
553
+ * keep their current values.
554
+ */
555
+ configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
482
556
  const state = this.adaptivePerformanceState;
483
557
  const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
484
558
  const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
@@ -492,7 +566,8 @@ export class BatchedRenderer extends TransformNode {
492
566
  this.lastAppliedQuality = Number.NaN;
493
567
  }
494
568
 
495
- disableAdaptivePerformance(resetQuality = true) {
569
+ /** Turns adaptive performance off, and by default puts every system back at `maxQuality`. */
570
+ disableAdaptivePerformance(resetQuality = true): void {
496
571
  this.adaptivePerformanceState.enabled = false;
497
572
  if (resetQuality) {
498
573
  this.adaptivePerformanceState.currentQuality = this.adaptivePerformanceState.maxQuality;
@@ -500,6 +575,7 @@ export class BatchedRenderer extends TransformNode {
500
575
  }
501
576
  }
502
577
 
578
+ /** A copy of the adaptive performance settings and the current quality. */
503
579
  getAdaptivePerformanceState(): AdaptivePerformanceState {
504
580
  return {...this.adaptivePerformanceState};
505
581
  }
@@ -573,8 +649,19 @@ export class BatchedRenderer extends TransformNode {
573
649
  return ordered;
574
650
  }
575
651
 
576
- update(delta: number) {
652
+ /**
653
+ * Advances every visible system by `delta` seconds and refills the batches. Call it once per
654
+ * frame. A system whose emitter is disabled is frozen; one no camera sees pauses if its
655
+ * {@link ParticleSystem.cullingMode} says so.
656
+ */
657
+ update(delta: number): void {
577
658
  this.elapsed += delta;
659
+ if (!this.warnedAboutTransform && !this.getWorldMatrix().isIdentity()) {
660
+ this.warnedAboutTransform = true;
661
+ console.warn(
662
+ `[babylon.quarks] BatchedRenderer "${this.name}" is transformed; particles are placed in the world by their emitters, so the renderer's own transform moves them again. Keep the renderer at the origin.`
663
+ );
664
+ }
578
665
  const adaptiveState = this.adaptivePerformanceState;
579
666
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
580
667
  if (adaptiveState.enabled) {
@@ -669,10 +756,15 @@ export class BatchedRenderer extends TransformNode {
669
756
 
670
757
  /** Per-batch particle vs GPU instance counts — for diagnosing stale render state. */
671
758
  getBatchRenderStats(): Array<{
759
+ /** Index into {@link batches}. */
672
760
  batchIndex: number;
761
+ /** The batch's render mode. */
673
762
  renderMode: RenderMode;
763
+ /** Instances the batch mesh draws. */
674
764
  instanceCount: number;
765
+ /** Live particles of the batch's systems. */
675
766
  particleCount: number;
767
+ /** The emitter names of the batch's systems. */
676
768
  systemNames: string[];
677
769
  }> {
678
770
  return this.batches.map((batch, batchIndex) => {
@@ -693,13 +785,24 @@ export class BatchedRenderer extends TransformNode {
693
785
  });
694
786
  }
695
787
 
788
+ /**
789
+ * Disposes the batches and their meshes and materials, and lets the systems go: they keep
790
+ * their particles and can be added to another renderer. The renderer cannot be used again.
791
+ */
696
792
  dispose(): void {
697
793
  for (const batch of this.batches) {
698
794
  batch.dispose();
699
795
  }
700
796
  this.batches = [];
701
797
  this.batchEmptyFrames.length = 0;
798
+ for (const system of this.systems) {
799
+ if ((system as ParticleSystem)._renderer === this) {
800
+ (system as ParticleSystem)._renderer = undefined;
801
+ }
802
+ }
702
803
  this.systems.length = 0;
804
+ this.orderedSystems = [];
805
+ this.orderedSystemsStale = true;
703
806
  this.systemToBatchIndex.clear();
704
807
  super.dispose();
705
808
  }
package/src/EffectPool.ts CHANGED
@@ -2,9 +2,11 @@ import {Vector3} from '@babylonjs/core/Maths/math.vector';
2
2
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
3
  import {Node} from '@babylonjs/core/node';
4
4
  import {BatchedRenderer} from './BatchedRenderer';
5
+ import {disposeEffect} from './effectResources';
5
6
  import {ParticleSystem} from './ParticleSystem';
6
7
  import {QuarksUtil} from './QuarksUtil';
7
8
 
9
+ /** Options for a new {@link EffectPool}. */
8
10
  export interface EffectPoolOptions {
9
11
  /** The renderer every copy the pool makes is added to. */
10
12
  renderer: BatchedRenderer;
@@ -146,7 +148,8 @@ export class EffectPool {
146
148
  system.dispose();
147
149
  }
148
150
  this.systemsOf.delete(copy);
149
- copy.dispose();
151
+ // Lets go of the textures the copy shares with the template.
152
+ disposeEffect(copy);
150
153
  }
151
154
 
152
155
  /** The copy a system belongs to is done when all of its systems are. */
@@ -1,8 +1,18 @@
1
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
2
2
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
3
3
  import {EmitterShape, IParticleSystem, Particle, Plugin, random, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
4
-
4
+ import {quarksNodeId} from './nodeId';
5
+
6
+ /**
7
+ * Emits from the surface of a Babylon mesh (Unity's Shape: Mesh, Unreal's Skeletal/Static Mesh
8
+ * Location): a point picked at random over the triangles, weighted by their area, moving along the
9
+ * triangle's normal at the particle's start speed. The mesh's vertices are read once, when it is
10
+ * set, in the mesh's own space, and laid in the emitter's space as they are, moved by
11
+ * {@link transform}; moving the mesh node afterwards does not move the shape. A shape without a
12
+ * mesh emits from the origin along +Z.
13
+ */
5
14
  export class MeshSurfaceEmitter implements EmitterShape {
15
+ /** The shape's type in effect files. */
6
16
  type = 'mesh_surface';
7
17
 
8
18
  private _triangleIndexToArea: number[] = [];
@@ -13,12 +23,16 @@ export class MeshSurfaceEmitter implements EmitterShape {
13
23
  /** Offset, rotation and scale of the mesh within the particle system. */
14
24
  transform?: ShapeTransform;
15
25
 
16
- get mesh() {
26
+ /** The mesh emitted from. Setting it reads its positions and indices; a mesh without both emits from the origin. */
27
+ get mesh(): Mesh | undefined {
17
28
  return this._mesh;
18
29
  }
19
30
 
20
31
  set mesh(mesh: Mesh | undefined) {
21
32
  this._mesh = mesh;
33
+ this._positions = undefined;
34
+ this._indices = undefined;
35
+ this._triangleIndexToArea.length = 0;
22
36
  if (!mesh) return;
23
37
 
24
38
  const positions = mesh.getVerticesData(VertexBuffer.PositionKind);
@@ -55,13 +69,19 @@ export class MeshSurfaceEmitter implements EmitterShape {
55
69
  }
56
70
  }
57
71
 
72
+ /**
73
+ * @param mesh The mesh to emit from.
74
+ * @param meshReferenceId The id of a mesh node in the effect file, kept until the loader links it.
75
+ * @param transform Offset, rotation and scale of the mesh within the system.
76
+ */
58
77
  constructor(mesh?: Mesh, meshReferenceId?: string, transform?: ShapeTransform) {
59
78
  this._meshReferenceId = meshReferenceId;
60
79
  this.transform = transform;
61
80
  if (mesh) this.mesh = mesh;
62
81
  }
63
82
 
64
- initialize(p: Particle) {
83
+ /** Places a new particle on the surface and sets its velocity along the triangle's normal. */
84
+ initialize(p: Particle): void {
65
85
  if (!this._positions || !this._indices) {
66
86
  p.position.set(0, 0, 0);
67
87
  p.velocity.set(0, 0, 1).multiplyScalar(p.startSpeed);
@@ -114,25 +134,30 @@ export class MeshSurfaceEmitter implements EmitterShape {
114
134
  this.transform?.apply(p);
115
135
  }
116
136
 
137
+ /** The shape in effect-file form; the mesh is written as its node id. */
117
138
  toJSON(): ShapeJSON {
118
139
  return {
119
140
  type: 'mesh_surface',
120
- mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
141
+ mesh: this._mesh ? quarksNodeId(this._mesh) : (this._meshReferenceId ?? ''),
121
142
  ...ShapeTransform.toShapeJSON(this.transform),
122
143
  };
123
144
  }
124
145
 
146
+ /** Reads {@link toJSON} output; the mesh stays an id until `QuarksLoader` links it. */
125
147
  static fromJSON(json: any): MeshSurfaceEmitter {
126
148
  return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
127
149
  }
128
150
 
151
+ /** A copy emitting from the same mesh. */
129
152
  clone(): EmitterShape {
130
153
  return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId, this.transform?.clone());
131
154
  }
132
155
 
156
+ /** Does nothing: the shape keeps no state between steps. */
133
157
  update(_system: IParticleSystem, _delta: number): void {}
134
158
  }
135
159
 
160
+ /** Registers {@link MeshSurfaceEmitter} with quarks.core's shape table; loaded when the package is imported. */
136
161
  export const MeshSurfaceEmitterPlugin: Plugin = {
137
162
  id: 'babylon.quarks',
138
163
  initialize: () => {},