A simple and easy-to-use vanilla Three.js library for creating visual effects.
npm install wawa-vfx-vanillaA simple and easy-to-use vanilla Three.js library for creating visual effects.
Live demo - Fireworks demo - Wizard Game demo
> This powerful VFX particle system was developed as part of the comprehensive VFX & Advanced Rendering Chapter in my React Three Fiber: The Ultimate Guide to 3D Web Development course.
>
> In the course, we break down every aspect of this system, explaining the mathematics, optimization techniques, and design patterns that make it work.
``bash`
npm install wawa-vfx-vanilla
Wawa VFX Vanilla uses a two-class system:
- VFXParticles: Defines the particle system and its rendering propertiesVFXEmitter
- : Controls how and when particles are emitted into the scene
`javascript
import * as THREE from 'three';
import { VFXEmitter, VFXParticles, AppearanceMode } from 'wawa-vfx-vanilla';
// Create a basic Three.js scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Create particle system
const particles = new VFXParticles('fireParticles', {
nbParticles: 10000,
gravity: [0, -2, 0],
renderMode: 'billboard',
intensity: 2,
appearance: AppearanceMode.Circular,
easeFunction: 'easeOutCubic',
fadeAlpha: [0.1, 0.9],
fadeSize: [0.2, 0.8],
blendingMode: THREE.AdditiveBlending,
});
// Add to scene
scene.add(particles.getMesh());
// Create emitter
const emitter = new VFXEmitter('fireParticles', {
loop: true,
duration: 2,
nbParticles: 50,
spawnMode: 'time',
delay: 0,
// Particle lifetime
particlesLifetime: [0.5, 2.0],
// Position range
startPositionMin: [-0.2, 0, -0.2],
startPositionMax: [0.2, 0.5, 0.2],
// Rotation
startRotationMin: [0, 0, 0],
startRotationMax: [Math.PI * 2, 0, 0],
rotationSpeedMin: [-1, 0, -1],
rotationSpeedMax: [1, 0, 1],
// Direction and movement
directionMin: [-0.5, 1, -0.5],
directionMax: [0.5, 2, 0.5],
size: [0.02, 0.1],
speed: [2, 5],
// Colors
colorStart: ['#ff6b35', '#f7931e'],
colorEnd: ['#ff0000', '#8b0000'],
// Use local direction
useLocalDirection: false,
});
// Add emitter to scene
scene.add(emitter);
// Position camera
camera.position.z = 3;
// Animation loop
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
const elapsedTime = clock.getElapsedTime();
// Update VFX system
emitter.update(elapsedTime, deltaTime);
particles.update(elapsedTime);
renderer.render(scene, camera);
}
animate();
`
You can use any Three.js geometry for your particles:
`javascript
import * as THREE from 'three';
import { VFXEmitter, VFXParticles } from 'wawa-vfx-vanilla';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// Load a custom model
const loader = new GLTFLoader();
loader.load('/models/sword.glb', (gltf) => {
const swordGeometry = gltf.scene.children[0].geometry;
// Create particles with custom geometry
const swordParticles = new VFXParticles('swords', {
nbParticles: 1000,
renderMode: 'mesh',
intensity: 2,
gravity: [0, -5, 0],
}, undefined, undefined, swordGeometry); // Pass geometry as 5th parameter
scene.add(swordParticles.getMesh());
// Create emitter for sword particles
const swordEmitter = new VFXEmitter('swords', {
spawnMode: 'burst',
nbParticles: 100,
particlesLifetime: [2, 4],
startPositionMin: [-5, 5, -5],
startPositionMax: [5, 10, 5],
directionMin: [-1, -1, -1],
directionMax: [1, -0.5, 1],
size: [0.5, 1.5],
speed: [3, 8],
colorStart: ['#silver', '#gold'],
colorEnd: ['#darkgray'],
});
scene.add(swordEmitter);
});
`
`javascript`
new VFXParticles(name, settings, store?, alphaMap?, geometry?)
| Parameter | Type | Description |
| ---------- | ----------------- | ------------------------------------------------ |
| name | string | Unique identifier for this particle system |settings
| | object | Configuration options for particles |store
| | VFXStore | Optional store instance (uses global if not provided) |alphaMap
| | THREE.Texture | Optional texture for particle alpha/transparency |geometry
| | THREE.BufferGeometry | Optional custom geometry for particles |
#### VFXParticles Settings
| Setting | Type | Default | Description |
| ------------- | ----------------------- | ----------------- | -------------------------------------------------------------- |
| nbParticles | number | 1000 | Maximum number of particles |
| intensity | number | 1 | Brightness multiplier |
| renderMode | 'billboard' \| 'mesh' \| 'stretchBillboard' | 'mesh' | How particles are rendered |
| stretchScale | number | 1.0 | Stretch factor for stretchBillboard mode |
| fadeSize | [number, number] | [0.1, 0.9] | Size fade in/out range (0-1 of lifetime) |
| fadeAlpha | [number, number] | [0, 1.0] | Opacity fade in/out range |
| gravity | [number, number, number]| [0, 0, 0] | Gravity force applied to particles |
| frustumCulled | boolean | true | Whether particles are frustum culled |
| appearance | AppearanceMode | AppearanceMode.Square | Particle appearance (Square or Circular) |
| easeFunction | EaseFunction | 'easeLinear' | Easing function for particle animations |
| blendingMode | THREE.Blending | AdditiveBlending | How particles blend with the scene |
| shadingHooks | object | {} | Custom GLSL shader hooks for advanced rendering effects |
`javascript`
new VFXEmitter(targetParticles, settings, store?)
| Parameter | Type | Description |
| ---------------- | --------- | ------------------------------------------- |
| targetParticles| string | Name of the target particle system |settings
| | object | Configuration options for emission behavior |store
| | VFXStore | Optional store instance (uses global if not provided) |
#### VFXEmitter Settings
| Setting | Type | Default | Description |
| ----------------- | -------------------------- | ---------- | ------------------------------------------------ |
| loop | boolean | true | Continuously emit particles |
| duration | number | 1 | Emission cycle duration in seconds |
| nbParticles | number | 100 | Number of particles to emit per cycle |
| spawnMode | 'time' \| 'burst' | 'time' | How particles are spawned |
| delay | number | 0 | Time delay before starting emission |
| particlesLifetime | [number, number] | [0.1, 1] | Particle lifetime range [min, max] |
| startPositionMin | [number, number, number] | [-0.1, -0.1, -0.1] | Minimum start position |
| startPositionMax | [number, number, number] | [0.1, 0.1, 0.1] | Maximum start position |
| startRotationMin | [number, number, number] | [0, 0, 0] | Minimum start rotation |
| startRotationMax | [number, number, number] | [0, 0, 0] | Maximum start rotation |
| rotationSpeedMin | [number, number, number] | [0, 0, 0] | Minimum rotation speed |
| rotationSpeedMax | [number, number, number] | [0, 0, 0] | Maximum rotation speed |
| directionMin | [number, number, number] | [-1, 0, -1]| Minimum emission direction |
| directionMax | [number, number, number] | [1, 1, 1] | Maximum emission direction |
| size | [number, number] | [0.01, 0.25] | Particle size range [min, max] |
| speed | [number, number] | [1, 12] | Particle speed range [min, max] |
| colorStart | string[] | ['white'] | Colors at start (randomly selected) |
| colorEnd | string[] | ['white'] | Colors at end (randomly selected) |
| useLocalDirection | boolean | false | Use emitter's local space for directions |
| Method | Description |
| ------------------------- | ----------------------------------------- |
| getMesh() | Get the Three.js mesh for adding to scene|update(elapsedTime)
| | Update particles (call in animation loop)|updateSettings(settings)
| | Update particle settings at runtime |dispose()
| | Clean up resources |
| Method | Description |
| ------------------------- | ----------------------------------------- |
| update(elapsedTime, deltaTime) | Update emitter (call in animation loop) |startEmitting(reset?)
| | Start particle emission |stopEmitting()
| | Stop particle emission |emitAtPos(position, reset?)
| | Emit particles at specific position |
You can programmatically control emitters using the VFX store:
`javascript
import { vfxStore } from 'wawa-vfx-vanilla';
const store = vfxStore.getState();
// Emit particles manually
store.emit('myParticles', 10, () => ({
position: [0, 0, 0],
direction: [0, 1, 0],
scale: [1, 1, 1],
rotation: [0, 0, 0],
rotationSpeed: [0, 0, 0],
lifetime: [1, 2],
colorStart: '#ff0000',
colorEnd: '#0000ff',
speed: [5],
}));
// Register/unregister emitters
store.registerEmitter('myEmitter', emitFunction);
store.unregisterEmitter('myEmitter');
`
#### Stretch Billboard Mode
Render particles as billboards that stretch along their velocity direction, perfect for trails and speed lines:
`javascript`
const particles = new VFXParticles('trails', {
renderMode: 'stretchBillboard',
stretchScale: 2.0,
// ...
});
#### Particle Easings
Choose from 42 easing functions for smooth particle animations:
`javascript`
const particles = new VFXParticles('smooth', {
easeFunction: 'easeInOutCubic', // Available: easeLinear, easeInQuad, easeOutBounce, etc.
// ...
});
#### Appearance Modes
Control particle shape:
`javascript
import { AppearanceMode } from 'wawa-vfx-vanilla';
const particles = new VFXParticles('circles', {
appearance: AppearanceMode.Circular, // or AppearanceMode.Square
// ...
});
`
#### Local Direction Control
Control emission in local vs world space:
`javascript`
const emitter = new VFXEmitter('particles', {
useLocalDirection: true, // Particles follow emitter rotation
// ...
});
#### Custom Shader Hooks
Add custom GLSL shader code to modify particle rendering:
`javascript
const particles = new VFXParticles('customShader', {
nbParticles: 5000,
renderMode: 'billboard',
shadingHooks: {
fragmentBeforeOutput: / glsl /
// Add a simple pulsing effect based on time
float pulse = 0.5 + 0.5 sin(uTime 10.0 + vProgress * 10.0);
finalColor.rgb *= pulse;
finalColor.r = 1.0; // Enhance red channel
,`
},
// ... other settings
});
Available shader hooks:
- customUniforms: Add custom uniform declarations
- customVaryings: Add custom varying declarations
- vertexBeforeOutput: Modify vertex shader output before final gl_Position
- fragmentBeforeOutput: Modify the final color before it's written to the screen
Built-in shader variables accessible in hooks:
- uTime: Current time uniformvProgress
- : Particle lifetime progress (0 to 1)finalColor
- : The computed particle color in fragment shader
The library includes 42 easing functions:
- Linear: easeLineareaseInQuad
- Quad: , easeOutQuad, easeInOutQuadeaseInCubic
- Cubic: , easeOutCubic, easeInOutCubiceaseInQuart
- Quart: , easeOutQuart, easeInOutQuarteaseInQuint
- Quint: , easeOutQuint, easeInOutQuinteaseInSine
- Sine: , easeOutSine, easeInOutSineeaseInExpo
- Expo: , easeOutExpo, easeInOutExpoeaseInCirc
- Circ: , easeOutCirc, easeInOutCirceaseInBack
- Back: , easeOutBack, easeInOutBackeaseInElastic
- Elastic: , easeOutElastic, easeInOutElasticeaseInBounce
- Bounce: , easeOutBounce, easeInOutBounce
- 🚀 Pure Three.js: No React dependencies, works with any Three.js project
- 🎨 Flexible Customization: Extensive settings for fine-tuning visual effects
- ⚡ Performance Optimized: Uses instanced rendering for efficient particle systems
- 🔧 TypeScript Support: Full type definitions for better development experience
- 📦 Custom Geometry: Support for any Three.js geometry as particles
- 🎯 Programmatic Control: Full control through the VFX store
- 🌊 Advanced Rendering: Multiple render modes including stretch billboards
- 📐 Mathematical Precision: 42 easing functions for smooth animations
Check out the Live demo to see various effects in action:
- Fire effects
- Fireworks
- Magic spells
- Environmental effects
- Custom geometry particles
If you're using React Three Fiber, check out our React package: wawa-vfx
1. Reuse particle systems: Create one VFXParticles instance and multiple emittersnbParticles
2. Optimize particle counts: Start with lower and increase as neededbillboard
3. Use appropriate render modes: is faster than mesh for simple particlesupdate()` methods in your animation loop
4. Manage geometry: Custom geometries should be optimized (low poly count)
5. Update efficiently: Only call
MIT
Contributions are welcome! Please feel free to submit a Pull Request.