sparcoon 0.7.3 → 0.9.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 (155) hide show
  1. package/README.md +124 -76
  2. package/dist/artifact/FXArtifact.d.ts +120 -0
  3. package/dist/artifact/FXArtifact.js +1 -0
  4. package/dist/coreLayout.d.ts +22 -0
  5. package/dist/coreLayout.js +32 -0
  6. package/dist/effect/FXEffect.d.ts +77 -0
  7. package/dist/effect/FXEffect.js +454 -0
  8. package/dist/effect/FXEffectSampling.Internal.d.ts +24 -0
  9. package/dist/effect/FXEffectSampling.Internal.js +165 -0
  10. package/dist/effect/FXEffectSpec.d.ts +115 -0
  11. package/dist/effect/FXEffectSpec.js +1 -0
  12. package/dist/emitter/FXEmitter.Internal.d.ts +23 -58
  13. package/dist/emitter/FXEmitter.Internal.js +13 -29
  14. package/dist/emitter/FXEmitter.d.ts +46 -48
  15. package/dist/emitter/FXEmitter.js +355 -186
  16. package/dist/emitter/FXFromArtifactsOptions.d.ts +13 -0
  17. package/dist/emitter/FXFromArtifactsOptions.js +1 -0
  18. package/dist/emitter/FXObjectMotion.Internal.d.ts +33 -0
  19. package/dist/emitter/FXObjectMotion.Internal.js +87 -0
  20. package/dist/emitter/FXSimulationHolder.d.ts +1 -0
  21. package/dist/emitter/FXSimulationHolder.js +43 -0
  22. package/dist/index.d.ts +10 -39
  23. package/dist/index.js +13 -36
  24. package/dist/instancedParticle/FXInstancedParticle.d.ts +1 -22
  25. package/dist/instancedParticle/FXInstancedParticle.js +145 -85
  26. package/dist/instancedParticle/glTypeInfo.d.ts +5 -0
  27. package/dist/instancedParticle/glTypeInfo.js +3 -0
  28. package/dist/instancedParticle/primitiveGeometry.d.ts +1 -0
  29. package/dist/instancedParticle/primitiveGeometry.js +30 -0
  30. package/dist/miscellaneous/asserts.d.ts +1 -18
  31. package/dist/miscellaneous/asserts.js +21 -28
  32. package/dist/miscellaneous/fxMath.d.ts +26 -0
  33. package/dist/miscellaneous/fxMath.js +261 -0
  34. package/dist/miscellaneous/math.d.ts +2 -7
  35. package/dist/miscellaneous/math.js +8 -9
  36. package/dist/miscellaneous/texture/fxDataTexture.d.ts +26 -0
  37. package/dist/miscellaneous/texture/fxDataTexture.js +20 -0
  38. package/dist/render/FXArtifactMaterial.Internal.d.ts +16 -0
  39. package/dist/render/FXArtifactMaterial.Internal.js +944 -0
  40. package/dist/render/FXArtifactMaterial.d.ts +1 -0
  41. package/dist/render/FXArtifactMaterial.js +79 -0
  42. package/dist/render/FXMaterialOptions.d.ts +27 -0
  43. package/dist/render/FXMaterialOptions.js +5 -0
  44. package/dist/render/FXMeshMaterial.d.ts +40 -0
  45. package/dist/render/FXMeshMaterial.js +77 -0
  46. package/dist/world/FXWorld.d.ts +42 -0
  47. package/dist/world/FXWorld.js +103 -0
  48. package/package.json +45 -29
  49. package/dist/_virtual/2d.js +0 -3
  50. package/dist/_virtual/3d.js +0 -3
  51. package/dist/_virtual/4d.js +0 -3
  52. package/dist/_virtual/mod.js +0 -5
  53. package/dist/_virtual/mod2.js +0 -3
  54. package/dist/emitter/behavior/FXBehavior.d.ts +0 -24
  55. package/dist/emitter/behavior/FXBehavior.js +0 -10
  56. package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +0 -23
  57. package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +0 -72
  58. package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +0 -36
  59. package/dist/emitter/behavior/FXBehaviorPointForce.js +0 -94
  60. package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +0 -24
  61. package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +0 -55
  62. package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +0 -20
  63. package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +0 -48
  64. package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +0 -25
  65. package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +0 -67
  66. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +0 -19
  67. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +0 -41
  68. package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +0 -20
  69. package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +0 -50
  70. package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +0 -25
  71. package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +0 -76
  72. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +0 -22
  73. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +0 -55
  74. package/dist/emitter/spawn/FXSpawn.d.ts +0 -11
  75. package/dist/emitter/spawn/FXSpawn.js +0 -10
  76. package/dist/emitter/spawn/FXSpawnBox.d.ts +0 -18
  77. package/dist/emitter/spawn/FXSpawnBox.js +0 -50
  78. package/dist/emitter/spawn/FXSpawnLifetime.d.ts +0 -17
  79. package/dist/emitter/spawn/FXSpawnLifetime.js +0 -44
  80. package/dist/emitter/spawn/FXSpawnOffset.d.ts +0 -21
  81. package/dist/emitter/spawn/FXSpawnOffset.js +0 -49
  82. package/dist/emitter/spawn/FXSpawnPoint.d.ts +0 -18
  83. package/dist/emitter/spawn/FXSpawnPoint.js +0 -46
  84. package/dist/emitter/spawn/FXSpawnRotation.d.ts +0 -17
  85. package/dist/emitter/spawn/FXSpawnRotation.js +0 -42
  86. package/dist/emitter/spawn/FXSpawnScale.d.ts +0 -22
  87. package/dist/emitter/spawn/FXSpawnScale.js +0 -56
  88. package/dist/emitter/spawn/FXSpawnSphere.d.ts +0 -36
  89. package/dist/emitter/spawn/FXSpawnSphere.js +0 -87
  90. package/dist/emitter/spawn/FXSpawnTorque.d.ts +0 -17
  91. package/dist/emitter/spawn/FXSpawnTorque.js +0 -42
  92. package/dist/emitter/spawn/FXSpawnVelocity.d.ts +0 -29
  93. package/dist/emitter/spawn/FXSpawnVelocity.js +0 -91
  94. package/dist/instancedParticle/FXInstancedParticle.Internal.d.ts +0 -2
  95. package/dist/instancedParticle/shared.d.ts +0 -19
  96. package/dist/instancedParticle/shared.js +0 -116
  97. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +0 -24
  98. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +0 -275
  99. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +0 -75
  100. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +0 -63
  101. package/dist/materials/FXMaterial/FXBlending.Internal.d.ts +0 -24
  102. package/dist/materials/FXMaterial/FXBlending.Internal.js +0 -64
  103. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +0 -4
  104. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +0 -144
  105. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +0 -4
  106. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +0 -175
  107. package/dist/materials/FXMaterial/FXMaterial.d.ts +0 -70
  108. package/dist/materials/FXMaterial/FXMaterial.js +0 -51
  109. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +0 -7
  110. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +0 -103
  111. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.d.ts +0 -7
  112. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +0 -21
  113. package/dist/miscellaneous/color/FXColor.Internal.d.ts +0 -7
  114. package/dist/miscellaneous/color/FXColor.Internal.js +0 -42
  115. package/dist/miscellaneous/color/FXColor.d.ts +0 -234
  116. package/dist/miscellaneous/color/FXColor.js +0 -636
  117. package/dist/miscellaneous/curve/FXCurve1D.Internal.d.ts +0 -20
  118. package/dist/miscellaneous/curve/FXCurve1D.d.ts +0 -17
  119. package/dist/miscellaneous/curve/FXCurve1D.js +0 -94
  120. package/dist/miscellaneous/miscellaneous.d.ts +0 -34
  121. package/dist/miscellaneous/miscellaneous.js +0 -85
  122. package/dist/miscellaneous/texture/FXTextureView.Internal.d.ts +0 -49
  123. package/dist/miscellaneous/texture/FXTextureView.Internal.js +0 -6
  124. package/dist/miscellaneous/texture/FXTextureView.d.ts +0 -72
  125. package/dist/miscellaneous/texture/FXTextureView.js +0 -234
  126. package/dist/miscellaneous/webglCapabilities.d.ts +0 -1
  127. package/dist/miscellaneous/webglCapabilities.js +0 -45
  128. package/dist/node_modules/fast-simplex-noise/lib/2d.js +0 -93
  129. package/dist/node_modules/fast-simplex-noise/lib/3d.js +0 -142
  130. package/dist/node_modules/fast-simplex-noise/lib/4d.js +0 -196
  131. package/dist/node_modules/fast-simplex-noise/lib/mod.js +0 -24
  132. package/dist/nodes/FXNode.d.ts +0 -5
  133. package/dist/nodes/FXNode.js +0 -10
  134. package/dist/nodes/blending/FXNodeBlending.d.ts +0 -6
  135. package/dist/nodes/blending/FXNodeBlending.js +0 -9
  136. package/dist/nodes/blending/FXNodeLightnessBlendingMask.d.ts +0 -19
  137. package/dist/nodes/blending/FXNodeLightnessBlendingMask.js +0 -65
  138. package/dist/nodes/color/FXNodeColor.d.ts +0 -6
  139. package/dist/nodes/color/FXNodeColor.js +0 -9
  140. package/dist/nodes/color/FXNodeColorOverLife.d.ts +0 -22
  141. package/dist/nodes/color/FXNodeColorOverLife.js +0 -64
  142. package/dist/nodes/color/FXNodeSphericalClip.d.ts +0 -21
  143. package/dist/nodes/color/FXNodeSphericalClip.js +0 -54
  144. package/dist/nodes/normal/FXNodeFlatNormal.d.ts +0 -6
  145. package/dist/nodes/normal/FXNodeFlatNormal.js +0 -16
  146. package/dist/nodes/normal/FXNodeNormal.d.ts +0 -6
  147. package/dist/nodes/normal/FXNodeNormal.js +0 -9
  148. package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +0 -6
  149. package/dist/nodes/normal/FXNodeSphericalNormal.js +0 -23
  150. package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +0 -52
  151. package/dist/nodes/texture/FXNodeAnimatedTexture.js +0 -151
  152. package/dist/nodes/texture/FXNodeStaticTexture.d.ts +0 -20
  153. package/dist/nodes/texture/FXNodeStaticTexture.js +0 -67
  154. package/dist/nodes/texture/FXNodeTexture.d.ts +0 -6
  155. package/dist/nodes/texture/FXNodeTexture.js +0 -9
