babylon.quarks 0.27.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 (52) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +3 -1
  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 +4256 -831
  10. package/dist/types/index.d.ts +4256 -831
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +93 -78
  13. package/src/EffectPool.ts +91 -12
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +31 -3
  16. package/src/ParticleSystem.ts +92 -25
  17. package/src/QuarksLoader.ts +289 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +43 -15
  20. package/src/QuarksUtil.ts +64 -9
  21. package/src/SpriteBatch.ts +21 -6
  22. package/src/TrailBatch.ts +12 -6
  23. package/src/VFXBatch.ts +70 -528
  24. package/src/batchKey.ts +4 -2
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +21 -24
  28. package/src/envAtlas.ts +8 -2
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +39 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +110 -16
  33. package/src/shaderVariants.ts +24 -4
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/soft.ts +6 -5
  37. package/src/shaders/chunks/surface.ts +2 -2
  38. package/src/shaders/particle_frag.glsl.ts +3 -1
  39. package/src/shaders/particle_frag.wgsl.ts +2 -0
  40. package/src/shaders/particle_physics_frag.glsl.ts +5 -2
  41. package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
  42. package/src/shaders/trail_frag.glsl.ts +4 -1
  43. package/src/shaders/trail_frag.wgsl.ts +2 -0
  44. package/src/system/EmissionController.ts +2 -4
  45. package/src/system/EventDispatcher.ts +26 -10
  46. package/src/system/geometry.ts +22 -29
  47. package/src/system/materialResolve.ts +56 -7
  48. package/src/system/normalize.ts +6 -1
  49. package/src/system/serialization.ts +54 -33
  50. package/src/system/settingsSchema.ts +32 -10
  51. package/src/system/types.ts +67 -28
  52. package/src/trailSettings.ts +9 -6
@@ -1,5 +1,6 @@
1
1
  import {Constants} from '@babylonjs/core/Engines/constants';
2
2
  import {hasSizeLimits, takesPivotAndSizeLimits, tilesSheet} from './batchKey';
3
+ import {readsUV1, RenderMode, StoredBatchSettings} from './batchSettings';
3
4
  import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
4
5
  import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
5
6
  import particle_frag from './shaders/particle_frag.glsl';
@@ -15,7 +16,6 @@ import trail_frag from './shaders/trail_frag.glsl';
15
16
  import trail_frag_wgsl from './shaders/trail_frag.wgsl';
16
17
  import trail_vert from './shaders/trail_vert.glsl';
17
18
  import trail_vert_wgsl from './shaders/trail_vert.wgsl';
18
- import {readsUV1, RenderMode, StoredBatchSettings} from './VFXBatch';
19
19
 
