three-emitter
v0.0.5
Published
High performance particle emitter for THREE.js
Downloads
101
Maintainers
Readme
three-emitter
A high performance particle emitter for THREE.js.
The library provides an abstraction layer for InstancedBufferGeometry and RawShaderMaterial allowing you to spawn multiple particle emitters running on the same shader program.
Demos
Single Particle Emitter (500k particles)
Multiple Particle Emitters
Installation
Dependencies
Has a peer dependency to THREE.js.
NPM
Install via:
npm install three three-emitter
Import in ESM projects:
import * as THREE from "three";
import { Emitter, EmitterInstance } from "three-emitter";
Import in CJS projects:
const THREE = require("three");
const { Emitter, EmitterInstance } = require("three-emitter");
Browser / CDN
Configure import map:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@latest/build/three.module.js",
"three-emitter": "https://cdn.jsdelivr.net/npm/three-emitter@latest/dist/browser/three-emitter.min.js",
}
}
</script>
Import in your project:
import * as THREE from "three";
import { Emitter, EmitterInstance } from "three-emitter";
Usage
For details see the documentation and the source code for single emitters and multiple emitters.
The library provides an abstraction layer for InstancedBufferGeometry and RawShaderMaterial allowing you to spawn multiple particle emitters running on the same shader program. There are two main interfaces: Emitter
to define particle emitters and EmitterInstance
to spawn multiple particle emitters per definition:
Particle Emitter Definition
Before adding particle emitters to the scene, they'll need to be defined.
Instantiate the emitter:
import * as THREE from "three";
import { Emitter, EmitterInstance } from "three-emitter";
const emitter = new Emitter();
Set the maximum number of particles:
emitter.maxParticles = 100000;
Define the base geometry to be used for particles. This is optional as shapes can alternatively be calculated in the fragment shader. Defining a geometry will run the shader with default geometry attributes/uniforms (see THREE.WebGLProgram):
emitter.setAttributesFromGeometry(THREE.PlaneGeometry(1, 1));
Define material parameters. This is optional. Emitter
will internally create a RawShaderMaterial and use those parameters to instantiate it:
emitter.setMaterialParameters({
transparent: true,
side: THREE.FrontSide
});
Define emitter uniforms. They will be available in the corresponding shaders. Uniforms are per-emitter (not per-particle) so the memory footprint is small. Use them to update emitter-wide attributes, e.g. global color values, textures, etc.
emitter.setUniforms({
color: new THREE.Uniform([1.0, 0.1, 1.0, 0.5]),
});
This uniform will then be available in the shader:
uniform vec4 color;
void main() {
// ...
}
Define emitter attributes. They will be available in the corresponding shaders. Attributes are per-particle (not per-emitter) so the memory footprint is higher compared to uniforms as the shader stores one value per particle instance. For example a 3D vector for 1000 instances will generate an ArrayBuffer with a length of 3000. Use them to update per-particle attributes, e.g. local position, randomized values etc.
Note, THREE.js provides a standard type for shader attributes. The first parameter is the length of the buffer. This should always be maxParticles * attributeDimensions
(e.g. maxParticles * 3
for a 3D vector). The second parameter indicates the dimensional shape itself. The renderer will use the second parameter to determine attribute type (3
= vec3
, 2
= vec2
, 1
= float
etc.)
emitter.setAttributes({
rng: new THREE.InstancedBufferAttribute(new Float32Array(emitter.maxParticles), 1),
});
This attrbute will then be available in the shader:
attribute float rng;
void main() {
// ...
}
Define the vertex shader to render particles:
emitter.setVertexShader(`
attribute float rng;
void main() {
// ...
}
`);
Define the fragment shader to render particles:
emitter.setVertexShader(`
uniform vec4 color;
void main() {
// ...
}
`);
All of the above combined:
import * as THREE from "three";
import { Emitter, EmitterInstance } from "three-emitter";
const maxParticles = 10000;
const emitter = new Emitter({
geometry: new THREE.CircleGeometry(1.0, 8),
materialParameters: {
transparent: true,
side: THREE.FrontSide
},
maxParticles,
attributes: {
rng: new THREE.InstancedBufferAttribute(new Float32Array(emitter.maxParticles), 1),
},
uniforms: {
color: new THREE.Uniform([1.0, 0.1, 1.0, 0.5]),
},
vert: `
precision highp float;
// From default geometry
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
// From custom attributes
attribute float rng;
// From Emitter
uniform float time;
void main() {
vec4 animPos = vec4(position.x + cos(time * -rng) * 5.0, position.y + sin(time + rng) * 5.0, position.z, 0.0);
gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0) + animPos);
}`,
frag: `
precision highp float;
// From custom uniforms
uniform vec4 color;
void main() {
gl_FragColor = color;
}`
});
Spawn one particle emitter
To spawn a single particle emitter, no further instancing is required. Add the emitter to the scene, set the particle amount and fill array buffers:
emitter.particleAmount = 1000;
emitter.fillAttribute("rng", () => Math.random());
myScene.add(emitter);
Optionally graudually mutate attributes/uniforms:
function tick(ms = 0) {
// Modulate opacity
emitter.uniforms.color.value[3] = Math.sin(ms);
// Some other, highly questionable calculations
emitter.attributes.fillAttribute("rng", i => Math.min(1, Math.max(0, emitter.attributes.rng.array[i] + Math.random() * 0.1, 0, 1)));
requestAnimationFrame(tick);
}
tick();
Spawn multiple emitters
Use EmitterInstance
to spawn multiple particle emitters off of one Emitter
definition. All EmitterInstance
s will run on the same shader program. Re-useing the previous emitter definition, add another attribute and update the vertex shader to allow position manipulation:
emitter.setAttributes({
emitterPosition: new THREE.InstancedBufferAttribute(new Float32Array(emitter.maxParticles * 3), 3),
});
emitter.setVertexShader(`
precision highp float;
// From default geometry
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
// From custom attributes
attribute float rng;
attribute vec3 emitterPosition;
// From Emitter
uniform float time;
void main() {
vec4 pos = vec4(position.x, position.y, position.z, 0.0) +
vec4(position.x + cos(time * -rng) * 5.0, position.y + sin(time + rng) * 5.0, position.z, 0.0) +
vec4(emitterPosition, 0.0);
gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0) + pos);
}
`);
Add the emitter to the scene and spawn multiple instances:
myScene.add(emitter);
const particlesPerEmitter = 1000;
const instances = [];
for (let i = 0; i <= 10; i++) {
const instance = new EmitterInstance(emitter, particlesPerEmitter);
// The following is the same as `emitter.fillAttribute` but instead of populating the entire ArrayBuffer
// it'll only fill the portion of the ArrayBuffer that is provided to the particle instances associated
// with this emitter instance
instance.fillAttribute("rng", () => Math.random());
instance.fillAttribute("emitterPosition", [i, i, i]);
}
Optionally mutate attributes/uniforms:
function tick(ms = 0) {
for (const [i, instance] of instances.entries()) {
// Modulate opacity
emitter.uniforms.color.value[3] = Math.sin(ms);
// Move particle emitters in circles:
emitter.attributes.fillAttribute("emitterPosition", [i + Math.sin(ms), i, i + Math.cos(ms)]);
}
requestAnimationFrame(tick);
}
tick();
The scene now consists of 10 independently positioned/animated particle emitters. All in one draw call.
Documentation
Classes
| Class | Description | | ------ | ------ | | Emitter | A particle emitter. Provides an easy to use interface for THREE's InstancedBufferGeometry and RawShaderMaterial to spawn and manage particle emitters. Can emit particles on its own or can be used to create EmitterInstances with distinct shader attributes/uniforms while running on the same shader program. | | EmitterInstance | A particle emitter instance. Provides the possibility to define and mutate shader attributes on a subset of particles while running on a shared shader program. Possible use cases: - Emit particles in different spacial locations - Emit particles with different shader attributes |
Interfaces
| Interface | Description |
| ------ | ------ |
| IEmitterOptions | Options for the Emitter
. |