Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
GPU-accelerated particle systems with physics simulation, texture sheet animation, and advanced behaviors for fire, smoke, rain, and custom effects.
Web Engine's particle system uses GPU instancing for high-performance rendering of thousands of particles. Each emitter supports physics, color/size interpolation, texture sheet animation, and audio playback.
Render 100k+ particles at 60 FPS using instanced meshes
Gravity, velocity, curl noise turbulence, and forces
Sprite sheet animation with configurable tiles and speed
Sphere, cone, and box emission shapes
Color, size, opacity, and rotation over lifetime
Continuous and burst emission modes
Add a ParticleEmitter component to an entity to create a particle system:
import { createEntity, addComponent } from 'bitecs';import {Transform,ParticleEmitter} from '@web-engine-dev/core/engine/ecs/components';// Create emitter entityconst emitter = createEntity(world);addComponent(world, emitter, Transform);addComponent(world, emitter, ParticleEmitter);// Position emitterTransform.position[emitter][0] = 0;Transform.position[emitter][1] = 5;Transform.position[emitter][2] = 0;// Configure emitterParticleEmitter.playing[emitter] = 1; // EnableParticleEmitter.maxParticles[emitter] = 1000; // Max particlesParticleEmitter.emissionRate[emitter] = 50; // Particles/sec// Particle propertiesParticleEmitter.lifetime[emitter] = 2.0; // SecondsParticleEmitter.speed[emitter] = 5.0; // Initial velocityParticleEmitter.size[emitter] = 0.5; // Start sizeParticleEmitter.sizeEnd[emitter] = 0.1; // End size// Color (RGB 0-1)ParticleEmitter.color[emitter][0] = 1.0; // RedParticleEmitter.color[emitter][1] = 0.5; // GreenParticleEmitter.color[emitter][2] = 0.0; // BlueParticleEmitter.colorEnd[emitter][0] = 1.0;ParticleEmitter.colorEnd[emitter][1] = 0.0;ParticleEmitter.colorEnd[emitter][2] = 0.0;// OpacityParticleEmitter.opacity[emitter] = 1.0;ParticleEmitter.opacityEnd[emitter] = 0.0; // Fade out
Control the direction and spread of emitted particles:
| Shape | Value | Description |
|---|---|---|
| Sphere | 0 | Emit in all directions (fireworks, explosions) |
| Cone | 1 | Emit in a cone shape (fire, smoke) |
| Box | 2 | Emit upward in a box (fountains) |
// Sphere emission (omnidirectional)ParticleEmitter.shape[emitter] = 0;// Cone emission (directional)ParticleEmitter.shape[emitter] = 1;ParticleEmitter.angle[emitter] = 0.5; // Cone angle in radians// Box emission (upward)ParticleEmitter.shape[emitter] = 2;
Apply gravity or custom forces to particles:
// Gravity (world units per second squared)ParticleEmitter.gravity[emitter][0] = 0; // XParticleEmitter.gravity[emitter][1] = -9.8; // Y (downward)ParticleEmitter.gravity[emitter][2] = 0; // Z// Or custom forcesParticleEmitter.gravity[emitter][0] = 2; // Wind to the rightParticleEmitter.gravity[emitter][1] = -5; // Slight downwardParticleEmitter.gravity[emitter][2] = 0;
Add organic, swirling motion to particles using curl noise:
// Turbulence strengthParticleEmitter.noiseStrength[emitter] = 2.0;// Noise frequency (higher = more detail)ParticleEmitter.noiseFrequency[emitter] = 0.5;// Creates divergence-free turbulence// Perfect for smoke, fire, magic effects
// Rotation speed (radians per second)ParticleEmitter.rotationSpeed[emitter] = 2.0;// Variation (randomness)ParticleEmitter.rotationVariation[emitter] = 1.0;
Animate particles using sprite sheets for effects like fire, explosions, or magical spells:
// Load textureconst textureAssetId = AssetRegistry.loadTexture('particles/fire-sheet.png');// Configure texture sheetParticleEmitter.textureAssetId[emitter] = textureAssetId;ParticleEmitter.tilesX[emitter] = 4; // 4 columnsParticleEmitter.tilesY[emitter] = 4; // 4 rows (16 frames total)// Animation speed (frames per second)ParticleEmitter.animationSpeed[emitter] = 12;// Frames advance automatically based on particle age
Creating Sprite Sheets
Arrange animation frames in a grid (e.g., 4x4 or 8x8). The engine automatically calculates UV offsets and plays the animation over the particle's lifetime or at a fixed frame rate.
Emit particles in bursts instead of continuously:
// Enable burst modeParticleEmitter.emissionBursts[emitter] = 5; // Number of burstsParticleEmitter.emissionBurstSize[emitter] = 100; // Particles per burstParticleEmitter.emissionBurstInterval[emitter] = 1.0; // Seconds between bursts// Continuous emission rate (set to 0 for burst-only)ParticleEmitter.emissionRate[emitter] = 0;// Bursts decrement automatically// When emissionBursts reaches 0, emission stops
Add variation to make particles look more natural:
// Velocity variation (+/- random value)ParticleEmitter.velocityVariation[emitter] = 2.0;// Size variationParticleEmitter.sizeVariation[emitter] = 0.2;// Rotation variationParticleEmitter.rotationVariation[emitter] = 1.0;// Color variationParticleEmitter.colorVariation[emitter] = 0.1;// Each particle gets random values within these ranges
Trigger sounds when particles emit for enhanced immersion:
// Load audio assetconst soundAssetId = AssetRegistry.loadAudio('sounds/fire-whoosh.mp3');// Configure emitter audioParticleEmitter.audioAssetId[emitter] = soundAssetId;ParticleEmitter.audioVolume[emitter] = 0.8;ParticleEmitter.audioPitch[emitter] = 1.0;// Spatial audio (3D positioned)ParticleEmitter.audioSpatial[emitter] = 1;ParticleEmitter.audioRefDistance[emitter] = 5; // Full volume distanceParticleEmitter.audioMaxDistance[emitter] = 50; // Falloff distanceParticleEmitter.audioRolloff[emitter] = 1.0; // Falloff factor
const fireEmitter = createEntity(world);addComponent(world, fireEmitter, Transform);addComponent(world, fireEmitter, ParticleEmitter);// PositionTransform.position[fireEmitter][1] = 0;// EmissionParticleEmitter.playing[fireEmitter] = 1;ParticleEmitter.maxParticles[fireEmitter] = 500;ParticleEmitter.emissionRate[fireEmitter] = 100;ParticleEmitter.shape[fireEmitter] = 1; // ConeParticleEmitter.angle[fireEmitter] = 0.3;// PhysicsParticleEmitter.lifetime[fireEmitter] = 1.5;ParticleEmitter.speed[fireEmitter] = 3.0;ParticleEmitter.gravity[fireEmitter][1] = 2.0; // Rise upParticleEmitter.noiseStrength[fireEmitter] = 1.5; // Turbulence// SizeParticleEmitter.size[fireEmitter] = 0.5;ParticleEmitter.sizeEnd[fireEmitter] = 1.5; // Grow// Color (yellow to red to black)ParticleEmitter.color[fireEmitter][0] = 1.0; // YellowParticleEmitter.color[fireEmitter][1] = 0.8;ParticleEmitter.color[fireEmitter][2] = 0.0;ParticleEmitter.colorEnd[fireEmitter][0] = 0.3; // Dark redParticleEmitter.colorEnd[fireEmitter][1] = 0.0;ParticleEmitter.colorEnd[fireEmitter][2] = 0.0;// OpacityParticleEmitter.opacity[fireEmitter] = 0.8;ParticleEmitter.opacityEnd[fireEmitter] = 0.0;// RotationParticleEmitter.rotationSpeed[fireEmitter] = 1.0;
const rainEmitter = createEntity(world);addComponent(world, rainEmitter, Transform);addComponent(world, rainEmitter, ParticleEmitter);// Position high above groundTransform.position[rainEmitter][1] = 20;// EmissionParticleEmitter.playing[rainEmitter] = 1;ParticleEmitter.maxParticles[rainEmitter] = 2000;ParticleEmitter.emissionRate[rainEmitter] = 500;ParticleEmitter.shape[rainEmitter] = 2; // Box// PhysicsParticleEmitter.lifetime[rainEmitter] = 3.0;ParticleEmitter.speed[rainEmitter] = 0.0; // Start stillParticleEmitter.gravity[rainEmitter][1] = -20; // Fall fast// Size (thin streaks)ParticleEmitter.size[rainEmitter] = 0.05;ParticleEmitter.sizeEnd[rainEmitter] = 0.05;// Color (light blue)ParticleEmitter.color[rainEmitter][0] = 0.7;ParticleEmitter.color[rainEmitter][1] = 0.8;ParticleEmitter.color[rainEmitter][2] = 1.0;// OpacityParticleEmitter.opacity[rainEmitter] = 0.6;ParticleEmitter.opacityEnd[rainEmitter] = 0.0;
const explosionEmitter = createEntity(world);addComponent(world, explosionEmitter, Transform);addComponent(world, explosionEmitter, ParticleEmitter);// Burst emission (one-shot)ParticleEmitter.playing[explosionEmitter] = 1;ParticleEmitter.maxParticles[explosionEmitter] = 500;ParticleEmitter.emissionRate[explosionEmitter] = 0;ParticleEmitter.emissionBursts[explosionEmitter] = 1;ParticleEmitter.emissionBurstSize[explosionEmitter] = 500;ParticleEmitter.shape[explosionEmitter] = 0; // Sphere// PhysicsParticleEmitter.lifetime[explosionEmitter] = 2.0;ParticleEmitter.speed[explosionEmitter] = 15.0; // Fast initial velocityParticleEmitter.velocityVariation[explosionEmitter] = 5.0;ParticleEmitter.gravity[explosionEmitter][1] = -5.0;// SizeParticleEmitter.size[explosionEmitter] = 1.0;ParticleEmitter.sizeEnd[explosionEmitter] = 0.1;// Color (white to orange to black)ParticleEmitter.color[explosionEmitter][0] = 1.0;ParticleEmitter.color[explosionEmitter][1] = 1.0;ParticleEmitter.color[explosionEmitter][2] = 1.0;ParticleEmitter.colorEnd[explosionEmitter][0] = 0.0;ParticleEmitter.colorEnd[explosionEmitter][1] = 0.0;ParticleEmitter.colorEnd[explosionEmitter][2] = 0.0;// AudioParticleEmitter.audioAssetId[explosionEmitter] = explosionSoundId;
| Optimization | Benefit | Trade-off |
|---|---|---|
| Reduce maxParticles | Less GPU/CPU work | Less dense effects |
| Lower emission rate | Fewer particles spawned | Thinner effects |
| Disable distant emitters | Skip invisible work | Requires distance checks |
| Use simpler textures | Faster rendering | Less visual detail |
| Reduce particle lifetime | Fewer active particles | Shorter effects |
// Disable emitters far from camerafunction updateParticleEmitters(world, camera) {const entities = emitterQuery(world);for (let i = 0; i < entities.length; i++) {const eid = entities[i];const distance = camera.position.distanceTo(new THREE.Vector3(Transform.position[eid][0],Transform.position[eid][1],Transform.position[eid][2]));// Disable if too farParticleEmitter.playing[eid] = distance < 100 ? 1 : 0;}}
The particle system respects the prefers-reduced-motion setting:
import { motionPreferences } from '@web-engine-dev/core/engine/accessibility';// Check motion preferencesconst prefs = motionPreferences.getPreferences();if (!prefs.enableParticles) {// Particles are completely disabled// Emitters are hidden but kept alive}// Reduced particle density for reduced motionconst density = prefs.particleDensityMultiplier; // 0.25 for reduced motionParticleEmitter.emissionRate[eid] *= density;ParticleEmitter.emissionBurstSize[eid] *= density;
Overdraw
Particles use transparent materials which require sorting and can cause overdraw (multiple layers rendered on top of each other). Avoid large, overlapping particles and prefer smaller particles with higher counts for better performance.