20
20
  /**
21
21
  * The part of a batch's settings that decides its shader: the render mode, which textures and
@@ -111,12 +111,94 @@ export function samplesMap(settings: {renderMode: RenderMode; texture: unknown})
111
111
  return settings.texture !== null || settings.renderMode === RenderMode.Mesh;
112
112
  }
113
113
 
114
- /** Whether the blend mode adds to the frame, in which case fog and fade darken instead of mixing. */
115
- export function blendsAdditively(settings: {materialTransparent: boolean; materialBlendMode: number}): boolean {
116
- return (
117
- settings.materialTransparent &&
118
- (settings.materialBlendMode === Constants.ALPHA_ADD || settings.materialBlendMode === Constants.ALPHA_ONEONE)
119
- );
114
+ /**
115
+ * How a particle's colour has to leave the fragment shader for its blend mode, so that its alpha
116
+ * means coverage: alpha 0 leaves the frame as it was, alpha 1 draws the colour in full, and the
117
+ * fades (soft particles, erosion, a rim fade, alpha over life) only lower alpha. Unity's particle
118
+ * shaders do the same for their Alpha, Premultiply, Additive and Multiply modes. Alpha has no
119
+ * such meaning for an opaque material, nor under `ALPHA_INTERPOLATE` (constant blend colour),
120
+ * `ALPHA_REPLACE_COLOR` (replaces the frame) and `ALPHA_DUAL_SRC0_ADD_SRC1xDST` (reads a second
121
+ * shader output the particle shaders do not write); these leave the colour as it is.
122
+ *
123
+ * - `'straight'`: the blend weighs the colour by its alpha itself (Babylon's `ALPHA_COMBINE`,
124
+ * `ALPHA_ADD`, `ALPHA_LAYER_ACCUMULATE`, …) or does not blend; the colour leaves as it is.
125
+ * - `'premultiply'`: the blend takes the colour at full weight, or the frame by one minus the
126
+ * colour (`ALPHA_PREMULTIPLIED`, `ALPHA_ONEONE`, `ALPHA_SCREENMODE`, `ALPHA_SUBTRACT`,
127
+ * `ALPHA_MAXIMIZED`, `ALPHA_MAX`, …), so the shader multiplies it by its alpha: black, which
128
+ * these modes leave the frame alone with, at alpha 0. Textures stay straight alpha, as in
129
+ * Unity's Premultiply mode. The fade is linear in alpha except under `ALPHA_MAXIMIZED`, whose
130
+ * blend weighs the colour by alpha once more, and `ALPHA_MAX`.
131
+ * - `'multiply'`: the blend multiplies the frame by the colour (`ALPHA_MULTIPLY`, `ALPHA_MIN`), so
132
+ * the shader moves it toward white, which leaves the frame alone, as alpha falls. `ALPHA_MIN`
133
+ * clamps a frame brighter than 1 (an HDR target) to 1 where a particle draws, even at alpha 0.
134
+ */
135
+ export type BlendOutput = 'straight' | 'premultiply' | 'multiply';
136
+
137
+ /**
138
+ * What the scene's fog turns a particle's colour into with distance: the fog colour where the
139
+ * particle covers the frame (`'color'`), black where it adds to it (`'black'`, or the fog would
140
+ * add itself to the whole screen), white where it multiplies it (`'white'`). Unity fogs its
141
+ * additive and multiply particles toward black and white for the same reason.
142
+ */
143
+ export type FogTarget = 'color' | 'black' | 'white';
144
+
145
+ /** The {@link BlendOutput} and {@link FogTarget} of a blend mode. */
146
+ export interface BlendCategory {
147
+ /** How the colour leaves the shader. */
148
+ output: BlendOutput;
149
+ /** Where the fog takes the colour. */
150
+ fogTarget: FogTarget;
151
+ }
152
+
153
+ const PREMULTIPLY_MODES = new Set<number>([
154
+ Constants.ALPHA_SUBTRACT,
155
+ Constants.ALPHA_MAXIMIZED,
156
+ Constants.ALPHA_ONEONE,
157
+ Constants.ALPHA_PREMULTIPLIED,
158
+ Constants.ALPHA_PREMULTIPLIED_PORTERDUFF,
159
+ Constants.ALPHA_SCREENMODE,
160
+ Constants.ALPHA_ONEONE_ONEONE,
161
+ Constants.ALPHA_ALPHATOCOLOR,
162
+ Constants.ALPHA_REVERSEONEMINUS,
163
+ Constants.ALPHA_SRC_DSTONEMINUSSRCALPHA,
164
+ Constants.ALPHA_ONEONE_ONEZERO,
165
+ Constants.ALPHA_EXCLUSION,
166
+ Constants.ALPHA_MAX,
167
+ ]);
168
+ const MULTIPLY_MODES = new Set<number>([Constants.ALPHA_MULTIPLY, Constants.ALPHA_MIN]);
169
+ /** The modes whose fog is the fog colour: those that lay the colour over the frame or replace it. */
170
+ const OVER_MODES = new Set<number>([
171
+ Constants.ALPHA_COMBINE,
172
+ Constants.ALPHA_PREMULTIPLIED,
173
+ Constants.ALPHA_PREMULTIPLIED_PORTERDUFF,
174
+ Constants.ALPHA_INTERPOLATE,
175
+ Constants.ALPHA_SRC_DSTONEMINUSSRCALPHA,
176
+ Constants.ALPHA_LAYER_ACCUMULATE,
177
+ Constants.ALPHA_REPLACE_COLOR,
178
+ ]);
179
+
180
+ /**
181
+ * The {@link BlendCategory} of a material: an opaque one (`materialTransparent` false, or
182
+ * `ALPHA_DISABLE`) is straight and fogs toward the fog colour; a transparent one by its Babylon
183
+ * alpha mode. A mode this table does not know is straight and fogs toward the fog colour.
184
+ */
185
+ export function blendCategory(settings: {materialTransparent: boolean; materialBlendMode: number}): BlendCategory {
186
+ const mode = settings.materialBlendMode;
187
+ if (!settings.materialTransparent || mode === Constants.ALPHA_DISABLE) {
188
+ return {output: 'straight', fogTarget: 'color'};
189
+ }
190
+ const output: BlendOutput = PREMULTIPLY_MODES.has(mode)
191
+ ? 'premultiply'
192
+ : MULTIPLY_MODES.has(mode)
193
+ ? 'multiply'
194
+ : 'straight';
195
+ const fogTarget: FogTarget =
196
+ output === 'multiply'
197
+ ? 'white'
198
+ : OVER_MODES.has(mode) || (output === 'straight' && mode !== Constants.ALPHA_ADD)
199
+ ? 'color'
200
+ : 'black';
201
+ return {output, fogTarget};
120
202
  }
121
203
 
122
204
  /** The shader program a batch with `settings` compiles, given what it knows about its textures. */
@@ -146,7 +228,7 @@ function spriteShaderProgram(settings: ShaderSettings, context: ShaderContext):
146
228
  fragment = {glsl: particle_frag, wgsl: particle_frag_wgsl};
147
229
  }
