babylon.quarks 0.26.0 → 0.28.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 (83) hide show
  1. package/CHANGELOG.md +1194 -8
  2. package/README.md +13 -9
  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 +10108 -0
  10. package/dist/types/index.d.ts +5794 -998
  11. package/package.json +18 -7
  12. package/src/BatchedRenderer.ts +274 -156
  13. package/src/EffectPool.ts +94 -12
  14. package/src/MeshSurfaceEmitter.ts +50 -14
  15. package/src/ParticleEmitter.ts +75 -17
  16. package/src/ParticleSystem.ts +981 -2066
  17. package/src/QuarksLoader.ts +666 -194
  18. package/src/QuarksPrefab.ts +367 -73
  19. package/src/QuarksSerializer.ts +512 -0
  20. package/src/QuarksUtil.ts +77 -7
  21. package/src/SpriteBatch.ts +127 -252
  22. package/src/TrailBatch.ts +545 -348
  23. package/src/VFXBatch.ts +238 -596
  24. package/src/batchKey.ts +239 -0
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +76 -0
  28. package/src/envAtlas.ts +126 -18
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +69 -35
  31. package/src/nodeId.ts +36 -0
  32. package/src/shaderProgram.ts +419 -0
  33. package/src/shaderVariants.ts +245 -0
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +12 -5
  36. package/src/shaders/chunks/sheet.ts +111 -0
  37. package/src/shaders/chunks/soft.ts +85 -0
  38. package/src/shaders/chunks/surface.ts +12 -2
  39. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  40. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  41. package/src/shaders/particle_frag.glsl.ts +5 -22
  42. package/src/shaders/particle_frag.wgsl.ts +5 -23
  43. package/src/shaders/particle_physics_frag.glsl.ts +16 -26
  44. package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
  45. package/src/shaders/particle_vert.glsl.ts +5 -27
  46. package/src/shaders/particle_vert.wgsl.ts +5 -26
  47. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  48. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  49. package/src/shaders/trail_frag.glsl.ts +13 -0
  50. package/src/shaders/trail_frag.wgsl.ts +13 -0
  51. package/src/shaders/trail_vert.glsl.ts +45 -36
  52. package/src/shaders/trail_vert.wgsl.ts +46 -40
  53. package/src/softDepth.ts +136 -0
  54. package/src/system/BehaviorPlan.ts +85 -0
  55. package/src/system/BoundsTracker.ts +181 -0
  56. package/src/system/EmissionController.ts +358 -0
  57. package/src/system/EventDispatcher.ts +60 -0
  58. package/src/system/ParticleSpawner.ts +274 -0
  59. package/src/system/SimulationClock.ts +130 -0
  60. package/src/system/geometry.ts +104 -0
  61. package/src/system/integrate.ts +428 -0
  62. package/src/system/materialResolve.ts +165 -0
  63. package/src/system/normalize.ts +100 -0
  64. package/src/system/serialization.ts +422 -0
  65. package/src/system/settingsSchema.ts +339 -0
  66. package/src/system/types.ts +449 -0
  67. package/src/textureSource.ts +30 -0
  68. package/src/trailSettings.ts +54 -0
  69. package/src/BatchedParticleRenderer.ts +0 -3
  70. package/src/materials/ParticleMaterials.ts +0 -23
  71. package/src/materials/index.ts +0 -1
  72. package/src/shaders/chunks/index.ts +0 -1
  73. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  74. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  75. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  76. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  77. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  78. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  79. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  80. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  81. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  82. package/src/shaders/index.ts +0 -9
  83. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -4,72 +4,103 @@ 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,
23
+ RenderFace,
25
24
  RenderMode,
26
25
  SortMode,
27
- StoredBatchSettings,
28
26
  TextureUV,
29
- uvDistortionEqual,
30
27
  VFXBatch,
31
28
  } from './VFXBatch';
32
29
 
