sparcoon 0.4.1 → 0.4.3
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.
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# sparcoon
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/sparcoon)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Instanced-mesh particle system for Three.js. WebGL 1 compatible. **[EDITOR](https://jango-git.github.io/sparcoon/)**
|
|
7
|
+
|
|
8
|
+
## API
|
|
9
|
+
|
|
10
|
+
- **FXEmitter** extends `Object3D`. Manages emission, lifetime, pooling, rendering. One draw call per emitter. Optional back-to-front sorting.
|
|
11
|
+
- **Spawn modules** set initial particle state: position (box/sphere/point), velocity (cone), rotation, scale, lifetime. Numeric params accept ranges.
|
|
12
|
+
- **Behavior modules** update particles per frame: directional/point gravity, velocity/torque damping, simplex noise, scale-over-life.
|
|
13
|
+
- **Materials**: `FXUnlitMaterial` (MeshBasicMaterial), `FXDiffuseMaterial` (MeshLambertMaterial + optional subsurface scatter). Shadow support.
|
|
14
|
+
- **Nodes** are composable material inputs: color-over-life, static/animated textures, normals (flat/spherical/map).
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
Smoke volume with animated sprite sheet, subsurface scattering, and shadow receiving:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const RATE = 10;
|
|
22
|
+
const LIFETIME = 6;
|
|
23
|
+
|
|
24
|
+
const smoke = new FXEmitter(
|
|
25
|
+
[
|
|
26
|
+
new FXSpawnBox(areaMin, areaMax),
|
|
27
|
+
new FXSpawnRandomLifetime({ min: LIFETIME * 0.5, max: LIFETIME }),
|
|
28
|
+
new FXSpawnRandomScale({ min: 8, max: 16 }),
|
|
29
|
+
new FXSpawnRandomRotation(),
|
|
30
|
+
],
|
|
31
|
+
[],
|
|
32
|
+
new FXDiffuseMaterial({
|
|
33
|
+
enableScatter: true,
|
|
34
|
+
scatterPower: 10,
|
|
35
|
+
forwardScatterStrength: 1,
|
|
36
|
+
backScatterStrength: 0.25,
|
|
37
|
+
shadowSensitivity: 0.25,
|
|
38
|
+
premultipliedAlpha: true,
|
|
39
|
+
albedoNodes: [
|
|
40
|
+
new FXAnimatedTextureNode({
|
|
41
|
+
texture: smokeAtlas,
|
|
42
|
+
rows: 8,
|
|
43
|
+
columns: 8,
|
|
44
|
+
interpolate: true,
|
|
45
|
+
}),
|
|
46
|
+
new FXColorOverLifeNode([
|
|
47
|
+
new FXColor(0xffffff, 0),
|
|
48
|
+
new FXColor(0xffffff, 0.4),
|
|
49
|
+
new FXColor(0xffffff, 0.4),
|
|
50
|
+
new FXColor(0xffffff, 0),
|
|
51
|
+
]),
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
{
|
|
55
|
+
expectedCapacity: Math.ceil(LIFETIME * RATE) + 1,
|
|
56
|
+
receiveShadow: true,
|
|
57
|
+
sortCamera: camera,
|
|
58
|
+
sortFraction: 0.25,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
scene.add(smoke);
|
|
63
|
+
smoke.play(RATE);
|
|
64
|
+
|
|
65
|
+
// render loop
|
|
66
|
+
FXEmitter.onWillRender(delta);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
npm install sparcoon
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Peer dep: `three` >=0.157.0 <0.180.0
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -17,7 +17,7 @@ export declare class FXEmitter extends Object3D {
|
|
|
17
17
|
private readonly spawnSequence;
|
|
18
18
|
private readonly behaviorSequence;
|
|
19
19
|
automaticallyDestroyModules: boolean;
|
|
20
|
-
sortCamera
|
|
20
|
+
sortCamera?: Camera;
|
|
21
21
|
sortFraction: number;
|
|
22
22
|
private readonly mesh;
|
|
23
23
|
private readonly material;
|
|
@@ -40,6 +40,7 @@ function wrapShadowCalls(source, uniformName) {
|
|
|
40
40
|
function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, premultipliedAlpha, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity) {
|
|
41
41
|
const useScatter = scatterUniforms !== undefined;
|
|
42
42
|
const useShadowSensitivity = shadowSensitivity !== undefined;
|
|
43
|
+
const useNormals = normalNodes.length > 0;
|
|
43
44
|
const attributeDeclarations = [];
|
|
44
45
|
const varyingDeclarations = [];
|
|
45
46
|
const vertexAssignments = [];
|
|
@@ -67,6 +68,7 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
67
68
|
PARTICLE_DEFINES,
|
|
68
69
|
attributeDeclarations.join("\n"),
|
|
69
70
|
"varying vec2 p_uv;",
|
|
71
|
+
useNormals ? "varying vec2 p_billboardSinCos;" : "",
|
|
70
72
|
useScatter ? "varying vec3 v_viewPosition;" : "",
|
|
71
73
|
varyingDeclarations.join("\n"),
|
|
72
74
|
].join("\n");
|
|
@@ -96,6 +98,9 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
96
98
|
" billboardOffset.x * sinRotation + billboardOffset.y * cosRotation",
|
|
97
99
|
");",
|
|
98
100
|
"",
|
|
101
|
+
// Pass the exact sin/cos that rotated the billboard to the fragment shader.
|
|
102
|
+
useNormals ? "p_billboardSinCos = vec2(sinRotation, cosRotation);" : "",
|
|
103
|
+
"",
|
|
99
104
|
"vec3 particleCenter = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);",
|
|
100
105
|
"vec4 mvPosition = modelViewMatrix * vec4(particleCenter, 1.0);",
|
|
101
106
|
"mvPosition.xy += billboardOffset;",
|
|
@@ -153,6 +158,7 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
153
158
|
const fragmentPreamble = [
|
|
154
159
|
PARTICLE_DEFINES,
|
|
155
160
|
"varying vec2 p_uv;",
|
|
161
|
+
useNormals ? "varying vec2 p_billboardSinCos;" : "",
|
|
156
162
|
varyingDeclarations.join("\n"),
|
|
157
163
|
albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
158
164
|
uniqueHelpers(albedoNodes),
|
|
@@ -183,7 +189,7 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
183
189
|
Object.assign(shader.uniforms, node.uniforms);
|
|
184
190
|
}
|
|
185
191
|
let fragmentShader = shader.fragmentShader;
|
|
186
|
-
if (
|
|
192
|
+
if (useNormals) {
|
|
187
193
|
const normalExpressions = normalNodes.map((node) => {
|
|
188
194
|
if (node instanceof FXTextureNode) {
|
|
189
195
|
return `normalize(${node.colorExpression}.rgb * 2.0 - 1.0)`;
|
|
@@ -197,6 +203,17 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
197
203
|
"float faceDirection = gl_FrontFacing ? 1.0 : -1.0;",
|
|
198
204
|
`vec3 normal = ${normalExpressions[0]};`,
|
|
199
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
|
+
"",
|
|
200
217
|
"normal *= faceDirection;",
|
|
201
218
|
].join("\n");
|
|
202
219
|
fragmentShader = fragmentShader.replace("#include <normal_fragment_begin>", normalCode);
|
|
@@ -211,8 +228,6 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
211
228
|
fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", `totalEmissiveRadiance = (${combinedExpression}).rgb;`);
|
|
212
229
|
}
|
|
213
230
|
if (useScatter) {
|
|
214
|
-
// Per-fragment Mie scatter for all light types.
|
|
215
|
-
// viewDirection and all light directions/positions are in view space.
|
|
216
231
|
fragmentShader = fragmentShader.replace("#include <opaque_fragment>", `
|
|
217
232
|
#ifdef OPAQUE
|
|
218
233
|
diffuseColor.a = 1.0;
|
|
@@ -247,13 +262,6 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
247
262
|
}
|
|
248
263
|
#endif
|
|
249
264
|
|
|
250
|
-
// calculateScatter() includes a "* alpha" density factor. Compositing
|
|
251
|
-
// will apply another "* alpha" (via blend equation in straight mode,
|
|
252
|
-
// or via premultiplied_alpha_fragment + One/OneMinusSrcAlpha blend).
|
|
253
|
-
// Without correction, scatter would get alpha² while diffuse lighting
|
|
254
|
-
// only gets alpha¹. Dividing diffuseColor.rgb by alpha cancels the
|
|
255
|
-
// extra factor, so scatter composites at the same alpha power as
|
|
256
|
-
// the rest of outgoingLight.
|
|
257
265
|
vec3 albedo = diffuseColor.a > 0.001
|
|
258
266
|
? diffuseColor.rgb / diffuseColor.a
|
|
259
267
|
: vec3(0.0);
|