Skip to content

Commit

Permalink
New emitters! (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
hmans committed Aug 27, 2022
1 parent be5ca02 commit 85f851f
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-avocados-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vfx-composer-r3f": minor
---

**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.
7 changes: 3 additions & 4 deletions apps/examples/src/examples/FireflyExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ export const FireflyExample = () => {
<Modules.Lifetime {...particles} />
</ComposableMaterial>

<mesh ref={mesh}>
<mesh ref={mesh} castShadow>
<dodecahedronGeometry args={[0.2]} />
<meshStandardMaterial color="hotpink" />

<Emitter
continuous
count={10}
setup={({ position, rotation }) => {
rate={700}
setup={({ position }) => {
/*
The position automatically inherits the emitter's position, but let's
add a little random offset to spice things up!
Expand Down
3 changes: 2 additions & 1 deletion apps/examples/src/examples/FogExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export const Fog = () => {
</ComposableMaterial>

<Emitter
count={50}
limit={50}
rate={Infinity}
setup={({ position }) => {
position.set(plusMinus(3), between(-2, 4), plusMinus(3))
velocity.value.randomDirection().multiplyScalar(upTo(0.05))
Expand Down
5 changes: 2 additions & 3 deletions apps/examples/src/examples/MagicWellExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,12 @@ export default function MagicWellExample() {
</ComposableMaterial>

<Emitter
continuous
count={2}
rate={250}
setup={({ position, rotation }) => {
const theta = plusMinus(Math.PI)
const power = Math.pow(Math.random(), 3)
const r = power * 1.2
position.set(Math.cos(theta) * r, -2, Math.sin(theta) * r)
position.set(Math.cos(theta) * r, -1, Math.sin(theta) * r)

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

Expand Down
12 changes: 6 additions & 6 deletions apps/examples/src/examples/PlasmaStormScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ const SuckyParticles = () => {

<Repeat seconds={1 / frequency}>
<Emitter
count={5000 / frequency}
rate={5000}
setup={({ position }) => {
particles.setLifetime(between(1, 2), random() / frequency)
particles.setLifetime(between(1, 2), Math.random())

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

Expand Down Expand Up @@ -123,11 +123,11 @@ const FloorEruption = () => {

<Repeat seconds={1 / frequency}>
<Emitter
count={200 / frequency}
rate={200}
setup={({ position }) => {
const s = onCircle(between(3, 3.2))
position.set(s.x, 0, s.y)
particles.setLifetime(4, random() / frequency)
particles.setLifetime(4)

velocity.value
.set(position.x, 5, position.z)
Expand Down Expand Up @@ -201,9 +201,9 @@ export const Fog = () => {

<Repeat seconds={1 / frequency}>
<Emitter
count={50 / frequency}
rate={50}
setup={({ position }) => {
particles.setLifetime(6, random() / frequency)
particles.setLifetime(6)
position.set(-10, between(0, 1), plusMinus(10))
velocity.value.set(between(3, 10), 0, 0)
rotation.value = plusMinus(0.2)
Expand Down
4 changes: 2 additions & 2 deletions apps/examples/src/examples/Simple.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useTexture } from "@react-three/drei"
import { ComposableMaterial, Modules } from "material-composer-r3f"
import { between, plusMinus } from "randomish"
import { between, plusMinus, upTo } from "randomish"
import { OneMinus } from "shader-composer"
import { AdditiveBlending, Vector3 } from "three"
import {
Expand Down Expand Up @@ -45,7 +45,7 @@ export const Simple = () => {
every new particle spawned, which gives us an opportunity to further
customize each particle's behavior as needed. */}
<Emitter
continuous
rate={100}
setup={() => {
/* Set a particle lifetime: */
particles.setLifetime(between(1, 3))
Expand Down
6 changes: 3 additions & 3 deletions apps/examples/src/examples/SoftParticlesExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComposableMaterial, Modules } from "material-composer-r3f"
import { useRenderPipeline } from "r3f-stage"
import { Layers, useRenderPipeline } from "r3f-stage"
import { useUniformUnit } from "shader-composer-r3f"
import { MeshStandardMaterial } from "three"
import { Emitter, Particles } from "vfx-composer-r3f"
Expand All @@ -8,7 +8,7 @@ export const SoftParticlesExample = () => {
const depthTexture = useUniformUnit("sampler2D", useRenderPipeline().depth)

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

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

<Emitter />
<Emitter limit={1} rate={Infinity} />
</Particles>
)
}
38 changes: 16 additions & 22 deletions apps/examples/src/examples/Stress.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ComposableMaterial, Modules } from "material-composer-r3f"
import { between, plusMinus, random, upTo } from "randomish"
import { between, plusMinus, upTo } from "randomish"
import { OneMinus } from "shader-composer"
import { Color, Vector3 } from "three"
import { Repeat } from "timeline-composer"
import {
makeParticles,
useParticleAttribute,
Expand All @@ -11,8 +10,6 @@ import {

const Effect = makeParticles()

const FREQ = 30

export const Stress = () => {
const particles = useParticles()
const velocity = useParticleAttribute(() => new Vector3())
Expand All @@ -38,24 +35,21 @@ export const Stress = () => {
</ComposableMaterial>
</Effect.Root>

<Repeat seconds={1 / FREQ}>
<Effect.Emitter
count={100_000 / FREQ}
setup={({ position, rotation }) => {
const t = particles.time.value

/* Randomize the instance transform */
position.randomDirection().multiplyScalar(upTo(1))
rotation.random()

/* Write values into the instanced attributes */
const start = t + random() / FREQ
particles.setLifetime(between(1, 3))
velocity.value.set(plusMinus(2), between(2, 8), plusMinus(2))
color.value.setScalar(Math.random() * 2)
}}
/>
</Repeat>
<Effect.Emitter
rate={100_000}
setup={({ position, rotation }) => {
const t = particles.time.value

/* Randomize the instance transform */
position.randomDirection().multiplyScalar(upTo(1))
rotation.random()

/* Write values into the instanced attributes */
particles.setLifetime(between(1, 3))
velocity.value.set(plusMinus(2), between(2, 8), plusMinus(2))
color.value.setScalar(Math.random() * 2)
}}
/>
</group>
)
}
76 changes: 75 additions & 1 deletion packages/vfx-composer-r3f/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
# vfx-composer-r3f

...needs a README :-)
## Emitters

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.

The default configuration will emit 10 particles per second, with no limit:

```jsx
<Emitter />
```

You can configure it to emit particles at a specific `rate` (in particles per second):

```jsx
<Emitter rate={20} />
```

You can `limit` the total number of particles emitted (the default being `Infinity`):

```jsx
<Emitter limit={100} />
```

You can obviously combine the two:

```jsx
<Emitter rate={20} limit={100} />
```

You can set `rate` to `Infinity` to immediately emit all particles at once:

```jsx
<Emitter rate={Infinity} limit={1000} />
```

> **Warning**
>
> You can not set both `limit` and `rate` to `Infinity`. This will result in an error.
### Pairing Emitters with Timeline Composer

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:

```jsx
<Repeat seconds={2} times={5}>
<Lifetime seconds={1}>
<Emitter rate={50} />
</Lifetime>
</Repeat>
```

### Configuring Particles

_TODO_

### Emitters are Scene Objects!

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.

### Multiple Emitters

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.)

All particles spawned will be part of the `<Particles>` instance the emitters use, so plan their particles capacity accordingly.

### Connecting Emitters to Particles Meshes

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:

```jsx
<Emitter particles={particlesRef} />
```

Now the emitter may live outside of the Particles mesh it is connected to, and will still emit particles from it.

[timeline composer]: https://github.com/hmans/timeline-composer
77 changes: 50 additions & 27 deletions packages/vfx-composer-r3f/src/Emitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, {
MutableRefObject,
RefObject,
useCallback,
useEffect,
useImperativeHandle,
useRef
} from "react"
Expand All @@ -14,8 +13,8 @@ import { useParticlesContext } from "./Particles"

export type EmitterProps = Object3DProps & {
particles?: MutableRefObject<Particles> | RefObject<Particles>
count?: number
continuous?: boolean
limit?: number
rate?: number
setup?: InstanceSetupCallback
}

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

export const Emitter = forwardRef<Object3D, EmitterProps>(
(
{
particles: particlesProp,
count = 1,
continuous = false,
setup,
...props
},
{ particles: particlesProp, limit = Infinity, rate = 10, setup, ...props },
ref
) => {
const object = useRef<Object3D>(null!)
const origin = useRef<Object3D>(null!)
const particlesFromContext = useParticlesContext()
const queuedParticles = useRef(0)
const remainingParticles = useRef(limit)

if (rate === Infinity && limit === Infinity) {
throw new Error(
"Emitter: rate and limit cannot both be Infinity. Please set one of them to a finite value."
)
}

const emitterSetup = useCallback<InstanceSetupCallback>(
(props) => {
tmpMatrix
.copy(object.current.matrixWorld)
.copy(origin.current.matrixWorld)
.premultiply(particlesMatrix)
.decompose(props.position, props.rotation, props.scale)

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

useEffect(() => {
const particles = particlesProp?.current || particlesFromContext
const emit = useCallback(
(dt: number) => {
if (remainingParticles.current <= 0) return

/* Grab a reference to the particles mesh */
const particles = particlesProp?.current || particlesFromContext
if (!particles) return

/* Increase the accumulated number of particles we're supposed to emit. */
if (rate === Infinity) {
queuedParticles.current = Infinity
} else {
queuedParticles.current += dt * rate
}

if (!particles) return
if (continuous) return
/* Is it time to emit? */
if (queuedParticles.current >= 1 || rate === Infinity) {
/* Determine the amount of particles to emit. Don't go over the number of
remaining particles. */
const amount = Math.min(
Math.trunc(queuedParticles.current),
remainingParticles.current
)

particlesMatrix.copy(particles!.matrixWorld).invert()
particles.emit(count, emitterSetup)
}, [particlesFromContext])
/* Emit! */
particlesMatrix.copy(particles.matrixWorld).invert()
particles.emit(amount, emitterSetup)

useFrame(() => {
const particles = particlesProp?.current || particlesFromContext
/* Update the remaining number of particles, and the accumulator. */
queuedParticles.current -= amount
remainingParticles.current -= amount
}
},
[particlesProp, particlesFromContext, emitterSetup]
)

if (!particles) return
if (!continuous) return
particlesMatrix.copy(particles!.matrixWorld).invert()
particles.emit(count, emitterSetup)
useFrame((_, dt) => {
if (!rate) return
emit(dt)
})

useImperativeHandle(ref, () => object.current)
useImperativeHandle(ref, () => origin.current)

return <object3D {...props} ref={object} />
return <object3D {...props} ref={origin} />
}
)

0 comments on commit 85f851f

Please sign in to comment.