Skip to content

Commit 85f851f

Browse files
authored
New emitters! (#173)
1 parent be5ca02 commit 85f851f

File tree

10 files changed

+164
-69
lines changed

10 files changed

+164
-69
lines changed

.changeset/chatty-avocados-accept.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vfx-composer-r3f": minor
3+
---
4+
5+
**Breaking Change:** `<Emitter>` received a big overhaul and now supports `rate` and `limit` props, next to the `setup` callback prop that was already there. Together with the helper components from Timeline Composer, this should now allow for all typical particle emission workloads.

apps/examples/src/examples/FireflyExample.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,13 @@ export const FireflyExample = () => {
5656
<Modules.Lifetime {...particles} />
5757
</ComposableMaterial>
5858

59-
<mesh ref={mesh}>
59+
<mesh ref={mesh} castShadow>
6060
<dodecahedronGeometry args={[0.2]} />
6161
<meshStandardMaterial color="hotpink" />
6262

6363
<Emitter
64-
continuous
65-
count={10}
66-
setup={({ position, rotation }) => {
64+
rate={700}
65+
setup={({ position }) => {
6766
/*
6867
The position automatically inherits the emitter's position, but let's
6968
add a little random offset to spice things up!

apps/examples/src/examples/FogExample.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export const Fog = () => {
4545
</ComposableMaterial>
4646

4747
<Emitter
48-
count={50}
48+
limit={50}
49+
rate={Infinity}
4950
setup={({ position }) => {
5051
position.set(plusMinus(3), between(-2, 4), plusMinus(3))
5152
velocity.value.randomDirection().multiplyScalar(upTo(0.05))

apps/examples/src/examples/MagicWellExample.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,12 @@ export default function MagicWellExample() {
3535
</ComposableMaterial>
3636

3737
<Emitter
38-
continuous
39-
count={2}
38+
rate={250}
4039
setup={({ position, rotation }) => {
4140
const theta = plusMinus(Math.PI)
4241
const power = Math.pow(Math.random(), 3)
4342
const r = power * 1.2
44-
position.set(Math.cos(theta) * r, -2, Math.sin(theta) * r)
43+
position.set(Math.cos(theta) * r, -1, Math.sin(theta) * r)
4544

4645
rotation.setFromEuler(new Euler(0, plusMinus(Math.PI), 0))
4746

apps/examples/src/examples/PlasmaStormScene.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ const SuckyParticles = () => {
7676

7777
<Repeat seconds={1 / frequency}>
7878
<Emitter
79-
count={5000 / frequency}
79+
rate={5000}
8080
setup={({ position }) => {
81-
particles.setLifetime(between(1, 2), random() / frequency)
81+
particles.setLifetime(between(1, 2), Math.random())
8282

8383
const direction = onCircle(between(4, 5))
8484

@@ -123,11 +123,11 @@ const FloorEruption = () => {
123123

124124
<Repeat seconds={1 / frequency}>
125125
<Emitter
126-
count={200 / frequency}
126+
rate={200}
127127
setup={({ position }) => {
128128
const s = onCircle(between(3, 3.2))
129129
position.set(s.x, 0, s.y)
130-
particles.setLifetime(4, random() / frequency)
130+
particles.setLifetime(4)
131131

132132
velocity.value
133133
.set(position.x, 5, position.z)
@@ -201,9 +201,9 @@ export const Fog = () => {
201201

202202
<Repeat seconds={1 / frequency}>
203203
<Emitter
204-
count={50 / frequency}
204+
rate={50}
205205
setup={({ position }) => {
206-
particles.setLifetime(6, random() / frequency)
206+
particles.setLifetime(6)
207207
position.set(-10, between(0, 1), plusMinus(10))
208208
velocity.value.set(between(3, 10), 0, 0)
209209
rotation.value = plusMinus(0.2)

apps/examples/src/examples/Simple.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useTexture } from "@react-three/drei"
22
import { ComposableMaterial, Modules } from "material-composer-r3f"
3-
import { between, plusMinus } from "randomish"
3+
import { between, plusMinus, upTo } from "randomish"
44
import { OneMinus } from "shader-composer"
55
import { AdditiveBlending, Vector3 } from "three"
66
import {
@@ -45,7 +45,7 @@ export const Simple = () => {
4545
every new particle spawned, which gives us an opportunity to further
4646
customize each particle's behavior as needed. */}
4747
<Emitter
48-
continuous
48+
rate={100}
4949
setup={() => {
5050
/* Set a particle lifetime: */
5151
particles.setLifetime(between(1, 3))

apps/examples/src/examples/SoftParticlesExample.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComposableMaterial, Modules } from "material-composer-r3f"
2-
import { useRenderPipeline } from "r3f-stage"
2+
import { Layers, useRenderPipeline } from "r3f-stage"
33
import { useUniformUnit } from "shader-composer-r3f"
44
import { MeshStandardMaterial } from "three"
55
import { Emitter, Particles } from "vfx-composer-r3f"
@@ -8,7 +8,7 @@ export const SoftParticlesExample = () => {
88
const depthTexture = useUniformUnit("sampler2D", useRenderPipeline().depth)
99

1010
return (
11-
<Particles>
11+
<Particles layers-mask={Layers.TransparentFX}>
1212
<planeGeometry args={[5, 5]} />
1313

1414
<ComposableMaterial
@@ -21,7 +21,7 @@ export const SoftParticlesExample = () => {
2121
<Modules.Softness softness={2} depthTexture={depthTexture} />
2222
</ComposableMaterial>
2323

24-
<Emitter />
24+
<Emitter limit={1} rate={Infinity} />
2525
</Particles>
2626
)
2727
}

apps/examples/src/examples/Stress.tsx

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { ComposableMaterial, Modules } from "material-composer-r3f"
2-
import { between, plusMinus, random, upTo } from "randomish"
2+
import { between, plusMinus, upTo } from "randomish"
33
import { OneMinus } from "shader-composer"
44
import { Color, Vector3 } from "three"
5-
import { Repeat } from "timeline-composer"
65
import {
76
makeParticles,
87
useParticleAttribute,
@@ -11,8 +10,6 @@ import {
1110

1211
const Effect = makeParticles()
1312

14-
const FREQ = 30
15-
1613
export const Stress = () => {
1714
const particles = useParticles()
1815
const velocity = useParticleAttribute(() => new Vector3())
@@ -38,24 +35,21 @@ export const Stress = () => {
3835
</ComposableMaterial>
3936
</Effect.Root>
4037

41-
<Repeat seconds={1 / FREQ}>
42-
<Effect.Emitter
43-
count={100_000 / FREQ}
44-
setup={({ position, rotation }) => {
45-
const t = particles.time.value
46-
47-
/* Randomize the instance transform */
48-
position.randomDirection().multiplyScalar(upTo(1))
49-
rotation.random()
50-
51-
/* Write values into the instanced attributes */
52-
const start = t + random() / FREQ
53-
particles.setLifetime(between(1, 3))
54-
velocity.value.set(plusMinus(2), between(2, 8), plusMinus(2))
55-
color.value.setScalar(Math.random() * 2)
56-
}}
57-
/>
58-
</Repeat>
38+
<Effect.Emitter
39+
rate={100_000}
40+
setup={({ position, rotation }) => {
41+
const t = particles.time.value
42+
43+
/* Randomize the instance transform */
44+
position.randomDirection().multiplyScalar(upTo(1))
45+
rotation.random()
46+
47+
/* Write values into the instanced attributes */
48+
particles.setLifetime(between(1, 3))
49+
velocity.value.set(plusMinus(2), between(2, 8), plusMinus(2))
50+
color.value.setScalar(Math.random() * 2)
51+
}}
52+
/>
5953
</group>
6054
)
6155
}

packages/vfx-composer-r3f/README.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,77 @@
11
# vfx-composer-r3f
22

3-
...needs a README :-)
3+
## Emitters
4+
5+
This library provides an `<Emitter>` component that, attached to an instance of `<Particles>`, will trigger particles to be emitted. It can be configured to do this at a specific rate, and optionally to a specific number of total particles emitted. It can also accept a callback function that will be invoked once per emitted particle, and can be used to configure the particle's initial state.
6+
7+
The default configuration will emit 10 particles per second, with no limit:
8+
9+
```jsx
10+
<Emitter />
11+
```
12+
13+
You can configure it to emit particles at a specific `rate` (in particles per second):
14+
15+
```jsx
16+
<Emitter rate={20} />
17+
```
18+
19+
You can `limit` the total number of particles emitted (the default being `Infinity`):
20+
21+
```jsx
22+
<Emitter limit={100} />
23+
```
24+
25+
You can obviously combine the two:
26+
27+
```jsx
28+
<Emitter rate={20} limit={100} />
29+
```
30+
31+
You can set `rate` to `Infinity` to immediately emit all particles at once:
32+
33+
```jsx
34+
<Emitter rate={Infinity} limit={1000} />
35+
```
36+
37+
> **Warning**
38+
>
39+
> You can not set both `limit` and `rate` to `Infinity`. This will result in an error.
40+
41+
### Pairing Emitters with Timeline Composer
42+
43+
You can use the very useful timeline animation components from [Timeline Composer] to give emitters a lifetime, delay the start of emission, or even configure repeated bursts:
44+
45+
```jsx
46+
<Repeat seconds={2} times={5}>
47+
<Lifetime seconds={1}>
48+
<Emitter rate={50} />
49+
</Lifetime>
50+
</Repeat>
51+
```
52+
53+
### Configuring Particles
54+
55+
_TODO_
56+
57+
### Emitters are Scene Objects!
58+
59+
Emitters created through `<Emitter>` are actual scene objects in your Three.js scene, meaning that you can animate them just like you would animate any other scene object, or parent them to other objects, and so on. Newly spawned particles will inherit the emitter's position, rotation, and scale.
60+
61+
### Multiple Emitters
62+
63+
Nothing is stopping you from having more than one emitter! If you have multiple emitters, thay can even have completely different configurations (including different setup callbacks.)
64+
65+
All particles spawned will be part of the `<Particles>` instance the emitters use, so plan their particles capacity accordingly.
66+
67+
### Connecting Emitters to Particles Meshes
68+
69+
By default, `<Emitter>` will use React Context to find the nearest `<Particles>` component in the tree, and emit particles from that. This is convenient, but it can be a bit limiting. You may explicitly connect an emitter to a specific particles mesh through the `particles` prop:
70+
71+
```jsx
72+
<Emitter particles={particlesRef} />
73+
```
74+
75+
Now the emitter may live outside of the Particles mesh it is connected to, and will still emit particles from it.
76+
77+
[timeline composer]: https://github.com/hmans/timeline-composer

packages/vfx-composer-r3f/src/Emitter.tsx

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React, {
44
MutableRefObject,
55
RefObject,
66
useCallback,
7-
useEffect,
87
useImperativeHandle,
98
useRef
109
} from "react"
@@ -14,8 +13,8 @@ import { useParticlesContext } from "./Particles"
1413

1514
export type EmitterProps = Object3DProps & {
1615
particles?: MutableRefObject<Particles> | RefObject<Particles>
17-
count?: number
18-
continuous?: boolean
16+
limit?: number
17+
rate?: number
1918
setup?: InstanceSetupCallback
2019
}
2120

@@ -24,22 +23,24 @@ const particlesMatrix = new Matrix4()
2423

2524
export const Emitter = forwardRef<Object3D, EmitterProps>(
2625
(
27-
{
28-
particles: particlesProp,
29-
count = 1,
30-
continuous = false,
31-
setup,
32-
...props
33-
},
26+
{ particles: particlesProp, limit = Infinity, rate = 10, setup, ...props },
3427
ref
3528
) => {
36-
const object = useRef<Object3D>(null!)
29+
const origin = useRef<Object3D>(null!)
3730
const particlesFromContext = useParticlesContext()
31+
const queuedParticles = useRef(0)
32+
const remainingParticles = useRef(limit)
33+
34+
if (rate === Infinity && limit === Infinity) {
35+
throw new Error(
36+
"Emitter: rate and limit cannot both be Infinity. Please set one of them to a finite value."
37+
)
38+
}
3839

3940
const emitterSetup = useCallback<InstanceSetupCallback>(
4041
(props) => {
4142
tmpMatrix
42-
.copy(object.current.matrixWorld)
43+
.copy(origin.current.matrixWorld)
4344
.premultiply(particlesMatrix)
4445
.decompose(props.position, props.rotation, props.scale)
4546

@@ -48,27 +49,49 @@ export const Emitter = forwardRef<Object3D, EmitterProps>(
4849
[setup]
4950
)
5051

51-
useEffect(() => {
52-
const particles = particlesProp?.current || particlesFromContext
52+
const emit = useCallback(
53+
(dt: number) => {
54+
if (remainingParticles.current <= 0) return
55+
56+
/* Grab a reference to the particles mesh */
57+
const particles = particlesProp?.current || particlesFromContext
58+
if (!particles) return
59+
60+
/* Increase the accumulated number of particles we're supposed to emit. */
61+
if (rate === Infinity) {
62+
queuedParticles.current = Infinity
63+
} else {
64+
queuedParticles.current += dt * rate
65+
}
5366

54-
if (!particles) return
55-
if (continuous) return
67+
/* Is it time to emit? */
68+
if (queuedParticles.current >= 1 || rate === Infinity) {
69+
/* Determine the amount of particles to emit. Don't go over the number of
70+
remaining particles. */
71+
const amount = Math.min(
72+
Math.trunc(queuedParticles.current),
73+
remainingParticles.current
74+
)
5675

57-
particlesMatrix.copy(particles!.matrixWorld).invert()
58-
particles.emit(count, emitterSetup)
59-
}, [particlesFromContext])
76+
/* Emit! */
77+
particlesMatrix.copy(particles.matrixWorld).invert()
78+
particles.emit(amount, emitterSetup)
6079

61-
useFrame(() => {
62-
const particles = particlesProp?.current || particlesFromContext
80+
/* Update the remaining number of particles, and the accumulator. */
81+
queuedParticles.current -= amount
82+
remainingParticles.current -= amount
83+
}
84+
},
85+
[particlesProp, particlesFromContext, emitterSetup]
86+
)
6387

64-
if (!particles) return
65-
if (!continuous) return
66-
particlesMatrix.copy(particles!.matrixWorld).invert()
67-
particles.emit(count, emitterSetup)
88+
useFrame((_, dt) => {
89+
if (!rate) return
90+
emit(dt)
6891
})
6992

70-
useImperativeHandle(ref, () => object.current)
93+
useImperativeHandle(ref, () => origin.current)
7194

72-
return <object3D {...props} ref={object} />
95+
return <object3D {...props} ref={origin} />
7396
}
7497
)

0 commit comments

Comments
 (0)