@@ -0,0 +1,944 @@
1
+ import { DoubleSide, NormalBlending, ShaderMaterial, UniformsLib, UniformsUtils } from "three";
2
+ import { FX_CORE_PARTICLE_DEFINES } from "../coreLayout.js";
3
+ /** The Three material transparency/depth flags for a render mode. Blending is always `NormalBlending`
4
+ * (`premultipliedAlpha`) - additive is done in-shader (see {@link blendTail}), not a separate mode. */
5
+ function materialFlags(renderMode) {
6
+ const blended = renderMode === "blending";
7
+ return { transparent: blended, depthWrite: !blended, alphaHash: renderMode === "alphaHash" };
8
+ }
9
+ // Each property becomes an `a_<name>` attribute mirrored into a `p_<name>` varying. Shared by both adapters.
10
+ function declareVaryings(varyings) {
11
+ const attributeDeclarations = [];
12
+ const varyingDeclarations = [];
13
+ const vertexAssignments = [];
14
+ for (const name in varyings) {
15
+ const { glslTypeName } = varyings[name];
16
+ attributeDeclarations.push(`attribute ${glslTypeName} a_${name};`);
17
+ varyingDeclarations.push(`varying ${glslTypeName} p_${name};`);
18
+ vertexAssignments.push(`p_${name} = a_${name};`);
19
+ }
20
+ return {
21
+ attributeDeclarations: attributeDeclarations.join("\n"),
22
+ varyingDeclarations: varyingDeclarations.join("\n"),
23
+ vertexAssignments: vertexAssignments.join("\n"),
24
+ };
25
+ }
26
+ /** GLSL expression for a vertex output slot, or its default when the slot is unbound. */
27
+ function outputOrDefault(outputs, slot, fallback) {
28
+ return slot in outputs ? outputs[slot] : fallback;
29
+ }
30
+ /**
31
+ * Builds `fxModelPos = particleCenter + (particleTransform * (vertexTransform * position)).xyz` from
32
+ * the two optional mat4 outputs (identity when unwired). Geometry-agnostic - a billboard is just a
33
+ * plane whose `particleTransform` is a camera-facing rotation. `withNormal` also emits `fxNormalXform`
34
+ * (mat3) for the lambert adapter's real mesh normal.
35
+ */
36
+ function transformPreamble(outputs, withNormal) {
37
+ const particleTransform = outputOrDefault(outputs, "particleTransform", "mat4(1.0)");
38
+ const vertexTransform = outputOrDefault(outputs, "vertexTransform", "mat4(1.0)");
39
+ const lines = [
40
+ `mat4 fxParticleXform = ${particleTransform};`,
41
+ `mat4 fxVertexXform = ${vertexTransform};`,
42
+ "vec3 particleCenter = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);",
43
+ "vec3 fxModelPos = particleCenter + (fxParticleXform * (fxVertexXform * vec4(position, 1.0))).xyz;",
44
+ ];
45
+ if (withNormal) {
46
+ lines.push("mat3 fxNormalXform = mat3(fxParticleXform) * mat3(fxVertexXform);");
47
+ }
48
+ return lines.join("\n");
49
+ }
50
+ // `#include <project_vertex>` replacement: projects `fxModelPos`, records camera distance, feeds alpha-hash.
51
+ function projectFromModelPos() {
52
+ return [
53
+ "vec4 mvPosition = modelViewMatrix * vec4(fxModelPos, 1.0);",
54
+ "p_cameraDistance = length(mvPosition.xyz);",
55
+ "gl_Position = projectionMatrix * mvPosition;",
56
+ "#ifdef USE_ALPHAHASH",
57
+ " vPosition = fxModelPos;",
58
+ "#endif",
59
+ ].join("\n");
60
+ }
61
+ /** Binds the artifact's live uniform slots into the Three shader (shared object -> live scrub). */
62
+ function bindUniforms(shader, render, timeUniform, deltaUniform, externalSlots) {
63
+ shader.uniforms["u_time"] = timeUniform;
64
+ shader.uniforms["u_deltaTime"] = deltaUniform;
65
+ for (const name in render.uniforms) {
66
+ const uniform = render.uniforms[name];
67
+ if ("external" in uniform) {
68
+ // External texture resolves per emitter: bind the material's durable slot, not the shared module slot.
69
+ shader.uniforms[name] = externalSlots[name];
70
+ }
71
+ else {
72
+ // Share the artifact's slot object so a scrub of `render.uniforms[name].value` is picked up next frame.
73
+ shader.uniforms[name] = uniform;
74
+ }
75
+ }
76
+ }
77
+ /** Binds `objectVelocity`/`objectAngularVelocity` into the Three shader. */
78
+ function bindObjectMotionUniforms(shader, velocityUniform, angularVelocityUniform) {
79
+ shader.uniforms["objectVelocity"] = velocityUniform;
80
+ shader.uniforms["objectAngularVelocity"] = angularVelocityUniform;
81
+ }
82
+ /** Deterministic djb2 hash of the artifact's GLSL - a content-addressed program cache key. */
83
+ function hashString(input) {
84
+ let hash = 5381;
85
+ for (let i = 0; i < input.length; i++) {
86
+ hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;
87
+ }
88
+ return (hash >>> 0).toString(36);
89
+ }
90
+ // Stable program-cache key from the artifact's GLSL: identical artifacts share one compiled program.
91
+ export function fxRenderArtifactCacheKey(render) {
92
+ return hashString([
93
+ render.lightingIntrinsics.join(","),
94
+ render.uniformDeclarations.join("|"),
95
+ render.vertex.varyingDeclarations.join("|"),
96
+ render.vertex.helperFunctions.join("|"),
97
+ render.vertex.body.join("|"),
98
+ render.fragment.varyingDeclarations.join("|"),
99
+ render.fragment.helperFunctions.join("|"),
100
+ render.fragment.body.join("|"),
101
+ Object.entries(render.outputs)
102
+ .map(([slot, expression]) => `${slot}=${expression}`)
103
+ .join("|"),
104
+ ].join("\n"));
105
+ }
106
+ // The fragment alpha-discard for a render mode (operates on `diffuseColor`). `alphaThresholdExpression`
107
+ // is the hard cutoff (default 0.0075, clamped to [0,1] so an out-of-range graph expression can't force
108
+ // an always/never discard), applied in every mode except `opaque` - `opaque` returns early with no
109
+ // discard at all, so it keeps the GPU's early depth test. `blending` also drops near-zero fragments;
110
+ // `alphaHash` adds the stochastic `alphahash_fragment` discard (needs `alphahash_pars_fragment` +
111
+ // `vPosition` in scope).
112
+ function alphaDiscardLines(renderMode, alphaThresholdExpression) {
113
+ if (renderMode === "opaque") {
114
+ return "";
115
+ }
116
+ const cutoff = `if (diffuseColor.a < clamp(${alphaThresholdExpression}, 0.0, 1.0)) discard;`;
117
+ switch (renderMode) {
118
+ case "blending":
119
+ return `if (diffuseColor.a < 0.0035) discard;\n${cutoff}`;
120
+ case "alphaHash":
121
+ return `${cutoff}\n#include <alphahash_fragment>`;
122
+ case "alphaTest":
123
+ return cutoff;
124
+ }
125
+ }
126
+ // The blend tail after the color is assembled: premultiply, then (blending mode only) fold `additivity`
127
+ // into the OUTPUT alpha so a single premultiplied `NormalBlending` interpolates over -> additive: the
128
+ // color stays premultiplied by the true alpha, but reducing `gl_FragColor.a` toward 0 preserves more of
129
+ // the destination (`a=0` => pure additive). `additivity` defaults to 0 (plain "over").
130
+ function blendTail(renderMode, outputs) {
131
+ const lines = ["gl_FragColor.rgb *= gl_FragColor.a;"];
132
+ if (renderMode === "blending") {
133
+ lines.push(`gl_FragColor.a *= (1.0 - clamp(${outputOrDefault(outputs, "additivity", "0.0")}, 0.0, 1.0));`);
134
+ }
135
+ return lines.join("\n");
136
+ }
137
+ // Builds an unlit artifact as a self-contained ShaderMaterial: fog uniforms/defines come from Three
138
+ // (`fog: true`), the tone-mapping + output-colorspace functions from Three's fragment prefix; the rest
139
+ // is ours. The fragment tail mirrors MeshBasic (`opaque_fragment` -> tonemapping -> colorspace -> fog
140
+ // -> premultiply) by reusing those exact ShaderChunks, so pixels match the former onBeforeCompile splice.
141
+ // Deliberately dropped vs MeshBasic: clipping planes, logarithmic depth, dithering (unused by particles).
142
+ // The view-space surface normal (`vNormal` -> `geometryNormal`) is set up unconditionally (a small
143
+ // constant cost) so the `geometryNormal` builtin - fresnel, rim, normal masks - works with no lighting
144
+ // node present; the lit builder does the same.
145
+ export function buildUnlitArtifactMaterial(render, varyings, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
146
+ const options = render.options ?? {};
147
+ const renderMode = options.renderMode ?? "blending";
148
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
149
+ if (!("albedo" in render.outputs)) {
150
+ throw new Error("buildUnlitArtifactMaterial: render artifact has no 'albedo' output");
151
+ }
152
+ const albedo = render.outputs["albedo"];
153
+ const { attributeDeclarations, varyingDeclarations, vertexAssignments } = declareVaryings(varyings);
154
+ const cacheKey = fxRenderArtifactCacheKey(render);
155
+ const vertexShader = [
156
+ FX_CORE_PARTICLE_DEFINES,
157
+ attributeDeclarations,
158
+ "varying vec2 p_uv;",
159
+ "uniform float u_time;",
160
+ "uniform float u_deltaTime;",
161
+ "uniform vec3 objectVelocity;",
162
+ "uniform vec3 objectAngularVelocity;",
163
+ "varying float p_cameraDistance;",
164
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): the canonical author space for
165
+ // normals. Written in the vertex stage, read as the geometryNormal/geometryTangent/worldPosition
166
+ // builtins in the fragment stage.
167
+ "varying vec3 vWorldNormal;",
168
+ "varying vec3 vWorldTangent;",
169
+ "varying vec3 vWorldPos;",
170
+ varyingDeclarations,
171
+ render.vertex.varyingDeclarations.join("\n"),
172
+ render.uniformDeclarations.join("\n"),
173
+ render.vertex.helperFunctions.join("\n"),
174
+ // `common` declares `vPosition` (under USE_ALPHAHASH); `fog_pars_vertex` declares `vFogDepth`;
175
+ // `normal_pars_vertex` declares `vNormal` (the surface normal for the `geometryNormal` builtin).
176
+ "#include <common>",
177
+ "#include <fog_pars_vertex>",
178
+ "#include <normal_pars_vertex>",
179
+ "void main() {",
180
+ "p_uv = uv;",
181
+ vertexAssignments,
182
+ render.vertex.body.join("\n"),
183
+ transformPreamble(render.outputs, true),
184
+ // Our objectNormal (replaces beginnormal_vertex); stock defaultnormal/normal_vertex build vNormal.
185
+ "vec3 objectNormal = fxNormalXform * normal;",
186
+ "#include <defaultnormal_vertex>",
187
+ "#include <normal_vertex>",
188
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): mat3(modelMatrix) is exact for a
189
+ // plane's frame under uniform scale; the tangent is the plane's local +X (the u axis), so billboard
190
+ // roll/orientation is baked in here ONCE for every consumer (normal maps, spherical normal, fresnel).
191
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
192
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
193
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
194
+ projectFromModelPos(),
195
+ "#include <fog_vertex>",
196
+ "}",
197
+ ].join("\n");
198
+ // `opaque_fragment` below forces alpha to 1 when the material is opaque (the hash case,
199
+ // `transparent: false`), exactly as MeshBasic did.
200
+ const fragmentShader = [
201
+ FX_CORE_PARTICLE_DEFINES,
202
+ "varying vec2 p_uv;",
203
+ "uniform float u_time;",
204
+ "uniform float u_deltaTime;",
205
+ "uniform vec3 objectVelocity;",
206
+ "uniform vec3 objectAngularVelocity;",
207
+ "varying float p_cameraDistance;",
208
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): the canonical author space for
209
+ // normals. Written in the vertex stage, read as the geometryNormal/geometryTangent/worldPosition
210
+ // builtins in the fragment stage.
211
+ "varying vec3 vWorldNormal;",
212
+ "varying vec3 vWorldTangent;",
213
+ "varying vec3 vWorldPos;",
214
+ varyingDeclarations,
215
+ render.fragment.varyingDeclarations.join("\n"),
216
+ render.uniformDeclarations.join("\n"),
217
+ render.fragment.helperFunctions.join("\n"),
218
+ "#include <common>",
219
+ "#include <alphahash_pars_fragment>",
220
+ "#include <fog_pars_fragment>",
221
+ // `normal_pars_fragment` declares the interpolated `vNormal` used for `geometryNormal` below.
222
+ "#include <normal_pars_fragment>",
223
+ "void main() {",
224
+ // Front-facing sign for the surface frame below (a back-facing fragment flips the normal).
225
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
226
+ // World-space surface frame + position - the canonical author space (NORMAL_SPACE_PLAN). The
227
+ // lighting intrinsics convert world->view at their own boundary; geometryTangent/worldPosition feed
228
+ // normal maps and camera-correct fresnel. Declared before the node body so any reader resolves.
229
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
230
+ "vec3 geometryTangent = normalize( vWorldTangent );",
231
+ "vec3 worldPosition = vWorldPos;",
232
+ render.fragment.body.join("\n"),
233
+ `vec4 diffuseColor = ${albedo};`,
234
+ alphaDiscardLines(renderMode, alphaThreshold),
235
+ "vec3 outgoingLight = diffuseColor.rgb;",
236
+ "#include <opaque_fragment>",
237
+ "#include <tonemapping_fragment>",
238
+ "#include <colorspace_fragment>",
239
+ "#include <fog_fragment>",
240
+ blendTail(renderMode, render.outputs),
241
+ "}",
242
+ ].join("\n");
243
+ // ShaderMaterial does not auto-merge fog uniforms (unlike the built-in materials), so seed the fog
244
+ // slots here; `bindUniforms` then wires the live clock + artifact slots by reference (live scrub).
245
+ const uniforms = { ...UniformsUtils.clone(UniformsLib.fog) };
246
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
247
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
248
+ const material = new ShaderMaterial({
249
+ name: "FXUnlitArtifactMaterial",
250
+ uniforms,
251
+ vertexShader,
252
+ fragmentShader,
253
+ fog: true,
254
+ ...materialFlags(renderMode),
255
+ depthTest: true,
256
+ blending: NormalBlending,
257
+ side: DoubleSide,
258
+ forceSinglePass: true,
259
+ premultipliedAlpha: true,
260
+ });
261
+ material.customProgramCacheKey = () => `fx-unlit_${cacheKey}`;
262
+ return material;
263
+ }
264
+ // The lighting-node intrinsics the graph calls by name (`fn.raw(... "fxLambertShade" ...)`) but never
265
+ // defines - the frozen `fx_` cross-repo ABI. Each wraps the exact stock Lambert light sequence in a
266
+ // function of (color, WORLD-space normal) -> shaded color: the graph authors normals in world space
267
+ // (NORMAL_SPACE_PLAN), and each intrinsic converts world->view on its first line before Three's
268
+ // view-space light chain runs, so view space stays a private detail behind this ABI. A node emits a
269
+ // plain expression that can sit anywhere in the graph (mid-graph mix included). Placed AFTER the pars chunks (which supply the
270
+ // `ReflectedLight`/`LambertMaterial` types, the `RE_*` macros, `vViewPosition`, the light uniforms) and
271
+ // BEFORE `main`. `#pragma unroll_loop_start` inside a function is expanded by the renderer's shader-wide
272
+ // loop-unroll pass, so the per-light loops resolve here too.
273
+ const FX_LIGHTING_NODE_INTRINSICS = [
274
+ "vec4 fxLambertShade(vec4 diffuseColor, vec3 worldNormal) {",
275
+ " vec3 normal = normalize( mat3( viewMatrix ) * worldNormal );",
276
+ " float specularStrength = 1.0;",
277
+ " ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );",
278
+ " #include <lights_lambert_fragment>",
279
+ " #include <lights_fragment_begin>",
280
+ " #include <lights_fragment_end>",
281
+ " return vec4( reflectedLight.directDiffuse + reflectedLight.indirectDiffuse, diffuseColor.a );",
282
+ "}",
283
+ // Indirect-only diffuse (ambient + light probe / SH + hemisphere), no direct-light loop or shadows -
284
+ // the exact indirect block the old ambient builder inlined, wrapped in a function.
285
+ "vec4 fxAmbientShade(vec4 diffuseColor, vec3 worldNormal) {",
286
+ " vec3 normal = normalize( mat3( viewMatrix ) * worldNormal );",
287
+ " float specularStrength = 1.0;",
288
+ " ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );",
289
+ " #include <lights_lambert_fragment>",
290
+ " vec3 irradiance = getAmbientLightIrradiance( ambientLightColor );",
291
+ " #if defined( USE_LIGHT_PROBES )",
292
+ " irradiance += getLightProbeIrradiance( lightProbe, normal );",
293
+ " #endif",
294
+ " #if ( NUM_HEMI_LIGHTS > 0 )",
295
+ " #pragma unroll_loop_start",
296
+ " for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {",
297
+ " irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], normal );",
298
+ " }",
299
+ " #pragma unroll_loop_end",
300
+ " #endif",
301
+ " RE_IndirectDiffuse( irradiance, vec3( 0.0 ), normal, vec3( 0.0 ), vec3( 0.0 ), material, reflectedLight );",
302
+ " return vec4( reflectedLight.indirectDiffuse, diffuseColor.a );",
303
+ "}",
304
+ ].join("\n");
305
+ // The lit material variant ({@link buildArtifactMaterial} picks it when the capability is non-empty):
306
+ // the graph's `albedo` already carries the shaded color (a lighting node emitted `fxLambertShade(...)` /
307
+ // `fxAmbientShade(...)`), so `main` uses the flat unlit tail (`outgoingLight = diffuseColor.rgb`) - no
308
+ // stock light chain runs in `main`. The material supplies Three's full Lambert light infrastructure
309
+ // (light + fog uniforms, pars chunks, view-space `vNormal`/`vViewPosition` + per-particle shadow
310
+ // plumbing) so the intrinsics resolve. The `geometryNormal` builtin (the surface normal every fragment
311
+ // node may read, and a lighting node's default `normal` argument) is computed at the top of `main`. Shading
312
+ // normal/emission are node inputs, not surface slots.
313
+ function buildLitArtifactMaterial(render, varyings, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
314
+ const options = render.options ?? {};
315
+ const renderMode = options.renderMode ?? "blending";
316
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
317
+ if (!("albedo" in render.outputs)) {
318
+ throw new Error("buildLitArtifactMaterial: render artifact has no 'albedo' output");
319
+ }
320
+ const albedo = render.outputs["albedo"];
321
+ const { attributeDeclarations, varyingDeclarations, vertexAssignments } = declareVaryings(varyings);
322
+ const cacheKey = fxRenderArtifactCacheKey(render);
323
+ const vertexShader = [
324
+ "#define LAMBERT",
325
+ FX_CORE_PARTICLE_DEFINES,
326
+ attributeDeclarations,
327
+ "varying vec2 p_uv;",
328
+ "uniform float u_time;",
329
+ "uniform float u_deltaTime;",
330
+ "uniform vec3 objectVelocity;",
331
+ "uniform vec3 objectAngularVelocity;",
332
+ "varying float p_cameraDistance;",
333
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): the canonical author space for
334
+ // normals. Written in the vertex stage, read as the geometryNormal/geometryTangent/worldPosition
335
+ // builtins in the fragment stage.
336
+ "varying vec3 vWorldNormal;",
337
+ "varying vec3 vWorldTangent;",
338
+ "varying vec3 vWorldPos;",
339
+ "varying vec3 vViewPosition;",
340
+ varyingDeclarations,
341
+ render.vertex.varyingDeclarations.join("\n"),
342
+ render.uniformDeclarations.join("\n"),
343
+ render.vertex.helperFunctions.join("\n"),
344
+ "#include <common>",
345
+ "#include <fog_pars_vertex>",
346
+ "#include <normal_pars_vertex>",
347
+ "#include <shadowmap_pars_vertex>",
348
+ "void main() {",
349
+ "p_uv = uv;",
350
+ vertexAssignments,
351
+ render.vertex.body.join("\n"),
352
+ transformPreamble(render.outputs, true),
353
+ "vec3 objectNormal = fxNormalXform * normal;",
354
+ "#include <defaultnormal_vertex>",
355
+ "#include <normal_vertex>",
356
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): mat3(modelMatrix) is exact for a
357
+ // plane's frame under uniform scale; the tangent is the plane's local +X (the u axis), so billboard
358
+ // roll/orientation is baked in here ONCE for every consumer (normal maps, spherical normal, fresnel).
359
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
360
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
361
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
362
+ "vec3 transformed = position;",
363
+ projectFromModelPos(),
364
+ "vViewPosition = - mvPosition.xyz;",
365
+ "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined( USE_SHADOWMAP ) || defined( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0",
366
+ " vec4 worldPosition = modelMatrix * vec4(particleCenter, 1.0);",
367
+ "#endif",
368
+ "#include <shadowmap_vertex>",
369
+ "#include <fog_vertex>",
370
+ "}",
371
+ ].join("\n");
372
+ const fragmentShader = [
373
+ "#define LAMBERT",
374
+ FX_CORE_PARTICLE_DEFINES,
375
+ "varying vec2 p_uv;",
376
+ "uniform float u_time;",
377
+ "uniform float u_deltaTime;",
378
+ "uniform vec3 objectVelocity;",
379
+ "uniform vec3 objectAngularVelocity;",
380
+ "varying float p_cameraDistance;",
381
+ // World-space surface frame + position (NORMAL_SPACE_PLAN): the canonical author space for
382
+ // normals. Written in the vertex stage, read as the geometryNormal/geometryTangent/worldPosition
383
+ // builtins in the fragment stage.
384
+ "varying vec3 vWorldNormal;",
385
+ "varying vec3 vWorldTangent;",
386
+ "varying vec3 vWorldPos;",
387
+ varyingDeclarations,
388
+ render.fragment.varyingDeclarations.join("\n"),
389
+ render.uniformDeclarations.join("\n"),
390
+ render.fragment.helperFunctions.join("\n"),
391
+ "#include <common>",
392
+ "#include <packing>",
393
+ "#include <alphahash_pars_fragment>",
394
+ "#include <fog_pars_fragment>",
395
+ "#include <bsdfs>",
396
+ "#include <lights_pars_begin>",
397
+ "#include <normal_pars_fragment>",
398
+ "#include <lights_lambert_pars_fragment>",
399
+ "#include <shadowmap_pars_fragment>",
400
+ FX_LIGHTING_NODE_INTRINSICS,
401
+ "void main() {",
402
+ // Front-facing sign (matching `normal_fragment_begin` under DOUBLE_SIDED): a back-facing fragment
403
+ // flips the surface frame below.
404
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
405
+ // World-space surface frame + position - the canonical author space (NORMAL_SPACE_PLAN). The
406
+ // lighting intrinsics convert world->view at their own boundary; geometryTangent/worldPosition feed
407
+ // normal maps and camera-correct fresnel. Declared before the node body so any reader resolves.
408
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
409
+ "vec3 geometryTangent = normalize( vWorldTangent );",
410
+ "vec3 worldPosition = vWorldPos;",
411
+ render.fragment.body.join("\n"),
412
+ `vec4 diffuseColor = ${albedo};`,
413
+ alphaDiscardLines(renderMode, alphaThreshold),
414
+ "vec3 outgoingLight = diffuseColor.rgb;",
415
+ "#include <opaque_fragment>",
416
+ "#include <tonemapping_fragment>",
417
+ "#include <colorspace_fragment>",
418
+ "#include <fog_fragment>",
419
+ blendTail(renderMode, render.outputs),
420
+ "}",
421
+ ].join("\n");
422
+ const uniforms = UniformsUtils.merge([
423
+ UniformsLib.lights,
424
+ UniformsLib.fog,
425
+ ]);
426
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
427
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
428
+ const material = new ShaderMaterial({
429
+ name: "FXLightingNodesArtifactMaterial",
430
+ uniforms,
431
+ vertexShader,
432
+ fragmentShader,
433
+ lights: true,
434
+ fog: true,
435
+ ...materialFlags(renderMode),
436
+ depthTest: true,
437
+ blending: NormalBlending,
438
+ side: DoubleSide,
439
+ forceSinglePass: true,
440
+ premultipliedAlpha: true,
441
+ });
442
+ material.customProgramCacheKey = () => `fx-lit-nodes_${cacheKey}`;
443
+ return material;
444
+ }
445
+ // Single entry point the material driver calls: the render artifact's lighting capability
446
+ // ({@link FXRenderArtifact.lightingIntrinsics}) picks the variant. Empty => unlit (flat albedo, no
447
+ // light infrastructure); non-empty => the lit variant, which defines the intrinsics and lights up
448
+ // Three's infrastructure. There is no per-model dispatch - the shading lives in the graph.
449
+ export function buildArtifactMaterial(render, varyings, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
450
+ const build = render.lightingIntrinsics.length > 0 ? buildLitArtifactMaterial : buildUnlitArtifactMaterial;
451
+ return build(render, varyings, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots);
452
+ }
453
+ // VFX-mesh material path: a single, non-instanced mesh (compiled against the mesh render target).
454
+ // Everything below reuses the particle path's shared, attribute-free helpers (bindUniforms,
455
+ // alphaDiscardLines, blendTail, materialFlags, the lighting intrinsics, the cache key) but drops the
456
+ // three per-particle couplings: FX_CORE_PARTICLE_DEFINES, the `a_`/`p_` instanced-attribute varyings
457
+ // (declareVaryings), and the particle-center vertex epilogue. It never touches the particle builders.
458
+ // Mesh twin of transformPreamble with NO particle center: a mesh is one object, so its vertices
459
+ // project straight from model space. The two optional mat4 outputs are the mesh's object-local
460
+ // transform (identity when unwired); withNormal emits fxNormalXform for the real mesh normal.
461
+ function meshTransformPreamble(outputs, withNormal) {
462
+ const particleTransform = outputOrDefault(outputs, "particleTransform", "mat4(1.0)");
463
+ const vertexTransform = outputOrDefault(outputs, "vertexTransform", "mat4(1.0)");
464
+ const lines = [
465
+ `mat4 fxParticleXform = ${particleTransform};`,
466
+ `mat4 fxVertexXform = ${vertexTransform};`,
467
+ "vec3 fxModelPos = (fxParticleXform * (fxVertexXform * vec4(position, 1.0))).xyz;",
468
+ ];
469
+ if (withNormal) {
470
+ lines.push("mat3 fxNormalXform = mat3(fxParticleXform) * mat3(fxVertexXform);");
471
+ }
472
+ return lines.join("\n");
473
+ }
474
+ // Mesh `#include <project_vertex>` replacement: plain object-space projection. No `p_cameraDistance`
475
+ // (a mesh has no per-particle camera-distance builtin); still feeds alpha-hash `vPosition`.
476
+ function meshProjectFromModelPos() {
477
+ return [
478
+ "vec4 mvPosition = modelViewMatrix * vec4(fxModelPos, 1.0);",
479
+ "gl_Position = projectionMatrix * mvPosition;",
480
+ "#ifdef USE_ALPHAHASH",
481
+ " vPosition = fxModelPos;",
482
+ "#endif",
483
+ ].join("\n");
484
+ }
485
+ // Unlit mesh material: the flat-albedo twin of buildUnlitArtifactMaterial with the particle plumbing
486
+ // removed. The world surface frame (geometryNormal/geometryTangent/worldPosition) is built from the
487
+ // mesh's real normal, so fresnel/rim/normal-maps work with no lighting node present.
488
+ function buildMeshUnlitArtifactMaterial(render, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
489
+ const options = render.options ?? {};
490
+ const renderMode = options.renderMode ?? "blending";
491
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
492
+ if (!("albedo" in render.outputs)) {
493
+ throw new Error("buildMeshUnlitArtifactMaterial: render artifact has no 'albedo' output");
494
+ }
495
+ const albedo = render.outputs["albedo"];
496
+ const cacheKey = fxRenderArtifactCacheKey(render);
497
+ const vertexShader = [
498
+ "varying vec2 p_uv;",
499
+ "uniform float u_time;",
500
+ "uniform float u_deltaTime;",
501
+ "uniform vec3 objectVelocity;",
502
+ "uniform vec3 objectAngularVelocity;",
503
+ "varying vec3 vWorldNormal;",
504
+ "varying vec3 vWorldTangent;",
505
+ "varying vec3 vWorldPos;",
506
+ render.vertex.varyingDeclarations.join("\n"),
507
+ render.uniformDeclarations.join("\n"),
508
+ render.vertex.helperFunctions.join("\n"),
509
+ "#include <common>",
510
+ "#include <fog_pars_vertex>",
511
+ "#include <normal_pars_vertex>",
512
+ "void main() {",
513
+ "p_uv = uv;",
514
+ render.vertex.body.join("\n"),
515
+ meshTransformPreamble(render.outputs, true),
516
+ "vec3 objectNormal = fxNormalXform * normal;",
517
+ "#include <defaultnormal_vertex>",
518
+ "#include <normal_vertex>",
519
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
520
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
521
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
522
+ meshProjectFromModelPos(),
523
+ "#include <fog_vertex>",
524
+ "}",
525
+ ].join("\n");
526
+ const fragmentShader = [
527
+ "varying vec2 p_uv;",
528
+ "uniform float u_time;",
529
+ "uniform float u_deltaTime;",
530
+ "uniform vec3 objectVelocity;",
531
+ "uniform vec3 objectAngularVelocity;",
532
+ "varying vec3 vWorldNormal;",
533
+ "varying vec3 vWorldTangent;",
534
+ "varying vec3 vWorldPos;",
535
+ render.fragment.varyingDeclarations.join("\n"),
536
+ render.uniformDeclarations.join("\n"),
537
+ render.fragment.helperFunctions.join("\n"),
538
+ "#include <common>",
539
+ "#include <alphahash_pars_fragment>",
540
+ "#include <fog_pars_fragment>",
541
+ "#include <normal_pars_fragment>",
542
+ "void main() {",
543
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
544
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
545
+ "vec3 geometryTangent = normalize( vWorldTangent );",
546
+ "vec3 worldPosition = vWorldPos;",
547
+ render.fragment.body.join("\n"),
548
+ `vec4 diffuseColor = ${albedo};`,
549
+ alphaDiscardLines(renderMode, alphaThreshold),
550
+ "vec3 outgoingLight = diffuseColor.rgb;",
551
+ "#include <opaque_fragment>",
552
+ "#include <tonemapping_fragment>",
553
+ "#include <colorspace_fragment>",
554
+ "#include <fog_fragment>",
555
+ blendTail(renderMode, render.outputs),
556
+ "}",
557
+ ].join("\n");
558
+ const uniforms = { ...UniformsUtils.clone(UniformsLib.fog) };
559
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
560
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
561
+ const material = new ShaderMaterial({
562
+ name: "FXMeshUnlitArtifactMaterial",
563
+ uniforms,
564
+ vertexShader,
565
+ fragmentShader,
566
+ fog: true,
567
+ ...materialFlags(renderMode),
568
+ depthTest: true,
569
+ blending: NormalBlending,
570
+ side: DoubleSide,
571
+ forceSinglePass: true,
572
+ premultipliedAlpha: true,
573
+ });
574
+ material.customProgramCacheKey = () => `fx-mesh-unlit_${cacheKey}`;
575
+ return material;
576
+ }
577
+ // Lit mesh material: the lighting-nodes twin of buildLitArtifactMaterial (Three's Lambert light
578
+ // infrastructure + the fx_ shade intrinsics) with the particle plumbing removed. The graph's `albedo`
579
+ // already carries the shaded color, so `main` uses the flat tail.
580
+ function buildMeshLitArtifactMaterial(render, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
581
+ const options = render.options ?? {};
582
+ const renderMode = options.renderMode ?? "blending";
583
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
584
+ if (!("albedo" in render.outputs)) {
585
+ throw new Error("buildMeshLitArtifactMaterial: render artifact has no 'albedo' output");
586
+ }
587
+ const albedo = render.outputs["albedo"];
588
+ const cacheKey = fxRenderArtifactCacheKey(render);
589
+ const vertexShader = [
590
+ "#define LAMBERT",
591
+ "varying vec2 p_uv;",
592
+ "uniform float u_time;",
593
+ "uniform float u_deltaTime;",
594
+ "uniform vec3 objectVelocity;",
595
+ "uniform vec3 objectAngularVelocity;",
596
+ "varying vec3 vWorldNormal;",
597
+ "varying vec3 vWorldTangent;",
598
+ "varying vec3 vWorldPos;",
599
+ "varying vec3 vViewPosition;",
600
+ render.vertex.varyingDeclarations.join("\n"),
601
+ render.uniformDeclarations.join("\n"),
602
+ render.vertex.helperFunctions.join("\n"),
603
+ "#include <common>",
604
+ "#include <fog_pars_vertex>",
605
+ "#include <normal_pars_vertex>",
606
+ "#include <shadowmap_pars_vertex>",
607
+ "void main() {",
608
+ "p_uv = uv;",
609
+ render.vertex.body.join("\n"),
610
+ meshTransformPreamble(render.outputs, true),
611
+ "vec3 objectNormal = fxNormalXform * normal;",
612
+ "#include <defaultnormal_vertex>",
613
+ "#include <normal_vertex>",
614
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
615
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
616
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
617
+ "vec3 transformed = position;",
618
+ meshProjectFromModelPos(),
619
+ "vViewPosition = - mvPosition.xyz;",
620
+ "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined( USE_SHADOWMAP ) || defined( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0",
621
+ " vec4 worldPosition = modelMatrix * vec4(fxModelPos, 1.0);",
622
+ "#endif",
623
+ "#include <shadowmap_vertex>",
624
+ "#include <fog_vertex>",
625
+ "}",
626
+ ].join("\n");
627
+ const fragmentShader = [
628
+ "#define LAMBERT",
629
+ "varying vec2 p_uv;",
630
+ "uniform float u_time;",
631
+ "uniform float u_deltaTime;",
632
+ "uniform vec3 objectVelocity;",
633
+ "uniform vec3 objectAngularVelocity;",
634
+ "varying vec3 vWorldNormal;",
635
+ "varying vec3 vWorldTangent;",
636
+ "varying vec3 vWorldPos;",
637
+ render.fragment.varyingDeclarations.join("\n"),
638
+ render.uniformDeclarations.join("\n"),
639
+ render.fragment.helperFunctions.join("\n"),
640
+ "#include <common>",
641
+ "#include <packing>",
642
+ "#include <alphahash_pars_fragment>",
643
+ "#include <fog_pars_fragment>",
644
+ "#include <bsdfs>",
645
+ "#include <lights_pars_begin>",
646
+ "#include <normal_pars_fragment>",
647
+ "#include <lights_lambert_pars_fragment>",
648
+ "#include <shadowmap_pars_fragment>",
649
+ FX_LIGHTING_NODE_INTRINSICS,
650
+ "void main() {",
651
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
652
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
653
+ "vec3 geometryTangent = normalize( vWorldTangent );",
654
+ "vec3 worldPosition = vWorldPos;",
655
+ render.fragment.body.join("\n"),
656
+ `vec4 diffuseColor = ${albedo};`,
657
+ alphaDiscardLines(renderMode, alphaThreshold),
658
+ "vec3 outgoingLight = diffuseColor.rgb;",
659
+ "#include <opaque_fragment>",
660
+ "#include <tonemapping_fragment>",
661
+ "#include <colorspace_fragment>",
662
+ "#include <fog_fragment>",
663
+ blendTail(renderMode, render.outputs),
664
+ "}",
665
+ ].join("\n");
666
+ const uniforms = UniformsUtils.merge([
667
+ UniformsLib.lights,
668
+ UniformsLib.fog,
669
+ ]);
670
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
671
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
672
+ const material = new ShaderMaterial({
673
+ name: "FXMeshLightingNodesArtifactMaterial",
674
+ uniforms,
675
+ vertexShader,
676
+ fragmentShader,
677
+ lights: true,
678
+ fog: true,
679
+ ...materialFlags(renderMode),
680
+ depthTest: true,
681
+ blending: NormalBlending,
682
+ side: DoubleSide,
683
+ forceSinglePass: true,
684
+ premultipliedAlpha: true,
685
+ });
686
+ material.customProgramCacheKey = () => `fx-mesh-lit-nodes_${cacheKey}`;
687
+ return material;
688
+ }
689
+ // Mesh entry point (the mesh twin of buildArtifactMaterial): the lighting capability picks flat vs lit.
690
+ // No `varyings` argument - a VFX mesh has no per-particle instanced attributes.
691
+ export function buildMeshArtifactMaterial(render, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
692
+ const build = render.lightingIntrinsics.length > 0
693
+ ? buildMeshLitArtifactMaterial
694
+ : buildMeshUnlitArtifactMaterial;
695
+ return build(render, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots);
696
+ }
697
+ // The fragment tail that replaces the color output (opaque_fragment -> tonemapping -> ... ->
698
+ // blendTail) in the depth builders below. `packDepthToRGBA` (from `<packing>`) always packs RGBA:
699
+ // Three's shadow map only ever requests `RGBADepthPacking`, so `BasicDepthPacking` is unsupported.
700
+ const DEPTH_FRAGMENT_TAIL = [
701
+ "float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;",
702
+ "gl_FragColor = packDepthToRGBA( fragCoordZ );",
703
+ ];
704
+ // Depth (shadow-caster) twin of buildArtifactMaterial: the SAME compiled render IR spliced into a
705
+ // customDepthMaterial so a cast shadow's silhouette honors the authored shape/deformation and the
706
+ // exact same alpha cutout (dissolve, spherical-clip, blend-mode trim - all via alphaDiscardLines) as
707
+ // the visible material. A stock MeshDepthMaterial would ignore all of that and, worse, collapse every
708
+ // instanced particle onto one un-instanced vertex. It mirrors buildUnlit/LitArtifactMaterial's whole
709
+ // vertex+fragment scope (every builtin the IR body reads must be declared, since the body is the same
710
+ // IR) and only swaps the color tail for the RGBA depth pack. The lit branch keeps Three's full Lambert
711
+ // light infrastructure so the fx_ shade intrinsics the graph calls in `albedo` still compile - only
712
+ // the discarded RGB depends on shading, and fxLambertShade returns the input alpha unchanged, so the
713
+ // depth cutout stays correct. Fog/logdepth/clipping-planes are dropped, same as the 4 color builders.
714
+ export function buildArtifactDepthMaterial(render, varyings, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
715
+ const options = render.options ?? {};
716
+ const renderMode = options.renderMode ?? "blending";
717
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
718
+ if (!("albedo" in render.outputs)) {
719
+ throw new Error("buildArtifactDepthMaterial: render artifact has no 'albedo' output");
720
+ }
721
+ const albedo = render.outputs["albedo"];
722
+ const lit = render.lightingIntrinsics.length > 0;
723
+ const { attributeDeclarations, varyingDeclarations, vertexAssignments } = declareVaryings(varyings);
724
+ const cacheKey = fxRenderArtifactCacheKey(render);
725
+ const vertexShader = [
726
+ ...(lit ? ["#define LAMBERT"] : []),
727
+ FX_CORE_PARTICLE_DEFINES,
728
+ attributeDeclarations,
729
+ "varying vec2 p_uv;",
730
+ "uniform float u_time;",
731
+ "uniform float u_deltaTime;",
732
+ "uniform vec3 objectVelocity;",
733
+ "uniform vec3 objectAngularVelocity;",
734
+ "varying float p_cameraDistance;",
735
+ "varying vec3 vWorldNormal;",
736
+ "varying vec3 vWorldTangent;",
737
+ "varying vec3 vWorldPos;",
738
+ ...(lit ? ["varying vec3 vViewPosition;"] : []),
739
+ // High-precision NDC z/w carried to the fragment for the depth pack (mirrors stock depth.glsl.js).
740
+ "varying vec2 vHighPrecisionZW;",
741
+ varyingDeclarations,
742
+ render.vertex.varyingDeclarations.join("\n"),
743
+ render.uniformDeclarations.join("\n"),
744
+ render.vertex.helperFunctions.join("\n"),
745
+ "#include <common>",
746
+ "#include <normal_pars_vertex>",
747
+ ...(lit ? ["#include <shadowmap_pars_vertex>"] : []),
748
+ "void main() {",
749
+ "p_uv = uv;",
750
+ vertexAssignments,
751
+ render.vertex.body.join("\n"),
752
+ transformPreamble(render.outputs, true),
753
+ "vec3 objectNormal = fxNormalXform * normal;",
754
+ "#include <defaultnormal_vertex>",
755
+ "#include <normal_vertex>",
756
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
757
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
758
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
759
+ ...(lit ? ["vec3 transformed = position;"] : []),
760
+ projectFromModelPos(),
761
+ ...(lit
762
+ ? [
763
+ "vViewPosition = - mvPosition.xyz;",
764
+ "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined( USE_SHADOWMAP ) || defined( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0",
765
+ " vec4 worldPosition = modelMatrix * vec4(particleCenter, 1.0);",
766
+ "#endif",
767
+ "#include <shadowmap_vertex>",
768
+ ]
769
+ : []),
770
+ "vHighPrecisionZW = gl_Position.zw;",
771
+ "}",
772
+ ].join("\n");
773
+ const fragmentShader = [
774
+ ...(lit ? ["#define LAMBERT"] : []),
775
+ FX_CORE_PARTICLE_DEFINES,
776
+ "varying vec2 p_uv;",
777
+ "uniform float u_time;",
778
+ "uniform float u_deltaTime;",
779
+ "uniform vec3 objectVelocity;",
780
+ "uniform vec3 objectAngularVelocity;",
781
+ "varying float p_cameraDistance;",
782
+ "varying vec3 vWorldNormal;",
783
+ "varying vec3 vWorldTangent;",
784
+ "varying vec3 vWorldPos;",
785
+ "varying vec2 vHighPrecisionZW;",
786
+ varyingDeclarations,
787
+ render.fragment.varyingDeclarations.join("\n"),
788
+ render.uniformDeclarations.join("\n"),
789
+ render.fragment.helperFunctions.join("\n"),
790
+ "#include <common>",
791
+ "#include <packing>",
792
+ "#include <alphahash_pars_fragment>",
793
+ // Fragment include ORDER mirrors buildLit/UnlitArtifactMaterial exactly (minus fog): the only hard
794
+ // dependency is lights_lambert_pars_fragment needing bsdfs + lights_pars_begin declared before it.
795
+ ...(lit
796
+ ? [
797
+ "#include <bsdfs>",
798
+ "#include <lights_pars_begin>",
799
+ "#include <normal_pars_fragment>",
800
+ "#include <lights_lambert_pars_fragment>",
801
+ "#include <shadowmap_pars_fragment>",
802
+ FX_LIGHTING_NODE_INTRINSICS,
803
+ ]
804
+ : ["#include <normal_pars_fragment>"]),
805
+ "void main() {",
806
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
807
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
808
+ "vec3 geometryTangent = normalize( vWorldTangent );",
809
+ "vec3 worldPosition = vWorldPos;",
810
+ render.fragment.body.join("\n"),
811
+ `vec4 diffuseColor = ${albedo};`,
812
+ alphaDiscardLines(renderMode, alphaThreshold),
813
+ ...DEPTH_FRAGMENT_TAIL,
814
+ "}",
815
+ ].join("\n");
816
+ // A pure depth target: blending/transparency are meaningless, so force the depth flags regardless
817
+ // of the color material's render mode (never `materialFlags`). DoubleSide so a two-sided billboard
818
+ // or open mesh casts from both faces.
819
+ const uniforms = lit ? UniformsUtils.merge([UniformsLib.lights]) : {};
820
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
821
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
822
+ const material = new ShaderMaterial({
823
+ name: "FXArtifactDepthMaterial",
824
+ uniforms,
825
+ vertexShader,
826
+ fragmentShader,
827
+ ...(lit ? { lights: true } : {}),
828
+ depthTest: true,
829
+ depthWrite: true,
830
+ side: DoubleSide,
831
+ });
832
+ material.customProgramCacheKey = () => `fx-depth_${cacheKey}`;
833
+ return material;
834
+ }
835
+ // Mesh twin of buildArtifactDepthMaterial (the mesh-path depth builder): mirrors buildMeshUnlit/
836
+ // LitArtifactMaterial's scope with the particle plumbing removed, swapping the color tail for the
837
+ // depth pack. Same lit/unlit internal branch. No `varyings` - a VFX mesh has no instanced attributes.
838
+ export function buildMeshArtifactDepthMaterial(render, timeUniform, deltaUniform, velocityUniform, angularVelocityUniform, externalSlots = {}) {
839
+ const options = render.options ?? {};
840
+ const renderMode = options.renderMode ?? "blending";
841
+ const alphaThreshold = outputOrDefault(render.outputs, "alphaThreshold", "0.0075");
842
+ if (!("albedo" in render.outputs)) {
843
+ throw new Error("buildMeshArtifactDepthMaterial: render artifact has no 'albedo' output");
844
+ }
845
+ const albedo = render.outputs["albedo"];
846
+ const lit = render.lightingIntrinsics.length > 0;
847
+ const cacheKey = fxRenderArtifactCacheKey(render);
848
+ const vertexShader = [
849
+ ...(lit ? ["#define LAMBERT"] : []),
850
+ "varying vec2 p_uv;",
851
+ "uniform float u_time;",
852
+ "uniform float u_deltaTime;",
853
+ "uniform vec3 objectVelocity;",
854
+ "uniform vec3 objectAngularVelocity;",
855
+ "varying vec3 vWorldNormal;",
856
+ "varying vec3 vWorldTangent;",
857
+ "varying vec3 vWorldPos;",
858
+ ...(lit ? ["varying vec3 vViewPosition;"] : []),
859
+ "varying vec2 vHighPrecisionZW;",
860
+ render.vertex.varyingDeclarations.join("\n"),
861
+ render.uniformDeclarations.join("\n"),
862
+ render.vertex.helperFunctions.join("\n"),
863
+ "#include <common>",
864
+ "#include <normal_pars_vertex>",
865
+ ...(lit ? ["#include <shadowmap_pars_vertex>"] : []),
866
+ "void main() {",
867
+ "p_uv = uv;",
868
+ render.vertex.body.join("\n"),
869
+ meshTransformPreamble(render.outputs, true),
870
+ "vec3 objectNormal = fxNormalXform * normal;",
871
+ "#include <defaultnormal_vertex>",
872
+ "#include <normal_vertex>",
873
+ "vWorldNormal = normalize( mat3( modelMatrix ) * objectNormal );",
874
+ "vWorldTangent = normalize( mat3( modelMatrix ) * ( fxNormalXform * vec3( 1.0, 0.0, 0.0 ) ) );",
875
+ "vWorldPos = ( modelMatrix * vec4( fxModelPos, 1.0 ) ).xyz;",
876
+ ...(lit ? ["vec3 transformed = position;"] : []),
877
+ meshProjectFromModelPos(),
878
+ ...(lit
879
+ ? [
880
+ "vViewPosition = - mvPosition.xyz;",
881
+ "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined( USE_SHADOWMAP ) || defined( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0",
882
+ " vec4 worldPosition = modelMatrix * vec4(fxModelPos, 1.0);",
883
+ "#endif",
884
+ "#include <shadowmap_vertex>",
885
+ ]
886
+ : []),
887
+ "vHighPrecisionZW = gl_Position.zw;",
888
+ "}",
889
+ ].join("\n");
890
+ const fragmentShader = [
891
+ ...(lit ? ["#define LAMBERT"] : []),
892
+ "varying vec2 p_uv;",
893
+ "uniform float u_time;",
894
+ "uniform float u_deltaTime;",
895
+ "uniform vec3 objectVelocity;",
896
+ "uniform vec3 objectAngularVelocity;",
897
+ "varying vec3 vWorldNormal;",
898
+ "varying vec3 vWorldTangent;",
899
+ "varying vec3 vWorldPos;",
900
+ "varying vec2 vHighPrecisionZW;",
901
+ render.fragment.varyingDeclarations.join("\n"),
902
+ render.uniformDeclarations.join("\n"),
903
+ render.fragment.helperFunctions.join("\n"),
904
+ "#include <common>",
905
+ "#include <packing>",
906
+ "#include <alphahash_pars_fragment>",
907
+ // Fragment include ORDER mirrors buildMeshLit/UnlitArtifactMaterial exactly (minus fog).
908
+ ...(lit
909
+ ? [
910
+ "#include <bsdfs>",
911
+ "#include <lights_pars_begin>",
912
+ "#include <normal_pars_fragment>",
913
+ "#include <lights_lambert_pars_fragment>",
914
+ "#include <shadowmap_pars_fragment>",
915
+ FX_LIGHTING_NODE_INTRINSICS,
916
+ ]
917
+ : ["#include <normal_pars_fragment>"]),
918
+ "void main() {",
919
+ "float fxFaceDirection = gl_FrontFacing ? 1.0 : - 1.0;",
920
+ "vec3 geometryNormal = normalize( vWorldNormal ) * fxFaceDirection;",
921
+ "vec3 geometryTangent = normalize( vWorldTangent );",
922
+ "vec3 worldPosition = vWorldPos;",
923
+ render.fragment.body.join("\n"),
924
+ `vec4 diffuseColor = ${albedo};`,
925
+ alphaDiscardLines(renderMode, alphaThreshold),
926
+ ...DEPTH_FRAGMENT_TAIL,
927
+ "}",
928
+ ].join("\n");
929
+ const uniforms = lit ? UniformsUtils.merge([UniformsLib.lights]) : {};
930
+ bindUniforms({ uniforms }, render, timeUniform, deltaUniform, externalSlots);
931
+ bindObjectMotionUniforms({ uniforms }, velocityUniform, angularVelocityUniform);
932
+ const material = new ShaderMaterial({
933
+ name: "FXMeshArtifactDepthMaterial",
934
+ uniforms,
935
+ vertexShader,
936
+ fragmentShader,
937
+ ...(lit ? { lights: true } : {}),
938
+ depthTest: true,
939
+ depthWrite: true,
940
+ side: DoubleSide,
941
+ });
942
+ material.customProgramCacheKey = () => `fx-mesh-depth_${cacheKey}`;
943
+ return material;
944
+ }