featured image
ThreeJS

ThreeJS Particles

 - 

This article is part of my ongoing series of medium difficulty ThreeJS tutorials. I’ve long wanted something in between the intro “How to draw a cube” and “Let’s fill the screen with shader madness” levels. So here it is.

Let’s make some particles.

As with all particle systems we need an array of particles. They should each have, at a minimum: a position, a size, a velocity, and a constant for gravity.

We are going to make the particle system a couple of times so that you get a feel for how they work, and you can see why we must make the choices we make. Let’s start off with just a naive implementation: an array of full 3D objects with some extra properties for velocity that we update on every tick. Our pseudocode looks like this:

create array of Object3D
   has position vector, start at 0,0,0
   add velocity vector, set at random x and z, y is fixed up
   add all to a group
   render as a small mesh
on every tick
   update velocity based on acceleration of gravity
   update position based on velocity

This is the real code to create a group full of spheres.

const rand = (min,max) => min + Math.random()*(max-min)
let scene, camera, renderer
let particles
const MAX = 100
function setupScene() {
    particles = new THREE.Group()
    const geo = new THREE.SphereBufferGeometry(0.1)
    const mat = new THREE.MeshLambertMaterial({color: ‘red’})
    for(let i=0; i<MAX; i++) {
        const particle = new THREE.Mesh(geo,mat)
        particle.velocity = new THREE.Vector3(
           rand(-0.01, 0.01),
           0.06, 
           rand(-0.01, 0.01))
        particle.acceleration = new THREE.Vector3(0,-0.001,0)
        particle.position.x = rand(-1,1)
        particle.position.z = rand(-1,1)
        particle.position.y = rand(-1,-3)
        particles.add(particle)
    }
    particles.position.z = -4
    scene.add(particles)
 }

This is how we update on every tick. For each particle add the acceleration to the velocity and the velocity to the position. Over time this will move the particles and speed them up.

function render(time) {
    particles.children.forEach(p => {
       p.velocity.add(p.acceleration)
       p.position.add(p.velocity)
    })
    renderer.render( scene, camera );
}

Particles as Spheres

So.. this does work but it’s gonna be slow. Really slow.

First, we have a full geometry for every particle. That’s fine if there’s just a few, like maybe for a bunch of balloons, but for doing full particle effects of fire and smoke this is way too heavy, even when we reuse materials and geometry like above.

The other reason this code is heavy is that full geometry and material means a full lighting pass. Instead we should use points. They are made to be fast.

Version 2

This time let’s create a lightweight array of JS objects and link just to the positions for the Points().

const MAX = 100
function setupScene() {
    particles = []
    const geo = new THREE.Geometry()
    for(let i=0; i<MAX; i++) {
        const particle = {
            position: new THREE.Vector3(
                  rand(-1, 1),
                  rand(-1, 1),
                  rand(-1, -3)),
            velocity: new THREE.Vector3(
                  rand(-0.01, 0.01),
                  0.06,
                  rand(-0.01, 0.01)),
            acceleration: new THREE.Vector3(0, -0.001, 0),
        }
        particles.push(particle)
        geo.vertices.push(particle.position)
    }
    const mat = new THREE.PointsMaterial({color:0xffffff,size: 0.1})
    mesh = new THREE.Points(geo,mat)
    mesh.position.z = -4
    scene.add(mesh)
}
function render(time) {
    particles.forEach(p => {
       p.velocity.add(p.acceleration)
       p.position.add(p.velocity)
    })
    mesh.geometry.verticesNeedUpdate = true
    renderer.render( scene, camera );
 }

The code above looks similar to what we started with.The main difference is that only the position is actually passed to the Geometry, and therefore into the GPU. For each pass we calculate new velocities and positions on the Javascript side, then pass just the new geometry to the GPU. This is much faster.

particles02

However, our particles could be faster still. We must always remember the fundamental constraint on GPU based graphics programming:

Change costs

The fewer things you need to change, the less must be sent to the GPU which costs bandwidth. Ideally we would just have a single variable to send to the GPU on each frame: time. Everything else could be calculated based on that.

This is in fact what the ThreeJS example GPUParticles does, though the code which does it is a bit complicated. Let’s come back to this next time.

Get Noticed

By the way, if you are working on a cool WebVR experience that you’d like to have showcased right inside Firefox Reality, let us know.