babylon.quarks 0.25.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 +854 -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 +2210 -613
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +215 -106
  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 +1012 -1962
  17. package/src/QuarksLoader.ts +463 -75
  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 +149 -238
  22. package/src/TrailBatch.ts +586 -286
  23. package/src/VFXBatch.ts +517 -153
  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 +51 -35
  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 +132 -42
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +28 -27
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +30 -28
  37. package/src/shaders/particle_frag.glsl.ts +6 -22
  38. package/src/shaders/particle_frag.wgsl.ts +6 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +22 -29
  40. package/src/shaders/particle_physics_frag.wgsl.ts +22 -30
  41. package/src/shaders/particle_vert.glsl.ts +11 -26
  42. package/src/shaders/particle_vert.wgsl.ts +11 -25
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +21 -39
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +22 -40
  45. package/src/shaders/trail_frag.glsl.ts +14 -1
  46. package/src/shaders/trail_frag.wgsl.ts +14 -0
  47. package/src/shaders/trail_vert.glsl.ts +53 -36
  48. package/src/shaders/trail_vert.wgsl.ts +54 -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,37 +1,32 @@
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,
15
15
  SpriteParticle,
16
16
  StretchedBillBoardSettings,
17
17
  Vector3,
18
+ Vector4,
18
19
  } from 'quarks.core';
19
20
  import {VFXBatchSettings} from './BatchedRenderer';
21
+ import {hasSizeLimits, takesPivotAndSizeLimits, tilesSheet} from './batchKey';
20
22
  import {gather, sortByKey, SortScratch} from './instanceSort';
21
23
  import type {ParticleSystem} from './ParticleSystem';
22
- import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
23
- import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
24
- import particle_frag from './shaders/particle_frag.glsl';
25
- import particle_frag_wgsl from './shaders/particle_frag.wgsl';
26
- import particle_physics_frag from './shaders/particle_physics_frag.glsl';
27
- import particle_physics_frag_wgsl from './shaders/particle_physics_frag.wgsl';
28
- import particle_vert from './shaders/particle_vert.glsl';
29
- import particle_vert_wgsl from './shaders/particle_vert.wgsl';
30
- import {registerShaders, shaderLanguageFor, ShaderSources} from './shaders/shaderLanguageSupport';
31
- import stretched_bb_particle_vert from './shaders/stretched_bb_particle_vert.glsl';
32
- 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';
33
27
  import {defaultMeshLighting, MeshLightingSettings, RenderMode, VFXBatch} from './VFXBatch';
34
28
 
