featured image
Tutorials

Snowflake Particles: When Points just aren’t enough

   - 

So far everything we’ve built with particles have been classic GPU particles, meaning we used points and the entire lifetime of a particle is calculated from time and constants available when the particle is born. This has some great advantages, namely:

  • super fast. GPU does everything. minimal data transfer.
  • predictable, we know exactly what every particle with do.
  • works on most any hardware, no tricky modern features.

There are a few downsides, however.

  • GLPoints have a max size, and it varies from device to device & driver to driver.
  • The points always face the camera. you can’t rotate them at all.
  • Complex behavior that can’t be defined by a simple pure function can’t be done this way (without complex modern features like transform feedback).

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.

So when would you run up against these limits? In practice I’ve found points up to at least 50 pixels are supported everywhere, and for a whole lot of effects I don’t actually need to rotate the particles. They tend to be small and numerous, so their aggregate behavior is what creates the cool effect, not the orientation of any particular particle.

Needing a pure function is more of a challenge but generally I can work around it unless I want some sort of true physics system like particles that collide with each other or bird flocking behavior.

Let’s look at a real world example that I faced and how I had to use a different solution than GPU particles to solve it: Snowflakes.

Particles vs Planes

I can already make snow with GPU particles, but they are just little dots of blurry white that gently fall down. Great for a peaceful landscape, but I want something different. For an upcoming Christmas demo I want some snowflakes. Big giant huge cartoon-like snowflakes. Snowflakes that look like they came out of a stop motion Christmas special. The biggest challenge is rotation. These flakes will be huge and should rotate as they float through the screen. GLPoints simply can’t do this. So what to do?

Let’s consider what points really are. It’s a special mode that lets us send a buffer full of vertices to the GPU. The GPU will then draw each vertex as a different point. Since the bottleneck in modern GPUs is data transfer to the GPU rather than the actual drawing within the GPU, we’d like to send as little data as possible for as much cool stuff on the screen as possible.

Points are great for particles because there is only one vertex in the geometry for every object you seen on screen. That’s a one to one ratio. Pretty good.

Consider a triangle. For a triangle we have to send three vertices for every object on screen. A 3:1 ratio, not nearly as good. For 100 particles we’d need to send 300 vertices. In most cases we’d really want to render a quad instead of a triangle, which is 4x1.

It turns out 4:1 or 3:1 might not be so bad. In the case of my snowflakes I’m unlikely to have more than a few hundred particles on the screen at once, certainly not the tens of thousands I’d have with a fire effect. This is an okay trade off in this case. So how can we do this? Using the built in Plane object.

ThreeJS’s PlaneBufferGeometry uses only four vertices for the whole object when the widthSegments and heightSegments parameters are set to 1,1. Each snowflake is a full Object3D mesh, so we also have additional data tagging along for the position and orientation matrixes; but again for a few hundred snowflakes this should be fine.

Building the CPU Particles

With all of that decided building the new particle system is easy. Create N mesh objects with Plane geometry and a big snowflake texture. Give them attributes like current velocity, rotation angle, etc. On every render tick rotate and move the particles. Done. Essentially this is what we did in the first particle system before we learned about GPU particles. Call these CPU particles because all of the work is done on the CPU.

This is the code to create 100 snowflake objects.

//load the snowflake texture
const texture_loader = new THREE.TextureLoader()
const texture = texture_loader.load(‘./snowflake.png’)

//a group to hold all the snowflakes
flakes = new THREE.Group()
const geom = new THREE.PlaneBufferGeometry(1,1)
const mat = new THREE.MeshLambertMaterial({
    color:’white’, map:texture, 
    transparent:true, side:THREE.DoubleSide})

//make 100 snowflakes

for(let i=0; i<100; i++) {
   const flake = new THREE.Mesh(geom,mat)
   flake.position.set(rand(-10,10),rand(-5,7),rand(-5,0))
   flake.velocity = new THREE.Vector3(0,rand(-5,-30),0)
   const rot = randi(0,3)
   if(rot === 0) flake.rotationVelocity
       = new THREE.Vector3(rand(-30,30),0,0)
   if(rot === 1) flake.rotationVelocity
       = new THREE.Vector3(0,rand(-30,30),0)
   if(rot === 2) flake.rotationVelocity 
       = new THREE.Vector3(0,0,rand(-30,30))
   flakes.add(flake)
}

flakes.position.z = -2
scene.add(flakes)

In the code above each snowflake gets a random position within a certain range, a random velocity in the Y direction (since they are falling), and a random rotation velocity in one of the X, Y, or Z axes. It also puts all of the snowflakes into a single flakes group so that they can be positioned in the scene as a single unit.

The next bit of code updates the snowflakes on every tick:

 //called on every frame
 function render(time) {
   if(flakes) {
     flakes.children.forEach(flake => {
         flake.position.x += flake.velocity.x/1000
         flake.position.y += flake.velocity.y/1000
         flake.position.z += flake.velocity.z/1000
         flake.rotation.x += flake.rotationVelocity.x/1000
         flake.rotation.y += flake.rotationVelocity.y/1000
         flake.rotation.z += flake.rotationVelocity.z/1000
         if(flake.position.y < -10) flake.position.y += 20
     })
   }
   renderer.render( scene, camera );
 }

Note the conditional that if the Y value is less than -10 to move the flake back up top. This essentially “recycles” the particles.

Now we have a particle system that looks like this:

snowflakes1

Beautiful Giant Cartoon-like Snowflakes

Try it out live!

Perfect. Just what I wanted. To improve the look I could add some sideways velocity or some kind of a sine wave to make them flutter around a bit. For snow this is probably fine but if I was going to do leaves falling in an autumn scene the wave motion would be nice. In other words: season to taste.

Efficiency improvements

This approach could be improved. The default plane object uses 4 vertexes. Using some clever use instanced geometry we could combine all of our plane meshes into one, but since the total object count is so low it’s easier to just use Planes.

Also, all of the per frame updates are done on the CPU. This means updating matrices on every snowflake for every frame. Since the motion is still technically a pure function (depending only on initial conditions and time), to be more efficient I could do the position and rotation updates in a vertex shader.

However, for now performance is good enough. I’m going to mark these as a future opportunity for improvement in case performance becomes an issue later on. For now it works and I can get back to building a cool winter scene.

Until Next Time

Remember, if you are working on a cool WebVR experience that you’d like to have showcased right in Firefox Reality, let me know.