featured image
ThreeJS

ThreeJS Particles: Color Interpolation

   - 

Our particle system is working pretty well. We can spawn particles whenever we want, in any direction with any position, velocity, or acceleration. We can even set the color. What we can’t do is change the color over time.

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.

Using a single color is fine for something like snow. Here’s a quick example that randomly positions the particles in the sky above a little Christmas landscape, then has them fall down at a constant speed (acceleration is 0).

const options = {
     maxParticles: 10000,
     position: new THREE.Vector3(0,5,-5),
     positionRandomness: 0.0,
     baseVelocity: new THREE.Vector3(0.0, -1.0, 0.0),
     velocity: new THREE.Vector3(0.0, -0.5, 0.0),
     velocityRandomness: 1.0,
     acceleration: new THREE.Vector3(0.0,0.0,0.0),
     color: new THREE.Color(1.0,1.0,1.0),
     endColor: new THREE.Color(1.0,1.0,1.0),
     colorRandomness: 0.0,
     lifetime: 30.0,
     size: 20,
     sizeRandomness: 30.0,
     particleSpriteTex: texture,
     blending: THREE.NormalBlending,
     onTick:(system,time) => {
         for(let i=0; i<1; i++) {
             options.position.set(rand(-5,5), 8, rand(0,-5))
             options.color.set(0xffffff)
             system.spawnParticle(options);
        }
     }
}
snow = new GPUParticleSystem(options)
snow.position.z = -3
scene.add(snow)

Snow Scene

Multiple Colors

Now let’s suppose we want to create a flame. By setting the blend mode to additive we can have yellow that adds up to white in the center, but it’ll never fade to red. What we want is to make the color go from yellow to red over time. Fortunately this is pretty easy. We just need to add another buffer attribute for the end color.

This code is scattered over several places, so let’s get going.

In the vertex shader add an attribute for the endColor, and copy it out to a varying for the fragment shader to use next.

attribute vec3 endColor;
varying vec4 vEndColor;
…
void main() {
…
 vColor = vec4( color, 1.0 );
 vEndColor = vec4( endColor, 1.0);
…

In the fragment shader calculate the final color by mixing the start and end colors using the lifetime value.

varying vec4 vColor;
varying vec4 vEndColor;
varying float lifeLeft;
uniform sampler2D tSprite;
void main() {
 vec4 tex = texture2D( tSprite, gl_PointCoord );
 vec4 color = mix(vColor, vEndColor, 1.0-lifeLeft);
 gl_FragColor = vec4( color.rgb, lifeLeft * tex.a);
}

Now add endColor next to color where the new attributes are created in the GPUParticleSystem constructor

//regular color 
this.geometry.addAttribute(‘color’, new THREE.BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3) .setDynamic(true));
//end color
this.geometry.addAttribute(‘endColor’, new THREE.BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true));

And update spawnParticle to set the value as well

colorAttribute.array[i * 3 + 0] = color.r;
colorAttribute.array[i * 3 + 1] = color.g;
colorAttribute.array[i * 3 + 2] = color.b;

endcolorAttribute.array[i * 3 + 0] = endColor.r;
endcolorAttribute.array[i * 3 + 1] = endColor.g;
endcolorAttribute.array[i * 3 + 2] = endColor.b;

That’s it. It’s just another attribute. Pretty much every place that uses color now also uses endColor. You can see the full code here.

Flames!

Now let’s move on to the torch.

We want the torch to have particles which start in one place, so positionRandomness is 0. The velocity is upward with a randomness of 0.3 so they will fly slightly to the sides. Acceleration is 0. The color and endColor are yellow and red with a slight randomness. I set lifetime to 3 and size to 40, but you’d need to play with these values to find the right ones. Also remember to set the blending to Additive.

This is the code to initialize the torch

const options = {
  maxParticles: 1000,
  position: new THREE.Vector3(0,0,0),
  positionRandomness: 0.0,
  baseVelocity: new THREE.Vector3(0.0, 1.0, 0.0),
  velocity: new THREE.Vector3(0.0, 0.0, 0.0),
  velocityRandomness: 0.3,
  acceleration: new THREE.Vector3(0.0,0.0,0.0),
  color: new THREE.Color(0.5,0.3,0.0),
  endColor: new THREE.Color(0.5,0.0,0.0),
  colorRandomness: 0.5,
  lifetime: 3.0,
  size: 40,
  sizeRandomness: 1.0,
  particleSpriteTex: textureLoader.load(‘./tex/particle2.png’),
  blending: THREE.AdditiveBlending,
  onTick:(system,time) => {
    options.velocity.x = options.baseVelocity.x 
           + system.random() *    options.velocityRandomness
    options.velocity.y = options.baseVelocity.y 
           + system.random() * options.velocityRandomness*2.0
    options.velocity.z = options.baseVelocity.z 
           + system.random() * options.velocityRandomness
    system.spawnParticle( options );
   }
 }

Note how each particle is given a final velocity by adding a random value times the velocityRandomness setting plus the base velocity. Also notice that the vertical velocity has twice the randomness of the x and y values. Again I just played with a few values and this looked the best.

With three torches plus a floor and torch holders I got this cool scene.

Torches with Flames

And by changing the particle size I got this:

Bigger flame particles

Live demo here!

It’s amazing what we can do with particle systems by just changing the settings. See you 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.