30
+ /**
31
+ * Everything about a system that decides how it is drawn: {@link ParticleSystem.getRendererSettings}
32
+ * returns it, and the renderer draws systems whose settings match from one batch. Only the
33
+ * settings a render mode reads count: two billboards that differ in a mesh-only setting, such as
34
+ * `rim`, share a batch.
35
+ *
36
+ * A batch is one draw with one material, so its settings fall into two groups, as a Unity
37
+ * material's keywords and texture slots against its MaterialPropertyBlock values, or an Unreal
38
+ * material against a dynamic material instance's parameters. The shader key (render mode,
39
+ * geometry, textures, blend and depth state, and which effects are on) decides the material.
40
+ * The live parameters (the fade distances, alpha cutoff, pivot, size limits, reflection level,
41
+ * rim colour, each effect's numbers, and each texture's tiling, offset and scroll) are uniforms:
42
+ * changing one on a system that is alone in its batch writes the material in place, with no new
43
+ * batch or material. A texture's UV channel is part of the shader key, so changing it rebuilds.
44
+ *
45
+ * Prefer the system's own setters, which also move it to a matching batch; after changing a
46
+ * field directly, set {@link ParticleSystem.neededToUpdateRender}.
47
+ */
33
48
  export interface VFXBatchSettings {
49
+ /** Mesh particles' vertex positions, see {@link ParticleSystem.instancingGeometry}. A quad for other modes. */
34
50
  instancingGeometry: Float32Array;
51
+ /** Triangle indices into {@link instancingGeometry}. */
35
52
  instancingIndices: Uint32Array | Uint16Array;
53
+ /** The mesh's first UV set, two values per vertex. */
36
54
  instancingUVs?: Float32Array;
37
55
  /** The mesh's second UV set, see {@link UVChannel}. */
38
56
  instancingUVs2?: Float32Array;
57
+ /** The mesh's vertex normals; mesh particles fall back to +Z. */
39
58
  instancingNormals?: Float32Array;
59
+ /** See {@link ParticleSystem.renderMode}. */
40
60
  renderMode: RenderMode;
61
+ /** See {@link ParticleSystem.renderOrder}. */
41
62
  renderOrder: number;
63
+ /** See {@link ParticleSystem.uTileCount}. */
42
64
  uTileCount: number;
65
+ /** See {@link ParticleSystem.vTileCount}. */
43
66
  vTileCount: number;
67
+ /** See {@link ParticleSystem.blendTiles}; read with a sheet, and not by trails. */
44
68
  blendTiles: boolean;
69
+ /** See {@link ParticleSystem.softParticles}. */
45
70
  softParticles: boolean;
71
+ /** See {@link ParticleSystem.softNearFade}. */
46
72
  softNearFade: number;
73
+ /** See {@link ParticleSystem.softFarFade}. */
47
74
  softFarFade: number;
75
+ /** Babylon alpha mode, see {@link ParticleSystem.blending}. */
48
76
  materialBlendMode: number;
77
+ /** Whether the batch draws in the transparent pass. */
49
78
  materialTransparent: boolean;
79
+ /** Whether particles are hidden behind nearer geometry. */
50
80
  materialDepthTest: boolean;
81
+ /** Whether particles write depth. */
51
82
  materialDepthWrite: boolean;
83
+ /** Alpha below which a fragment is discarded; 0 discards nothing. */
52
84
  materialAlphaTest: number;
85
+ /** See {@link ParticleSystem.texture}. */
53
86
  texture: Texture | null;
54
87
  /**
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.
88
+ * The reflection a mesh particle's material carries (a Babylon material's `reflectionTexture`
89
+ * or `environmentTexture`). It is not sampled itself: a cube texture is unfolded into
90
+ * {@link reflectionAtlas} once it has loaded and sampled from there; anything else is kept
91
+ * only to tell materials apart. Other render modes ignore it.
58
92
  */
59
93
  reflectionTexture: BaseTexture | null;
60
94
  /** Multiplier for the reflection sample — mirrors `texture.level`. */
61
95
  reflectionLevel: number;
62
96
  /**
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.
97
+ * A 3×2 atlas of cube faces (+X +Y +Z over -X -Y -Z) that mesh particles reflect, through one
98
+ * `sampler2D`: the path that stays valid on iOS WebKit ShaderMaterial draws, where a
99
+ * `samplerCube` or six face samplers raise GL_INVALID_OPERATION. Built from
100
+ * {@link reflectionTexture} when that is a cube texture. Other render modes ignore it.
71
101
  */
72
102
  reflectionAtlas: BaseTexture | null;
103
+ /** Camera layer mask of the batch mesh, see {@link ParticleSystem.layers}. */
73
104
  layerMask: number;
74
105
  /**
75
106
  * Babylon rendering group (`mesh.renderingGroupId`) the batch draws in,
@@ -78,8 +109,13 @@ export interface VFXBatchSettings {
78
109
  renderingGroupId: number;
79
110
  /**
80
111
  * 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.
112
+ * mesh; true by default. Particles that lay over the frame (`ALPHA_COMBINE`,
113
+ * `ALPHA_PREMULTIPLIED`, opaque ones) mix toward the fog colour; particles
114
+ * that add to it or subtract from it (`ALPHA_ADD`, `ALPHA_ONEONE`,
115
+ * `ALPHA_SCREENMODE`, `ALPHA_SUBTRACT`, …) fade toward black, since the fog
116
+ * colour would fog the whole screen; particles that multiply it
117
+ * (`ALPHA_MULTIPLY`) fade toward white. See {@link blendCategory}. Systems
118
+ * that differ are batched apart.
83
119
  */
84
120
  applyFog: boolean;
85
121
  /**
@@ -92,6 +128,11 @@ export interface VFXBatchSettings {
92
128
  pivot?: Vector3;
93
129
  /** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
94
130
  sortMode?: SortMode;
131
+ /**
132
+ * Which faces a mesh particle draws, see {@link RenderFace}. Unset is `'both'`. Mesh systems
133
+ * that differ are batched apart; other render modes ignore it.
134
+ */
135
+ renderFace?: RenderFace;
95
136
  /**
96
137
  * Smallest size a billboard or stretched particle may have on screen, as a
97
138
  * fraction of the viewport height (Unity's Min Particle Size); 0 by default.
@@ -121,47 +162,70 @@ export interface VFXBatchSettings {
121
162
  erosion?: ParticleErosion;
122
163
  }
123
164
 
165
+ /**
166
+ * How {@link BatchedRenderer.configureAdaptivePerformance} trades particles for frame time: each
167
+ * update that runs over `targetFrameMs` lowers every system's quality factor (see
168
+ * {@link ParticleSystem.setQualityFactor}) by `decreaseStep`, each one under it raises the factor
169
+ * by `increaseStep`, within `minQuality` and `maxQuality`.
170
+ */
124
171
  export interface AdaptivePerformanceOptions {
172
+ /** CPU time in milliseconds the renderer's update may take before quality drops. Default 16.7. */
125
173
  targetFrameMs: number;
174
+ /** Lowest quality factor, 0.1 to 1 ({@link ParticleSystem.setQualityFactor} clamps lower values). Default 0.35. */
126
175
  minQuality: number;
176
+ /** Highest quality factor, `minQuality` to 1. Default 1. */
127
177
  maxQuality: number;
178
+ /** Quality lost per update over budget. Default 0.08. */
128
179
  decreaseStep: number;
180
+ /** Quality regained per update within budget. Default 0.02. */
129
181
  increaseStep: number;
130
182
  }
131
183
 
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
-
184
+ /** A snapshot of the renderer's adaptive performance, from {@link BatchedRenderer.getAdaptivePerformanceState}. */
153
185
  export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
186
+ /** Whether adaptive performance is on. */
154
187
  enabled: boolean;
188
+ /** The quality factor the systems run at now. */
155
189
  currentQuality: number;
190
+ /** CPU time the last measured update took, in milliseconds; kept from the last update while off. */
156
191
  lastFrameCpuMs: number;
157
192
  }
