This is meant to be a nice and flexible Particle System for three.js. In its core it utilizes miniplex
as ECS.
Every particle is represented as entity in an ECS world. It's components holds all the data needed to simulate the particle behaviour.
Systems iterate over sets of particles every frame and advance their values accordingly to their behaviour.
A BufferGeometry reads the values and puts them into its attributes. Vertex and fragment shader uses them to appropriate display each particle.
At the moment sprudel is tested in a react-three-fiber environment to avoid boilerplate. It's designed to be used in both worlds. Below the r3f examples there's a short hint on plain three.js usage.
First, import ParticleGeometry
and ParticleMaterial
and extend them to make sure r3f will know them.
import {extend} from "@react-three/fiber";
import {
ParticleGeometry,
ParticleMaterial
} from "sprudel";
extend({ParticleGeometry, ParticleMaterial})
In your Component wrap them in a points object.
const Particles = () => {
return (
<points>
<particleGeometry />
<particleMaterial />
</points>
)
}
Since particles will move and change its appearance we need to update a few things and its up to you to trigger it. You need to create the ParticleSystem and update both, the geometry and the system.
const Particles = () => {
const ref = useRef()
const particleSystem = useMemo(() => new ParticleSystem(), [])
useFrame((state, delta) => {
particleSystem.update(delta)
ref.current.update()
});
return (
<points>
<particleGeometry args={[particleSystem]} ref={ref} />
<particleMaterial />
</points>
)
}
Now you are ready to add your first particle to your scene.
useEffect(() => {
const main = particleSystem.addParticle({
startSize: 3,
emitting: [
{
sprite: 1,
size: 3,
rateOverTime: 10,
startLifetime: 2,
startSpeed: 0.3,
startRotation: [1, 1, 0],
},
]
});
return () => particleSystem.destroyParticle(main);
}, []);
There are a bunch of examples showing different configurations and behaviours in /examples
In plain three.js you just create the objects like you are used to.
const geo = new ParticleGeometry()
const mat = new ParticleMaterial()
const points = new Points(geo, mat)
scene.add(points)
You'd call geo.update() and the systems in your render loop and create the particle wherever you want.
Your Particle System is defined by a data structure.
Each particle you create or emit can be an emitter too. Emitting can be done by a rate over time, defining how many particles shall be emitted in a second. Props in an emitting object describe the particles that will be emitted.
{
emitting: [
{
rateOverTime: 10,
startLifetime: 1,
size: 4,
// ...
},
{
rateOverTime: 30,
startLifetime: .5,
color: [1, 0, 0],
// ...
}
]
}
Or you create bursts which emit an amount of particles at the same time. By setting rateOverTime to zero, particles are only emitted by the burst.
You can have multiple emitters and bursts on the same particle.
{
emitting: [
{
// ...
rateOverTime: 0,
startLifetime: 2,
bursts: [
{
count: 80,
cycleCount: -1,
repeatInterval: 1,
time: 0,
}
],
}
]
}
Visuals can be static or change over lifetime. It's simple like that.
const particle = {
color: [1, 1, 1],
size: 1,
opacity: 1,
// ...
}
To change appearance over lifetime, assign a …OverLifetime prop. We use the three.js KeyframeTracks.
const particle = {
sizeOverLifetime: new NumberKeyframeTrack('Glowing Smoke Size', [0, 1], [3, 7]),
opacityOverLifetime: new NumberKeyframeTrack('Glowing Smoke Opacity', [0, .6, 1], [.3, .2, 0]),
colorOverLifetime: new ColorKeyframeTrack('Glowing Smoke Color', [0, .7], [.6, 0, 2, 0, 0, 0]),
// ...
}
There are several props influencing particle and emitting behaviour.
Prop | Description |
---|---|
startDelay |
time to wait until animation starts |
startLifetime |
initial lifetime (maximum age) |
startSpeed |
initial speed |
startPosition |
initial position |
startRotation |
initial rotation |
randomizeLifetime |
randomize initial lifetime |
randomizeSpeed |
randomize initial speed |
randomizePosition |
randomize initial position |
randomizeRotation |
randomize initial rotation |
speedModifier |
factor to change speed every frame |
mass |
factor to change y-position every frame |
By putting all the data into one BufferGeometry it takes only one drawcall disregarding how many particles you display.
Theres a spritesheet option to put all textures into one file and let each particle use another image.
RibbonRenderer isn't optimized at all.
There's the plan to built in an object pool or ring buffer to reuse entities instead of creating and deleting them over and over again.
Build the package npm run build
. Go to /examples
, install the packages and run npm run dev
to show them in your browser.
- Hotfix: Instant emit burst without waiting one interval
- Clean build and publish
- Refactored RibbonRenderer to plain three.js
- Removed all react dependencies from src
- Separated systems into concerns. Color, size and opacity are handled individually now.
- Allow multiple instances by creating a new world along with a new ParticleSystem
- Entity creation moved inside ParticleSystem and aliased with addParticle and destroyParticle
- Changed blending to Blend Add
- Renamed randomizeDirection to randomizeRotation
- Added a lot of documentation
- Polished examples
- Refactored particle rendering to plain three.js geometry and material
- Added a lot of documentation
- Initial setup