29
+ /** The batch of billboard, stretched, mesh and beam particles. @internal */
35
30
  export class SpriteBatch extends VFXBatch {
36
31
  private static whiteTextureByScene = new WeakMap<Scene, RawTexture>();
37
32
 
@@ -52,8 +47,11 @@ export class SpriteBatch extends VFXBatch {
52
47
  private uvTileBuffer!: Float32Array;
53
48
  private velocityBuffer?: Float32Array;
54
49
 
55
- /** Concretely typed handle on the batch material, see syncStretchedSpeedFactor. */
50
+ /** Concretely typed handle on the batch material, for the lighting and live-parameter writes. */
56
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;
57
55
  /** The `'fixed'` mesh light, as shader uniforms; see applyMeshLighting. */
58
56
  private readonly fixedLightDirection = new BVector3();
59
57
  private readonly fixedLightColor = new BVector3();
@@ -66,14 +64,15 @@ export class SpriteBatch extends VFXBatch {
66
64
  private uvTileVB!: VertexBuffer;
67
65
  private velocityVB?: VertexBuffer;
68
66
 
69
- constructor(settings: VFXBatchSettings, scene: Scene) {
70
- super(settings, scene);
67
+ constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
68
+ super(settings, scene, depth);
71
69
  this.storeFixedLighting(defaultMeshLighting());
72
70
  this.maxParticles = 1000;
73
71
  this.setupBuffers();
74
72
  this.rebuildMaterial();
75
73
  }
76
74
 
75
+ /** @internal */
77
76
  setupBuffers(): void {
78
77
  this.mesh.dispose();
79
78
  this.mesh = new Mesh('spriteBatch', this.scene);
@@ -91,6 +90,9 @@ export class SpriteBatch extends VFXBatch {
91
90
  } else {
92
91
  vertexData.uvs = new Float32Array(vertexCount * 2);
93
92
  }
93
+ if (this.meshUV2()) {
94
+ vertexData.uvs2 = this.settings.instancingUVs2!;
95
+ }
94
96
  if (this.settings.instancingNormals) {
95
97
  vertexData.normals = this.settings.instancingNormals;
96
98
  }
@@ -99,6 +101,33 @@ export class SpriteBatch extends VFXBatch {
99
101
  this.buildExpandableBuffers();
100
102
  }
101
103
 
104
+ /**
105
+ * True when a mesh batch reads UV channel 1 and has a second UV set to
106
+ * read it from, two values per vertex. A set of any other length is left
107
+ * out with a warning, and channel 1 then reads the first set.
108
+ */
109
+ private warnedUV2 = false;
110
+
111
+ private meshUV2(): boolean {
112
+ const uvs2 = this.settings.instancingUVs2;
113
+ if (this.settings.renderMode !== RenderMode.Mesh || !uvs2 || !this.usesUV1()) {
114
+ return false;
115
+ }
116
+ const expected = (this.settings.instancingGeometry.length / 3) * 2;
117
+ if (uvs2.length !== expected) {
118
+ if (this.warnedUV2) {
119
+ return false;
120
+ }
121
+ this.warnedUV2 = true;
122
+ console.warn(
123
+ `[babylon.quarks] instancingUVs2 has ${uvs2.length} values for ${expected / 2} vertices ` +
124
+ '(two per vertex expected); UV channel 1 reads the first UV set instead.'
125
+ );
126
+ return false;
127
+ }
128
+ return true;
129
+ }
130
+
102
131
  /** Releases the instance vertex buffers so a resize does not leak GPU memory. */
103
132
  private disposeExpandableBuffers(): void {
104
133
  this.offsetVB?.dispose();
@@ -139,7 +168,7 @@ export class SpriteBatch extends VFXBatch {
139
168
  }
140
169
  this.mesh.setVerticesBuffer(this.rotationVB);
141
170
 
142
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
171
+ if (drawsAlongVelocity(this.settings.renderMode)) {
143
172
  this.velocityBuffer = new Float32Array(this.maxParticles * 4);
144
173
  this.velocityVB = new VertexBuffer(engine, this.velocityBuffer, 'velocity', true, false, 4, true);
145
174
  this.mesh.setVerticesBuffer(this.velocityVB);
@@ -149,6 +178,7 @@ export class SpriteBatch extends VFXBatch {
149
178
  this.mesh.doNotSyncBoundingInfo = true;
150
179
  }
151
180
 
181
+ /** @internal */
152
182
  expandBuffers(target: number): void {
153
183
  while (target >= this.maxParticles) {
154
184
  this.maxParticles *= 2;
@@ -162,10 +192,7 @@ export class SpriteBatch extends VFXBatch {
162
192
  /** The screen-size limits as a uniform value, or null when the batch has none that apply. */
163
193
  private sizeLimits(): BVector2 | null {
164
194
  const {minParticleSize, maxParticleSize} = this.settings;
165
- if (!SpriteBatch.takesPivotAndFlip(this.settings.renderMode)) {
166
- return null;
167
- }
168
- if (minParticleSize <= 0 && maxParticleSize === Infinity) {
195
+ if (!takesPivotAndSizeLimits(this.settings.renderMode) || !hasSizeLimits(this.settings)) {
169
196
  return null;
170
197
  }
171
198
  // The shader clamps; an unlimited maximum is just a very large one.
@@ -264,147 +291,47 @@ export class SpriteBatch extends VFXBatch {
264
291
  return spare;
265
292
  }
266
293
 
294
+ /** @internal */
267
295
  rebuildMaterial(): void {
268
- // The shader sources only depend on the render mode, so the store entry is
269
- // registered once per mode. A unique name per rebuild would leak entries in
270
- // Effect.ShadersStore and defeat Babylon's compiled-effect cache.
271
- const shaderName = `quarksParticle_${this.settings.renderMode}`;
272
- this.lastStretchedSpeedFactor = Number.NaN;
273
- const defines: string[] = this.fogDefines();
274
-
275
- let vertexShader: ShaderSources;
276
- let fragmentShader: ShaderSources;
277
- if (this.settings.renderMode === RenderMode.Mesh) {
278
- vertexShader = {glsl: local_particle_physics_vert, wgsl: local_particle_physics_vert_wgsl};
279
- fragmentShader = {glsl: particle_physics_frag, wgsl: particle_physics_frag_wgsl};
280
- } else if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
281
- vertexShader = {glsl: stretched_bb_particle_vert, wgsl: stretched_bb_particle_vert_wgsl};
282
- fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
283
- } else {
284
- vertexShader = {glsl: particle_vert, wgsl: particle_vert_wgsl};
285
- fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
286
- }
287
-
296
+ const isMesh = this.settings.renderMode === RenderMode.Mesh;
288
297
  // Mesh batches without a diffuse map still need a sampler2D on iOS WebKit —
289
298
  // a zero-sampler particle mesh effect often never becomes drawable there.
290
- const mapTexture =
291
- this.settings.texture ??
292
- (this.settings.renderMode === RenderMode.Mesh ? SpriteBatch.whiteTexture(this.scene) : null);
293
- if (mapTexture) {
294
- defines.push('USE_MAP');
295
- }
299
+ const mapTexture = this.settings.texture ?? (isMesh ? SpriteBatch.whiteTexture(this.scene) : null);
296
300
  const atlas = this.settings.reflectionAtlas;
297
- const atlasPending = this.settings.renderMode === RenderMode.Mesh && !!atlas && !atlas.isReady();
301
+ const atlasPending = isMesh && !!atlas && !atlas.isReady();
302
+ this.cancelAtlasRebuild?.();
303
+ this.cancelAtlasRebuild = null;
298
304
  if (atlasPending && atlas) {
299
- const onLoad = (atlas as {onLoadObservable?: {addOnce: (cb: () => void) => void}}).onLoadObservable;
300
- onLoad?.addOnce(() => {
301
- this.rebuildMaterial();
302
- });
303
- }
304
- const useEnvAtlas = this.settings.renderMode === RenderMode.Mesh && !!atlas && atlas.isReady();
305
- if (useEnvAtlas) {
306
- defines.push('USE_ENVMAP_ATLAS');
307
- }
308
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
309
- defines.push('UV_TILE');
310
- }
311
- if (this.settings.blendTiles) {
312
- defines.push('TILE_BLEND');
313
- }
314
- if (this.settings.softParticles) {
315
- defines.push('SOFT_PARTICLES');
316
- }
317
- if (this.settings.materialAlphaTest > 0) {
318
- defines.push('USE_ALPHATEST');
319
- }
320
- if (this.settings.renderMode === RenderMode.VerticalBillBoard) {
321
- defines.push('VERTICAL');
322
- }
323
- if (this.settings.renderMode === RenderMode.HorizontalBillBoard) {
324
- defines.push('HORIZONTAL');
325
- }
326
- if (this.settings.renderMode === RenderMode.Beam) {
327
- defines.push('BEAM');
328
- }
329
- const pivot = SpriteBatch.takesPivotAndFlip(this.settings.renderMode) ? this.settings.pivot : undefined;
330
- if (pivot) {
331
- defines.push('PIVOT');
332
- }
333
- const sizeLimits = this.sizeLimits();
334
- if (sizeLimits) {
335
- 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
+ }
336
319
  }
337
- 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;
338
328
  const lighting = this.settings.lighting ?? 'fixed';
339
329
  const lit = isMesh && lighting !== 'unlit';
340
- if (isMesh && !lit) {
341
- defines.push('MESH_UNLIT');
342
- }
343
330
  const rim = isMesh ? this.settings.rim : undefined;
344
- if (rim) {
345
- defines.push('MESH_RIM');
346
- if (rim.mode === 'mix') {
347
- defines.push('MESH_RIM_MIX');
348
- }
349
- }
350
331
  const needsEyePosition = useEnvAtlas || !!rim;
351
- defines.push(...this.surfaceDefines());
352
332
 
353
333
  const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
354
- registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
355
-
356
- const attributes = ['position', 'uv', 'offset', 'color', 'size', 'rotation', 'uvTile'];
357
- if (this.settings.renderMode === RenderMode.Mesh) {
358
- attributes.push('normal');
359
- }
360
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
361
- attributes.push('velocity');
362
- }
363
-
364
- const uniforms = ['world', 'view', 'projection', 'worldView', 'worldViewProjection', ...VFXBatch.FOG_UNIFORMS];
365
- const samplers: string[] = [];
366
-
367
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
368
- uniforms.push('tileCountX');
369
- uniforms.push('tileCountY');
370
- }
371
- if (mapTexture) {
372
- samplers.push('map');
373
- }
374
- if (useEnvAtlas) {
375
- samplers.push('envAtlas');
376
- uniforms.push('reflectionLevel');
377
- }
378
- if (needsEyePosition) {
379
- uniforms.push('eyePosition');
380
- }
381
- if (rim) {
382
- uniforms.push('rimColor');
383
- uniforms.push('rimParams');
384
- }
385
- this.surfaceUniforms(uniforms, samplers);
386
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
387
- uniforms.push('speedFactor');
388
- }
389
- if (this.settings.softParticles) {
390
- uniforms.push('softParams');
391
- uniforms.push('projParams');
392
- samplers.push('depthTexture');
393
- }
394
- if (this.settings.materialAlphaTest > 0) {
395
- uniforms.push('alphaTest');
396
- }
397
- if (pivot) {
398
- uniforms.push('pivot');
399
- }
400
- if (sizeLimits) {
401
- uniforms.push('sizeLimits');
402
- }
403
- if (lit) {
404
- uniforms.push('lightDirection');
405
- uniforms.push('lightColor');
406
- uniforms.push('ambientColor');
407
- }
334
+ registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
408
335
 
409
336
  const mat = new ShaderMaterial(
410
337
  shaderName,
@@ -425,11 +352,6 @@ export class SpriteBatch extends VFXBatch {
425
352
  }
426
353
  if (useEnvAtlas && atlas) {
427
354
  mat.setTexture('envAtlas', atlas);
428
- mat.setFloat('reflectionLevel', this.settings.reflectionLevel);
429
- }
430
- if (rim) {
431
- mat.setVector3('rimColor', new BVector3(rim.color[0], rim.color[1], rim.color[2]));
432
- mat.setVector2('rimParams', new BVector2(rim.power, rim.strength));
433
355
  }
434
356
  if (needsEyePosition) {
435
357
  const eyePosition = new BVector3();
@@ -440,44 +362,15 @@ export class SpriteBatch extends VFXBatch {
440
362
  return;
441
363
  }
442
364
  eyePosition.copyFrom(camera.globalPosition);
443
- mat.setVector3('eyePosition', eyePosition);
365
+ VFXBatch.writeBound(mat, (target) => target.setVector3('eyePosition', eyePosition));
444
366
  });
445
367
  }
446
- if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
368
+ if (tilesSheet(this.settings)) {
447
369
  mat.setFloat('tileCountX', this.settings.uTileCount);
448
370
  mat.setFloat('tileCountY', this.settings.vTileCount);
449
371
  }
450
- if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
451
- mat.setFloat('speedFactor', this.settings.softNearFade > 0 ? this.settings.softNearFade : 1.0);
452
- }
453
- if (this.settings.softParticles) {
454
- mat.setVector2(
455
- 'softParams',
456
- new BVector2(
457
- this.settings.softNearFade,
458
- 1.0 / Math.max(this.settings.softFarFade - this.settings.softNearFade, 0.0001)
459
- )
460
- );
461
- // Reused across binds: this runs on every draw call.
462
- const projParams = new BVector4(0, 0, 0, 0);
463
- mat.onBindObservable.add(() => {
464
- const camera = this.scene.activeCamera;
465
- if (camera) {
466
- projParams.x = camera.minZ;
467
- projParams.y = camera.maxZ;
468
- mat.setVector4('projParams', projParams);
469
- }
470
- });
471
- }
472
- if (this.settings.materialAlphaTest > 0) {
473
- mat.setFloat('alphaTest', this.settings.materialAlphaTest);
474
- }
475
- if (pivot) {
476
- mat.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
477
- }
478
- if (sizeLimits) {
479
- mat.setVector2('sizeLimits', sizeLimits);
480
- }
372
+ this.bindSoft(mat);
373
+ this.writeSpriteParams(mat, useEnvAtlas);
481
374
  if (lit) {
482
375
  this.writeFixedLighting(mat);
483
376
  if (lighting === 'scene') {
@@ -485,13 +378,15 @@ export class SpriteBatch extends VFXBatch {
485
378
  const color = new BVector3();
486
379
  const ambient = new BVector3();
487
380
  mat.onBindObservable.add(() => {
488
- if (SpriteBatch.readSceneLight(this.scene, direction, color, ambient)) {
489
- mat.setVector3('lightDirection', direction);
490
- mat.setVector3('lightColor', color);
491
- mat.setVector3('ambientColor', ambient);
492
- } else {
493
- this.writeFixedLighting(mat);
494
- }
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
+ );
495
390
  });
496
391
  }
497
392
  }
@@ -505,14 +400,52 @@ export class SpriteBatch extends VFXBatch {
505
400
  } else {
506
401
  mat.alphaMode = Constants.ALPHA_DISABLE;
507
402
  }
508
- 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;
509
406
  mat.forceDepthWrite = this.settings.materialDepthWrite;
510
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;
511
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);
512
414
  this.mesh.material = mat;
513
415
  this.shaderMaterial = mat;
514
416
  }
515
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
+
516
449
  applyMeshLighting(lighting: MeshLightingSettings): void {
517
450
  this.storeFixedLighting(lighting);
518
451
  const material = this.shaderMaterial;
@@ -527,7 +460,7 @@ export class SpriteBatch extends VFXBatch {
527
460
  this.fixedAmbientColor.set(lighting.ambient.r, lighting.ambient.g, lighting.ambient.b);
528
461
  }
529
462
 
530
- private writeFixedLighting(material: ShaderMaterial): void {
463
+ private writeFixedLighting(material: ShaderMaterial | Effect): void {
531
464
  material.setVector3('lightDirection', this.fixedLightDirection);
532
465
  material.setVector3('lightColor', this.fixedLightColor);
533
466
  material.setVector3('ambientColor', this.fixedAmbientColor);
@@ -581,10 +514,10 @@ export class SpriteBatch extends VFXBatch {
581
514
  private quaternion5_ = new Quaternion();
582
515
  private rotationMat_ = new Matrix3();
583
516
  private rotationMat2_ = new Matrix3();
584
- private lastStretchedSpeedFactor = Number.NaN;
585
517
  /** Slot 0 holds the zero-alpha instance since the batch last went empty. */
586
518
  private clearedInstanceUploaded = false;
587
519
 
520
+ /** @internal */
588
521
  update(): void {
589
522
  let index = 0;
590
523
  let particleCount = 0;
@@ -593,7 +526,7 @@ export class SpriteBatch extends VFXBatch {
593
526
  const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
594
527
  const isBeamRender = renderMode === RenderMode.Beam;
595
528
  const usesVelocity = isStretchedRender || isBeamRender;
596
- const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
529
+ const flipsSprites = takesPivotAndSizeLimits(renderMode);
597
530
 
598
531
  const visibleSystems = this.getVisibleSystems();
599
532
  for (let i = 0; i < visibleSystems.length; i++) {
@@ -894,7 +827,7 @@ export class SpriteBatch extends VFXBatch {
894
827
  const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
895
828
  const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
896
829
  const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
897
- let vel: Vector3 = this.vector_;
830
+ const vel: Vector3 = this.vector_;
898
831
  vel.set(vx, vy, vz);
899
832
  if (!systemWorldSpace) {
900
833
  if (particle.parentMatrix) {
@@ -913,6 +846,10 @@ export class SpriteBatch extends VFXBatch {
913
846
  }
914
847
  }
915
848
 
849
+ const tint = (system as ParticleSystem).tint;
850
+ if (tint !== undefined && index > firstIndex) {
851
+ SpriteBatch.applyTint(this.colorBuffer, firstIndex * 4, index - firstIndex, tint);
852
+ }
916
853
  if (flipsSprites && system.flip !== undefined) {
917
854
  if (store !== undefined) {
918
855
  SpriteBatch.applyFlip(this.sizeBuffer, firstIndex * 3, store.flip, particleNum);
@@ -925,10 +862,6 @@ export class SpriteBatch extends VFXBatch {
925
862
  }
926
863
  }
927
864
 
928
- if (isStretchedRender && visibleSystems.length > 0) {
929
- this.syncStretchedSpeedFactor(visibleSystems[0]);
930
- }
931
-
932
865
  // Keep the mesh enabled even when count is 0 — toggling visibility off for a
933
866
  // single frame (common with short lifetimes + low emission overlap) causes flicker.
934
867
  this.mesh.forcedInstanceCount = index;
@@ -961,32 +894,6 @@ export class SpriteBatch extends VFXBatch {
961
894
  }
962
895
  }
963
896
 
964
- /**
965
- * Pushes the stretched-billboard speed factor to the shader when it changed.
966
- *
967
- * Deliberately kept out of `update`: touching the loosely typed
968
- * `mesh.material` from inside that function gives V8 a generic property
969
- * access it has no feedback for, which knocks the whole particle loop out of
970
- * optimised code on every frame.
971
- */
972
- private syncStretchedSpeedFactor(system: IParticleSystem): void {
973
- const material = this.shaderMaterial;
974
- if (material === null) {
975
- return;
976
- }
977
- const speedFactor = (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor ?? 1.0;
978
- const clampedSpeedFactor = speedFactor === 0 ? 0.001 : speedFactor;
979
- if (clampedSpeedFactor !== this.lastStretchedSpeedFactor) {
980
- material.setFloat('speedFactor', clampedSpeedFactor);
981
- this.lastStretchedSpeedFactor = clampedSpeedFactor;
982
- }
983
- }
984
-
985
- /** Stretched billboards and beams: drawn along a per-instance vector, by the stretched shader. */
986
- private static drawsAlongVelocity(renderMode: RenderMode): boolean {
987
- return renderMode === RenderMode.StretchedBillBoard || renderMode === RenderMode.Beam;
988
- }
989
-
990
897
  private readonly beamEnd_ = new Vector3();
991
898
 
992
899
  /**
@@ -1010,14 +917,14 @@ export class SpriteBatch extends VFXBatch {
1010
917
  }
1011
918
  }
1012
919
 
1013
- /** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes or beams. */
1014
- private static takesPivotAndFlip(renderMode: RenderMode): boolean {
1015
- return (
1016
- renderMode === RenderMode.BillBoard ||
1017
- renderMode === RenderMode.StretchedBillBoard ||
1018
- renderMode === RenderMode.VerticalBillBoard ||
1019
- renderMode === RenderMode.HorizontalBillBoard
1020
- );
920
+ /** Multiplies `count` RGBA colours starting at `base` by a system's {@link ParticleSystem.tint}. */
921
+ private static applyTint(colors: Float32Array, base: number, count: number, tint: Vector4): void {
922
+ for (let j = 0, o = base; j < count; j++, o += 4) {
923
+ colors[o] *= tint.x;
924
+ colors[o + 1] *= tint.y;
925
+ colors[o + 2] *= tint.z;
926
+ colors[o + 3] *= tint.w;
927
+ }
1021
928
  }
1022
929
 
1023
930
  /**
@@ -1281,7 +1188,11 @@ export class SpriteBatch extends VFXBatch {
1281
1188
  }
1282
1189
  }
1283
1190
 
1191
+ /** @internal */
1284
1192
  dispose(): void {
1193
+ this.disposed = true;
1194
+ this.cancelAtlasRebuild?.();
1195
+ this.cancelAtlasRebuild = null;
1285
1196
  if (this.mesh.material) {
1286
1197
  this.mesh.material.dispose();
1287
1198
  }