158
193
 
194
+ /**
195
+ * Draws particle systems, batching the ones that draw alike into one instanced mesh each, and
196
+ * advances them every frame. It is a node in the scene; make one per scene, add each system with
197
+ * {@link addSystem}, and call {@link update} once per frame:
198
+ *
199
+ * ```ts
200
+ * const renderer = new BatchedRenderer('particles', scene);
201
+ * renderer.addSystem(system);
202
+ * scene.onBeforeRenderObservable.add(() => renderer.update(scene.getEngine().getDeltaTime() / 1000));
203
+ * ```
204
+ *
205
+ * Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
206
+ *
207
+ * The renderer is a node so the batches have a parent to live under: disabling it hides them,
208
+ * disposing it disposes them. Its transform moves nothing: particles are placed in the world by
209
+ * their emitters, and the batch meshes draw them there whatever the renderer's position, rotation
210
+ * or scale. A system belongs to one renderer at a time: adding it to another takes it out of the
211
+ * first.
212
+ */
159
213
  export class BatchedRenderer extends TransformNode {
160
- batches: Array<VFXBatch> = [];
214
+ private readonly batchList: Array<VFXBatch> = [];
215
+ private readonly batchIndexOf = new Map<IParticleSystem, number>();
216
+
217
+ /** The batches, one draw each, to inspect what is drawn; the renderer makes and frees them. */
218
+ get batches(): ReadonlyArray<VFXBatch> {
219
+ return this.batchList;
220
+ }
221
+
222
+ /** Index into {@link batches} of each system's batch, to inspect; the renderer keeps it. */
223
+ get systemToBatchIndex(): ReadonlyMap<IParticleSystem, number> {
224
+ return this.batchIndexOf;
225
+ }
161
226
  /** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
162
227
  private elapsed = 0;
163
- systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
164
- depthTexture: BaseTexture | null = null;
228
+ private depth: DepthBinding | null = null;
165
229
  /** Systems in insertion order — iterated every frame, unlike the map. */
166
230
  private systems: Array<IParticleSystem> = [];
167
231
  /**
@@ -223,8 +287,12 @@ export class BatchedRenderer extends TransformNode {
223
287
  */
224
288
  simulationBudgetMs = Infinity;
225
289
 
226
- /** Frustum planes of the active cameras, six per camera, rebuilt each update. */
227
- private frustumPlanes: Plane[] = [];
290
+ /** Frustum planes of each active camera, six per camera, reused across updates. */
291
+ private readonly cameraPlanes: Plane[][] = [];
292
+ /** Each active camera's `layerMask`, parallel to {@link cameraPlanes}. */
293
+ private readonly cameraMasks: number[] = [];
294
+ /** How many cameras {@link cameraPlanes} holds this update. */
295
+ private cameraCount = 0;
228
296
  private readonly boundsMin = new Vector3();
229
297
  private readonly boundsMax = new Vector3();
230
298
 
@@ -233,21 +301,16 @@ export class BatchedRenderer extends TransformNode {
233
301
  }
234
302
 
235
303
  /**
236
- * Gathers the frustum planes of the cameras the scene renders with, six per
237
- * camera. Empty when there is no camera yet; systems are then never culled.
304
+ * Gathers the frustum planes and layer masks of the cameras the scene renders with, and returns
305
+ * how many. 0 when there is no camera yet; systems are then never culled. Allocates only when a
306
+ * camera is added.
238
307
  */
239
- private collectFrustumPlanes(): Plane[] {
308
+ private collectCameras(): number {
240
309
  const scene = this.getScene();
241
- const planes = this.frustumPlanes;
242
310
  const activeCameras = scene.activeCameras;
243
311
  const activeCamera = scene.activeCamera;
244
- const cameras: Camera[] =
312
+ const cameras: readonly Camera[] =
245
313
  activeCameras && activeCameras.length > 0 ? activeCameras : activeCamera ? [activeCamera] : [];
246
- const needed = cameras.length * 6;
247
- while (planes.length < needed) {
248
- planes.push(new Plane(0, 0, 0, 0));
249
- }
250
- planes.length = needed;
251
314
  for (let c = 0; c < cameras.length; c++) {
252
315
  const camera = cameras[c];
253
316
  // Both matrices are recomputed only on demand: a camera moved since
@@ -255,31 +318,33 @@ export class BatchedRenderer extends TransformNode {
255
318
  // that has not rendered yet has no projection at all.
256
319
  camera.getViewMatrix();
257
320
  camera.getProjectionMatrix();
258
- const transform = camera.getTransformationMatrix();
259
- const slice = c === 0 ? planes : planes.slice(c * 6, c * 6 + 6);
260
- Frustum.GetPlanesToRef(transform, slice);
261
- if (c !== 0) {
262
- for (let p = 0; p < 6; p++) {
263
- planes[c * 6 + p] = slice[p];
264
- }
265
- }
321
+ const planes = (this.cameraPlanes[c] ??= [0, 1, 2, 3, 4, 5].map(() => new Plane(0, 0, 0, 0)));
322
+ Frustum.GetPlanesToRef(camera.getTransformationMatrix(), planes);
323
+ this.cameraMasks[c] = camera.layerMask;
266
324
  }
267
- return planes;
325
+ this.cameraCount = cameras.length;
326
+ return cameras.length;
268
327
  }
269
328
 
270
329
  /**
271
- * True when `system`'s world bounds reach into at least one camera's
272
- * frustum, or when it has no bounds to test (empty, or not yet measured).
330
+ * True when `system`'s world bounds reach into the frustum of at least one camera whose
331
+ * `layerMask` shares a bit with the system's (a camera that does not render the system's layer
332
+ * does not see it), or when it has no bounds to test (empty, or not yet measured).
273
333
  */
274
- private isSystemInView(system: ParticleSystem, planes: Plane[]): boolean {
334
+ private isSystemInView(system: ParticleSystem): boolean {
275
335
  const min = this.boundsMin;
276
336
  const max = this.boundsMax;
277
337
  if (!system.getWorldBounds(min, max)) {
278
338
  return true;
279
339
  }
280
- for (let c = 0; c < planes.length; c += 6) {
340
+ const layerMask = system.layers.mask;
341
+ for (let c = 0; c < this.cameraCount; c++) {
342
+ if ((this.cameraMasks[c] & layerMask) === 0) {
343
+ continue;
344
+ }
345
+ const planes = this.cameraPlanes[c];
281
346
  let inside = true;
282
- for (let p = c; p < c + 6; p++) {
347
+ for (let p = 0; p < 6; p++) {
283
348
  const plane = planes[p];
284
349
  const n = plane.normal;
285
350
  // The box corner farthest along the plane's normal: if even it
@@ -303,52 +368,29 @@ export class BatchedRenderer extends TransformNode {
303
368
  return Math.max(min, Math.min(max, value));
304
369
  }
305
370
 
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) {
371
+ /**
372
+ * Starts drawing and updating `system`, in the batch whose settings match its own (a new one if
373
+ * none does). Add each system of an effect, or use `QuarksUtil.addToBatchRenderer` on its root.
374
+ */
375
+ addSystem(system: IParticleSystem): void {
376
+ if (this.isDisposed()) {
377
+ throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
378
+ }
349
379
  const particleSystem = system as ParticleSystem;
380
+ if (particleSystem.emitter.getScene() !== this.getScene()) {
381
+ // Its batch would hold meshes and materials of two scenes.
382
+ throw new Error('BatchedRenderer.addSystem: the system belongs to another scene');
383
+ }
384
+ const previous = particleSystem._renderer;
385
+ if (previous !== undefined && previous !== this) {
386
+ // One renderer at a time, or two would step and draw it.
387
+ previous.deleteSystem(system);
388
+ }
350
389
  particleSystem._renderer = this;
351
- if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
390
+ // Added again after its settings changed: out of the old batch first, so it lands in the
391
+ // one that matches now rather than in both.
392
+ this.removeFromBatch(system);
393
+ if (this.systems.indexOf(system) === -1) {
352
394
  this.systems.push(system);
353
395
  this.orderedSystemsStale = true;
354
396
  }
@@ -362,10 +404,11 @@ export class BatchedRenderer extends TransformNode {
362
404
  private assignToBatch(particleSystem: ParticleSystem) {
363
405
  const system = particleSystem as IParticleSystem;
364
406
  const settings = particleSystem.getRendererSettings();
365
- for (let i = 0; i < this.batches.length; i++) {
366
- if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
367
- this.batches[i].addSystem(system);
368
- this.systemToBatchIndex.set(system, i);
407
+ const stored = storedBatchSettings(settings);
408
+ for (let i = 0; i < this.batchList.length; i++) {
409
+ if (batchSettingsEqual(this.batchList[i].settings, stored)) {
410
+ this.batchList[i].addSystem(system);
411
+ this.batchIndexOf.set(system, i);
369
412
  return;
370
413
  }
371
414
  }
@@ -373,7 +416,7 @@ export class BatchedRenderer extends TransformNode {
373
416
  const scene = this.getScene();
374
417
  switch (settings.renderMode) {
375
418
  case RenderMode.Trail:
376
- batch = new TrailBatch(settings, scene);
419
+ batch = new TrailBatch(settings, scene, this.depth);
377
420
  break;
378
421
  case RenderMode.Mesh:
379
422
  case RenderMode.BillBoard:
@@ -381,23 +424,24 @@ export class BatchedRenderer extends TransformNode {
381
424
  case RenderMode.HorizontalBillBoard:
382
425
  case RenderMode.StretchedBillBoard:
383
426
  case RenderMode.Beam:
384
- batch = new SpriteBatch(settings, scene);
427
+ batch = new SpriteBatch(settings, scene, this.depth);
385
428
  break;
386
429
  default:
387
430
  throw new Error(`Unsupported render mode: ${settings.renderMode}`);
388
431
  }
389
- batch.mesh.parent = this;
390
- if (this.depthTexture) {
391
- batch.applyDepthTexture(this.depthTexture);
392
- }
432
+ batch.attachTo(this);
393
433
  batch.applyMeshLighting(this.meshLighting);
394
434
  batch.addSystem(system);
395
- this.batches.push(batch);
435
+ this.batchList.push(batch);
396
436
  this.batchEmptyFrames.push(0);
397
- this.systemToBatchIndex.set(system, this.batches.length - 1);
437
+ this.batchIndexOf.set(system, this.batchList.length - 1);
398
438
  }
399
439
 
400
- deleteSystem(system: IParticleSystem) {
440
+ /**
441
+ * Stops drawing and updating `system`; disposing the system calls it. Its particles are left as they are.
442
+ * @returns Whether the system was in one of this renderer's batches.
443
+ */
444
+ deleteSystem(system: IParticleSystem): boolean {
401
445
  const removed = this.removeFromBatch(system);
402
446
  const index = this.systems.indexOf(system);
403
447
  if (index !== -1) {
@@ -416,31 +460,76 @@ export class BatchedRenderer extends TransformNode {
416
460
 
417
461
  /** Detaches a system from its batch, leaving the renderer's system list alone. */
418
462
  private removeFromBatch(system: IParticleSystem): boolean {
419
- const batchIndex = this.systemToBatchIndex.get(system);
463
+ const batchIndex = this.batchIndexOf.get(system);
420
464
  if (batchIndex === undefined) {
421
465
  return false;
422
466
  }
423
- this.batches[batchIndex].removeSystem(system);
424
- this.systemToBatchIndex.delete(system);
467
+ this.batchList[batchIndex].removeSystem(system);
468
+ this.batchIndexOf.delete(system);
425
469
  return true;
426
470
  }
427
471
 
428
472
  /**
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.
473
+ * Takes a system's changed render settings. When only live parameters changed (see
474
+ * {@link VFXBatchSettings}) and the system is alone in its batch, the batch's material is
475
+ * written in place. Otherwise the system moves to the batch its settings now match. The
476
+ * system keeps its place in the update order, so this is safe to call mid-frame.
431
477
  */
432
478
  updateSystem(system: IParticleSystem) {
479
+ const batchIndex = this.batchIndexOf.get(system);
480
+ if (batchIndex !== undefined) {
481
+ const batch = this.batchList[batchIndex];
482
+ const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
483
+ if (
484
+ batch.systems.size === 1 &&
485
+ shaderKeyEqual(batch.settings, settings) &&
486
+ // A batch that already draws with these values takes the system instead, so two
487
+ // batches never hold the same settings.
488
+ !this.batchList.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
489
+ ) {
490
+ batch.applyLiveSettings(settings);
491
+ return;
492
+ }
493
+ }
433
494
  this.removeFromBatch(system);
434
495
  this.addSystem(system);
435
496
  }
436
497
 
437
- setDepthTexture(depthTexture: BaseTexture | null) {
438
- this.depthTexture = depthTexture;
439
- for (const batch of this.batches) {
440
- batch.applyDepthTexture(depthTexture);
498
+ /**
499
+ * Sets the scene depth that soft particles fade against. Without one, soft particles are not
500
+ * compiled in and draw hard, as Unity does for a camera with no depth texture.
501
+ *
502
+ * Pass the `DepthRenderer` itself to have its encoding read off it, for example
503
+ * `renderer.setDepthTexture(scene.enableDepthRenderer(camera))`. A bare texture is read as
504
+ * Babylon's default `'linear'` metric unless `options.encoding` says otherwise; see
505
+ * {@link SoftDepthEncoding}. The depth should be drawn by the camera that draws the particles.
506
+ * The texture is held, not the renderer: set it again if the `DepthRenderer` is disposed or
507
+ * replaced.
508
+ *
509
+ * @param depth - A depth texture, a Babylon `DepthRenderer`, or null to turn soft particles off.
510
+ * @param options - What the texture stores, when it cannot be read off a `DepthRenderer`.
511
+ */
512
+ setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
513
+ this.depth = depth ? depthBinding(depth, options) : null;
514
+ for (const batch of this.batchList) {
515
+ batch.applyDepthTexture(this.depth);
441
516
  }
442
517
  }
443
518
 
519
+ /** The depth texture soft particles fade against, see {@link setDepthTexture}. */
520
+ get depthTexture(): BaseTexture | null {
521
+ return this.depth?.texture ?? null;
522
+ }
523
+
524
+ set depthTexture(texture: BaseTexture | null) {
525
+ this.setDepthTexture(texture);
526
+ }
527
+
528
+ /** What the depth texture stores, see {@link setDepthTexture}. */
529
+ get depthEncoding(): SoftDepthEncoding | null {
530
+ return this.depth?.encoding ?? null;
531
+ }
532
+
444
533
  /**
445
534
  * Sets the light mesh particles with `lighting: 'fixed'` (the default) are
446
535
  * lit by, for every mesh batch of this renderer, including ones created
@@ -463,7 +552,7 @@ export class BatchedRenderer extends TransformNode {
463
552
  if (lighting.ambient) {
464
553
  current.ambient = lighting.ambient.clone();
465
554
  }
466
- for (const batch of this.batches) {
555
+ for (const batch of this.batchList) {
467
556
  batch.applyMeshLighting(current);
468
557
  }
469
558
  }
@@ -478,9 +567,14 @@ export class BatchedRenderer extends TransformNode {
478
567
  };
479
568
  }
480
569
 
481
- configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}) {
570
+ /**
571
+ * Turns on adaptive performance: the renderer measures its own update and thins every system's
572
+ * emission while it runs over budget, see {@link AdaptivePerformanceOptions}. Options left out
573
+ * keep their current values.
574
+ */
575
+ configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
482
576
  const state = this.adaptivePerformanceState;
483
- const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
577
+ const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.1, 1);
484
578
  const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
485
579
  state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
486
580
  state.minQuality = minQuality;
@@ -492,7 +586,8 @@ export class BatchedRenderer extends TransformNode {
492
586
  this.lastAppliedQuality = Number.NaN;
493
587
  }
494
588
 
495
- disableAdaptivePerformance(resetQuality = true) {
589
+ /** Turns adaptive performance off, and by default puts every system back at `maxQuality`. */
590
+ disableAdaptivePerformance(resetQuality = true): void {
496
591
  this.adaptivePerformanceState.enabled = false;
497
592
  if (resetQuality) {
498
593
  this.adaptivePerformanceState.currentQuality = this.adaptivePerformanceState.maxQuality;
@@ -500,6 +595,7 @@ export class BatchedRenderer extends TransformNode {
500
595
  }
501
596
  }
502
597
 
598
+ /** A copy of the adaptive performance settings and the current quality. */
503
599
  getAdaptivePerformanceState(): AdaptivePerformanceState {
504
600
  return {...this.adaptivePerformanceState};
505
601
  }
@@ -543,7 +639,9 @@ export class BatchedRenderer extends TransformNode {
543
639
  if (behavior.type !== 'EmitSubParticleSystem') {
544
640
  continue;
545
641
  }
546
- const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
642
+ // A uuid string is a link the loader could not resolve: no target.
643
+ const sub = (behavior as EmitSubParticleSystem).subParticleSystem;
644
+ const target = typeof sub === 'object' ? sub.system : undefined;
547
645
  if (target !== undefined && known.has(target)) {
548
646
  targets.push(target);
549
647
  }
@@ -573,7 +671,12 @@ export class BatchedRenderer extends TransformNode {
573
671
  return ordered;
574
672
  }
575
673
 
576
- update(delta: number) {
674
+ /**
675
+ * Advances every visible system by `delta` seconds and refills the batches. Call it once per
676
+ * frame. A system whose emitter is disabled is frozen; one no camera sees pauses if its
677
+ * {@link ParticleSystem.cullingMode} says so.
678
+ */
679
+ update(delta: number): void {
577
680
  this.elapsed += delta;
578
681
  const adaptiveState = this.adaptivePerformanceState;
579
682
  const frameStart = adaptiveState.enabled ? performance.now() : 0;
@@ -581,7 +684,7 @@ export class BatchedRenderer extends TransformNode {
581
684
  this.applyAdaptiveQuality();
582
685
  }
583
686
  const systems = this.orderSystems();
584
- const planes = this.cullingEnabled ? this.collectFrustumPlanes() : null;
687
+ const cameraCount = this.cullingEnabled ? this.collectCameras() : 0;
585
688
  const budgetStart =
586
689
  this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
587
690
  const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
@@ -595,11 +698,10 @@ export class BatchedRenderer extends TransformNode {
595
698
  // A system no camera sees pauses, by its own choice. One that
596
699
  // only serves as a sub emitter target follows its parent.
597
700
  system.culled =
598
- planes !== null &&
599
- planes.length > 0 &&
701
+ cameraCount > 0 &&
600
702
  system.cullingMode !== 'always' &&
601
703
  system.onlyUsedByOther !== true &&
602
- !this.isSystemInView(system, planes);
704
+ !this.isSystemInView(system);
603
705
  system.update(delta, stepDeadline, catchUpDeadline);
604
706
  }
605
707
  // An autoDestroy system disposes itself from inside update and drops
@@ -632,8 +734,8 @@ export class BatchedRenderer extends TransformNode {
632
734
  /** Rebuild instance buffers from current particle state without advancing simulation. */
633
735
  refreshBatches(): void {
634
736
  const emptyFrames = this.batchEmptyFrames;
635
- for (let i = 0; i < this.batches.length; i++) {
636
- const batch = this.batches[i];
737
+ for (let i = 0; i < this.batchList.length; i++) {
738
+ const batch = this.batchList[i];
637
739
  batch.time = this.elapsed;
638
740
  if (batch.systems.size > 0) {
639
741
  emptyFrames[i] = 0;
@@ -657,25 +759,30 @@ export class BatchedRenderer extends TransformNode {
657
759
 
658
760
  /** Disposes the batch at `index` and repairs the system→batch mapping. */
659
761
  private removeBatch(index: number): void {
660
- this.batches[index].dispose();
661
- this.batches.splice(index, 1);
762
+ this.batchList[index].dispose();
763
+ this.batchList.splice(index, 1);
662
764
  this.batchEmptyFrames.splice(index, 1);
663
- for (const [system, batchIndex] of this.systemToBatchIndex) {
765
+ for (const [system, batchIndex] of this.batchIndexOf) {
664
766
  if (batchIndex > index) {
665
- this.systemToBatchIndex.set(system, batchIndex - 1);
767
+ this.batchIndexOf.set(system, batchIndex - 1);
666
768
  }
667
769
  }
668
770
  }
669
771
 
670
772
  /** Per-batch particle vs GPU instance counts — for diagnosing stale render state. */
671
773
  getBatchRenderStats(): Array<{
774
+ /** Index into {@link batches}. */
672
775
  batchIndex: number;
776
+ /** The batch's render mode. */
673
777
  renderMode: RenderMode;
778
+ /** Instances the batch mesh draws. */
674
779
  instanceCount: number;
780
+ /** Live particles of the batch's systems. */
675
781
  particleCount: number;
782
+ /** The emitter names of the batch's systems. */
676
783
  systemNames: string[];
677
784
  }> {
678
- return this.batches.map((batch, batchIndex) => {
785
+ return this.batchList.map((batch, batchIndex) => {
679
786
  let particleCount = 0;
680
787
  const systemNames: string[] = [];
681
788
  for (const system of batch.systems) {
@@ -693,14 +800,25 @@ export class BatchedRenderer extends TransformNode {
693
800
  });
694
801
  }
695
802
 
803
+ /**
804
+ * Disposes the batches and their meshes and materials, and lets the systems go: they keep
805
+ * their particles and can be added to another renderer. The renderer cannot be used again.
806
+ */
696
807
  dispose(): void {
697
- for (const batch of this.batches) {
808
+ for (const batch of this.batchList) {
698
809
  batch.dispose();
699
810
  }
700
- this.batches = [];
811
+ this.batchList.length = 0;
701
812
  this.batchEmptyFrames.length = 0;
813
+ for (const system of this.systems) {
814
+ if ((system as ParticleSystem)._renderer === this) {
815
+ (system as ParticleSystem)._renderer = undefined;
816
+ }
817
+ }
702
818
  this.systems.length = 0;
703
- this.systemToBatchIndex.clear();
819
+ this.orderedSystems = [];
820
+ this.orderedSystemsStale = true;
821
+ this.batchIndexOf.clear();
704
822
  super.dispose();
705
823
  }
706
824
  }