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
@@ -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
 
@@ -43,6 +37,13 @@ export class SpriteBatch extends VFXBatch {
43
37
  texture = RawTexture.CreateRGBATexture(new Uint8Array([255, 255, 255, 255]), 1, 1, scene);
44
38
  texture.name = 'quarksMeshWhite';
45
39
  SpriteBatch.whiteTextureByScene.set(scene, texture);
40
+ // Disposed from outside (a scene-wide texture sweep): the next batch makes a new one.
41
+ const made = texture;
42
+ texture.onDisposeObservable.addOnce(() => {
43
+ if (SpriteBatch.whiteTextureByScene.get(scene) === made) {
44
+ SpriteBatch.whiteTextureByScene.delete(scene);
45
+ }
46
+ });
46
47
  }
47
48
  return texture;
48
49
  }
@@ -53,8 +54,11 @@ export class SpriteBatch extends VFXBatch {
53
54
  private uvTileBuffer!: Float32Array;
54
55
  private velocityBuffer?: Float32Array;
55
56
 
56
- /** Concretely typed handle on the batch material, see syncStretchedSpeedFactor. */
57
+ /** Concretely typed handle on the batch material, for the lighting and live-parameter writes. */
57
58
  private shaderMaterial: ShaderMaterial | null = null;
59
+ /** Cancels the pending rebuild for an env atlas that has not loaded yet. */
60
+ private cancelAtlasRebuild: (() => void) | null = null;
61
+ private disposed = false;
58
62
  /** The `'fixed'` mesh light, as shader uniforms; see applyMeshLighting. */
59
63
  private readonly fixedLightDirection = new BVector3();
60
64
  private readonly fixedLightColor = new BVector3();
@@ -67,16 +71,17 @@ export class SpriteBatch extends VFXBatch {
67
71
  private uvTileVB!: VertexBuffer;
68
72
  private velocityVB?: VertexBuffer;
69
73
 
70
- constructor(settings: VFXBatchSettings, scene: Scene) {
71
- super(settings, scene);
74
+ constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
75
+ super(settings, scene, depth);
72
76
  this.storeFixedLighting(defaultMeshLighting());
73
- this.maxParticles = 1000;
77
+ this.maxParticles = this.initialCapacity;
74
78
  this.setupBuffers();
75
79
  this.rebuildMaterial();
76
80
  }
77
81
 
82
+ /** @internal */
78
83
  setupBuffers(): void {
79
- this.mesh.dispose();
84
+ this.mesh?.dispose();
80
85
  this.mesh = new Mesh('spriteBatch', this.scene);
81
86
  this.configureMesh();
82
87
 
@@ -170,7 +175,7 @@ export class SpriteBatch extends VFXBatch {
170
175
  }
171
176
  this.mesh.setVerticesBuffer(this.rotationVB);
172
177
 
173
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
178
+ if (drawsAlongVelocity(this.settings.renderMode)) {
174
179
  this.velocityBuffer = new Float32Array(this.maxParticles * 4);
175
180
  this.velocityVB = new VertexBuffer(engine, this.velocityBuffer, 'velocity', true, false, 4, true);
176
181
  this.mesh.setVerticesBuffer(this.velocityVB);
@@ -180,10 +185,19 @@ export class SpriteBatch extends VFXBatch {
180
185
  this.mesh.doNotSyncBoundingInfo = true;
181
186
  }
182
187
 
188
+ /** @internal */
189
+ protected readonly initialCapacity = 1000;
190
+
191
+ /** @internal */
183
192
  expandBuffers(target: number): void {
184
193
  while (target >= this.maxParticles) {
185
194
  this.maxParticles *= 2;
186
195
  }
196
+ this.resizeBuffers();
197
+ }
198
+
199
+ /** @internal */
200
+ protected resizeBuffers(): void {
187
201
  this.disposeExpandableBuffers();
188
202
  this.buildExpandableBuffers();
189
203
  this.sortScratch = undefined;
@@ -193,10 +207,7 @@ export class SpriteBatch extends VFXBatch {
193
207
  /** The screen-size limits as a uniform value, or null when the batch has none that apply. */
194
208
  private sizeLimits(): BVector2 | null {
195
209
  const {minParticleSize, maxParticleSize} = this.settings;
196
- if (!SpriteBatch.takesPivotAndFlip(this.settings.renderMode)) {
197
- return null;
198
- }
199
- if (minParticleSize <= 0 && maxParticleSize === Infinity) {
210
+ if (!takesPivotAndSizeLimits(this.settings.renderMode) || !hasSizeLimits(this.settings)) {
200
211
  return null;
201
212
  }
202
213
  // The shader clamps; an unlimited maximum is just a very large one.
@@ -295,153 +306,47 @@ export class SpriteBatch extends VFXBatch {
295
306
  return spare;
296
307
  }
297
308
 
309
+ /** @internal */
298
310
  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
-
311
+ const isMesh = this.settings.renderMode === RenderMode.Mesh;
319
312
  // Mesh batches without a diffuse map still need a sampler2D on iOS WebKit —
320
313
  // 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
- }
314
+ const mapTexture = this.settings.texture ?? (isMesh ? SpriteBatch.whiteTexture(this.scene) : null);
327
315
  const atlas = this.settings.reflectionAtlas;
328
- const atlasPending = this.settings.renderMode === RenderMode.Mesh && !!atlas && !atlas.isReady();
316
+ const atlasPending = isMesh && !!atlas && !atlas.isReady();
317
+ this.cancelAtlasRebuild?.();
318
+ this.cancelAtlasRebuild = null;
329
319
  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');
320
+ const onLoad = (
321
+ atlas as {
322
+ onLoadObservable?: {addOnce: (cb: () => void) => unknown; remove: (observer: unknown) => boolean};
323
+ }
324
+ ).onLoadObservable;
325
+ if (onLoad) {
326
+ const observer = onLoad.addOnce(() => {
327
+ this.cancelAtlasRebuild = null;
328
+ if (!this.disposed) {
329
+ this.rebuildMaterial();
330
+ }
331
+ });
332
+ this.cancelAtlasRebuild = () => onLoad.remove(observer);
333
+ }
367
334
  }
368
- const isMesh = this.settings.renderMode === RenderMode.Mesh;
335
+ const useEnvAtlas = this.usesEnvAtlas();
336
+ const program = shaderProgram(this.settings, {
337
+ depth: this.depth,
338
+ envAtlasReady: useEnvAtlas,
339
+ meshUV2: this.meshUV2(),
340
+ });
341
+ const {defines, attributes, uniforms, samplers} = program;
342
+ const shaderName = program.name;
369
343
  const lighting = this.settings.lighting ?? 'fixed';
370
344
  const lit = isMesh && lighting !== 'unlit';
371
- if (isMesh && !lit) {
372
- defines.push('MESH_UNLIT');
373
- }
374
345
  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
346
  const needsEyePosition = useEnvAtlas || !!rim;
384
- defines.push(...this.surfaceDefines());
385
347
 
386
348
  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
- }
349
+ registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
445
350
 
446
351
  const mat = new ShaderMaterial(
447
352
  shaderName,
@@ -462,11 +367,6 @@ export class SpriteBatch extends VFXBatch {
462
367
  }
463
368
  if (useEnvAtlas && atlas) {
464
369
  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
370
  }
471
371
  if (needsEyePosition) {
472
372
  const eyePosition = new BVector3();
@@ -477,44 +377,15 @@ export class SpriteBatch extends VFXBatch {
477
377
  return;
478
378
  }
479
379
  eyePosition.copyFrom(camera.globalPosition);
480
- mat.setVector3('eyePosition', eyePosition);
380
+ VFXBatch.writeBound(mat, (target) => target.setVector3('eyePosition', eyePosition));
481
381
  });
482
382
  }
483
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
383
+ if (tilesSheet(this.settings)) {
484
384
  mat.setFloat('tileCountX', this.settings.uTileCount);
485
385
  mat.setFloat('tileCountY', this.settings.vTileCount);
486
386
  }
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
- }
387
+ this.bindSoft(mat);
388
+ this.writeSpriteParams(mat, useEnvAtlas);
518
389
  if (lit) {
519
390
  this.writeFixedLighting(mat);
520
391
  if (lighting === 'scene') {
@@ -522,34 +393,76 @@ export class SpriteBatch extends VFXBatch {
522
393
  const color = new BVector3();
523
394
  const ambient = new BVector3();
524
395
  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
- }
396
+ const found = SpriteBatch.readSceneLight(this.scene, direction, color, ambient);
397
+ VFXBatch.writeBound(mat, (target) =>
398
+ found
399
+ ? target
400
+ .setVector3('lightDirection', direction)
401
+ .setVector3('lightColor', color)
402
+ .setVector3('ambientColor', ambient)
403
+ : this.writeFixedLighting(target)
404
+ );
532
405
  });
533
406
  }
534
407
  }
535
408
 
536
409
  this.bindFog(mat);
537
410
  this.bindSurface(mat);
538
- mat.backFaceCulling = false;
411
+ // Billboards, beams and the like are flat sheets: both faces, whatever the setting says.
412
+ mat.backFaceCulling = this.settings.renderFace !== 'both';
413
+ mat.cullBackFaces = this.settings.renderFace !== 'back';
539
414
 
540
415
  if (this.settings.materialTransparent) {
541
416
  mat.alphaMode = this.settings.materialBlendMode;
542
417
  } else {
543
418
  mat.alphaMode = Constants.ALPHA_DISABLE;
544
419
  }
545
- mat.needDepthPrePass = this.settings.materialDepthWrite;
420
+ // A transparent surface that writes depth is drawn into the depth buffer first, so its own
421
+ // overlapping pieces do not fight; an opaque one writes depth as it draws.
422
+ mat.needDepthPrePass = this.settings.materialDepthWrite && this.settings.materialTransparent;
546
423
  mat.forceDepthWrite = this.settings.materialDepthWrite;
547
424
  mat.disableDepthWrite = !this.settings.materialDepthWrite;
425
+ // 0 keeps the engine's depth function (LEQUAL); ALWAYS draws over everything, like ZTest Always.
426
+ mat.depthFunction = this.settings.materialDepthTest ? 0 : Constants.ALWAYS;
548
427
 
428
+ // A rebuild (an env atlas that finished loading) replaces the material; the old one and its
429
+ // bind observers go with it. Textures belong to the settings, so they stay.
430
+ this.shaderMaterial?.dispose(false, false);
549
431
  this.mesh.material = mat;
550
432
  this.shaderMaterial = mat;
551
433
  }
