Back to Overview

GPU Particles


Particle simulations - this time in parallel! Every single point is being simulated independently, but since everything runs on the GPU, it can handle millions of agents at once! Isn't WebGL awesome?

This Demo is only available on desktop browsers, since most mobile devices don't currently support the required FLOAT textures. I've explained the details at the bottom of this page.

Design Decisions

Each particle is being represented as a pixel in a simulation texture. Because every pixel has 4 color channels, 4 floating point numbers can be stored per particle. The first two represent the 2D position (x and y coordinate), the second two usually get used for velocity. Since the particles in this particular simulation will be travelling at constant speeds, I decided to only store the direction instead and keep a free channel for additional data, like mass. At each time step we use a fragment shader to update the simulation texture, by calculating the new velocity and position using a simple Euler integration step. The updated colors are written to one of two framebuffers, that get swapped each frame (Ping-Pong Shader).

Data texture containing the particle information
Data texture containing the particle information. Each pixels color represents position and velocity of an according particle.

Drawing to the screen

Obviously we don't want a noisy image like in the figure above, so how do we draw the particles to the screen? That happens in the Vertex Shader. For each pixel of the simulation texture, a vertex is created, whose coordinates get read from the red and green channels of the according pixel. Since the simulation is only 2D, the z coordinate of all particles is set to 0. Using the handy GL_POINTS rendering mode, these vertecies can then be drawn to the screen as points!

Rounding errors from using 8bit integer textures
Initial results. What happened here?

What happened? The particles should be moving outwards in a straight line, but instead they are all collecting in the upper right corner! This error is caused by the numeric precision of the texture. By default, textures store color values as unsigned 8bit integers - that means only 256 values ranging [0, 1] are available. That might be fine for the typical color data stored in textures, but a simulation requires both higher precision and range. I opted for 32bit floating point textures, which allow for arbitrary unclamped values with very high numeric precision.

Particle Simulation using a 512x512 simulation texture
Screenshot of the working particle simulation.

This looks a lot more like it! With the basic simulation up and running, many other concepts can be explored. One possibility would be to add force fields or interactions between particles. In the demo above, the particles get initialized with different starting directions and turn towards the center when they're outside of a predefined circle. Depending on the initial conditions, interesting geometric patterns can form. One other example I'll be exploring soon, are agent based simulations of slime molds.

2022 Update

What's worse than an off-by-one error? An off-by-one-half error!

After a several month break from the project, I took a closer look and noticed a major bug! I was sampling the texture containing the positions and velocities of every particle at the wrong places! Instead of sampling the individual pixels, i was sampling the border between the pixels. Particles that were close in the simulation texture were "bleeding" into each other, and their positions and velocities were being averaged.

My original (wrong) sampling strategy in red, the correct sampling in green
My original (wrong) sampling strategy in red, the correct sampling points in green

Because many of the patterns had locally similiar velocities anyways, it wasn't very noticable and only started causing issues when I started reusing the code for the Physarum simulation. The fix was annoyingly simple, just offset the samples by half a pixel in each direction. Now each pixel is sampled at the center of the pixel, and the particles are not influencing each other anymore.

Future optimizations

There is one more thing I'd eventually like to implement. Right now the implementation is using 16bit float textures, which aren't widely supported, especially on mobile devices. By encoding one 16bit float into two unsigned bytes, I could work around the issue and make it work on most devices.
Something I learned through this project, was that some Android phones have partial support for float textures, in the form of 8bit floats. As most of you will know, 8bits isn't very much to store a floating point number, leading to interesting artifacts like the following one:

The blocky patterns we see are litterally the numerical resolution of the phones GPU, isn't that cool?
BTW, This is how the pattern is supposed to look with full 16bit resolution:

GPU particle pattern with full 16bit resolution
GPU particle pattern using the full 16bit resolution