A glimpse into a world of 2D particles
This post is inspired by this beautiful youtube video from Tom Mohr (highly recommended watch). It does not really follow the modeling from Tom's video precisely, but it is quite similar. In the process of writing this post I learnt a lot! it felt like it'd been a long time since I did something this fun, therefore I am continuing this project hopefully in a couple of future parts (there is at least going to be a part 2). This series is all about efficiency, modeling, and performance when it comes to web based visualisations. This post is basically my attempt to share what I have learnt along the way and to also document it for myself.
Here's a table of contents for easy scroll to each section What are we looking for? How do we model it? How do we simulate it? (basic implementation) How does it look? (an interactive demo) Tips for better performance Example visualisations What's next? (Part 2)
What's the challenge here?
Picture this, you have a bunch of particles (tiny little circles) split into multiple colors. Each color represents a unique type, and the way these particles interact is governed (for the most part) by a fixed matrix, a kind of table where each cell tells you how strongly one type pulls (attraction) or pushes another (repulsion). The rows and columns correspond to the colors, and the number in each cell is how strongly a pair of particles with those colors pull or push one another. don't worry if it all sounds pretty abstract for now, I am going to explain all of this in more details very shortly. Our main goal is to bring this system to life visually and see how these particles interact with different configurations. In a second part to this post coming some time later I will dig deeper into how we can do this in scale with lots of particles and more efficientely in terms of performance and user experience as well.
In a nutshell our objective is to
- Model how particles move in every frame of time mathematically
- Design an algorithm that implements our modeling frame by frame
- Target a frame budget of 16 milliseconds per frame to maintain roughly a stable 60fps performance on Mac Mini M4 Pro hardware when simulating 1000 particles
For the most of this post and this part in particular we will focus on the first two objectives. towards the end of this post we cover the third objective and go even beyond our initial target and aim to make our demo efficient enough for a 60fps performance on a much less powerful hardware such an iPhone 13 Pro, or a Google Pixel 7a.
Modeling the Problem
So far we know we want particles that can move around, they have different colors, each color can attract or repel another color, and that's about it. It is entirely up to us how to mode and then implement these requirements. some modeling choices make it easier for us not only to communicate our final implementation with others (e.g. by use of familiar notations), but to even optimise it further down the road. for instance if we can model our problem in matrix notations, we could potentially use results from Linear Algebra to reduce number of computations in our simulation, or we can utilise GPUs to parallelise some computations. Anyways, in this section we clarify our modeling choices.
Modeling Motion
According to Newton's second law of motion, force is related to acceleration by
Let's assume all particles have unit mass (
The acceleration
This models how forces between particles drive their motion by connecting them to acceleration, velocity, and position updates.
In practice, we further apply a small damping factor
where
For more on modeling motion I highly recommend this amazing tutorial by Kenichi Yoneda. we can try and build upon this basic motion model later on but for now this works just fine.
Next we look into how we can model pairwise particle forces such that we get attraction and repulsion behaviour based on particle colors.
Modeling Forces
Earlier we briefly hinted at a table to represent pairwise forces. here we make it more explicit by considering it as a matrix called the force matrix. following is an example of this matrix for a system with three particle types
| Red | Green | Blue | |
|---|---|---|---|
| Red | 0.5 | 0.3 | -0.3 |
| Green | -0.5 | 0.2 | 0.5 |
| Blue | 0.2 | -0.2 | 0.3 |
Let's call this matrix
Each entry
means red particles attract each other by a force with a magnitude of
means red particles are attracted towards green particles by a force with a magnitude of. (a little weaker than the force from before)
means blue particles repel green ones by a force with a magnitude of
So far for a pair of particles we know how much force is determined by their types, but if this is the only source of force and assuming we have only 2 types that attract each other, after a while all particles collapse into each other. basically we need to model a global repulsive force as well so when particles are too close to one another they repel each other regardless of their type. there are so many possibilities but here we use a very simple one. we model a multiplier which is a function of the distance between particles. let's call it
- Min Cutoff distance
For particles any closer we clamp the repulsion force to -1 - Zero Force distance
A distance at which particles do not apply any force on one another, but any closer they repel and any farther they attract each other - Max Cutoff distance
For particles any farther we assume the force between them is negligible i.e. 0
Here's an illustration of this multiplier function
Using
With
Here
We can then define force vector
The net force on a particle is the sum of all such vectors i.e. forces coming from all the other particles towards
To understand the vector math above a little better imagine we have only 4 particles
Here
- Arrows with solid lines represent (difference) vectors between each pair of particles that include particle
. (these are the only pairs that affect ) - The arrow with dotted line represents the negated vector between particles
and , or more precisely . as you can see in the formula to compute this points towards the direction when or when . since we are are talking about forces applied to particle , this represents a repel force from particle - Dashed arrows represent vectors
and (to reduce visual clutter, arrow notation denoting vector quantities has been omitted from the graphics)
Let's further assume green and red both attract orange, but blue repels it (each can have a different constant factor). with these assumptions we can compute
Boundary Conditions
An important aspect of such a simulation is designing how particles behave at the edges of the 2D world. there are multiple options. Here are three well-known boundary options
- Reflecting
Particles bounce off the edges of the boundary - Fixed
Particles are stopped or pinned when they reach an edge of the boundary - Periodic (Wrap-around/toroidal/wrapping)
Particles leaving one side instantly reappear on the opposite side, creating a continuous loop
Each choice has implications for the resulting dynamics and can be selected based on our desired simulation behavior.
Implementation
To better understand how the theoretical model translates into a working simulation, we go one step closer towards the final visualisation by mapping out a flowchart of the simulation logic. this simulation is essentially governed by the modeling we already discussed. so for instance "Compute net force" step is a short way of saying we compute
Interactive Demo
In the following interactive demo you can see how this modeling and simulation logic all comes together towards realising our first two objectives. try playing around with different simulation parameters such as modifying entries of the color matrix, timestep, zero force distance, ... to see in real-time how these particles interact.
Thoughts on Performance
So far we saw the modeling and a demo, but didn't really discuss implementation details. the flowchart from earlier does give us an overal picture however. we have a long running loop and we want each iteration not to take any longer than 16 milliseconds or else it means our framerate drops to below 60fps. part of this has to do with the complexity of the problem itself i.e. the number of particles involved. no matter what we do there is always a large enough number of particles our implementation cannot handle efficiently. so let's focus on our 3rd objective (1k particles at 60fps on a modern Mac Mini) and see what we can do to support simulating that many particles.
Why 60 FPS and scale matter?
Our goal isn’t just to visualize particles, it’s to create a smooth 60fps simulation where you can watch complex behaviors emerge in real time. Think of it like a video game. If the framerate stutters, interactivity almost disappears. Every frame requires calculating forces between every pair of particles, which can quickly tank performance. So basically with each iteration of our implementation we are interested in knowing how many particles can we simulate before framerate drops below 60fps.
Let's briefly discuss the performance of a naive implementation of our modeling. this part requires some familiarity with typescript and p5js. (I am using p5js in instance mode so calls to p5 specific API look like p.createVector instead of createVector
In the spirit of OOP let's start with defining a couple of interfaces that we are going to use in our main code
We can then define our update logic like the following
The three methods used within updateParticles are as follows
computeForceBetweenParticles
This is where we compute force on particlefrom particle updatePhysics
This is where we compute new velocity and position of a particle given a net force vector according to our modeling of motionhandleBoundaryConditions
This is where we constrain particle motion based on reflecting boundary conditions
Here's how they can be implemented
Then we have
There are several options how to define boundary conditions and each has its own consequences and meanings. In this post for simplicity we only consider reflecting boundary conditions (without any energy loss) which can be implemented simply as below
In part 2 we will also explore implementation details of periodic boundary conditions.
Now that we have familiarised ourselves with the code, first point that comes to mind is that within updateParticles we are looking at every pair twice. once computing force from particle colorForceMatrix is not necessarily a symmetric one. However, one major (heavy) computation here is computing the distance between the two particles and in computing both updateParticles based on this observation (notice the highlighted lines)
With this simple change we reduce redundant force calculations by roughly half. This is just the start.
Now let's look at something a little more subtle. within updateParticles we use p.createVector and a couple of related methods such as .sub(), .mag(), and .setMag(). This essentialy incurs some object creation overhead and adds pressure to javascript engine's garbage collection. We could simply get rid of these and use direct numerical operations and primitive types. let's see how that looks
Even though in the first step we directly targetted redundant computations, performance effects from this new update were actually more noticable. This shows how much time savings within nested loops can impact performance.
The next small but useful step (V3) is to hoist simulation constants (cutoff distances, the force matrix) out of the nested loop into local const variables before the loop starts. Accessing a local variable is cheaper than a property lookup inside a hot loop that runs millions of times per frame.
V4 then goes one step further and looks at how we store particle data itself. OOP is nice and all but to speed up further we can look for more memory lookup efficiency. This can be achieved by storing particle data in a flat array instead of an array of objects. Here's how it looks
Other methods of course need to be adapted as we no longer have objects of type Particle.
With these optimisations we can easily render slightly over 2000 particles at 60fps on a Mac Mini M4 Pro which was our initial objective.
Examples
In this section I want to share a couple of examples of how this simulation can be used to create interesting visual effects. in all of these examples the simulation parameters are kept the same except for the color force matrix. Initially particles are distrubuted around a circle and the boundary conditions make it so they are constrained within a white rectangular box.
What's coming in part 2?
In part 2 we break the