sparcoon 0.6.1 → 0.7.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 (78) hide show
  1. package/dist/emitter/FXEmitter.d.ts +4 -0
  2. package/dist/emitter/FXEmitter.js +8 -0
  3. package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +0 -2
  4. package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +0 -2
  5. package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +0 -4
  6. package/dist/emitter/behavior/FXBehaviorPointForce.js +0 -4
  7. package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +4 -3
  8. package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +9 -3
  9. package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +0 -1
  10. package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +0 -1
  11. package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +0 -2
  12. package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +0 -2
  13. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +4 -2
  14. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +9 -2
  15. package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +0 -1
  16. package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +0 -1
  17. package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +0 -2
  18. package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +0 -2
  19. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +4 -2
  20. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +9 -2
  21. package/dist/emitter/spawn/FXSpawnBox.d.ts +0 -1
  22. package/dist/emitter/spawn/FXSpawnBox.js +0 -1
  23. package/dist/emitter/spawn/FXSpawnLifetime.d.ts +0 -1
  24. package/dist/emitter/spawn/FXSpawnLifetime.js +0 -1
  25. package/dist/emitter/spawn/FXSpawnOffset.d.ts +0 -1
  26. package/dist/emitter/spawn/FXSpawnOffset.js +0 -1
  27. package/dist/emitter/spawn/FXSpawnPoint.d.ts +0 -1
  28. package/dist/emitter/spawn/FXSpawnPoint.js +0 -1
  29. package/dist/emitter/spawn/FXSpawnRotation.d.ts +0 -1
  30. package/dist/emitter/spawn/FXSpawnRotation.js +0 -1
  31. package/dist/emitter/spawn/FXSpawnScale.d.ts +0 -2
  32. package/dist/emitter/spawn/FXSpawnScale.js +0 -2
  33. package/dist/emitter/spawn/FXSpawnSphere.d.ts +3 -10
  34. package/dist/emitter/spawn/FXSpawnSphere.js +8 -15
  35. package/dist/emitter/spawn/FXSpawnTorque.d.ts +0 -1
  36. package/dist/emitter/spawn/FXSpawnTorque.js +0 -1
  37. package/dist/emitter/spawn/FXSpawnVelocity.d.ts +3 -6
  38. package/dist/emitter/spawn/FXSpawnVelocity.js +6 -9
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +3 -1
  41. package/dist/instancedParticle/FXInstancedParticle.d.ts +1 -0
  42. package/dist/instancedParticle/FXInstancedParticle.js +4 -1
  43. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +4 -3
  44. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +42 -56
  45. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +1 -1
  46. package/dist/materials/FXMaterial/FXBlending.Internal.d.ts +24 -0
  47. package/dist/materials/FXMaterial/FXBlending.Internal.js +64 -0
  48. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +8 -3
  49. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +7 -3
  50. package/dist/materials/FXMaterial/FXMaterial.d.ts +18 -15
  51. package/dist/materials/FXMaterial/FXMaterial.js +22 -7
  52. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +3 -2
  53. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +16 -14
  54. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +1 -1
  55. package/dist/nodes/FXNode.d.ts +5 -0
  56. package/dist/nodes/FXNode.js +10 -0
  57. package/dist/nodes/blending/FXNodeBlending.d.ts +6 -0
  58. package/dist/nodes/blending/FXNodeBlending.js +9 -0
  59. package/dist/nodes/blending/FXNodeLightnessBlendingMask.d.ts +16 -0
  60. package/dist/nodes/blending/FXNodeLightnessBlendingMask.js +49 -0
  61. package/dist/nodes/color/FXNodeColor.d.ts +2 -3
  62. package/dist/nodes/color/FXNodeColor.js +3 -5
  63. package/dist/nodes/color/FXNodeColorOverLife.d.ts +0 -5
  64. package/dist/nodes/color/FXNodeColorOverLife.js +6 -8
  65. package/dist/nodes/color/FXNodeSphericalClip.js +6 -3
  66. package/dist/nodes/normal/FXNodeFlatNormal.d.ts +0 -1
  67. package/dist/nodes/normal/FXNodeFlatNormal.js +5 -5
  68. package/dist/nodes/normal/FXNodeNormal.d.ts +2 -3
  69. package/dist/nodes/normal/FXNodeNormal.js +3 -1
  70. package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +0 -1
  71. package/dist/nodes/normal/FXNodeSphericalNormal.js +11 -9
  72. package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +3 -3
  73. package/dist/nodes/texture/FXNodeAnimatedTexture.js +19 -17
  74. package/dist/nodes/texture/FXNodeStaticTexture.d.ts +3 -3
  75. package/dist/nodes/texture/FXNodeStaticTexture.js +21 -18
  76. package/dist/nodes/texture/FXNodeTexture.d.ts +2 -3
  77. package/dist/nodes/texture/FXNodeTexture.js +3 -5
  78. package/package.json +1 -1
