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
@@ -1,14 +1,14 @@
1
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
2
2
  import {Constants} from '@babylonjs/core/Engines/constants';
3
+ import type {Effect} from '@babylonjs/core/Materials/effect';
3
4
  import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
4
5
  import {RawTexture} from '@babylonjs/core/Materials/Textures/rawTexture';
5
- import {Vector2 as BVector2, Vector3 as BVector3, Vector4 as BVector4} from '@babylonjs/core/Maths/math.vector';
6
+ import {Vector2 as BVector2, Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
6
7
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
7
8
  import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
8
9
  import {Scene} from '@babylonjs/core/scene';
9
10
  import {
10
11
  IParticle,
11
- IParticleSystem,
12
12
  Matrix3,
13
13
  ParticleStore,
14
14
  Quaternion,
@@ -18,21 +18,15 @@ import {
18
18
  Vector4,
19
19
  } from 'quarks.core';
20
20
  import {VFXBatchSettings} from './BatchedRenderer';
21
+ import {hasSizeLimits, takesPivotAndSizeLimits, tilesSheet} from './batchKey';
21
22
  import {gather, sortByKey, SortScratch} from './instanceSort';
22
23
  import type {ParticleSystem} from './ParticleSystem';
23
- import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
24
- import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
25
- import particle_frag from './shaders/particle_frag.glsl';
26
- import particle_frag_wgsl from './shaders/particle_frag.wgsl';
27
- import particle_physics_frag from './shaders/particle_physics_frag.glsl';
28
- import particle_physics_frag_wgsl from './shaders/particle_physics_frag.wgsl';
29
- import particle_vert from './shaders/particle_vert.glsl';
30
- import particle_vert_wgsl from './shaders/particle_vert.wgsl';
31
- import {registerShaders, shaderLanguageFor, ShaderSources} from './shaders/shaderLanguageSupport';
32
- import stretched_bb_particle_vert from './shaders/stretched_bb_particle_vert.glsl';
33
- import stretched_bb_particle_vert_wgsl from './shaders/stretched_bb_particle_vert.wgsl';
24
+ import {drawsAlongVelocity, shaderProgram} from './shaderProgram';
25
+ import {registerShaders, shaderLanguageFor} from './shaders/shaderLanguageSupport';
26
+ import type {DepthBinding} from './softDepth';
34
27
  import {defaultMeshLighting, MeshLightingSettings, RenderMode, VFXBatch} from './VFXBatch';
35
28
 
29
+ /** The batch of billboard, stretched, mesh and beam particles. @internal */
36
30
  export class SpriteBatch extends VFXBatch {
37
31
  private static whiteTextureByScene = new WeakMap<Scene, RawTexture>();
38
32
 
@@ -53,8 +47,11 @@ export class SpriteBatch extends VFXBatch {
53
47
  private uvTileBuffer!: Float32Array;
54
48
  private velocityBuffer?: Float32Array;
55
49
 
56
- /** Concretely typed handle on the batch material, see syncStretchedSpeedFactor. */
50
+ /** Concretely typed handle on the batch material, for the lighting and live-parameter writes. */
57
51
  private shaderMaterial: ShaderMaterial | null = null;
52
+ /** Cancels the pending rebuild for an env atlas that has not loaded yet. */
53
+ private cancelAtlasRebuild: (() => void) | null = null;
54
+ private disposed = false;
58
55
  /** The `'fixed'` mesh light, as shader uniforms; see applyMeshLighting. */
59
56
  private readonly fixedLightDirection = new BVector3();
60
57
  private readonly fixedLightColor = new BVector3();
@@ -67,14 +64,15 @@ export class SpriteBatch extends VFXBatch {
67
64
  private uvTileVB!: VertexBuffer;
68
65
  private velocityVB?: VertexBuffer;
69
66
 
70
- constructor(settings: VFXBatchSettings, scene: Scene) {
71
- super(settings, scene);
67
+ constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
68
+ super(settings, scene, depth);
72
69
  this.storeFixedLighting(defaultMeshLighting());
73
70
  this.maxParticles = 1000;
74
71
  this.setupBuffers();
75
72
  this.rebuildMaterial();
76
73
  }
77
74
 
75
+ /** @internal */
78
76
  setupBuffers(): void {
79
77
  this.mesh.dispose();
80
78
  this.mesh = new Mesh('spriteBatch', this.scene);
@@ -170,7 +168,7 @@ export class SpriteBatch extends VFXBatch {
170
168
  }
171
169
  this.mesh.setVerticesBuffer(this.rotationVB);
172
170
 
173
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
171
+ if (drawsAlongVelocity(this.settings.renderMode)) {
174
172
  this.velocityBuffer = new Float32Array(this.maxParticles * 4);
175
173
  this.velocityVB = new VertexBuffer(engine, this.velocityBuffer, 'velocity', true, false, 4, true);
176
174
  this.mesh.setVerticesBuffer(this.velocityVB);
@@ -180,6 +178,7 @@ export class SpriteBatch extends VFXBatch {
180
178
  this.mesh.doNotSyncBoundingInfo = true;
181
179
  }
182
180
 
181
+ /** @internal */
183
182
  expandBuffers(target: number): void {
184
183
  while (target >= this.maxParticles) {
185
184
  this.maxParticles *= 2;
@@ -193,10 +192,7 @@ export class SpriteBatch extends VFXBatch {
193
192
  /** The screen-size limits as a uniform value, or null when the batch has none that apply. */
194
193
  private sizeLimits(): BVector2 | null {
195
194
  const {minParticleSize, maxParticleSize} = this.settings;
196
- if (!SpriteBatch.takesPivotAndFlip(this.settings.renderMode)) {
197
- return null;
198
- }
199
- if (minParticleSize <= 0 && maxParticleSize === Infinity) {
195
+ if (!takesPivotAndSizeLimits(this.settings.renderMode) || !hasSizeLimits(this.settings)) {
200
196
  return null;
201
197
  }
202
198
  // The shader clamps; an unlimited maximum is just a very large one.
@@ -295,153 +291,47 @@ export class SpriteBatch extends VFXBatch {
295
291
  return spare;
296
292
  }
297
293
 
294
+ /** @internal */
298
295
  rebuildMaterial(): void {
299
- // The shader sources only depend on the render mode, so the store entry is
300
- // registered once per mode. A unique name per rebuild would leak entries in
301
- // Effect.ShadersStore and defeat Babylon's compiled-effect cache.
302
- const shaderName = `quarksParticle_${this.settings.renderMode}`;
303
- this.lastStretchedSpeedFactor = Number.NaN;
304
- const defines: string[] = this.fogDefines();
305
-
306
- let vertexShader: ShaderSources;
307
- let fragmentShader: ShaderSources;
308
- if (this.settings.renderMode === RenderMode.Mesh) {
309
- vertexShader = {glsl: local_particle_physics_vert, wgsl: local_particle_physics_vert_wgsl};
310
- fragmentShader = {glsl: particle_physics_frag, wgsl: particle_physics_frag_wgsl};
311
- } else if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
312
- vertexShader = {glsl: stretched_bb_particle_vert, wgsl: stretched_bb_particle_vert_wgsl};
313
- fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
314
- } else {
315
- vertexShader = {glsl: particle_vert, wgsl: particle_vert_wgsl};
316
- fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
317
- }
318
-
296
+ const isMesh = this.settings.renderMode === RenderMode.Mesh;
319
297
  // Mesh batches without a diffuse map still need a sampler2D on iOS WebKit —
320
298
  // a zero-sampler particle mesh effect often never becomes drawable there.
321
- const mapTexture =
322
- this.settings.texture ??
323
- (this.settings.renderMode === RenderMode.Mesh ? SpriteBatch.whiteTexture(this.scene) : null);
324
- if (mapTexture) {
325
- defines.push('USE_MAP');
326
- }
299
+ const mapTexture = this.settings.texture ?? (isMesh ? SpriteBatch.whiteTexture(this.scene) : null);
327
300
  const atlas = this.settings.reflectionAtlas;
328
- const atlasPending = this.settings.renderMode === RenderMode.Mesh && !!atlas && !atlas.isReady();
301
+ const atlasPending = isMesh && !!atlas && !atlas.isReady();
302
+ this.cancelAtlasRebuild?.();
303
+ this.cancelAtlasRebuild = null;
329
304
  if (atlasPending && atlas) {
330
- const onLoad = (atlas as {onLoadObservable?: {addOnce: (cb: () => void) => void}}).onLoadObservable;
331
- onLoad?.addOnce(() => {
332
- this.rebuildMaterial();
333
- });
334
- }
335
- const useEnvAtlas = this.settings.renderMode === RenderMode.Mesh && !!atlas && atlas.isReady();
336
- if (useEnvAtlas) {
337
- defines.push('USE_ENVMAP_ATLAS');
338
- }
339
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
340
- defines.push('UV_TILE');
341
- }
342
- if (this.settings.blendTiles) {
343
- defines.push('TILE_BLEND');
344
- }
345
- if (this.settings.softParticles) {
346
- defines.push('SOFT_PARTICLES');
347
- }
348
- if (this.settings.materialAlphaTest > 0) {
349
- defines.push('USE_ALPHATEST');
350
- }
351
- if (this.settings.renderMode === RenderMode.VerticalBillBoard) {
352
- defines.push('VERTICAL');
353
- }
354
- if (this.settings.renderMode === RenderMode.HorizontalBillBoard) {
355
- defines.push('HORIZONTAL');
356
- }
357
- if (this.settings.renderMode === RenderMode.Beam) {
358
- defines.push('BEAM');
359
- }
360
- const pivot = SpriteBatch.takesPivotAndFlip(this.settings.renderMode) ? this.settings.pivot : undefined;
361
- if (pivot) {
362
- defines.push('PIVOT');
363
- }
364
- const sizeLimits = this.sizeLimits();
365
- if (sizeLimits) {
366
- defines.push('SIZE_LIMITS');
305
+ const onLoad = (
306
+ atlas as {
307
+ onLoadObservable?: {addOnce: (cb: () => void) => unknown; remove: (observer: unknown) => boolean};
308
+ }
309
+ ).onLoadObservable;
310
+ if (onLoad) {
311
+ const observer = onLoad.addOnce(() => {
312
+ this.cancelAtlasRebuild = null;
313
+ if (!this.disposed) {
314
+ this.rebuildMaterial();
315
+ }
316
+ });
317
+ this.cancelAtlasRebuild = () => onLoad.remove(observer);
318
+ }
367
319
  }
368
- const isMesh = this.settings.renderMode === RenderMode.Mesh;
320
+ const useEnvAtlas = this.usesEnvAtlas();
321
+ const program = shaderProgram(this.settings, {
322
+ depth: this.depth,
323
+ envAtlasReady: useEnvAtlas,
324
+ meshUV2: this.meshUV2(),
325
+ });
326
+ const {defines, attributes, uniforms, samplers} = program;
327
+ const shaderName = program.name;
369
328
  const lighting = this.settings.lighting ?? 'fixed';
370
329
  const lit = isMesh && lighting !== 'unlit';
371
- if (isMesh && !lit) {
372
- defines.push('MESH_UNLIT');
373
- }
374
330
  const rim = isMesh ? this.settings.rim : undefined;
375
- if (rim) {
376
- defines.push('MESH_RIM');
377
- if (rim.mode === 'fade') {
378
- defines.push('MESH_RIM_FADE');
379
- } else if (rim.mode === 'mix') {
380
- defines.push('MESH_RIM_MIX');
381
- }
382
- }
383
331
  const needsEyePosition = useEnvAtlas || !!rim;
384
- defines.push(...this.surfaceDefines());
385
332
 
386
333
  const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
387
- registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
388
-
389
- const attributes = ['position', 'uv', 'offset', 'color', 'size', 'rotation', 'uvTile'];
390
- if (this.settings.renderMode === RenderMode.Mesh) {
391
- attributes.push('normal');
392
- }
393
- if (this.usesUV1() && this.meshUV2()) {
394
- defines.push('MESH_UV2');
395
- attributes.push('uv2');
396
- }
397
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
398
- attributes.push('velocity');
399
- }
400
-
401
- const uniforms = ['world', 'view', 'projection', 'worldView', 'worldViewProjection', ...VFXBatch.FOG_UNIFORMS];
402
- const samplers: string[] = [];
403
-
404
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
405
- uniforms.push('tileCountX');
406
- uniforms.push('tileCountY');
407
- }
408
- if (mapTexture) {
409
- samplers.push('map');
410
- }
411
- if (useEnvAtlas) {
412
- samplers.push('envAtlas');
413
- uniforms.push('reflectionLevel');
414
- }
415
- if (needsEyePosition) {
416
- uniforms.push('eyePosition');
417
- }
418
- if (rim) {
419
- uniforms.push('rimColor');
420
- uniforms.push('rimParams');
421
- }
422
- this.surfaceUniforms(uniforms, samplers);
423
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
424
- uniforms.push('speedFactor');
425
- }
426
- if (this.settings.softParticles) {
427
- uniforms.push('softParams');
428
- uniforms.push('projParams');
429
- samplers.push('depthTexture');
430
- }
431
- if (this.settings.materialAlphaTest > 0) {
432
- uniforms.push('alphaTest');
433
- }
434
- if (pivot) {
435
- uniforms.push('pivot');
436
- }
437
- if (sizeLimits) {
438
- uniforms.push('sizeLimits');
439
- }
440
- if (lit) {
441
- uniforms.push('lightDirection');
442
- uniforms.push('lightColor');
443
- uniforms.push('ambientColor');
444
- }
334
+ registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
445
335
 
446
336
  const mat = new ShaderMaterial(
447
337
  shaderName,
@@ -462,11 +352,6 @@ export class SpriteBatch extends VFXBatch {
462
352
  }
463
353
  if (useEnvAtlas && atlas) {
464
354
  mat.setTexture('envAtlas', atlas);
465
- mat.setFloat('reflectionLevel', this.settings.reflectionLevel);
466
- }
467
- if (rim) {
468
- mat.setVector3('rimColor', new BVector3(rim.color[0], rim.color[1], rim.color[2]));
469
- mat.setVector2('rimParams', new BVector2(rim.power, rim.strength));
470
355
  }
471
356
  if (needsEyePosition) {
472
357
  const eyePosition = new BVector3();
@@ -477,44 +362,15 @@ export class SpriteBatch extends VFXBatch {
477
362
  return;
478
363
  }
479
364
  eyePosition.copyFrom(camera.globalPosition);
480
- mat.setVector3('eyePosition', eyePosition);
365
+ VFXBatch.writeBound(mat, (target) => target.setVector3('eyePosition', eyePosition));
481
366
  });
482
367
  }
483
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
368
+ if (tilesSheet(this.settings)) {
484
369
  mat.setFloat('tileCountX', this.settings.uTileCount);
485
370
  mat.setFloat('tileCountY', this.settings.vTileCount);
486
371
  }
487
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
488
- mat.setFloat('speedFactor', this.settings.softNearFade > 0 ? this.settings.softNearFade : 1.0);
489
- }
490
- if (this.settings.softParticles) {
491
- mat.setVector2(
492
- 'softParams',
493
- new BVector2(
494
- this.settings.softNearFade,
495
- 1.0 / Math.max(this.settings.softFarFade - this.settings.softNearFade, 0.0001)
496
- )
497
- );
498
- // Reused across binds: this runs on every draw call.
499
- const projParams = new BVector4(0, 0, 0, 0);
500
- mat.onBindObservable.add(() => {
501
- const camera = this.scene.activeCamera;
502
- if (camera) {
503
- projParams.x = camera.minZ;
504
- projParams.y = camera.maxZ;
505
- mat.setVector4('projParams', projParams);
506
- }
507
- });
508
- }
509
- if (this.settings.materialAlphaTest > 0) {
510
- mat.setFloat('alphaTest', this.settings.materialAlphaTest);
511
- }
512
- if (pivot) {
513
- mat.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
514
- }
515
- if (sizeLimits) {
516
- mat.setVector2('sizeLimits', sizeLimits);
517
- }
372
+ this.bindSoft(mat);
373
+ this.writeSpriteParams(mat, useEnvAtlas);
518
374
  if (lit) {
519
375
  this.writeFixedLighting(mat);
520
376
  if (lighting === 'scene') {
@@ -522,13 +378,15 @@ export class SpriteBatch extends VFXBatch {
522
378
  const color = new BVector3();
523
379
  const ambient = new BVector3();
524
380
  mat.onBindObservable.add(() => {
525
- if (SpriteBatch.readSceneLight(this.scene, direction, color, ambient)) {
526
- mat.setVector3('lightDirection', direction);
527
- mat.setVector3('lightColor', color);
528
- mat.setVector3('ambientColor', ambient);
529
- } else {
530
- this.writeFixedLighting(mat);
531
- }
381
+ const found = SpriteBatch.readSceneLight(this.scene, direction, color, ambient);
382
+ VFXBatch.writeBound(mat, (target) =>
383
+ found
384
+ ? target
385
+ .setVector3('lightDirection', direction)
386
+ .setVector3('lightColor', color)
387
+ .setVector3('ambientColor', ambient)
388
+ : this.writeFixedLighting(target)
389
+ );
532
390
  });
533
391
  }
534
392
  }
@@ -542,14 +400,52 @@ export class SpriteBatch extends VFXBatch {
542
400
  } else {
543
401
  mat.alphaMode = Constants.ALPHA_DISABLE;
544
402
  }
545
- mat.needDepthPrePass = this.settings.materialDepthWrite;
403
+ // A transparent surface that writes depth is drawn into the depth buffer first, so its own
404
+ // overlapping pieces do not fight; an opaque one writes depth as it draws.
405
+ mat.needDepthPrePass = this.settings.materialDepthWrite && this.settings.materialTransparent;
546
406
  mat.forceDepthWrite = this.settings.materialDepthWrite;
547
407
  mat.disableDepthWrite = !this.settings.materialDepthWrite;
408
+ // 0 keeps the engine's depth function (LEQUAL); ALWAYS draws over everything, like ZTest Always.
409
+ mat.depthFunction = this.settings.materialDepthTest ? 0 : Constants.ALWAYS;
548
410
 
411
+ // A rebuild (an env atlas that finished loading) replaces the material; the old one and its
412
+ // bind observers go with it. Textures belong to the settings, so they stay.
413
+ this.shaderMaterial?.dispose(false, false);
549
414
  this.mesh.material = mat;
550
415
  this.shaderMaterial = mat;
551
416
  }
552
417
 
418
+ /** Writes the live parameters only sprite and mesh batches have: reflection level, rim, pivot and size limits. */
419
+ private writeSpriteParams(material: ShaderMaterial, envAtlas: boolean): void {
420
+ if (envAtlas) {
421
+ material.setFloat('reflectionLevel', this.settings.reflectionLevel);
422
+ }
423
+ const rim = this.settings.renderMode === RenderMode.Mesh ? this.settings.rim : undefined;
424
+ if (rim) {
425
+ material.setVector3('rimColor', new BVector3(rim.color[0], rim.color[1], rim.color[2]));
426
+ material.setVector2('rimParams', new BVector2(rim.power, rim.strength));
427
+ }
428
+ const pivot = takesPivotAndSizeLimits(this.settings.renderMode) ? this.settings.pivot : undefined;
429
+ if (pivot) {
430
+ material.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
431
+ }
432
+ const sizeLimits = this.sizeLimits();
433
+ if (sizeLimits) {
434
+ material.setVector2('sizeLimits', sizeLimits);
435
+ }
436
+ }
437
+
438
+ protected override writeLiveUniforms(material: ShaderMaterial): void {
439
+ super.writeLiveUniforms(material);
440
+ this.writeSpriteParams(material, this.usesEnvAtlas());
441
+ }
442
+
443
+ /** True when the material samples the env atlas: a mesh batch whose atlas has loaded. */
444
+ private usesEnvAtlas(): boolean {
445
+ const atlas = this.settings.reflectionAtlas;
446
+ return this.settings.renderMode === RenderMode.Mesh && !!atlas && atlas.isReady();
447
+ }
448
+
553
449
  applyMeshLighting(lighting: MeshLightingSettings): void {
554
450
  this.storeFixedLighting(lighting);
555
451
  const material = this.shaderMaterial;
@@ -564,7 +460,7 @@ export class SpriteBatch extends VFXBatch {
564
460
  this.fixedAmbientColor.set(lighting.ambient.r, lighting.ambient.g, lighting.ambient.b);
565
461
  }
566
462
 
567
- private writeFixedLighting(material: ShaderMaterial): void {
463
+ private writeFixedLighting(material: ShaderMaterial | Effect): void {
568
464
  material.setVector3('lightDirection', this.fixedLightDirection);
569
465
  material.setVector3('lightColor', this.fixedLightColor);
570
466
  material.setVector3('ambientColor', this.fixedAmbientColor);
@@ -618,10 +514,10 @@ export class SpriteBatch extends VFXBatch {
618
514
  private quaternion5_ = new Quaternion();
619
515
  private rotationMat_ = new Matrix3();
620
516
  private rotationMat2_ = new Matrix3();
621
- private lastStretchedSpeedFactor = Number.NaN;
622
517
  /** Slot 0 holds the zero-alpha instance since the batch last went empty. */
623
518
  private clearedInstanceUploaded = false;
624
519
 
520
+ /** @internal */
625
521
  update(): void {
626
522
  let index = 0;
627
523
  let particleCount = 0;
@@ -630,7 +526,7 @@ export class SpriteBatch extends VFXBatch {
630
526
  const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
631
527
  const isBeamRender = renderMode === RenderMode.Beam;
632
528
  const usesVelocity = isStretchedRender || isBeamRender;
633
- const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
529
+ const flipsSprites = takesPivotAndSizeLimits(renderMode);
634
530
 
635
531
  const visibleSystems = this.getVisibleSystems();
636
532
  for (let i = 0; i < visibleSystems.length; i++) {
@@ -931,7 +827,7 @@ export class SpriteBatch extends VFXBatch {
931
827
  const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
932
828
  const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
933
829
  const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
934
- let vel: Vector3 = this.vector_;
830
+ const vel: Vector3 = this.vector_;
935
831
  vel.set(vx, vy, vz);
936
832
  if (!systemWorldSpace) {
937
833
  if (particle.parentMatrix) {
@@ -966,10 +862,6 @@ export class SpriteBatch extends VFXBatch {
966
862
  }
967
863
  }
968
864
 
969
- if (isStretchedRender && visibleSystems.length > 0) {
970
- this.syncStretchedSpeedFactor(visibleSystems[0]);
971
- }
972
-
973
865
  // Keep the mesh enabled even when count is 0 — toggling visibility off for a
974
866
  // single frame (common with short lifetimes + low emission overlap) causes flicker.
975
867
  this.mesh.forcedInstanceCount = index;
@@ -1002,32 +894,6 @@ export class SpriteBatch extends VFXBatch {
1002
894
  }
1003
895
  }
1004
896
 
1005
- /**
1006
- * Pushes the stretched-billboard speed factor to the shader when it changed.
1007
- *
1008
- * Deliberately kept out of `update`: touching the loosely typed
1009
- * `mesh.material` from inside that function gives V8 a generic property
1010
- * access it has no feedback for, which knocks the whole particle loop out of
1011
- * optimised code on every frame.
1012
- */
1013
- private syncStretchedSpeedFactor(system: IParticleSystem): void {
1014
- const material = this.shaderMaterial;
1015
- if (material === null) {
1016
- return;
1017
- }
1018
- const speedFactor = (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor ?? 1.0;
1019
- const clampedSpeedFactor = speedFactor === 0 ? 0.001 : speedFactor;
1020
- if (clampedSpeedFactor !== this.lastStretchedSpeedFactor) {
1021
- material.setFloat('speedFactor', clampedSpeedFactor);
1022
- this.lastStretchedSpeedFactor = clampedSpeedFactor;
1023
- }
1024
- }
1025
-
1026
- /** Stretched billboards and beams: drawn along a per-instance vector, by the stretched shader. */
1027
- private static drawsAlongVelocity(renderMode: RenderMode): boolean {
1028
- return renderMode === RenderMode.StretchedBillBoard || renderMode === RenderMode.Beam;
1029
- }
1030
-
1031
897
  private readonly beamEnd_ = new Vector3();
1032
898
 
1033
899
  /**
@@ -1051,16 +917,6 @@ export class SpriteBatch extends VFXBatch {
1051
917
  }
1052
918
  }
1053
919
 
1054
- /** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes or beams. */
1055
- private static takesPivotAndFlip(renderMode: RenderMode): boolean {
1056
- return (
1057
- renderMode === RenderMode.BillBoard ||
1058
- renderMode === RenderMode.StretchedBillBoard ||
1059
- renderMode === RenderMode.VerticalBillBoard ||
1060
- renderMode === RenderMode.HorizontalBillBoard
1061
- );
1062
- }
1063
-
1064
920
  /** Multiplies `count` RGBA colours starting at `base` by a system's {@link ParticleSystem.tint}. */
1065
921
  private static applyTint(colors: Float32Array, base: number, count: number, tint: Vector4): void {
1066
922
  for (let j = 0, o = base; j < count; j++, o += 4) {
@@ -1332,7 +1188,11 @@ export class SpriteBatch extends VFXBatch {
1332
1188
  }
1333
1189
  }
1334
1190
 
1191
+ /** @internal */
1335
1192
  dispose(): void {
1193
+ this.disposed = true;
1194
+ this.cancelAtlasRebuild?.();
1195
+ this.cancelAtlasRebuild = null;
1336
1196
  if (this.mesh.material) {
1337
1197
  this.mesh.material.dispose();
1338
1198
  }