sparcoon 0.6.1 → 0.7.1

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 +49 -63
  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 +10 -4
  49. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +9 -4
  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 +18 -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 type { 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,9 @@
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';
4
7
 
5
8
  /**
6
9
  * Wraps every `getShadow(...)` call in the shader source with
@@ -37,7 +40,7 @@ function wrapShadowCalls(source, uniformName) {
37
40
  }
38
41
  return result;
39
42
  }
40
- function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, premultipliedAlpha, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity) {
43
+ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity) {
41
44
  const useScatter = scatterUniforms !== undefined;
42
45
  const useShadowSensitivity = shadowSensitivity !== undefined;
43
46
  const useNormals = normalNodes.length > 0;
@@ -56,12 +59,12 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
56
59
  depthWrite: useAlphaHashing,
57
60
  depthTest: true,
58
61
  alphaHash: useAlphaHashing,
59
- blending,
62
+ blending: NormalBlending,
60
63
  side: DoubleSide,
61
64
  forceSinglePass: true,
62
65
  alphaTest,
63
66
  emissive: emissionNodes.length > 0 ? new Color(1, 1, 1) : new Color(0, 0, 0),
64
- premultipliedAlpha,
67
+ premultipliedAlpha: true,
65
68
  });
66
69
  material.onBeforeCompile = (shader) => {
67
70
  const vertexPreamble = [
@@ -117,25 +120,16 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
117
120
  );
118
121
  #endif
119
122
  `);
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
123
  const seenCacheKeys = new Set();
130
124
  const uniqueHelpers = (nodes) => nodes
131
- .filter((n) => {
132
- if (seenCacheKeys.has(n.cacheKey)) {
125
+ .filter((node) => {
126
+ if (seenCacheKeys.has(node.cacheKey)) {
133
127
  return false;
134
128
  }
135
- seenCacheKeys.add(n.cacheKey);
129
+ seenCacheKeys.add(node.cacheKey);
136
130
  return true;
137
131
  })
138
- .map((n) => n.helperFunctions)
132
+ .map((node) => node.helperFunctions)
139
133
  .join("\n");
140
134
  const scatterDeclarations = useScatter
141
135
  ? [
@@ -160,12 +154,11 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
160
154
  "varying vec2 p_uv;",
161
155
  useNormals ? "varying vec2 p_billboardSinCos;" : "",
162
156
  varyingDeclarations.join("\n"),
163
- albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
157
+ albedoNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
164
158
  uniqueHelpers(albedoNodes),
165
- normalNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
159
+ normalNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
166
160
  uniqueHelpers(normalNodes),
167
- blendReorientedNormalsHelper,
168
- emissionNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
161
+ emissionNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
169
162
  uniqueHelpers(emissionNodes),
170
163
  ...scatterDeclarations,
171
164
  ...shadowSensitivityDeclarations,
@@ -190,42 +183,32 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
190
183
  }
191
184
  let fragmentShader = shader.fragmentShader;
192
185
  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 = [
186
+ const normalLines = [
203
187
  "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);
188
+ "vec3 fxNormal = vec3(0.0, 0.0, 1.0);",
189
+ ];
190
+ for (const node of normalNodes) {
191
+ if (node instanceof FXNodeTexture || node instanceof FXNodeColor) {
192
+ normalLines.push(`fxNormal = normalize((${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "vec4(fxNormal, 1.0)")}).rgb * 2.0 - 1.0);`);
193
+ }
194
+ else {
195
+ normalLines.push(`fxNormal = ${node.normalExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxNormal")};`);
196
+ }
197
+ }
198
+ 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;");
199
+ fragmentShader = fragmentShader.replace("#include <normal_fragment_begin>", normalLines.join("\n"));
220
200
  }
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}`);
201
+ const albedoCode = buildAlbedoAndBlendingCode(albedoNodes, blending, useAlphaHashing);
202
+ if (albedoCode.mapFragment !== null) {
203
+ fragmentShader = fragmentShader.replace("#include <map_fragment>", albedoCode.mapFragment);
225
204
  }
226
205
  if (emissionNodes.length > 0) {
227
- const combinedExpression = emissionNodes.map((n) => n.colorExpression).join(" * ");
228
- fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", `totalEmissiveRadiance = (${combinedExpression}).rgb;`);
206
+ const emissiveLines = ["vec4 fxEmissive = vec4(1.0);"];
207
+ for (const node of emissionNodes) {
208
+ emissiveLines.push(`fxEmissive = ${node.colorExpression.replace(CURRENT_EXPRESSION_VALUE_PLACEHOLDER, "fxEmissive")};`);
209
+ }
210
+ emissiveLines.push("totalEmissiveRadiance = fxEmissive.rgb;");
211
+ fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", emissiveLines.join("\n"));
229
212
  }
230
213
  if (useScatter) {
231
214
  fragmentShader = fragmentShader.replace("#include <opaque_fragment>", `
@@ -271,18 +254,21 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
271
254
  if (useShadowSensitivity) {
272
255
  fragmentShader = wrapShadowCalls(fragmentShader, "u_shadowSensitivity");
273
256
  }
257
+ fragmentShader = fragmentShader.replace("#include <premultiplied_alpha_fragment>", albedoCode.premultChunk);
274
258
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
275
259
  };
276
- const scatterKey = useScatter ? `scatter(${scatterUniforms.u_ScatterPower.value})` : "no-scatter";
277
- const shadowKey = useShadowSensitivity ? "shadow-sens" : "no-shadow-sens";
278
- material.customProgramCacheKey = () => [
279
- "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",
283
- scatterKey,
284
- shadowKey,
285
- ].join("_");
260
+ // const scatterKey = useScatter ? `scatter(${scatterUniforms.u_ScatterPower.value})` : "no-scatter";
261
+ // const shadowKey = useShadowSensitivity ? "shadow-sens" : "no-shadow-sens";
262
+ // material.customProgramCacheKey = (): string =>
263
+ // [
264
+ // "fx",
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],
269
+ // scatterKey,
270
+ // shadowKey,
271
+ // ].join("_");
286
272
  return material;
287
273
  }
288
274
 
@@ -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)) {
@@ -132,7 +137,8 @@ function buildDepthMaterial(albedoNodes, depthAlphaTest, useDepthAlphaHash, useS
132
137
  }
133
138
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
134
139
  };
135
- material.customProgramCacheKey = () => `fx-depth-${useDepthAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${depthNodes.map((node) => node.cacheKey).join("-") || "none"}`;
140
+ // material.customProgramCacheKey = (): string =>
141
+ // `fx-depth-${useDepthAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${depthNodes.map((node) => node.cacheKey).join("-") || "none"}`;
136
142
  return material;
137
143
  }
138
144
 
@@ -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
  }
@@ -164,7 +168,8 @@ function buildDistanceMaterial(albedoNodes, distanceAlphaTest, useDistanceAlphaH
164
168
  }
165
169
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
166
170
  };
167
- material.customProgramCacheKey = () => `fx-distance-${useDistanceAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${distanceNodes.map((node) => node.cacheKey).join("-") || "none"}`;
171
+ // material.customProgramCacheKey = (): string =>
172
+ // `fx-distance-${useDistanceAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${distanceNodes.map((node) => node.cacheKey).join("-") || "none"}`;
168
173
  return material;
169
174
  }
170
175
 
@@ -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 type { 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,8 @@
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';
3
4
 
4
- function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, premultipliedAlpha, albedoNodes) {
5
+ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, albedoNodes) {
5
6
  const attributeDeclarations = [];
6
7
  const varyingDeclarations = [];
7
8
  const vertexAssignments = [];
@@ -17,11 +18,11 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, pr
17
18
  depthWrite: useAlphaHashing,
18
19
  depthTest: true,
19
20
  alphaHash: useAlphaHashing,
20
- blending,
21
+ blending: NormalBlending,
21
22
  side: DoubleSide,
22
23
  forceSinglePass: true,
23
24
  alphaTest,
24
- premultipliedAlpha,
25
+ premultipliedAlpha: true,
25
26
  });
26
27
  material.onBeforeCompile = (shader) => {
27
28
  const vertexPreamble = [
@@ -63,34 +64,37 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, pr
63
64
  `);
64
65
  const seenCacheKeys = new Set();
65
66
  const uniqueHelpers = (nodes) => nodes
66
- .filter((n) => {
67
- if (seenCacheKeys.has(n.cacheKey)) {
67
+ .filter((node) => {
68
+ if (seenCacheKeys.has(node.cacheKey)) {
68
69
  return false;
69
70
  }
70
- seenCacheKeys.add(n.cacheKey);
71
+ seenCacheKeys.add(node.cacheKey);
71
72
  return true;
72
73
  })
73
- .map((n) => n.helperFunctions)
74
+ .map((node) => node.helperFunctions)
74
75
  .join("\n");
75
76
  const fragmentPreamble = [
76
77
  PARTICLE_DEFINES,
77
78
  "varying vec2 p_uv;",
78
79
  varyingDeclarations.join("\n"),
79
- albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
80
+ albedoNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
80
81
  uniqueHelpers(albedoNodes),
81
82
  ].join("\n");
82
83
  for (const node of albedoNodes) {
83
84
  Object.assign(shader.uniforms, node.uniforms);
84
85
  }
86
+ const albedoCode = buildAlbedoAndBlendingCode(albedoNodes, blending, useAlphaHashing);
85
87
  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}`);
88
+ if (albedoCode.mapFragment !== null) {
89
+ fragmentShader = fragmentShader.replace("#include <map_fragment>", albedoCode.mapFragment);
90
90
  }
91
+ fragmentShader = fragmentShader.replace("#include <premultiplied_alpha_fragment>", albedoCode.premultChunk);
91
92
  shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
92
93
  };
93
- material.customProgramCacheKey = () => `fx-unlit-${albedoNodes.map((n) => n.cacheKey).join("-") || "none"}`;
94
+ // material.customProgramCacheKey = (): string =>
95
+ // ["fx-unlit", albedoNodes.map((node) => node.cacheKey).join("-") || "none", FXBlending[blending]].join(
96
+ // "_",
97
+ // );
94
98
  return material;
95
99
  }
96
100
 
@@ -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 };