@@ -27,6 +27,9 @@ class FXInstancedParticle extends Mesh {
27
27
  get instanceCount() {
28
28
  return this._private_instancedGeometry.instanceCount;
29
29
  }
30
+ get instanceCapacity() {
31
+ return this._private_capacity;
32
+ }
30
33
  createInstances(count) {
31
34
  const currentInstanceCount = this._private_instancedGeometry.instanceCount;
32
35
  this._private_ensureCapacity(currentInstanceCount + count);
@@ -151,7 +154,7 @@ class FXInstancedParticle extends Mesh {
151
154
  //
152
155
  // _maxInstanceCount is an internal renderer cache field set by
153
156
  // WebGLBindingStates.js. It was never promoted to a public API in the
154
- // r157–r180 range. Deleting it is the standard workaround used across
157
+ // r157-r180 range. Deleting it is the standard workaround used across
155
158
  // the Three.js ecosystem (see #19706, #26363, #27205).
156
159
  //
157
160
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,9 +1,10 @@
1
- import type { Blending } from "three";
2
1
  import { Color, MeshLambertMaterial } from "three";
3
2
  import type { GLTypeInfo } from "../../instancedParticle/shared";
4
- import type { FXNodeColor } from "../../nodes/color/FXNodeColor";
3
+ import type { FXNodeBlending } from "../../nodes/blending/FXNodeBlending";
4
+ import { FXNodeColor } from "../../nodes/color/FXNodeColor";
5
5
  import type { FXNodeNormal } from "../../nodes/normal/FXNodeNormal";
6
6
  import { FXNodeTexture } from "../../nodes/texture/FXNodeTexture";
7
+ import { FXBlending } from "../FXMaterial/FXMaterial";
7
8
  export interface FXScatterUniforms {
8
9
  u_ScatterTint: {
9
10
  value: Color;
@@ -18,6 +19,6 @@ export interface FXScatterUniforms {
18
19
  value: number;
19
20
  };
20
21
  }
21
- export declare function buildFXDiffuseMaterial(varyings: Record<string, GLTypeInfo>, blending: Blending, useAlphaHashing: boolean, alphaTest: number, premultipliedAlpha: boolean, albedoNodes: readonly (FXNodeColor | FXNodeTexture)[], normalNodes: readonly (FXNodeTexture | FXNodeNormal)[], emissionNodes: readonly (FXNodeColor | FXNodeTexture)[], scatterUniforms?: FXScatterUniforms, shadowSensitivity?: {
22
+ export declare function buildFXDiffuseMaterial(varyings: Record<string, GLTypeInfo>, blending: FXBlending, useAlphaHashing: boolean, alphaTest: number, albedoNodes: readonly (FXNodeColor | FXNodeTexture | FXNodeBlending)[], normalNodes: readonly (FXNodeTexture | FXNodeNormal)[], emissionNodes: readonly (FXNodeColor | FXNodeTexture)[], scatterUniforms?: FXScatterUniforms, shadowSensitivity?: {
22
23
  value: number;
23
24
  }): MeshLambertMaterial;
@@ -1,6 +1,10 @@
1
- import { MeshLambertMaterial, Color, DoubleSide } from 'three';
1
+ import { MeshLambertMaterial, Color, DoubleSide, NormalBlending } from 'three';
2
2
  import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
3
+ import { FXNodeColor } from '../../nodes/color/FXNodeColor.js';
4
+ import { CURRENT_EXPRESSION_VALUE_PLACEHOLDER } from '../../nodes/FXNode.js';
3
5
  import { FXNodeTexture } from '../../nodes/texture/FXNodeTexture.js';
6
+ import { buildAlbedoAndBlendingCode } from '../FXMaterial/FXBlending.Internal.js';
7
+ import { FXBlending } from '../FXMaterial/FXMaterial.js';
4
8
 
5
9
  /**
6
10
  * Wraps every `getShadow(...)` call in the shader source with
@@ -37,7 +41,7 @@ function wrapShadowCalls(source, uniformName) {
37
41
  }
38
42
  return result;
39
43
  }
40
- function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, premultipliedAlpha, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity) {
44
+ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity) {
41
45
  const useScatter = scatterUniforms !== undefined;
42
46
  const useShadowSensitivity = shadowSensitivity !== undefined;
43
47
  const useNormals = normalNodes.length > 0;
@@ -56,12 +60,12 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
56
60
  depthWrite: useAlphaHashing,
57
61
  depthTest: true,
58
62
  alphaHash: useAlphaHashing,
59
- blending,
63
+ blending: NormalBlending,
60
64
  side: DoubleSide,
61
65
  forceSinglePass: true,
62
66
  alphaTest,
63
67
  emissive: emissionNodes.length > 0 ? new Color(1, 1, 1) : new Color(0, 0, 0),
64
- premultipliedAlpha,
68
+ premultipliedAlpha: true,
65
69
  });
66
70
  material.onBeforeCompile = (shader) => {
67
71
  const vertexPreamble = [
@@ -117,25 +121,16 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
117
121
  );
118
122
  #endif
119
123
  `);
120
- const blendReorientedNormalsHelper = normalNodes.length > 1
121
- ? `
122
- vec3 blendReorientedNormals(vec3 base, vec3 detail) {
123
- base += vec3(0.0, 0.0, 1.0);
124
- detail *= vec3(-1.0, -1.0, 1.0);
125
- return normalize(base * dot(base, detail) / base.z - detail);
126
- }
127
- `
128
- : "";
129
124
  const seenCacheKeys = new Set();
130
125
  const uniqueHelpers = (nodes) => nodes
131
- .filter((n) => {
132
- if (seenCacheKeys.has(n.cacheKey)) {
126
+ .filter((node) => {
127
+ if (seenCacheKeys.has(node.cacheKey)) {
133
128
  return false;
134
129
  }
135
- seenCacheKeys.add(n.cacheKey);
130
+ seenCacheKeys.add(node.cacheKey);
136
131
  return true;
137
132
  })
138
- .map((n) => n.helperFunctions)
133
+ .map((node) => node.helperFunctions)
139
134
  .join("\n");
140
135
  const scatterDeclarations = useScatter
141
136
  ? [
@@ -160,12 +155,11 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
160
155
  "varying vec2 p_uv;",
161
156
  useNormals ? "varying vec2 p_billboardSinCos;" : "",
162
157
  varyingDeclarations.join("\n"),
163
- albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
158
+ albedoNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
164
159
  uniqueHelpers(albedoNodes),
165
- normalNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
160
+ normalNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
166
161
  uniqueHelpers(normalNodes),
167
- blendReorientedNormalsHelper,
168
- emissionNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
162
+ emissionNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
169
163
  uniqueHelpers(emissionNodes),
170
164
  ...scatterDeclarations,
171
165
  ...shadowSensitivityDeclarations,
@@ -190,42 +184,32 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
190
184
  }
191
185
  let fragmentShader = shader.fragmentShader;
192
186
  if (useNormals) {
193
- const normalExpressions = normalNodes.map((node) => {
194
- if (node instanceof FXNodeTexture) {
195
- return `normalize(${node.colorExpression}.rgb * 2.0 - 1.0)`;
196
- }
197
- return node.normalExpression;
198
- });
199
- const blendLines = normalExpressions
200
- .slice(1)
201
- .map((expression) => `normal = blendReorientedNormals(normal, ${expression});`);
202
- const normalCode = [
187
+ const normalLines = [
203
188
  "float faceDirection = gl_FrontFacing ? 1.0 : -1.0;",
204
- `vec3 normal = ${normalExpressions[0]};`,
205
- ...blendLines,
206
- "",
207
- "// Rotate tangent-space normal XY into view space to match billboard rotation.",
208
- "// p_billboardSinCos carries the exact sin/cos that rotated the quad vertices.",
209
- "float sinR = p_billboardSinCos.x;",
210
- "float cosR = p_billboardSinCos.y;",
211
- "normal = vec3(",
212
- " normal.x * cosR - normal.y * sinR,",
213
- " normal.x * sinR + normal.y * cosR,",
214
- " normal.z",
215
- ");",
216
- "",
217
- "normal *= faceDirection;",
218
- ].join("\n");
219
- fragmentShader = fragmentShader.replace("#include <normal_fragment_begin>", normalCode);
189
+ "vec3 fxNormal = vec3(0.0, 0.0, 1.0);",
190
+ ];
191
+ for (const node of normalNodes) {
192
+ if (node instanceof FXNodeTexture || node instanceof FXNodeColor) {
193
+ normalLines.push(`fxNormal = normalize((${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "vec4(fxNormal, 1.0)")}).rgb * 2.0 - 1.0);`);
194
+ }
195
+ else {
196
+ normalLines.push(`fxNormal = ${node.normalExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxNormal")};`);
197
+ }
198
+ }
199
+ normalLines.push("", "float sinR = p_billboardSinCos.x;", "float cosR = p_billboardSinCos.y;", "vec3 normal = vec3(", " fxNormal.x * cosR - fxNormal.y * sinR,", " fxNormal.x * sinR + fxNormal.y * cosR,", " fxNormal.z", ");", "", "normal *= faceDirection;");
200
+ fragmentShader = fragmentShader.replace("#include <normal_fragment_begin>", normalLines.join("\n"));
220
201
  }
221
- if (albedoNodes.length > 0) {
222
- const combinedExpression = albedoNodes.map((n) => n.colorExpression).join(" * ");
223
- const discardLine = useAlphaHashing ? "" : "if (diffuseColor.a < 0.0035) discard;";
224
- fragmentShader = fragmentShader.replace("#include <map_fragment>", `diffuseColor = ${combinedExpression}; ${discardLine}`);
202
+ const albedoCode = buildAlbedoAndBlendingCode(albedoNodes, blending, useAlphaHashing);
203
+ if (albedoCode.mapFragment !== null) {
204
+ fragmentShader = fragmentShader.replace("#include <map_fragment>", albedoCode.mapFragment);
225
205
  }
226
206
  if (emissionNodes.length > 0) {
227
- const combinedExpression = emissionNodes.map((n) => n.colorExpression).join(" * ");
228
- fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", `totalEmissiveRadiance = (${combinedExpression}).rgb;`);
207
+ const emissiveLines = ["vec4 fxEmissive = vec4(1.0);"];
208
+ for (const node of emissionNodes) {
209
+ emissiveLines.push(`fxEmissive = ${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxEmissive")};`);
210
+ }
211
+ emissiveLines.push("totalEmissiveRadiance = fxEmissive.rgb;");
212
+ fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", emissiveLines.join("\n"));
229
213
  }
230
214
  if (useScatter) {
231
215
  fragmentShader = fragmentShader.replace("#include <opaque_fragment>", `
@@ -271,15 +255,17 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
271
255
  if (useShadowSensitivity) {
272
256
  fragmentShader = wrapShadowCalls(fragmentShader, "u_shadowSensitivity");
273
257
  }
258
+ fragmentShader = fragmentShader.replace("#include <premultiplied_alpha_fragment>", albedoCode.premultChunk);
274
259
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
275
260
  };
276
261
  const scatterKey = useScatter ? `scatter(${scatterUniforms.u_ScatterPower.value})` : "no-scatter";
277
262
  const shadowKey = useShadowSensitivity ? "shadow-sens" : "no-shadow-sens";
278
263
  material.customProgramCacheKey = () => [
279
264
  "fx",
280
- albedoNodes.map((n) => n.cacheKey).join("-") || "none",
281
- normalNodes.map((n) => n.cacheKey).join("-") || "none",
282
- emissionNodes.map((n) => n.cacheKey).join("-") || "none",
265
+ albedoNodes.map((node) => node.cacheKey).join("-") || "none",
266
+ normalNodes.map((node) => node.cacheKey).join("-") || "none",
267
+ emissionNodes.map((node) => node.cacheKey).join("-") || "none",
268
+ FXBlending[blending],
283
269
  scatterKey,
284
270
  shadowKey,
285
271
  ].join("_");
@@ -41,7 +41,7 @@ class FXDiffuseMaterial extends FXMaterial {
41
41
  }
42
42
  /** @internal */
43
43
  buildThreeMaterial(varyings) {
44
- return buildFXDiffuseMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.premultipliedAlpha, this.albedoNodes, this._private_normalNodes, this._private_emissionNodes, this._private_scatterUniforms, this._private_shadowSensitivity);
44
+ return buildFXDiffuseMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.albedoNodes, this._private_normalNodes, this._private_emissionNodes, this._private_scatterUniforms, this._private_shadowSensitivity);
45
45
  }
46
46
  }
47
47
 
@@ -0,0 +1,24 @@
1
+ import { FXNodeBlending } from "../../nodes/blending/FXNodeBlending";
2
+ import type { FXNodeColor } from "../../nodes/color/FXNodeColor";
3
+ import type { FXNodeTexture } from "../../nodes/texture/FXNodeTexture";
4
+ import { FXBlending } from "./FXMaterial";
5
+ export interface AlbedoBlendingCode {
6
+ /** Replacement for #include <map_fragment>. null means: leave the include as-is */
7
+ mapFragment: string | null;
8
+ /** Replacement for #include <premultiplied_alpha_fragment> */
9
+ premultChunk: string;
10
+ }
11
+ /**
12
+ * Processes albedoNodes in order to produce shader code for both the color assignment
13
+ * and the per-pixel blending mode.
14
+ *
15
+ * Nodes are composed sequentially: each node receives the current accumulated value via
16
+ * EXPRESSION_CURRENT_VALUE_PLACEHOLDER and returns a new value. Nodes that do not use
17
+ * the placeholder act as sources and replace the current value entirely.
18
+ *
19
+ * When a FXNodeBlending appears, it receives both the current mask (via
20
+ * EXPRESSION_CURRENT_VALUE_PLACEHOLDER) and the current color (via
21
+ * EXPRESSION_CURRENT_COLOR_PLACEHOLDER), and returns the new mask value. Color nodes
22
+ * that follow a blending node do not affect the mask.
23
+ */
24
+ export declare function buildAlbedoAndBlendingCode(albedoNodes: readonly (FXNodeColor | FXNodeTexture | FXNodeBlending)[], blending: FXBlending, useAlphaHashing: boolean): AlbedoBlendingCode;
@@ -0,0 +1,64 @@
1
+ import { CURRENT_EXPRESSION_VALUE_PLACEHOLDER, BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER } from '../../nodes/FXNode.js';
2
+ import { FXNodeBlending } from '../../nodes/blending/FXNodeBlending.js';
3
+ import { FXBlending } from './FXMaterial.js';
4
+
5
+ /**
6
+ * Processes albedoNodes in order to produce shader code for both the color assignment
7
+ * and the per-pixel blending mode.
8
+ *
9
+ * Nodes are composed sequentially: each node receives the current accumulated value via
10
+ * EXPRESSION_CURRENT_VALUE_PLACEHOLDER and returns a new value. Nodes that do not use
11
+ * the placeholder act as sources and replace the current value entirely.
12
+ *
13
+ * When a FXNodeBlending appears, it receives both the current mask (via
14
+ * EXPRESSION_CURRENT_VALUE_PLACEHOLDER) and the current color (via
15
+ * EXPRESSION_CURRENT_COLOR_PLACEHOLDER), and returns the new mask value. Color nodes
16
+ * that follow a blending node do not affect the mask.
17
+ */
18
+ function buildAlbedoAndBlendingCode(albedoNodes, blending, useAlphaHashing) {
19
+ if (albedoNodes.length === 0) {
20
+ return { mapFragment: null, premultChunk: buildPremultChunk(blending, false) };
21
+ }
22
+ const hasBlendingNodes = albedoNodes.some((node) => node instanceof FXNodeBlending);
23
+ const lines = [];
24
+ lines.push("vec4 fxAlbedo = vec4(1.0);");
25
+ if (hasBlendingNodes) {
26
+ lines.push("float fxBlendMask = 1.0;");
27
+ }
28
+ for (const node of albedoNodes) {
29
+ if (node instanceof FXNodeBlending) {
30
+ lines.push(`fxBlendMask = ${node.blendingExpression
31
+ .replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxBlendMask")
32
+ .replace(BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER, "fxAlbedo")};`);
33
+ }
34
+ else {
35
+ lines.push(`fxAlbedo = ${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxAlbedo")};`);
36
+ }
37
+ }
38
+ const discardLine = useAlphaHashing ? "" : "if (diffuseColor.a < 0.0035) discard;";
39
+ lines.push(`diffuseColor = fxAlbedo; ${discardLine}`);
40
+ return {
41
+ mapFragment: lines.join("\n"),
42
+ premultChunk: buildPremultChunk(blending, hasBlendingNodes),
43
+ };
44
+ }
45
+ function buildPremultChunk(blending, hasBlendingNodes) {
46
+ if (!hasBlendingNodes) {
47
+ return `gl_FragColor.rgb *= gl_FragColor.a;`;
48
+ }
49
+ if (blending === FXBlending.MASKED_ADDITIVE) {
50
+ return `
51
+ gl_FragColor.rgb *= gl_FragColor.a;
52
+ gl_FragColor.a *= (1.0 - clamp(fxBlendMask, 0.0, 1.0));
53
+ `;
54
+ }
55
+ // MASKED_MULTIPLY: dark areas darken destination, bright areas normal-blend
56
+ return `
57
+ gl_FragColor.rgb *= gl_FragColor.a;
58
+ float fxLumPremult = dot(gl_FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
59
+ vec4 fxMultiply = vec4(0.0, 0.0, 0.0, gl_FragColor.a - fxLumPremult);
60
+ gl_FragColor = mix(gl_FragColor, fxMultiply, clamp(fxBlendMask, 0.0, 1.0));
61
+ `;
62
+ }
63
+
64
+ export { buildAlbedoAndBlendingCode };
@@ -1,5 +1,6 @@
1
1
  import { MeshDepthMaterial, DoubleSide, RGBADepthPacking } from 'three';
2
2
  import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
3
+ import { CURRENT_EXPRESSION_VALUE_PLACEHOLDER } from '../../nodes/FXNode.js';
3
4
 
4
5
  const DEPTH_VERTEX_SHADER = `
5
6
  #define PARTICLE_POSITION_X a_builtin[0][0]
@@ -116,9 +117,13 @@ function buildDepthMaterial(albedoNodes, depthAlphaTest, useDepthAlphaHash, useS
116
117
  }
117
118
  let fragmentShader = shader.fragmentShader;
118
119
  if (depthNodes.length > 0) {
119
- const combinedAlphaExpression = depthNodes.map((node) => node.colorExpression).join(" * ");
120
- fragmentShader = fragmentShader.replace("#include <map_fragment>", `diffuseColor.a = (${combinedAlphaExpression}).a;
121
- if (diffuseColor.a < ${depthAlphaTest.toFixed(4)}) discard;`);
120
+ const albedoLines = ["vec4 fxDepthAlbedo = vec4(1.0);"];
121
+ for (const node of depthNodes) {
122
+ albedoLines.push(`fxDepthAlbedo = ${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxDepthAlbedo")};`);
123
+ }
124
+ albedoLines.push(`diffuseColor.a = fxDepthAlbedo.a;`);
125
+ albedoLines.push(`if (diffuseColor.a < ${depthAlphaTest.toFixed(4)}) discard;`);
126
+ fragmentShader = fragmentShader.replace("#include <map_fragment>", albedoLines.join("\n"));
122
127
  }
123
128
  if (useSphericalDepth) {
124
129
  if (!FRAG_COORD_Z_PATTERN.test(fragmentShader)) {
@@ -1,5 +1,6 @@
1
1
  import { MeshDistanceMaterial, DoubleSide } from 'three';
2
2
  import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
3
+ import { CURRENT_EXPRESSION_VALUE_PLACEHOLDER } from '../../nodes/FXNode.js';
3
4
 
4
5
  /**
5
6
  * Custom vertex shader for point/spot light shadow maps.
@@ -134,9 +135,12 @@ function buildDistanceMaterial(albedoNodes, distanceAlphaTest, useDistanceAlphaH
134
135
  // <map_fragment> depending on the Three.js version. If the chunk is
135
136
  // absent, insert the alpha discard before the distance calculation.
136
137
  if (distanceNodes.length > 0) {
137
- const combinedAlphaExpression = distanceNodes.map((node) => node.colorExpression).join(" * ");
138
- const alphaDiscardCode = `float nodeAlpha = (${combinedAlphaExpression}).a;\n` +
139
- `if (nodeAlpha < ${distanceAlphaTest.toFixed(4)}) discard;`;
138
+ const albedoLines = ["vec4 fxDistAlbedo = vec4(1.0);"];
139
+ for (const node of distanceNodes) {
140
+ albedoLines.push(`fxDistAlbedo = ${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxDistAlbedo")};`);
141
+ }
142
+ albedoLines.push(`float nodeAlpha = fxDistAlbedo.a;`);
143
+ const alphaDiscardCode = albedoLines.join("\n") + `\nif (nodeAlpha < ${distanceAlphaTest.toFixed(4)}) discard;`;
140
144
  if (fragmentShader.includes("#include <map_fragment>")) {
141
145
  fragmentShader = fragmentShader.replace("#include <map_fragment>", alphaDiscardCode);
142
146
  }
@@ -1,6 +1,20 @@
1
- import type { Blending } from "three";
2
- import type { FXNodeColor } from "../../nodes/color/FXNodeColor";
3
- import type { FXNodeTexture } from "../../nodes/texture/FXNodeTexture";
1
+ import { FXNodeColor } from "../../nodes/color/FXNodeColor";
2
+ import { FXNodeTexture } from "../../nodes/texture/FXNodeTexture";
3
+ /**
4
+ * Per-particle blend mode used when compositing particles into the scene
5
+ */
6
+ export declare enum FXBlending {
7
+ /**
8
+ * Premultiplied additive blend - particle color is pre-multiplied by alpha and added to the
9
+ * destination with zero alpha contribution
10
+ */
11
+ MASKED_ADDITIVE = 0,
12
+ /**
13
+ * Luminance-aware multiply blend - dark areas darken the destination while bright areas
14
+ * normal-blend through a mask
15
+ */
16
+ MASKED_MULTIPLY = 1
17
+ }
4
18
  /**
5
19
  * Configuration options shared by all {@link FXMaterial} subclasses
6
20
  */
@@ -11,12 +25,7 @@ export interface FXMaterialOptions {
11
25
  * @defaultValue `[]`
12
26
  */
13
27
  albedoNodes: (FXNodeColor | FXNodeTexture)[];
14
- /**
15
- * Three.js blending mode
16
- *
17
- * @defaultValue `NormalBlending`
18
- */
19
- blending: Blending;
28
+ blending: FXBlending;
20
29
  /**
21
30
  * Use alpha hashing instead of standard transparency
22
31
  *
@@ -47,12 +56,6 @@ export interface FXMaterialOptions {
47
56
  * @defaultValue `true`
48
57
  */
49
58
  useSphericalDepth: boolean;
50
- /**
51
- * Use premultiplied alpha blending
52
- *
53
- * @defaultValue `true`
54
- */
55
- premultipliedAlpha: boolean;
56
59
  }
57
60
  /**
58
61
  * Abstract base class for particle materials
@@ -1,18 +1,33 @@
1
- import { NormalBlending } from 'three';
1
+ import { FXNodeColor } from '../../nodes/color/FXNodeColor.js';
2
+ import { FXNodeTexture } from '../../nodes/texture/FXNodeTexture.js';
2
3
  import { buildDepthMaterial } from './FXDepthMaterial.Internal.js';
3
4
  import { buildDistanceMaterial } from './FXDistanceMaterial.Internal.js';
4
5
 
6
+ /**
7
+ * Per-particle blend mode used when compositing particles into the scene
8
+ */
9
+ var FXBlending;
10
+ (function (FXBlending) {
11
+ /**
12
+ * Premultiplied additive blend - particle color is pre-multiplied by alpha and added to the
13
+ * destination with zero alpha contribution
14
+ */
15
+ FXBlending[FXBlending["MASKED_ADDITIVE"] = 0] = "MASKED_ADDITIVE";
16
+ /**
17
+ * Luminance-aware multiply blend - dark areas darken the destination while bright areas
18
+ * normal-blend through a mask
19
+ */
20
+ FXBlending[FXBlending["MASKED_MULTIPLY"] = 1] = "MASKED_MULTIPLY";
21
+ })(FXBlending || (FXBlending = {}));
5
22
  /**
6
23
  * Abstract base class for particle materials
7
24
  */
8
25
  class FXMaterial {
9
26
  constructor(options = {}) {
10
27
  this.albedoNodes = options.albedoNodes ?? [];
11
- this.blending = options.blending ?? NormalBlending;
28
+ this.blending = options.blending ?? FXBlending.MASKED_ADDITIVE;
12
29
  this.useAlphaHashing = options.useAlphaHashing ?? false;
13
30
  this.alphaTest = options.alphaTest ?? 0.0075;
14
- this.premultipliedAlpha =
15
- (options.useAlphaHashing === undefined && options.premultipliedAlpha) ?? true;
16
31
  this._private_depthAlphaTest = options.depthAlphaTest ?? 0.5;
17
32
  this._private_useDepthAlphaHash = options.useDepthAlphaHash ?? false;
18
33
  this._private_useSphericalDepth = options.useSphericalDepth ?? true;
@@ -25,12 +40,12 @@ class FXMaterial {
25
40
  }
26
41
  /** @internal */
27
42
  buildDepthMaterial() {
28
- return buildDepthMaterial(this.albedoNodes, this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
43
+ return buildDepthMaterial(this.albedoNodes.filter((node) => node instanceof FXNodeColor || node instanceof FXNodeTexture), this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
29
44
  }
30
45
  /** @internal */
31
46
  buildDistanceMaterial() {
32
- return buildDistanceMaterial(this.albedoNodes, this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
47
+ return buildDistanceMaterial(this.albedoNodes.filter((node) => node instanceof FXNodeColor || node instanceof FXNodeTexture), this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
33
48
  }
34
49
  }
35
50
 
36
- export { FXMaterial };
51
+ export { FXBlending, FXMaterial };
@@ -1,6 +1,7 @@
1
- import type { Blending } from "three";
2
1
  import { MeshBasicMaterial } from "three";
3
2
  import type { GLTypeInfo } from "../../instancedParticle/shared";
3
+ import type { FXNodeBlending } from "../../nodes/blending/FXNodeBlending";
4
4
  import type { FXNodeColor } from "../../nodes/color/FXNodeColor";
5
5
  import type { FXNodeTexture } from "../../nodes/texture/FXNodeTexture";
6
- export declare function buildFXUnlitMaterial(varyings: Record<string, GLTypeInfo>, blending: Blending, useAlphaHashing: boolean, alphaTest: number, premultipliedAlpha: boolean, albedoNodes: readonly (FXNodeColor | FXNodeTexture)[]): MeshBasicMaterial;
6
+ import { FXBlending } from "../FXMaterial/FXMaterial";
7
+ export declare function buildFXUnlitMaterial(varyings: Record<string, GLTypeInfo>, blending: FXBlending, useAlphaHashing: boolean, alphaTest: number, albedoNodes: readonly (FXNodeColor | FXNodeTexture | FXNodeBlending)[]): MeshBasicMaterial;
@@ -1,7 +1,9 @@
1
- import { MeshBasicMaterial, DoubleSide } from 'three';
1
+ import { MeshBasicMaterial, DoubleSide, NormalBlending } from 'three';
2
2
  import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
3
+ import { buildAlbedoAndBlendingCode } from '../FXMaterial/FXBlending.Internal.js';
4
+ import { FXBlending } from '../FXMaterial/FXMaterial.js';
3
5
 
4
- function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, premultipliedAlpha, albedoNodes) {
6
+ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, albedoNodes) {
5
7
  const attributeDeclarations = [];
6
8
  const varyingDeclarations = [];
7
9
  const vertexAssignments = [];
@@ -17,11 +19,11 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, pr
17
19
  depthWrite: useAlphaHashing,
18
20
  depthTest: true,
19
21
  alphaHash: useAlphaHashing,
20
- blending,
22
+ blending: NormalBlending,
21
23
  side: DoubleSide,
22
24
  forceSinglePass: true,
23
25
  alphaTest,
24
- premultipliedAlpha,
26
+ premultipliedAlpha: true,
25
27
  });
26
28
  material.onBeforeCompile = (shader) => {
27
29
  const vertexPreamble = [
@@ -63,34 +65,34 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, pr
63
65
  `);
64
66
  const seenCacheKeys = new Set();
65
67
  const uniqueHelpers = (nodes) => nodes
66
- .filter((n) => {
67
- if (seenCacheKeys.has(n.cacheKey)) {
68
+ .filter((node) => {
69
+ if (seenCacheKeys.has(node.cacheKey)) {
68
70
  return false;
69
71
  }
70
- seenCacheKeys.add(n.cacheKey);
72
+ seenCacheKeys.add(node.cacheKey);
71
73
  return true;
72
74
  })
73
- .map((n) => n.helperFunctions)
75
+ .map((node) => node.helperFunctions)
74
76
  .join("\n");
75
77
  const fragmentPreamble = [
76
78
  PARTICLE_DEFINES,
77
79
  "varying vec2 p_uv;",
78
80
  varyingDeclarations.join("\n"),
79
- albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
81
+ albedoNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
80
82
  uniqueHelpers(albedoNodes),
81
83
  ].join("\n");
82
84
  for (const node of albedoNodes) {
83
85
  Object.assign(shader.uniforms, node.uniforms);
84
86
  }
87
+ const albedoCode = buildAlbedoAndBlendingCode(albedoNodes, blending, useAlphaHashing);
85
88
  let fragmentShader = shader.fragmentShader;
86
- if (albedoNodes.length > 0) {
87
- const combinedExpression = albedoNodes.map((n) => n.colorExpression).join(" * ");
88
- const discardLine = useAlphaHashing ? "" : "if (diffuseColor.a < 0.0035) discard;";
89
- fragmentShader = fragmentShader.replace("#include <map_fragment>", `diffuseColor = ${combinedExpression}; ${discardLine}`);
89
+ if (albedoCode.mapFragment !== null) {
90
+ fragmentShader = fragmentShader.replace("#include <map_fragment>", albedoCode.mapFragment);
90
91
  }
92
+ fragmentShader = fragmentShader.replace("#include <premultiplied_alpha_fragment>", albedoCode.premultChunk);
91
93
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
92
94
  };
93
- material.customProgramCacheKey = () => `fx-unlit-${albedoNodes.map((n) => n.cacheKey).join("-") || "none"}`;
95
+ material.customProgramCacheKey = () => ["fx-unlit", albedoNodes.map((node) => node.cacheKey).join("-") || "none", FXBlending[blending]].join("_");
94
96
  return material;
95
97
  }
96
98
 
@@ -7,7 +7,7 @@ import { buildFXUnlitMaterial } from './FXUnlitMaterial.Internal.js';
7
7
  class FXUnlitMaterial extends FXMaterial {
8
8
  /** @internal */
9
9
  buildThreeMaterial(varyings) {
10
- return buildFXUnlitMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.premultipliedAlpha, this.albedoNodes);
10
+ return buildFXUnlitMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.albedoNodes);
11
11
  }
12
12
  }
13
13
 
@@ -0,0 +1,5 @@
1
+ export declare const CURRENT_EXPRESSION_VALUE_PLACEHOLDER = "CURRENT_EXPRESSION_VALUE_PLACEHOLDER";
2
+ export declare const BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER = "BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER";
3
+ export declare abstract class FXNode {
4
+ destroy?(): void;
5
+ }
@@ -0,0 +1,10 @@
1
+ const CURRENT_EXPRESSION_VALUE_PLACEHOLDER = "CURRENT_EXPRESSION_VALUE_PLACEHOLDER";
2
+ const BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER = "BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER";
3
+ class FXNode {
4
+ constructor() {
5
+ /** @internal */
6
+ this.affectsDepth = false;
7
+ }
8
+ }
9
+
10
+ export { BLENDING_EXPRESSION_CURRENT_COLOR_PLACEHOLDER, CURRENT_EXPRESSION_VALUE_PLACEHOLDER, FXNode };
@@ -0,0 +1,6 @@
1
+ import { FXNode } from "../FXNode";
2
+ /**
3
+ * Abstract base class for particle color nodes
4
+ */
5
+ export declare abstract class FXNodeBlending extends FXNode {
6
+ }
@@ -0,0 +1,9 @@
1
+ import { FXNode } from '../FXNode.js';
2
+
3
+ /**
4
+ * Abstract base class for particle color nodes
5
+ */
6
+ class FXNodeBlending extends FXNode {
7
+ }
8
+
9
+ export { FXNodeBlending };
@@ -0,0 +1,16 @@
1
+ import { FXNodeBlending } from "./FXNodeBlending";
2
+ export declare class FXNodeLightnessBlendingMask extends FXNodeBlending {
3
+ private readonly uniformEdge0;
4
+ private readonly uniformEdge1;
5
+ /**
6
+ * @param edge0 - brightness threshold where mask starts (below this: NormalBlending). Defaults to `0.3`
7
+ * @param edge1 - brightness threshold where mask is fully active (above this: full FXBlending effect). Defaults to `0.7`
8
+ */
9
+ constructor(edge0?: number, edge1?: number);
10
+ /** Brightness threshold where mask starts (below this: NormalBlending) */
11
+ get edge0(): number;
12
+ /** Brightness threshold where mask is fully active (above this: full FXBlending effect) */
13
+ get edge1(): number;
14
+ set edge0(value: number);
15
+ set edge1(value: number);
16
+ }