@cyberluke/three-particles 4.0.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.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/index.d.ts +2970 -0
- package/dist/index.js +3138 -0
- package/dist/index.js.map +1 -0
- package/dist/three-particles.min.js +1 -0
- package/dist/three-particles.min.js.map +1 -0
- package/dist/webgpu.js +2522 -0
- package/dist/webgpu.js.map +1 -0
- package/llms-full.txt +1005 -0
- package/llms.txt +339 -0
- package/package.json +111 -0
- package/webgpu.d.ts +98 -0
package/llms-full.txt
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
# @cyberluke/three-particles ??? Full API Reference
|
|
2
|
+
|
|
3
|
+
> Three.js-based high-performance particle system library
|
|
4
|
+
> License: MIT | Author: CyberLuke
|
|
5
|
+
> Repository: https://github.com/cyberluke/three-particles
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @cyberluke/three-particles three
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer dependency: `three` ^0.182.0
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Main API
|
|
20
|
+
|
|
21
|
+
### createParticleSystem(config: ParticleSystemConfig): ParticleSystem
|
|
22
|
+
|
|
23
|
+
Creates a new particle system and registers it for global updates.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createParticleSystem } from "@cyberluke/three-particles";
|
|
27
|
+
|
|
28
|
+
const system = createParticleSystem({
|
|
29
|
+
duration: 5,
|
|
30
|
+
looping: true,
|
|
31
|
+
maxParticles: 200,
|
|
32
|
+
startLifetime: 3,
|
|
33
|
+
startSpeed: 2,
|
|
34
|
+
startSize: 0.5,
|
|
35
|
+
emission: { rateOverTime: 30 },
|
|
36
|
+
shape: { shape: Shape.CONE, cone: { angle: 0.5, radius: 1 } },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
scene.add(system.instance);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### updateParticleSystems(cycleData: CycleData): void
|
|
43
|
+
|
|
44
|
+
Updates all registered particle systems. Call this in your animation loop.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { updateParticleSystems } from "@cyberluke/three-particles";
|
|
48
|
+
|
|
49
|
+
function animate() {
|
|
50
|
+
updateParticleSystems({
|
|
51
|
+
now: performance.now(),
|
|
52
|
+
delta: clock.getDelta(),
|
|
53
|
+
elapsed: clock.getElapsedTime(),
|
|
54
|
+
});
|
|
55
|
+
requestAnimationFrame(animate);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ParticleSystem (returned object)
|
|
62
|
+
|
|
63
|
+
| Property/Method | Type | Description |
|
|
64
|
+
|-----------------|------|-------------|
|
|
65
|
+
| `instance` | `THREE.Points \| THREE.Mesh` | The renderable object ??? add to scene (Mesh when using RendererType.INSTANCED or RendererType.MESH) |
|
|
66
|
+
| `pauseEmitter()` | `() => void` | Stop emitting new particles |
|
|
67
|
+
| `resumeEmitter()` | `() => void` | Resume emitting particles |
|
|
68
|
+
| `dispose()` | `() => void` | Destroy system, free resources |
|
|
69
|
+
| `update(cycleData)` | `(CycleData) => void` | Update this system individually |
|
|
70
|
+
| `updateConfig(config)` | `(Partial<ParticleSystemConfig>) => void` | Update configuration at runtime without recreating the system. System-level properties (gravity, force fields, noise, emission, color/size/opacity/rotation/velocity over lifetime) take effect immediately on the CPU backend. Per-particle spawn properties (startColor, startSize, etc.) affect only newly emitted particles. GPU backend limitation: modifier flags/curves are baked at creation and cannot change live. |
|
|
71
|
+
| `getActiveParticleCount()` | `() => number` | Number of currently alive particles. O(1) ??? derived from the internal free list. |
|
|
72
|
+
| `computeNode` | `unknown \| null` | GPU compute node for WebGPU dispatch. Non-null when GPU compute is active. Must be dispatched via `renderer.compute(system.computeNode)` every frame before `renderer.render()`. Null when CPU simulation is used. |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## CycleData
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
type CycleData = {
|
|
80
|
+
now: number; // Current timestamp in milliseconds (performance.now())
|
|
81
|
+
delta: number; // Time since last frame in seconds
|
|
82
|
+
elapsed: number; // Total elapsed time in seconds
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ParticleSystemConfig ??? Complete Reference
|
|
89
|
+
|
|
90
|
+
### General Properties
|
|
91
|
+
|
|
92
|
+
| Property | Type | Default | Description |
|
|
93
|
+
|----------|------|---------|-------------|
|
|
94
|
+
| `transform` | `Transform` | origin | Position, rotation, scale of emitter |
|
|
95
|
+
| `duration` | `number` | 5.0 | System duration in seconds |
|
|
96
|
+
| `looping` | `boolean` | true | Loop after duration ends |
|
|
97
|
+
| `startDelay` | `number \| {min,max}` | 0.0 | Delay before first emission |
|
|
98
|
+
| `maxParticles` | `number` | 100 | Maximum concurrent particles |
|
|
99
|
+
| `gravity` | `number` | 0.0 | Downward acceleration |
|
|
100
|
+
| `simulationSpace` | `SimulationSpace` | LOCAL | LOCAL or WORLD coordinate space |
|
|
101
|
+
| `simulationBackend` | `SimulationBackend` | AUTO | Simulation backend: AUTO (GPU if WebGPU available, else CPU), CPU, or GPU |
|
|
102
|
+
|
|
103
|
+
### Start Properties
|
|
104
|
+
|
|
105
|
+
All start properties support three value types:
|
|
106
|
+
- **Constant**: `number` ??? fixed value
|
|
107
|
+
- **Random range**: `{ min: number, max: number }` ??? random between min and max
|
|
108
|
+
- **Curve**: `BezierCurve | EasingCurve` ??? value based on system lifetime
|
|
109
|
+
|
|
110
|
+
| Property | Default | Description |
|
|
111
|
+
|----------|---------|-------------|
|
|
112
|
+
| `startLifetime` | 5.0 | Particle lifetime in seconds |
|
|
113
|
+
| `startSpeed` | 1.0 | Initial particle speed |
|
|
114
|
+
| `startSize` | 1.0 | Initial particle size |
|
|
115
|
+
| `startOpacity` | 1.0 | Initial particle opacity (0-1) |
|
|
116
|
+
| `startRotation` | 0.0 | Initial rotation in degrees |
|
|
117
|
+
| `startColor` | white | `MinMaxColor` ??? color range for randomization |
|
|
118
|
+
|
|
119
|
+
### startColor ??? MinMaxColor
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
type MinMaxColor = {
|
|
123
|
+
min?: Rgb; // { r?: number, g?: number, b?: number } ??? values 0.0 to 1.0
|
|
124
|
+
max?: Rgb;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Example: random orange-to-yellow
|
|
128
|
+
startColor: {
|
|
129
|
+
min: { r: 1.0, g: 0.3, b: 0.0 },
|
|
130
|
+
max: { r: 1.0, g: 1.0, b: 0.0 },
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Transform
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
type Transform = {
|
|
140
|
+
position?: THREE.Vector3;
|
|
141
|
+
rotation?: THREE.Vector3; // Radians per axis
|
|
142
|
+
scale?: THREE.Vector3;
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Emission
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
type Emission = {
|
|
152
|
+
rateOverTime?: number | { min, max } | LifetimeCurve; // Particles per second (default: 10)
|
|
153
|
+
rateOverDistance?: number | { min, max } | LifetimeCurve; // Particles per unit moved (default: 0)
|
|
154
|
+
bursts?: Burst[]; // Instantaneous emissions
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Burst
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
type Burst = {
|
|
162
|
+
time: number; // Trigger time in seconds
|
|
163
|
+
count: number | { min: number, max: number }; // Particle count
|
|
164
|
+
cycles?: number; // Repeat count (default: 1)
|
|
165
|
+
interval?: number; // Seconds between cycles
|
|
166
|
+
probability?: number; // 0-1 chance of firing (default: 1)
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Example ??? explosion with aftershocks:**
|
|
171
|
+
```typescript
|
|
172
|
+
emission: {
|
|
173
|
+
rateOverTime: 0,
|
|
174
|
+
bursts: [
|
|
175
|
+
{ time: 0, count: 100 }, // Main explosion
|
|
176
|
+
{ time: 0.3, count: { min: 20, max: 40 } }, // First aftershock
|
|
177
|
+
{ time: 0.5, count: 15, cycles: 4, interval: 0.1 }, // Debris pulses
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Shape Configuration
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
type ShapeConfig = {
|
|
188
|
+
shape?: Shape; // SPHERE | CONE | CIRCLE | RECTANGLE | BOX
|
|
189
|
+
sphere?: Sphere;
|
|
190
|
+
cone?: Cone;
|
|
191
|
+
circle?: Circle;
|
|
192
|
+
rectangle?: Rectangle;
|
|
193
|
+
box?: Box;
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Shape.SPHERE
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
type Sphere = {
|
|
201
|
+
radius?: number; // Default: 1
|
|
202
|
+
radiusThickness?: number; // 0-1, where 1 = solid sphere (default: 1)
|
|
203
|
+
arc?: number; // Radians, partial sphere (default: 2??)
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Shape.CONE
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
type Cone = {
|
|
211
|
+
angle?: number; // Cone angle in radians (default: ~0.44)
|
|
212
|
+
radius?: number; // Base radius (default: 1)
|
|
213
|
+
radiusThickness?: number; // 0-1 (default: 1)
|
|
214
|
+
arc?: number; // Radians (default: 2??)
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Shape.CIRCLE
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
type Circle = {
|
|
222
|
+
radius?: number; // Default: 1
|
|
223
|
+
radiusThickness?: number; // 0-1 (default: 1)
|
|
224
|
+
arc?: number; // Radians (default: 2??)
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Shape.RECTANGLE
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
type Rectangle = {
|
|
232
|
+
rotation?: Point3D; // { x?, y?, z? } in radians
|
|
233
|
+
scale?: Point3D; // { x?, y?, z? } dimensions
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Shape.BOX
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
type Box = {
|
|
241
|
+
scale?: Point3D; // { x?, y?, z? } dimensions
|
|
242
|
+
emitFrom?: EmitFrom; // VOLUME | SHELL | EDGE
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Lifetime Modifiers
|
|
249
|
+
|
|
250
|
+
### velocityOverLifetime
|
|
251
|
+
|
|
252
|
+
Modifies particle velocity over its lifetime. Supports linear (directional) and orbital (rotational) velocity.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
type VelocityOverLifetime = {
|
|
256
|
+
isActive: boolean;
|
|
257
|
+
linear: {
|
|
258
|
+
x?: number | { min, max } | LifetimeCurve;
|
|
259
|
+
y?: number | { min, max } | LifetimeCurve;
|
|
260
|
+
z?: number | { min, max } | LifetimeCurve;
|
|
261
|
+
};
|
|
262
|
+
orbital: { // Values in degrees
|
|
263
|
+
x?: number | { min, max } | LifetimeCurve;
|
|
264
|
+
y?: number | { min, max } | LifetimeCurve;
|
|
265
|
+
z?: number | { min, max } | LifetimeCurve;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### sizeOverLifetime
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
sizeOverLifetime: {
|
|
274
|
+
isActive: boolean;
|
|
275
|
+
lifetimeCurve: LifetimeCurve; // Multiplier on startSize
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### opacityOverLifetime
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
opacityOverLifetime: {
|
|
283
|
+
isActive: boolean;
|
|
284
|
+
lifetimeCurve: LifetimeCurve; // Multiplier on startOpacity
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### colorOverLifetime
|
|
289
|
+
|
|
290
|
+
Each RGB channel is a separate curve acting as a multiplier (0-1) on startColor.
|
|
291
|
+
**Important**: Set startColor to white `{ r:1, g:1, b:1 }` for full color transitions.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
colorOverLifetime: {
|
|
295
|
+
isActive: boolean;
|
|
296
|
+
r: LifetimeCurve;
|
|
297
|
+
g: LifetimeCurve;
|
|
298
|
+
b: LifetimeCurve;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### rotationOverLifetime
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
rotationOverLifetime: {
|
|
306
|
+
isActive: boolean;
|
|
307
|
+
min?: number; // Minimum rotation speed
|
|
308
|
+
max?: number; // Maximum rotation speed
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Noise
|
|
315
|
+
|
|
316
|
+
FBM (Fractal Brownian Motion) noise affecting particles dynamically.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
type NoiseConfig = {
|
|
320
|
+
isActive: boolean;
|
|
321
|
+
useRandomOffset: boolean; // Randomize noise per particle
|
|
322
|
+
strength: number; // Overall noise strength (default: 1)
|
|
323
|
+
frequency: number; // Noise frequency (default: 0.5)
|
|
324
|
+
octaves: number; // FBM octaves (default: 1)
|
|
325
|
+
positionAmount: number; // Position displacement (default: 1)
|
|
326
|
+
rotationAmount: number; // Rotation displacement (default: 0)
|
|
327
|
+
sizeAmount: number; // Size displacement (default: 0)
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Renderer
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
type Renderer = {
|
|
337
|
+
blending: THREE.Blending; // Default: NormalBlending
|
|
338
|
+
discardBackgroundColor: boolean; // Default: false
|
|
339
|
+
backgroundColorTolerance: number; // Default: 1.0
|
|
340
|
+
backgroundColor: Rgb; // Default: { r:0, g:0, b:0 }
|
|
341
|
+
transparent: boolean; // Default: true
|
|
342
|
+
depthTest: boolean; // Default: true
|
|
343
|
+
depthWrite: boolean; // Default: false
|
|
344
|
+
rendererType?: RendererType; // Default: RendererType.POINTS
|
|
345
|
+
trail?: TrailConfig; // Only for RendererType.TRAIL
|
|
346
|
+
mesh?: MeshConfig; // Only for RendererType.MESH
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
enum RendererType {
|
|
350
|
+
POINTS = 'POINTS', // Classic point sprites (THREE.Points). Default.
|
|
351
|
+
INSTANCED = 'INSTANCED', // Camera-facing quads via InstancedBufferGeometry.
|
|
352
|
+
// Removes gl_PointSize hardware limit, supports large particles.
|
|
353
|
+
// Recommended for 10 000+ particles or large on-screen sizes.
|
|
354
|
+
TRAIL = 'TRAIL', // Ribbon trails behind particles. Each particle records a position
|
|
355
|
+
// history and the renderer builds a camera-facing triangle-strip
|
|
356
|
+
// ribbon through those samples.
|
|
357
|
+
MESH = 'MESH', // 3D mesh particles via GPU instancing. Each particle is rendered
|
|
358
|
+
// as a full 3D mesh with quaternion-based rotation, normals, and
|
|
359
|
+
// directional lighting. Any THREE.BufferGeometry can be used.
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
type MeshConfig = {
|
|
363
|
+
geometry: THREE.BufferGeometry; // The geometry to render for each particle
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Common blending modes: `THREE.NormalBlending`, `THREE.AdditiveBlending`, `THREE.SubtractiveBlending`
|
|
368
|
+
|
|
369
|
+
### Mesh Particle Configuration
|
|
370
|
+
|
|
371
|
+
When using `RendererType.MESH`, configure mesh-specific properties via `renderer.mesh`:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
renderer: {
|
|
375
|
+
rendererType: RendererType.MESH,
|
|
376
|
+
blending: THREE.NormalBlending,
|
|
377
|
+
transparent: true,
|
|
378
|
+
depthTest: true,
|
|
379
|
+
depthWrite: true,
|
|
380
|
+
mesh: {
|
|
381
|
+
geometry: new THREE.BoxGeometry(1, 1, 1),
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Mesh particle features:
|
|
387
|
+
- GPU instancing (`InstancedBufferGeometry`) ??? one draw call for all particles
|
|
388
|
+
- Quaternion-based 3D rotation (particles rotate in all 3 axes)
|
|
389
|
+
- Normals preserved from the source geometry, simple directional lighting from camera
|
|
390
|
+
- Any `THREE.BufferGeometry` works: `BoxGeometry`, `SphereGeometry`, `IcosahedronGeometry`, custom meshes, etc.
|
|
391
|
+
- All modifiers work: sizeOverLifetime, colorOverLifetime, opacityOverLifetime, rotationOverLifetime, noise, force fields, sub-emitters
|
|
392
|
+
- Default texture: solid white 1??1 (preserves mesh shape); point/billboard renderers default to a circle texture
|
|
393
|
+
- Sub-emitter note: sub-emitters do not inherit `RendererType.MESH` or `RendererType.TRAIL` from the parent because mesh geometry and trail config cannot be passed through; sub-emitters fall back to their own `rendererType` or `POINTS` by default
|
|
394
|
+
- Note: `mesh.geometry` is a runtime object (not serializable to JSON); provide it programmatically
|
|
395
|
+
|
|
396
|
+
### Trail / Ribbon Configuration
|
|
397
|
+
|
|
398
|
+
When using `RendererType.TRAIL`, configure trail-specific properties via `renderer.trail`:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
type TrailConfig = {
|
|
402
|
+
length?: number; // Position history samples per particle. Default: 20
|
|
403
|
+
width?: number; // Base ribbon width in world units. Default: 1.0
|
|
404
|
+
widthOverTrail?: LifetimeCurve; // Width taper from head (0) to tail (1)
|
|
405
|
+
opacityOverTrail?: LifetimeCurve; // Opacity taper from head (0) to tail (1)
|
|
406
|
+
colorOverTrail?: { // Optional per-channel color multiplier curves
|
|
407
|
+
isActive: boolean;
|
|
408
|
+
r: LifetimeCurve; // Red channel multiplier (0=head, 1=tail)
|
|
409
|
+
g: LifetimeCurve; // Green channel multiplier
|
|
410
|
+
b: LifetimeCurve; // Blue channel multiplier
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Trail features:
|
|
416
|
+
- GPU billboard rendering (vertex shader computes perpendicular offset from camera direction)
|
|
417
|
+
- Soft-edge fade on ribbon borders (no hard edges)
|
|
418
|
+
- Optional texture mapping (texture modulates brightness via luminance)
|
|
419
|
+
- `colorOverTrail` multiplies the particle's current color at each trail position ??? use white `startColor` for full color transitions
|
|
420
|
+
- Works with all particle features: colorOverLifetime, noise, gravity, force fields
|
|
421
|
+
|
|
422
|
+
Example ??? comet trail with color shift:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
renderer: {
|
|
426
|
+
rendererType: RendererType.TRAIL,
|
|
427
|
+
blending: THREE.AdditiveBlending,
|
|
428
|
+
transparent: true,
|
|
429
|
+
depthWrite: false,
|
|
430
|
+
trail: {
|
|
431
|
+
length: 60,
|
|
432
|
+
width: 0.5,
|
|
433
|
+
widthOverTrail: {
|
|
434
|
+
type: LifeTimeCurve.BEZIER,
|
|
435
|
+
bezierPoints: [
|
|
436
|
+
{ x: 0, y: 1, percentage: 0 },
|
|
437
|
+
{ x: 0.5, y: 0.4 },
|
|
438
|
+
{ x: 1, y: 0, percentage: 1 },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
colorOverTrail: {
|
|
442
|
+
isActive: true,
|
|
443
|
+
r: { type: LifeTimeCurve.BEZIER, bezierPoints: [{ x: 0, y: 0.2, percentage: 0 }, { x: 0.5, y: 0.5 }, { x: 1, y: 0.8, percentage: 1 }] },
|
|
444
|
+
g: { type: LifeTimeCurve.BEZIER, bezierPoints: [{ x: 0, y: 1.0, percentage: 0 }, { x: 0.5, y: 0.5 }, { x: 1, y: 0.1, percentage: 1 }] },
|
|
445
|
+
b: { type: LifeTimeCurve.BEZIER, bezierPoints: [{ x: 0, y: 0.4, percentage: 0 }, { x: 0.5, y: 0.9 }, { x: 1, y: 1.0, percentage: 1 }] },
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Texture Sheet Animation
|
|
454
|
+
|
|
455
|
+
Animate sprite sheets over particle lifetime.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
type TextureSheetAnimation = {
|
|
459
|
+
tiles?: THREE.Vector2; // Grid dimensions (default: 1x1)
|
|
460
|
+
timeMode?: TimeMode; // LIFETIME or FPS (default: LIFETIME)
|
|
461
|
+
fps?: number; // Frames per second (default: 30)
|
|
462
|
+
startFrame?: number | { min, max }; // Starting frame (default: 0)
|
|
463
|
+
};
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Texture (map)
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
const texture = new THREE.TextureLoader().load("particle.png");
|
|
472
|
+
const system = createParticleSystem({
|
|
473
|
+
map: texture,
|
|
474
|
+
// ...
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Curve Types
|
|
481
|
+
|
|
482
|
+
### BezierCurve
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
{
|
|
486
|
+
type: LifeTimeCurve.BEZIER,
|
|
487
|
+
scale: 1, // Optional multiplier
|
|
488
|
+
bezierPoints: [
|
|
489
|
+
{ x: 0, y: 0, percentage: 0 }, // Start point
|
|
490
|
+
{ x: 0.3, y: 0.8 }, // Control point
|
|
491
|
+
{ x: 0.7, y: 0.2 }, // Control point
|
|
492
|
+
{ x: 1, y: 1, percentage: 1 }, // End point
|
|
493
|
+
],
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### EasingCurve
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
{
|
|
501
|
+
type: LifeTimeCurve.EASING,
|
|
502
|
+
scale: 1, // Optional multiplier
|
|
503
|
+
curveFunction: (time: number) => number, // 0???1 input, any output
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Enums
|
|
510
|
+
|
|
511
|
+
### Shape
|
|
512
|
+
`SPHERE` | `CONE` | `CIRCLE` | `RECTANGLE` | `BOX`
|
|
513
|
+
|
|
514
|
+
### EmitFrom
|
|
515
|
+
`VOLUME` | `SHELL` | `EDGE`
|
|
516
|
+
|
|
517
|
+
### SimulationSpace
|
|
518
|
+
`LOCAL` | `WORLD`
|
|
519
|
+
|
|
520
|
+
- **`LOCAL`** (default): particles are children of the emitter in the scene
|
|
521
|
+
graph and move/rotate with it. The buffer stores positions in the
|
|
522
|
+
emitter's local frame.
|
|
523
|
+
- **`WORLD`**: particles are decoupled from the emitter once spawned. The
|
|
524
|
+
buffer stores world coordinates directly and `instance.matrixWorld` is
|
|
525
|
+
held at identity. New particles spawn from the emitter's current world
|
|
526
|
+
pose (parent chain + `instance.position` / `instance.rotation`), so
|
|
527
|
+
moving the emitter only affects future emissions, not existing
|
|
528
|
+
particles. The shape-emission offset is scaled by the emitter's world
|
|
529
|
+
scale at spawn time (matching Unity's Shape module with `Scaling Mode =
|
|
530
|
+
Local/Hierarchy`); live particles are unaffected by later scale
|
|
531
|
+
changes. Matches Unity's world simulation space.
|
|
532
|
+
|
|
533
|
+
### TimeMode
|
|
534
|
+
`LIFETIME` | `FPS`
|
|
535
|
+
|
|
536
|
+
### LifeTimeCurve
|
|
537
|
+
`BEZIER` | `EASING`
|
|
538
|
+
|
|
539
|
+
### RendererType
|
|
540
|
+
`POINTS` (default ??? classic point sprites) | `INSTANCED` (GPU instanced quads, no gl_PointSize limit) | `TRAIL` (ribbon trails behind particles) | `MESH` (3D mesh particles via GPU instancing)
|
|
541
|
+
|
|
542
|
+
### CollisionPlaneMode
|
|
543
|
+
`KILL` (deactivate particle immediately) | `CLAMP` (stop particle at the plane surface) | `BOUNCE` (reflect velocity with optional dampen)
|
|
544
|
+
|
|
545
|
+
### SimulationBackend
|
|
546
|
+
`AUTO` (default ??? GPU compute when the WebGPU path is registered via `enableWebGPU()`, else CPU) | `CPU` (always JavaScript update loop) | `GPU` (same as AUTO ??? GPU compute requires the registered WebGPU path)
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Callbacks
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
createParticleSystem({
|
|
554
|
+
// Called every frame
|
|
555
|
+
onUpdate: ({ particleSystem, delta, elapsed, lifetime, iterationCount }) => {
|
|
556
|
+
// particleSystem: THREE.Points or THREE.Mesh instance
|
|
557
|
+
// delta: seconds since last frame
|
|
558
|
+
// elapsed: total seconds
|
|
559
|
+
// lifetime: system duration
|
|
560
|
+
// iterationCount: number of completed loops
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
// Called when a non-looping system completes, or each loop iteration ends
|
|
564
|
+
onComplete: () => {
|
|
565
|
+
console.log("Particle system iteration complete");
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Complete Example ??? Fire Effect
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
import * as THREE from "three";
|
|
576
|
+
import {
|
|
577
|
+
createParticleSystem,
|
|
578
|
+
updateParticleSystems,
|
|
579
|
+
Shape,
|
|
580
|
+
LifeTimeCurve,
|
|
581
|
+
SimulationSpace,
|
|
582
|
+
} from "@cyberluke/three-particles";
|
|
583
|
+
|
|
584
|
+
const fireTexture = new THREE.TextureLoader().load("fire-particle.png");
|
|
585
|
+
|
|
586
|
+
const fire = createParticleSystem({
|
|
587
|
+
duration: 5,
|
|
588
|
+
looping: true,
|
|
589
|
+
maxParticles: 200,
|
|
590
|
+
startLifetime: { min: 0.5, max: 1.5 },
|
|
591
|
+
startSpeed: { min: 1, max: 3 },
|
|
592
|
+
startSize: { min: 0.3, max: 0.8 },
|
|
593
|
+
startColor: {
|
|
594
|
+
min: { r: 1.0, g: 0.2, b: 0.0 },
|
|
595
|
+
max: { r: 1.0, g: 0.8, b: 0.0 },
|
|
596
|
+
},
|
|
597
|
+
gravity: -1,
|
|
598
|
+
simulationSpace: SimulationSpace.WORLD,
|
|
599
|
+
emission: { rateOverTime: 50 },
|
|
600
|
+
shape: {
|
|
601
|
+
shape: Shape.CONE,
|
|
602
|
+
cone: { angle: 0.2, radius: 0.3, radiusThickness: 1 },
|
|
603
|
+
},
|
|
604
|
+
map: fireTexture,
|
|
605
|
+
renderer: {
|
|
606
|
+
blending: THREE.AdditiveBlending,
|
|
607
|
+
transparent: true,
|
|
608
|
+
depthTest: true,
|
|
609
|
+
depthWrite: false,
|
|
610
|
+
discardBackgroundColor: false,
|
|
611
|
+
backgroundColorTolerance: 1,
|
|
612
|
+
backgroundColor: { r: 0, g: 0, b: 0 },
|
|
613
|
+
},
|
|
614
|
+
sizeOverLifetime: {
|
|
615
|
+
isActive: true,
|
|
616
|
+
lifetimeCurve: {
|
|
617
|
+
type: LifeTimeCurve.BEZIER,
|
|
618
|
+
scale: 1,
|
|
619
|
+
bezierPoints: [
|
|
620
|
+
{ x: 0, y: 1, percentage: 0 },
|
|
621
|
+
{ x: 0.5, y: 0.5 },
|
|
622
|
+
{ x: 1, y: 0, percentage: 1 },
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
opacityOverLifetime: {
|
|
627
|
+
isActive: true,
|
|
628
|
+
lifetimeCurve: {
|
|
629
|
+
type: LifeTimeCurve.BEZIER,
|
|
630
|
+
scale: 1,
|
|
631
|
+
bezierPoints: [
|
|
632
|
+
{ x: 0, y: 1, percentage: 0 },
|
|
633
|
+
{ x: 0.7, y: 0.8 },
|
|
634
|
+
{ x: 1, y: 0, percentage: 1 },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
scene.add(fire.instance);
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Sub-Emitters
|
|
646
|
+
|
|
647
|
+
Trigger child particle systems when particles are born or die.
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
type SubEmitter = {
|
|
651
|
+
trigger: SubEmitterTrigger; // BIRTH or DEATH
|
|
652
|
+
config: ParticleSystemConfig; // The child particle system config
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Usage
|
|
656
|
+
createParticleSystem({
|
|
657
|
+
// ... main system config
|
|
658
|
+
subEmitters: [
|
|
659
|
+
{ trigger: SubEmitterTrigger.BIRTH, config: sparkConfig },
|
|
660
|
+
{ trigger: SubEmitterTrigger.DEATH, config: explosionConfig },
|
|
661
|
+
],
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### SubEmitterTrigger
|
|
666
|
+
- `BIRTH` ??? Spawns child system at the position of each newly created particle
|
|
667
|
+
- `DEATH` ??? Spawns child system at the position of each dying particle
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Force Fields
|
|
672
|
+
|
|
673
|
+
Apply forces to particles dynamically. Supports point attraction/repulsion and directional wind.
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
type ForceField = {
|
|
677
|
+
type: ForceFieldType; // POINT or DIRECTIONAL
|
|
678
|
+
position?: Point3D; // For POINT type ??? center of force
|
|
679
|
+
direction?: Point3D; // For DIRECTIONAL type ??? force direction
|
|
680
|
+
strength: number; // Force strength (negative = repulsion for POINT)
|
|
681
|
+
radius?: number; // For POINT type ??? area of effect
|
|
682
|
+
falloff?: ForceFieldFalloff; // NONE, LINEAR, or QUADRATIC
|
|
683
|
+
};
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### ForceFieldType
|
|
687
|
+
- `POINT` ??? Attracts (positive strength) or repels (negative strength) particles toward/from a point
|
|
688
|
+
- `DIRECTIONAL` ??? Applies a constant force in a direction (e.g., wind)
|
|
689
|
+
|
|
690
|
+
### ForceFieldFalloff
|
|
691
|
+
- `NONE` ??? Constant force regardless of distance
|
|
692
|
+
- `LINEAR` ??? Force decreases linearly with distance
|
|
693
|
+
- `QUADRATIC` ??? Force decreases with the square of distance (realistic gravity-like falloff)
|
|
694
|
+
|
|
695
|
+
**Example ??? Vortex with wind:**
|
|
696
|
+
```typescript
|
|
697
|
+
createParticleSystem({
|
|
698
|
+
// ... particle config
|
|
699
|
+
forceFields: [
|
|
700
|
+
{
|
|
701
|
+
type: ForceFieldType.POINT,
|
|
702
|
+
position: { x: 0, y: 2, z: 0 },
|
|
703
|
+
strength: 5,
|
|
704
|
+
radius: 3,
|
|
705
|
+
falloff: ForceFieldFalloff.LINEAR,
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
type: ForceFieldType.DIRECTIONAL,
|
|
709
|
+
direction: { x: 1, y: 0, z: 0 },
|
|
710
|
+
strength: 2,
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## Collision Planes
|
|
719
|
+
|
|
720
|
+
Infinite planes that constrain particle positions. When a particle crosses from the front side (positive normal direction) to the back side, the configured response mode is triggered.
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
type CollisionPlaneConfig = {
|
|
724
|
+
isActive?: boolean; // Whether this plane is active (default: true)
|
|
725
|
+
position?: Point3D; // A point on the plane surface (default: {x:0, y:0, z:0})
|
|
726
|
+
normal?: Point3D; // Plane normal vector, defines the "front" side (default: {x:0, y:1, z:0})
|
|
727
|
+
mode?: CollisionPlaneMode; // Response mode (default: KILL)
|
|
728
|
+
dampen?: number; // Velocity dampen factor for BOUNCE mode, 0-1 (default: 0)
|
|
729
|
+
lifetimeLoss?: number; // Fraction of start lifetime to subtract on collision, 0-1 (default: 0)
|
|
730
|
+
};
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### CollisionPlaneMode
|
|
734
|
+
- `KILL` ??? Deactivate the particle immediately when it crosses the plane
|
|
735
|
+
- `CLAMP` ??? Stop the particle at the plane surface (zero velocity toward plane)
|
|
736
|
+
- `BOUNCE` ??? Reflect the velocity vector off the plane normal with optional dampen
|
|
737
|
+
|
|
738
|
+
**Example ??? Bouncy floor and kill ceiling:**
|
|
739
|
+
```typescript
|
|
740
|
+
createParticleSystem({
|
|
741
|
+
// ... particle config
|
|
742
|
+
collisionPlanes: [
|
|
743
|
+
{
|
|
744
|
+
position: { x: 0, y: 0, z: 0 },
|
|
745
|
+
normal: { x: 0, y: 1, z: 0 },
|
|
746
|
+
mode: CollisionPlaneMode.BOUNCE,
|
|
747
|
+
dampen: 0.6,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
position: { x: 0, y: 10, z: 0 },
|
|
751
|
+
normal: { x: 0, y: -1, z: 0 },
|
|
752
|
+
mode: CollisionPlaneMode.KILL,
|
|
753
|
+
},
|
|
754
|
+
],
|
|
755
|
+
});
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## Serialization
|
|
761
|
+
|
|
762
|
+
Save and load particle system configurations as JSON.
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import {
|
|
766
|
+
serializeParticleSystemConfig,
|
|
767
|
+
deserializeParticleSystemConfig,
|
|
768
|
+
} from "@cyberluke/three-particles";
|
|
769
|
+
|
|
770
|
+
// Serialize (config ??? JSON-safe object)
|
|
771
|
+
const json = serializeParticleSystemConfig(config);
|
|
772
|
+
const jsonString = JSON.stringify(json);
|
|
773
|
+
|
|
774
|
+
// Deserialize (JSON-safe object ??? config)
|
|
775
|
+
const restored = deserializeParticleSystemConfig(JSON.parse(jsonString));
|
|
776
|
+
const system = createParticleSystem(restored);
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Usage with React Three Fiber
|
|
782
|
+
|
|
783
|
+
The library works with [React Three Fiber](https://github.com/pmndrs/react-three-fiber) without any additional wrapper package. Use `createParticleSystem` directly with React hooks:
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
import { useRef, useEffect } from "react";
|
|
787
|
+
import { useFrame } from "@react-three/fiber";
|
|
788
|
+
import {
|
|
789
|
+
createParticleSystem,
|
|
790
|
+
Shape,
|
|
791
|
+
type ParticleSystem,
|
|
792
|
+
} from "@cyberluke/three-particles";
|
|
793
|
+
import * as THREE from "three";
|
|
794
|
+
|
|
795
|
+
function FireEffect({ config }: { config?: Record<string, unknown> }) {
|
|
796
|
+
const groupRef = useRef<THREE.Group>(null);
|
|
797
|
+
const systemRef = useRef<ParticleSystem | null>(null);
|
|
798
|
+
|
|
799
|
+
useEffect(() => {
|
|
800
|
+
const system = createParticleSystem({
|
|
801
|
+
duration: 5,
|
|
802
|
+
looping: true,
|
|
803
|
+
maxParticles: 200,
|
|
804
|
+
startLifetime: { min: 0.5, max: 1.5 },
|
|
805
|
+
startSpeed: { min: 1, max: 3 },
|
|
806
|
+
startSize: { min: 0.3, max: 0.8 },
|
|
807
|
+
startColor: {
|
|
808
|
+
min: { r: 1, g: 0.2, b: 0 },
|
|
809
|
+
max: { r: 1, g: 0.8, b: 0 },
|
|
810
|
+
},
|
|
811
|
+
gravity: -1,
|
|
812
|
+
emission: { rateOverTime: 50 },
|
|
813
|
+
shape: { shape: Shape.CONE, cone: { angle: 0.2, radius: 0.3 } },
|
|
814
|
+
renderer: {
|
|
815
|
+
blending: THREE.AdditiveBlending,
|
|
816
|
+
transparent: true,
|
|
817
|
+
depthWrite: false,
|
|
818
|
+
},
|
|
819
|
+
...config,
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
systemRef.current = system;
|
|
823
|
+
groupRef.current?.add(system.instance);
|
|
824
|
+
|
|
825
|
+
return () => {
|
|
826
|
+
system.dispose();
|
|
827
|
+
};
|
|
828
|
+
}, [config]);
|
|
829
|
+
|
|
830
|
+
useFrame((_, delta) => {
|
|
831
|
+
systemRef.current?.update({
|
|
832
|
+
now: performance.now(),
|
|
833
|
+
delta,
|
|
834
|
+
elapsed: 0,
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return <group ref={groupRef} />;
|
|
839
|
+
}
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Key Integration Points
|
|
843
|
+
|
|
844
|
+
- **`useEffect`** ??? Create the particle system on mount, dispose on unmount
|
|
845
|
+
- **`useFrame`** ??? Drive per-frame updates using R3F's render loop (use `system.update()` for individual system control instead of the global `updateParticleSystems()`)
|
|
846
|
+
- **`<group ref>`** ??? Attach `system.instance` to a group so R3F manages the scene graph
|
|
847
|
+
- **Config changes** ??? Pass `config` as a dependency to `useEffect` to recreate on changes
|
|
848
|
+
- **No wrapper package needed** ??? The imperative API integrates naturally with React hooks
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## WebGPU Compute Support
|
|
853
|
+
|
|
854
|
+
Optional GPU-accelerated particle simulation via Three.js WebGPU renderer and TSL (Three Shading Language). Offloads all per-particle physics and modifiers to GPU compute shaders, enabling **50K-350K+ particles** at interactive frame rates.
|
|
855
|
+
|
|
856
|
+
**Requirements:** Three.js r182+ with WebGPU build (`three/webgpu`), browser with WebGPU support (Chrome 113+, Edge 113+, Firefox Nightly). No breaking changes ??? all existing WebGL code works unchanged.
|
|
857
|
+
|
|
858
|
+
### Setup
|
|
859
|
+
|
|
860
|
+
```typescript
|
|
861
|
+
// 1. Create a WebGPU renderer
|
|
862
|
+
import * as THREE from "three/webgpu";
|
|
863
|
+
const renderer = new THREE.WebGPURenderer({ antialias: true });
|
|
864
|
+
await renderer.init();
|
|
865
|
+
|
|
866
|
+
// 2. Enable WebGPU support (once, before creating any particle system).
|
|
867
|
+
// Passing the renderer enables capability detection ??? with a renderer that
|
|
868
|
+
// cannot run compute shaders, registration is skipped with a warning.
|
|
869
|
+
import { enableWebGPU } from "@cyberluke/three-particles/webgpu";
|
|
870
|
+
const gpuEnabled = enableWebGPU(renderer); // returns boolean
|
|
871
|
+
// No outputColorSpace override needed ??? the library uses the standard
|
|
872
|
+
// three.js linear workflow and works with the default (SRGBColorSpace).
|
|
873
|
+
|
|
874
|
+
// 3. Create a GPU-accelerated particle system
|
|
875
|
+
import { createParticleSystem, SimulationBackend } from "@cyberluke/three-particles";
|
|
876
|
+
const system = createParticleSystem({
|
|
877
|
+
simulationBackend: SimulationBackend.AUTO, // GPU if WebGPU available, else CPU
|
|
878
|
+
maxParticles: 100000,
|
|
879
|
+
// ... rest of config (same API as CPU)
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
scene.add(system.instance);
|
|
883
|
+
|
|
884
|
+
// 4. In your render loop ??? dispatch compute before rendering
|
|
885
|
+
function animate() {
|
|
886
|
+
system.update({ now: performance.now(), delta, elapsed });
|
|
887
|
+
|
|
888
|
+
if (system.computeNode) {
|
|
889
|
+
renderer.compute(system.computeNode);
|
|
890
|
+
}
|
|
891
|
+
renderer.render(scene, camera);
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
For fine-grained control (e.g., selective registration of individual WebGPU functions), you can also use `registerTSLMaterialFactory()` directly ??? see the API reference below.
|
|
896
|
+
|
|
897
|
+
### SimulationBackend
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
enum SimulationBackend {
|
|
901
|
+
AUTO = 'AUTO', // GPU compute when the WebGPU path is registered, else CPU (default)
|
|
902
|
+
CPU = 'CPU', // Always JavaScript update loop (works with any renderer)
|
|
903
|
+
GPU = 'GPU', // Same as AUTO ??? GPU compute requires the registered WebGPU path
|
|
904
|
+
}
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
| Value | WebGPU path registered (`enableWebGPU()`) | Not registered |
|
|
908
|
+
|-------|------------------------------------------|----------------|
|
|
909
|
+
| `AUTO` | GPU compute | CPU (JavaScript) |
|
|
910
|
+
| `CPU` | CPU (JavaScript) | CPU (JavaScript) |
|
|
911
|
+
| `GPU` | GPU compute | CPU (JavaScript) |
|
|
912
|
+
|
|
913
|
+
Note: the library never inspects the renderer on its own ??? call
|
|
914
|
+
`enableWebGPU(renderer)` (returns boolean) to skip registration automatically
|
|
915
|
+
when the renderer cannot dispatch compute shaders (e.g. `WebGLRenderer`).
|
|
916
|
+
|
|
917
|
+
### What Runs on GPU
|
|
918
|
+
|
|
919
|
+
- **Core physics:** gravity, velocity integration, position update, lifetime tracking, death detection
|
|
920
|
+
- **All 7 modifiers:** size over lifetime, opacity over lifetime, color over lifetime (per-channel RGB curves), rotation over lifetime, linear velocity over lifetime (per-axis X/Y/Z curves), orbital velocity, noise (3D simplex FBM)
|
|
921
|
+
- **Force fields:** point attractors/repulsors and directional forces with all falloff modes (NONE, LINEAR, QUADRATIC), up to 16 per system
|
|
922
|
+
- **Collision planes:** kill/clamp/bounce modes with dampen and lifetime loss, encoded as packed uniform buffer
|
|
923
|
+
- **Curves:** baked into 256-sample Float32Array lookup tables at system creation for fast GPU evaluation (<0.4% interpolation error)
|
|
924
|
+
|
|
925
|
+
### What Stays on CPU
|
|
926
|
+
|
|
927
|
+
- **Emission:** particle activation, burst scheduling, rate-over-time, rate-over-distance
|
|
928
|
+
- **Sub-emitters:** birth/death trigger spawning (sub-emitters are always forced to `SimulationBackend.CPU`)
|
|
929
|
+
- **Configuration changes:** `updateConfig()` applies on the next frame
|
|
930
|
+
- **Trail renderer:** `RendererType.TRAIL` always uses CPU simulation (POINTS, INSTANCED, and MESH work with GPU compute)
|
|
931
|
+
|
|
932
|
+
### ParticleSystem.computeNode
|
|
933
|
+
|
|
934
|
+
When GPU compute is active, the returned `ParticleSystem` object exposes a `computeNode` property. This must be dispatched every frame **before** `renderer.render()`:
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
const system = createParticleSystem({ simulationBackend: SimulationBackend.GPU, ... });
|
|
938
|
+
|
|
939
|
+
// In render loop:
|
|
940
|
+
if (system.computeNode) {
|
|
941
|
+
renderer.compute(system.computeNode); // Dispatch GPU compute
|
|
942
|
+
}
|
|
943
|
+
renderer.render(scene, camera);
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
When CPU simulation is active (no WebGPU, or `simulationBackend: 'CPU'`), `computeNode` is `null`.
|
|
947
|
+
|
|
948
|
+
### Fallback Behavior
|
|
949
|
+
|
|
950
|
+
WebGPU is fully opt-in and non-breaking:
|
|
951
|
+
- If `enableWebGPU()` not called (or no TSL factory registered via `registerTSLMaterialFactory()`), the library uses GLSL shaders (existing WebGL path) and CPU simulation
|
|
952
|
+
- If `enableWebGPU(renderer)` is called with a renderer that lacks compute support, registration is skipped with a console warning and everything stays on the CPU/GLSL path
|
|
953
|
+
- The same `ParticleSystemConfig` produces the same visuals on both backends. GPU limitation: modifier flags and lifetime curves (sizeOverLifetime, opacityOverLifetime, colorOverLifetime, rotationOverLifetime, velocityOverLifetime, noise.isActive) are baked into the compute kernel at creation ??? `updateConfig()` cannot change them live (a console warning is logged); recreate the system instead. On the CPU backend these update live.
|
|
954
|
+
- Renderer detection is duck-typed (checks for `.compute()` and `.hasFeature()` methods), not class-based
|
|
955
|
+
|
|
956
|
+
### WebGPU with React Three Fiber
|
|
957
|
+
|
|
958
|
+
```tsx
|
|
959
|
+
import { useRef, useEffect } from "react";
|
|
960
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
961
|
+
import { createParticleSystem, SimulationBackend } from "@cyberluke/three-particles";
|
|
962
|
+
import { enableWebGPU } from "@cyberluke/three-particles/webgpu";
|
|
963
|
+
|
|
964
|
+
// Enable once at module level
|
|
965
|
+
enableWebGPU();
|
|
966
|
+
|
|
967
|
+
function GPUParticleEffect({ config }) {
|
|
968
|
+
const groupRef = useRef(null);
|
|
969
|
+
const systemRef = useRef(null);
|
|
970
|
+
const { gl: renderer } = useThree();
|
|
971
|
+
|
|
972
|
+
useEffect(() => {
|
|
973
|
+
const system = createParticleSystem({
|
|
974
|
+
simulationBackend: SimulationBackend.AUTO,
|
|
975
|
+
...config,
|
|
976
|
+
});
|
|
977
|
+
systemRef.current = system;
|
|
978
|
+
groupRef.current?.add(system.instance);
|
|
979
|
+
return () => system.dispose();
|
|
980
|
+
}, [config]);
|
|
981
|
+
|
|
982
|
+
useFrame((_, delta) => {
|
|
983
|
+
const system = systemRef.current;
|
|
984
|
+
if (!system) return;
|
|
985
|
+
system.update({ now: performance.now(), delta, elapsed: 0 });
|
|
986
|
+
if (system.computeNode) {
|
|
987
|
+
renderer.compute(system.computeNode);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
return <group ref={groupRef} />;
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Important:** R3F must be configured to use `WebGPURenderer` (via the `gl` prop on `<Canvas>`) for GPU compute to activate.
|
|
996
|
+
|
|
997
|
+
---
|
|
998
|
+
|
|
999
|
+
## Links
|
|
1000
|
+
|
|
1001
|
+
- Repository: https://github.com/cyberluke/three-particles
|
|
1002
|
+
- API docs: shipped TypeScript definitions (dist/index.d.ts, webgpu.d.ts)
|
|
1003
|
+
- Visual Editor: https://github.com/cyberluke/three-particles-editor
|
|
1004
|
+
- Editor: @cyberluke/three-particles-editor
|
|
1005
|
+
- npm: https://www.npmjs.com/package/@cyberluke/three-particles
|