148
230
 
149
- const defines = fogDefines(settings);
231
+ const defines = blendDefines(settings);
150
232
  const attributes = [...SPRITE_ATTRIBUTES];
151
233
  const uniforms = ['world', 'view', 'projection', ...FOG_UNIFORMS];
152
234
  const samplers: string[] = [];
@@ -225,7 +307,7 @@ function spriteShaderProgram(settings: ShaderSettings, context: ShaderContext):
225
307
 
226
308
  /** The program of a trail batch. */
227
309
  function trailShaderProgram(settings: ShaderSettings, context: ShaderContext): ShaderProgram {
228
- const defines = fogDefines(settings);
310
+ const defines = blendDefines(settings);
229
311
  const attributes = [...TRAIL_ATTRIBUTES];
230
312
  const uniforms = ['view', 'projection', ...FOG_UNIFORMS];
231
313
  const samplers: string[] = [];
@@ -251,15 +333,27 @@ function trailShaderProgram(settings: ShaderSettings, context: ShaderContext): S
251
333
  }
252
334
 
253
335
  /**
254
- * The fog defines: `FOG` when the settings apply the scene's fog, and `FOG_ADDITIVE` when the
255
- * particles add to the frame, so that they fade toward black instead of the fog colour, which
256
- * would fog the whole screen (what Unity does for additive materials).
336
+ * The blend defines: `BLEND_PREMULTIPLY` or `BLEND_MULTIPLY` for how the colour leaves the shader,
337
+ * and with `FOG` (when the settings apply the scene's fog) `FOG_ADDITIVE` or `FOG_MULTIPLY` for a
338
+ * fog toward black or white. See {@link blendCategory}.
257
339
  */
