Personal space
At the end of the last section, we had a nice Particle
emitter cursor trail. As you drag the cursor around, you leave a trail of hundreds of moving Particle
s. Every one of those Particle
s is responding to its initial starting velocity combined with a hint of Perlin noise. Each Particle
does its thing and is oblivious to what any of the Particle
s are doing. Until now.
We are going to implement a very basic repulsive force to each Particle
. Every single Particle
will push away every other Particle
. We will do this by giving each Particle
an acceleration vector called mAcc
.
Here is how it will work. From the ParticleController
, we will iterate through all the Particle
s. For each Particle
, we check it against all other Particle
s. If those two Particle
s are close, they will push each other away more strongly than if the two Particle
s are on opposite sides of the app window. We add that repulsion to the respective Particle
s' mAcc
vectors.
Once we have iterated through all the Particle
s, we add the acceleration to the velocity. Then we add the velocity to the position. We decay the velocity. We reset the acceleration. And we repeat.
The abridged version of what each Particle
will go through looks like this:
We are also adding a new variable to represent the Particle
's mass which is directly related to the radius. The actual relationship is a matter of personal taste. Once we make use of the mass variable, you might find you like how things behave if your Particle
s are really massy. I like my Particle
s a little more floaty.
This formula is not based on anything other than trial and error. I tried setting the mass equal to the radius. Didn't like that. I tried mass equal to radius squared. Didn't like that. I eventually settled on taking a fraction of the radius squared.
Basic Repulsive Force
Lets go through this step by step. First, you set up a for-loop that uses the list
iterator
to go through all the Particle
s, one by one, in the order they are sorted in the list.
Next, we create a second iterator
that also loops through the Particle
s, but it starts at one Particle
ahead of the first iterator
's position. Put another way, if we are on Particle
15 in the first iterator
, the second iterator
will loop through Particle
s 16 and higher. Particle
16 will iterate
through Particle
17 and higher, etc.
Put yet another way, imagine that we have 3 Particle
s. Each Particle
wants to repel every other Particle
. First round, p1 and p2 repel each other, and then p1 and p3 repel each other. Second round, we already handled all of p1's interactions so we move on to p2. Since p2 has already interacted with p1, all that is left is for p2 and p3 to repel each other. That is the logic for the nested iterator
s.
Now that we are inside of the second iterator
, we are dealing with a single pair of Particle
s, p1 and p2. We know both of their positions so we can find the vector between them by subtracting p1's position from p2's position. We can then find out how far apart they are by using length()
.
Here is where we run into our first problem. To do this repulsion force, we don't need the distance between the Particle
s. We need the distance squared. We could just multiply dist
by dist
and be done with it, but we have another option.
When finding out the length of a vector, you first find out the squared distance, then you take the square root. The code for finding the length()
looks like this:
You should try to avoid using sqrt()
when possible, especially inside of a huge nested loop. It will definitely slow things down as the square root calculation is much more processor-intensive than just adding or multiplying. The good news is there is also a lengthSquared()
method we can use.
Next, we make sure the distance squared is not equal to zero. One of the next steps is to normalize the direction vector and we already know that normalizing a vector with a length of zero is a bad thing.
Here is the sparkling jewel of our function. First, you go ahead and normalize the direction vector. This leaves you with a vector that has a length of one and can be thought of as an arrow pointing from p2 towards p1.
The first factor which determines how much push each Particle
has on the other is the inverse of the distance squared.
Since we already know that force equals mass times acceleration (Newton's 2nd law), we can find p2's contribution to p1's total acceleration by adding the force divided by p1's mass.
To find out p1's contribution to p2's total acceleration, you subtract force divided by p2's mass.
If this all seems confusing to you, worry not. It still confuses me from time to time. With a little bit of practice, this code will start to feel more familiar.
Now we can turn on our repulsion. In our App class, before we tell the ParticleController
to update all the Particle
s, we trigger the ParticleController::repulseParticles()
method. We can now run our code.
Every single Particle pushes away its neighbors which causes the Particle
s to spread out in a really natural manner. Here is a short video of the effect in action.