552
434
 
435
+ /** Writes the live parameters only sprite and mesh batches have: reflection level, rim, pivot and size limits. */
436
+ private writeSpriteParams(material: ShaderMaterial, envAtlas: boolean): void {
437
+ if (envAtlas) {
438
+ material.setFloat('reflectionLevel', this.settings.reflectionLevel);
439
+ }
440
+ const rim = this.settings.renderMode === RenderMode.Mesh ? this.settings.rim : undefined;
441
+ if (rim) {
442
+ material.setVector3('rimColor', new BVector3(rim.color[0], rim.color[1], rim.color[2]));
443
+ material.setVector2('rimParams', new BVector2(rim.power, rim.strength));
444
+ }
445
+ const pivot = takesPivotAndSizeLimits(this.settings.renderMode) ? this.settings.pivot : undefined;
446
+ if (pivot) {
447
+ material.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
448
+ }
449
+ const sizeLimits = this.sizeLimits();
450
+ if (sizeLimits) {
451
+ material.setVector2('sizeLimits', sizeLimits);
452
+ }
453
+ }
454
+
455
+ protected override writeLiveUniforms(material: ShaderMaterial): void {
456
+ super.writeLiveUniforms(material);
457
+ this.writeSpriteParams(material, this.usesEnvAtlas());
458
+ }
459
+
460
+ /** True when the material samples the env atlas: a mesh batch whose atlas has loaded. */
461
+ private usesEnvAtlas(): boolean {
462
+ const atlas = this.settings.reflectionAtlas;
463
+ return this.settings.renderMode === RenderMode.Mesh && !!atlas && atlas.isReady();
464
+ }
465
+
553
466
  applyMeshLighting(lighting: MeshLightingSettings): void {
554
467
  this.storeFixedLighting(lighting);
555
468
  const material = this.shaderMaterial;
@@ -564,7 +477,7 @@ export class SpriteBatch extends VFXBatch {
564
477
  this.fixedAmbientColor.set(lighting.ambient.r, lighting.ambient.g, lighting.ambient.b);
565
478
  }
566
479
 
567
- private writeFixedLighting(material: ShaderMaterial): void {
480
+ private writeFixedLighting(material: ShaderMaterial | Effect): void {
568
481
  material.setVector3('lightDirection', this.fixedLightDirection);
569
482
  material.setVector3('lightColor', this.fixedLightColor);
570
483
  material.setVector3('ambientColor', this.fixedAmbientColor);
@@ -618,10 +531,10 @@ export class SpriteBatch extends VFXBatch {
618
531
  private quaternion5_ = new Quaternion();
619
532
  private rotationMat_ = new Matrix3();
620
533
  private rotationMat2_ = new Matrix3();
621
- private lastStretchedSpeedFactor = Number.NaN;
622
534
  /** Slot 0 holds the zero-alpha instance since the batch last went empty. */
623
535
  private clearedInstanceUploaded = false;
624
536
 
537
+ /** @internal */
625
538
  update(): void {
626
539
  let index = 0;
627
540
  let particleCount = 0;
@@ -630,15 +543,13 @@ export class SpriteBatch extends VFXBatch {
630
543
  const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
631
544
  const isBeamRender = renderMode === RenderMode.Beam;
632
545
  const usesVelocity = isStretchedRender || isBeamRender;
633
- const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
546
+ const flipsSprites = takesPivotAndSizeLimits(renderMode);
634
547
 
635
548
  const visibleSystems = this.getVisibleSystems();
636
549
  for (let i = 0; i < visibleSystems.length; i++) {
637
550
  particleCount += visibleSystems[i].particleNum;
638
551
  }
639
- if (particleCount > this.maxParticles) {
640
- this.expandBuffers(particleCount);
641
- }
552
+ this.fitBuffers(particleCount);
642
553
 
643
554
  for (let s = 0; s < visibleSystems.length; s++) {
644
555
  const system = visibleSystems[s];
@@ -931,7 +842,7 @@ export class SpriteBatch extends VFXBatch {
931
842
  const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
932
843
  const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
933
844
  const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
934
- let vel: Vector3 = this.vector_;
845
+ const vel: Vector3 = this.vector_;
935
846
  vel.set(vx, vy, vz);
936
847
  if (!systemWorldSpace) {
937
848
  if (particle.parentMatrix) {
@@ -966,10 +877,6 @@ export class SpriteBatch extends VFXBatch {
966
877
  }
967
878
  }
968
879
 
969
- if (isStretchedRender && visibleSystems.length > 0) {
970
- this.syncStretchedSpeedFactor(visibleSystems[0]);
971
- }
972
-
973
880
  // Keep the mesh enabled even when count is 0 — toggling visibility off for a
974
881
  // single frame (common with short lifetimes + low emission overlap) causes flicker.
975
882
  this.mesh.forcedInstanceCount = index;
@@ -1002,32 +909,6 @@ export class SpriteBatch extends VFXBatch {
1002
909
  }
1003
910
  }
1004
911
 
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
912
  private readonly beamEnd_ = new Vector3();
1032
913
 
1033
914
  /**
@@ -1051,16 +932,6 @@ export class SpriteBatch extends VFXBatch {
1051
932
  }
1052
933
  }
1053
934
 
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
935
  /** Multiplies `count` RGBA colours starting at `base` by a system's {@link ParticleSystem.tint}. */
1065
936
  private static applyTint(colors: Float32Array, base: number, count: number, tint: Vector4): void {
1066
937
  for (let j = 0, o = base; j < count; j++, o += 4) {
@@ -1332,7 +1203,11 @@ export class SpriteBatch extends VFXBatch {
1332
1203
  }
1333
1204
  }
1334
1205
 
1206
+ /** @internal */
1335
1207
  dispose(): void {
1208
+ this.disposed = true;
1209
+ this.cancelAtlasRebuild?.();
1210
+ this.cancelAtlasRebuild = null;
1336
1211
  if (this.mesh.material) {
1337
1212
  this.mesh.material.dispose();
1338
1213
  }