258
- function fogDefines(settings: ShaderSettings): string[] {
259
- if (!settings.applyFog) {
260
- return [];
340
+ function blendDefines(settings: ShaderSettings): string[] {
341
+ const {output, fogTarget} = blendCategory(settings);
342
+ const defines: string[] = [];
343
+ if (output === 'premultiply') {
344
+ defines.push('BLEND_PREMULTIPLY');
345
+ } else if (output === 'multiply') {
346
+ defines.push('BLEND_MULTIPLY');
347
+ }
348
+ if (settings.applyFog) {
349
+ defines.push('FOG');
350
+ if (fogTarget === 'black') {
351
+ defines.push('FOG_ADDITIVE');
352
+ } else if (fogTarget === 'white') {
353
+ defines.push('FOG_MULTIPLY');
354
+ }
261
355
  }
262
- return blendsAdditively(settings) ? ['FOG', 'FOG_ADDITIVE'] : ['FOG'];
356
+ return defines;
263
357
  }
264
358
 
265
359
  /** Soft particles, when on and a depth texture is there to read, and the alpha test. */
@@ -123,6 +123,21 @@ const FOG: Axis[] = [
123
123
  {name: 'fog additive', settings: {applyFog: true, materialBlendMode: Constants.ALPHA_ADD}},
124
124
  ];
125
125
 
126
+ /** The blend resolves (see `blendCategory`), with and without the fog each one takes. */
127
+ const BLEND: Axis[] = [
128
+ {name: 'premultiplied', settings: {materialBlendMode: Constants.ALPHA_PREMULTIPLIED}},
129
+ {name: 'premultiplied fog', settings: {materialBlendMode: Constants.ALPHA_PREMULTIPLIED, applyFog: true}},
130
+ {name: 'one-one', settings: {materialBlendMode: Constants.ALPHA_ONEONE}},
131
+ {name: 'one-one fog', settings: {materialBlendMode: Constants.ALPHA_ONEONE, applyFog: true}},
132
+ {name: 'multiply', settings: {materialBlendMode: Constants.ALPHA_MULTIPLY}},
133
+ {name: 'multiply fog', settings: {materialBlendMode: Constants.ALPHA_MULTIPLY, applyFog: true}},
134
+ {
135
+ name: 'multiply fog alpha test',
136
+ settings: {materialBlendMode: Constants.ALPHA_MULTIPLY, applyFog: true, materialAlphaTest: 0.5},
137
+ },
138
+ {name: 'opaque', settings: {materialTransparent: false}},
139
+ ];
140
+
126
141
  /** Texture effects (`chunks/surface.ts`), alone and together, on both UV channels. */
127
142
  const SURFACE: Axis[] = (() => {
128
143
  const uv0 = normalizeMapUV({tiling: [2, 2]});
@@ -174,15 +189,16 @@ const MESH_UV2: Axis = {name: 'uv2', context: {meshUV2: true}};
174
189
 
175
190
  /** Soft, alpha-tested and fogged at once, to try the effects with the rest of the fragment. */
176
191
  const EVERYTHING_ELSE: Axis = {
177
- name: 'soft packed alpha test fog additive',
178
- settings: {softParticles: true, materialAlphaTest: 0.5, applyFog: true, materialBlendMode: Constants.ALPHA_ADD},
192
+ name: 'soft packed alpha test fog one-one',
193
+ settings: {softParticles: true, materialAlphaTest: 0.5, applyFog: true, materialBlendMode: Constants.ALPHA_ONEONE},
179
194
  context: {depth: {packed: true}},
180
195
  };
181
196
 
182
197
  /**
183
198
  * Every shader program the library can compile, one per distinct set of defines: every render
184
- * mode with every combination of sheet, map, soft particles, alpha test and fog, and the texture
185
- * effects alone and together on each shader family. What a shader validator compiles, and what
199
+ * mode with every combination of sheet, map, soft particles, alpha test and fog, every blend
200
+ * resolve with and without fog and soft particles, and the texture effects alone and together on
201
+ * each shader family. What a shader validator compiles, and what
186
202
  * an application can warm the shader cache with before its first frame (Unity's
187
203
  * ShaderVariantCollection). Built by {@link shaderProgram}, so a variant here is a material a
188
204
  * batch would build.
@@ -215,6 +231,10 @@ export function shaderVariants(): ShaderVariant[] {
215
231
  for (const alphaTest of ALPHA_TEST)
216
232
  for (const fog of FOG) add([target, quad, tile, map, soft, alphaTest, fog]);
217
233
 
234
+ for (const target of TARGETS)
235
+ for (const blend of BLEND)
236
+ for (const soft of SOFT.slice(0, 2)) for (const map of MAP) add([target, blend, soft, map]);
237
+
218
238
  for (const target of TARGETS.filter((t) => SURFACE_TARGETS.includes(t.name)))
219
239
  for (const uv2 of target.settings?.renderMode === RenderMode.Mesh ? [NONE, MESH_UV2] : [NONE])
220
240
  for (const surface of SURFACE)
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The blend resolve for the particle shaders, in both shader languages.
3
+ *
4
+ * Every fade in the shaders (soft particles, erosion, a rim fade, alpha over life) lowers alpha
5
+ * only, so that alpha means coverage. Blend modes that take the colour at full weight
6
+ * (`BLEND_PREMULTIPLY`: one-one, premultiplied, screen, subtract, …) would ignore it, so the
7
+ * resolve multiplies the colour by its alpha; modes that multiply the frame (`BLEND_MULTIPLY`)
8
+ * move it toward white instead. It runs last, after fog and the soft fade. See `blendCategory`.
9
+ */
10
+
11
+ /** Resolves the vec4 lvalue `color` for the blend mode, GLSL. */
12
+ export function blendFragmentGLSL(color: string): string {
13
+ return /* glsl */ `
14
+ #if defined(BLEND_PREMULTIPLY)
15
+ ${color}.rgb *= ${color}.a;
16
+ #elif defined(BLEND_MULTIPLY)
17
+ ${color}.rgb = mix(vec3(1.0), ${color}.rgb, ${color}.a);
18
+ #endif
19
+ `;
20
+ }
21
+
22
+ /** Resolves the `var` vec4 `color` for the blend mode, WGSL. */
23
+ export function blendFragmentWGSL(color: string): string {
24
+ return /* wgsl */ `
25
+ #if defined(BLEND_PREMULTIPLY)
26
+ ${color} = vec4f(${color}.rgb * ${color}.a, ${color}.a);
27
+ #elif defined(BLEND_MULTIPLY)
28
+ ${color} = vec4f(mix(vec3f(1.0), ${color}.rgb, ${color}.a), ${color}.a);
29
+ #endif
30
+ `;
31
+ }
@@ -11,7 +11,8 @@
11
11
  *
12
12
  * Additive particles cannot fade toward a fog colour (that would add fog to
13
13
  * the whole screen), so under `FOG_ADDITIVE` they fade toward black instead,
14
- * which is what Unity does for additive materials.
14
+ * and multiplying particles (`FOG_MULTIPLY`) toward white, which leaves the
15
+ * frame as it is: what Unity does for additive and multiply materials.
15
16
  */
16
17
 
17
18
  /** Vertex-side declaration, GLSL. */
@@ -62,8 +63,10 @@ export function fogFragmentGLSL(rgb: string): string {
62
63
  #ifdef FOG
63
64
  {
64
65
  float fogFactor = quarksFogFactor();
65
- #ifdef FOG_ADDITIVE
66
+ #if defined(FOG_ADDITIVE)
66
67
  ${rgb} *= fogFactor;
68
+ #elif defined(FOG_MULTIPLY)
69
+ ${rgb} = mix(vec3(1.0), ${rgb}, fogFactor);
67
70
  #else
68
71
  ${rgb} = mix(vFogColor, ${rgb}, fogFactor);
69
72
  #endif
@@ -110,8 +113,10 @@ fn quarksFog(rgb: vec3f, fogDistanceVector: vec3f) -> vec3f {
110
113
  fogCoeff = exp(-fogDistance * fogDistance * fogDensity * fogDensity);
111
114
  }
112
115
  let fogFactor = clamp(fogCoeff, 0.0, 1.0);
113
- #ifdef FOG_ADDITIVE
116
+ #if defined(FOG_ADDITIVE)
114
117
  return rgb * fogFactor;
118
+ #elif defined(FOG_MULTIPLY)
119
+ return mix(vec3f(1.0), rgb, fogFactor);
115
120
  #else
116
121
  return mix(uniforms.vFogColor, rgb, fogFactor);
117
122
  #endif
@@ -4,7 +4,8 @@
4
4
  * The fragment reads the scene depth behind it from `depthTexture`, turns it into view depth with
5
5
  * the coefficients in `projParams` (see `softDepthParams`), and fades out over the distance in
6
6
  * `softParams` as it nears the scene, as Unity's soft particles and Unreal's Depth Fade do. The
7
- * fade runs last, after fog, so both languages produce the same colour.
7
+ * fade lowers alpha only and runs after fog and before the blend resolve (`blend.ts`), so it reads
8
+ * as coverage in every blend mode and both languages produce the same colour.
8
9
  *
9
10
  * `SOFT_DEPTH_PACKED` reads a depth packed into four 8-bit channels, as a Babylon
10
11
  * `DepthRenderer` of type `UNSIGNED_BYTE` writes it.
@@ -33,14 +34,14 @@ float quarksSceneDepth(vec4 texel) {
33
34
  #endif
34
35
  `;
35
36
 
36
- /** Fades `target` as the fragment nears the scene behind it, GLSL. */
37
+ /** Fades the alpha of the vec4 lvalue `target` as the fragment nears the scene behind it, GLSL. */
37
38
  export function softFragmentGLSL(target: string): string {
38
39
  return /* glsl */ `
39
40
  #ifdef SOFT_PARTICLES
40
41
  {
41
42
  vec2 softUV = 0.5 * (projPosition.xy / projPosition.w) + 0.5;
42
43
  float sceneDepth = quarksSceneDepth(texture2D(depthTexture, softUV));
43
- ${target} *= clamp(softParams.y * ((sceneDepth - softParams.x) - linearDepth), 0.0, 1.0);
44
+ ${target}.a *= clamp(softParams.y * ((sceneDepth - softParams.x) - linearDepth), 0.0, 1.0);
44
45
  }
45
46
  #endif
46
47
  `;
@@ -70,14 +71,14 @@ fn quarksSceneDepth(texel: vec4f) -> f32 {
70
71
  #endif
71
72
  `;
72
73
 
73
- /** Fades `target` as the fragment nears the scene behind it, WGSL. */
74
+ /** Fades the alpha of the `var` vec4 `target` as the fragment nears the scene behind it, WGSL. */
74
75
  export function softFragmentWGSL(target: string): string {
75
76
  return /* wgsl */ `
76
77
  #ifdef SOFT_PARTICLES
77
78
  {
78
79
  let softUV = 0.5 * (fragmentInputs.projPosition.xy / fragmentInputs.projPosition.w) + 0.5;
79
80
  let sceneDepth = quarksSceneDepth(textureSample(depthTexture, depthTextureSampler, softUV));
80
- ${target} *= clamp(uniforms.softParams.y * ((sceneDepth - uniforms.softParams.x) - fragmentInputs.linearDepth), 0.0, 1.0);
81
+ ${target}.a *= clamp(uniforms.softParams.y * ((sceneDepth - uniforms.softParams.x) - fragmentInputs.linearDepth), 0.0, 1.0);
81
82
  }
82
83
  #endif
83
84
  `;
@@ -35,8 +35,8 @@
35
35
  * on the way down the shape eats away from its lowest mask values first — a
36
36
  * noise mask dissolves it, a gradient mask wipes it. `erosionParams` is
37
37
  * (softness, edge width); the edge band just above the cut glows in
38
- * `erosionEdgeColor`. Erosion only changes alpha, so blend modes that
39
- * ignore alpha (one-one, premultiplied) show a hard cut.
38
+ * `erosionEdgeColor`. Erosion only changes alpha, which the blend resolve
39
+ * (`blend.ts`) turns into coverage in every blend mode.
40
40
  */
41
41
 
42
42
  /** Fragment-side declarations, GLSL. */
@@ -1,3 +1,4 @@
1
+ import {blendFragmentGLSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
3
4
  import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
@@ -35,7 +36,8 @@ ${surfaceColorGLSL(true)}
35
36
  #endif
36
37
 
37
38
  ${fogFragmentGLSL('baseColor.rgb')}
39
+ ${softFragmentGLSL('baseColor')}
40
+ ${blendFragmentGLSL('baseColor')}
38
41
  gl_FragColor = baseColor;
39
- ${softFragmentGLSL('gl_FragColor')}
40
42
  }
41
43
  `;
@@ -1,3 +1,4 @@
1
+ import {blendFragmentWGSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
3
4
  import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
@@ -38,6 +39,7 @@ ${surfaceColorWGSL(true)}
38
39
 
39
40
  ${fogFragmentWGSL('baseColor')}
40
41
  ${softFragmentWGSL('baseColor')}
42
+ ${blendFragmentWGSL('baseColor')}
41
43
  fragmentOutputs.color = baseColor;
42
44
  }
43
45
  `;
@@ -1,3 +1,4 @@
1
+ import {blendFragmentGLSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
3
4
  import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
@@ -130,7 +131,9 @@ ${surfaceColorGLSL(true)}
130
131
  #endif
131
132
 
132
133
  ${fogFragmentGLSL('color')}
133
- gl_FragColor = vec4(color, baseColor.a);
134
- ${softFragmentGLSL('gl_FragColor')}
134
+ vec4 finalColor = vec4(color, baseColor.a);
135
+ ${softFragmentGLSL('finalColor')}
136
+ ${blendFragmentGLSL('finalColor')}
137
+ gl_FragColor = finalColor;
135
138
  }
136
139
  `;
@@ -1,3 +1,4 @@
1
+ import {blendFragmentWGSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
3
4
  import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
@@ -133,6 +134,7 @@ ${surfaceColorWGSL(true)}
133
134
 
134
135
  ${fogFragmentWGSL('finalColor')}
135
136
  ${softFragmentWGSL('finalColor')}
137
+ ${blendFragmentWGSL('finalColor')}
136
138
  fragmentOutputs.color = finalColor;
137
139
  }
138
140
  `;
@@ -1,3 +1,4 @@
1
+ import {blendFragmentGLSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
3
4
  import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
@@ -29,6 +30,8 @@ ${surfaceColorGLSL(false)}
29
30
  #endif
30
31
 
31
32
  ${fogFragmentGLSL('baseColor.rgb')}
33
+ ${softFragmentGLSL('baseColor')}
34
+ ${blendFragmentGLSL('baseColor')}
32
35
  gl_FragColor = baseColor;
33
- ${softFragmentGLSL('gl_FragColor')}}
36
+ }
34
37
  `;
@@ -1,3 +1,4 @@
1
+ import {blendFragmentWGSL} from './chunks/blend';
1
2
  import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
2
3
  import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
3
4
  import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
@@ -32,6 +33,7 @@ ${surfaceColorWGSL(false)}
32
33
 
33
34
  ${fogFragmentWGSL('baseColor')}
34
35
  ${softFragmentWGSL('baseColor')}
36
+ ${blendFragmentWGSL('baseColor')}
35
37
  fragmentOutputs.color = baseColor;
36
38
  }
37
39
  `;
@@ -167,10 +167,8 @@ export class EmissionController {
167
167
  if (emissionState.burstWaves !== undefined) {
168
168
  emissionState.burstWaves.length = 0;
169
169
  }
170
- const behaviors = system.behaviors;
171
- for (let i = 0; i < behaviors.length; i++) {
172
- behaviors[i].reset();
173
- }
170
+ // Behaviors carry on across the loop (a noise field keeps scrolling, sub emissions
171
+ // of live particles keep going); they reset only when the system restarts.
174
172
  } else {
175
173
  if (!system.isEmitEnded && !system.onlyUsedByOther) {
176
174
  system.endEmit();
@@ -2,26 +2,41 @@ import type {ParticleSystemEvent, ParticleSystemEventType} from 'quarks.core';
2
2
 
3
3
  type Listener = (event: ParticleSystemEvent) => void;
4
4
 
5
- /** The callbacks registered on a {@link ParticleSystem}, by event type. */
5
+ /**
6
+ * The callbacks registered on a {@link ParticleSystem}, by event type.
7
+ *
8
+ * The lists are copied on write, not on fire: adding or removing a callback while an event is
9
+ * being fired (a listener removing itself, say) changes the next firing and never the one in
10
+ * progress, so every callback registered when the event fired is called once.
11
+ */
6
12
  export class EventDispatcher {
7
- private readonly listeners: {[event: string]: Listener[]} = {};
13
+ private readonly listeners: {[event: string]: readonly Listener[]} = {};
8
14
 
9
15
  /** Registers `callback` for `event`. */
10
16
  add(event: ParticleSystemEventType, callback: Listener): void {
11
- if (!this.listeners[event]) this.listeners[event] = [];
12
- this.listeners[event].push(callback);
17
+ this.listeners[event] = [...(this.listeners[event] ?? []), callback];
13
18
  }
14
19
 
15
- /** Drops every callback for `event`. */
16
- removeAll(event: ParticleSystemEventType): void {
17
- if (this.listeners[event]) this.listeners[event] = [];
20
+ /** Drops every callback for `event`, or for every event when it is left out. */
21
+ removeAll(event?: ParticleSystemEventType): void {
22
+ if (event === undefined) {
23
+ for (const key of Object.keys(this.listeners)) {
24
+ delete this.listeners[key];
25
+ }
26
+ return;
27
+ }
28
+ delete this.listeners[event];
18
29
  }
19
30
 
20
31
  /** Drops one callback for `event`. */
21
32
  remove(event: ParticleSystemEventType, callback: Listener): void {
22
- if (this.listeners[event]) {
23
- const index = this.listeners[event].indexOf(callback);
24
- if (index !== -1) this.listeners[event].splice(index, 1);
33
+ const callbacks = this.listeners[event];
34
+ if (callbacks === undefined) {
35
+ return;
36
+ }
37
+ const index = callbacks.indexOf(callback);
38
+ if (index !== -1) {
39
+ this.listeners[event] = callbacks.filter((_, i) => i !== index);
25
40
  }
26
41
  }
27
42
 
@@ -33,6 +48,7 @@ export class EventDispatcher {
33
48
 
34
49
  /** Calls the callbacks registered for the event's type, in the order they were added. */
35
50
  fire(event: ParticleSystemEvent): void {
51
+ // The list as it is now: a callback added or removed by a listener does not shift it.
36
52
  const callbacks = this.listeners[event.type];
37
53
  if (callbacks === undefined) {
38
54
  return;
@@ -1,3 +1,4 @@
1
+ import type {BufferGeometryDataJSON, GeometryArrays, GeometryEntry} from '../effectDocumentJSON';
1
2
  import {ensureTriangleIndices} from '../geometryUtil';
2
3
 
3
4
  /** Positions of the unit quad billboards are drawn with, and the default mesh. */
@@ -47,19 +48,19 @@ export function isFallbackNormals(normals: Float32Array | undefined, positions:
47
48
  return normals !== undefined && normals === fallbackNormals.get(positions);
48
49
  }
49
50
 
51
+ /** `values` as a Float32Array: itself when it is one, else a copy. */
52
+ function floats(values: number[] | Float32Array | undefined): Float32Array | undefined {
53
+ return values ? (values instanceof Float32Array ? values : new Float32Array(values)) : undefined;
54
+ }
55
+
50
56
  /**
51
57
  * Typed arrays from a geometry entry of an effect document: quarks' own `{positions, indices, uvs,
52
- * uvs2, normals}`, or a three.js `BufferGeometry` JSON. Anything else gives the default quad.
58
+ * uvs2, normals}`, or a three.js `BufferGeometry` JSON or its `data`. Anything else gives the
59
+ * default quad.
53
60
  */
54
- export function resolveGeometryData(entry: any): {
55
- positions: Float32Array;
56
- indices: Uint32Array | Uint16Array;
57
- uvs?: Float32Array;
58
- uvs2?: Float32Array;
59
- normals?: Float32Array;
60
- } {
61
- if (entry?.positions) {
62
- const positions = entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions);
61
+ export function resolveGeometryData(entry: GeometryEntry | BufferGeometryDataJSON | undefined): GeometryArrays {
62
+ if (entry && 'positions' in entry && entry.positions) {
63
+ const positions = floats(entry.positions)!;
63
64
  const rawIndices =
64
65
  entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
65
66
  ? entry.indices
@@ -69,23 +70,17 @@ export function resolveGeometryData(entry: any): {
69
70
  return {
70
71
  positions,
71
72
  indices: ensureTriangleIndices(positions, rawIndices),
72
- uvs: entry.uvs ? (entry.uvs instanceof Float32Array ? entry.uvs : new Float32Array(entry.uvs)) : undefined,
73
- uvs2: entry.uvs2
74
- ? entry.uvs2 instanceof Float32Array
75
- ? entry.uvs2
76
- : new Float32Array(entry.uvs2)
77
- : undefined,
78
- normals: entry.normals
79
- ? entry.normals instanceof Float32Array
80
- ? entry.normals
81
- : new Float32Array(entry.normals)
82
- : undefined,
73
+ uvs: floats(entry.uvs),
74
+ uvs2: floats(entry.uvs2),
75
+ normals: floats(entry.normals),
83
76
  };
84
77
  }
85
78
 
86
- const data = entry?.data ?? entry;
87
- if (data?.attributes?.position?.array) {
88
- const positions = new Float32Array(data.attributes.position.array);
79
+ const data = entry && 'data' in entry ? entry.data : entry;
80
+ const attributes = data && 'attributes' in data ? data.attributes : undefined;
81
+ const position = attributes?.position?.array;
82
+ if (data && 'attributes' in data && attributes && position) {
83
+ const positions = new Float32Array(position);
89
84
  let indices: Uint32Array | Uint16Array | undefined;
90
85
  if (data.index?.array) {
91
86
  const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
@@ -94,12 +89,10 @@ export function resolveGeometryData(entry: any): {
94
89
  return {
95
90
  positions,
96
91
  indices: ensureTriangleIndices(positions, indices),
97
- uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
92
+ uvs: floats(attributes.uv?.array),
98
93
  // three.js names the second set `uv1` (r151 on), `uv2` before.
99
- uvs2: (data.attributes.uv1 ?? data.attributes.uv2)?.array
100
- ? new Float32Array((data.attributes.uv1 ?? data.attributes.uv2).array)
101
- : undefined,
102
- normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
94
+ uvs2: floats((attributes.uv1 ?? attributes.uv2)?.array),
95
+ normals: floats(attributes.normal?.array),
103
96
  };
104
97
  }
105
98