I would like to create a particle animation that shows a particles flowing and slipping around objects instead of penetrating them. The effect I want to achieve is shown in this gif by Étienne Jacob:
I will divide the answer into two parts being:
- Implementing a simple particle system in Animation Nodes that is adaptable to achieve all the effects we want.
- Adapt the particle system to avoid and slip around the sphere as shown in the gif posted.
The advantages of giving up on blender's particle system and using Animation Nodes instead are numerous. It is intuitive, easy to use and flexible. However, one of the main issues with implementing such system is losing render engine support, both Cycles and BI have options specific just for rendering particles and we won't have such options if we don't use blender's particle system.
A particle system is an iterative implicit discretized system, meaning we can't evaluate the particles locations at anytime we want, to compute the particles location at a certain frame, we have to know their locations in the previous frame. Subsequently, if we want to get the particles locations at some frame, we have to compute the particle locations at all of the frames before it; because you have to know the previous frame and to know the previous, you have to know its previous and so on.
A particle system has an initial condition, which is the state the system was at the beginning of the time, in our case, the initial condition is the particle locations at the beginning of the time. At the next frame, the particles are moved in a certain direction based on some factor that is either: their location, index or simply a random arbitrary movement. At the frame after that, the particles are also moved based on the new locations computed at previous frame and so on.
Practically, in Animation Nodes, to implement such iterative system, you would need to store the new locations of the particle somewhere where you can access them in the next execution. For instance, lets say we have a particle that has some initial position, we will store that position in memory, at the next execution (frame) we use the stored position to compute the new one, then replace the old positions in memory with it, at next execution, we compute the new position based on that in memory and replace the old one with it and so on.
Currently, there is no way to pass data across executions, however, there are ways around it that are easily implemented, the way we will be using is described in my answer here. Make sure to read this answer carefully before continuing.
The first step is to create the initial condition of the system. In our case, it will be a grid of random vectors included in the xy plane, I will generate random vectors such that they belong to the visible part of the xy plane, there will be a good portion of the point lying outside of the view, but since our implementation won't be expensive we can safely ignore them:
And then I am going to use that as the initial condition for our particle system. I will define the velocity vector field as a constant vector going to the positive y direction. By adding the vector field to the particles, we observe them as they start to move:
Notice that the first expression node has the expression:
initial if bpy.data.scenes.frame_current == 1 else animation_nodes.particles
Which basically says, if this is frame one, return the input "initial", if not, return whatever is stored at
animation_nodes.particles. The data is stored using the second expression node using the expression:
setattr(animation_nodes, "particles", data)
So, at the first frame, the current particles are viewed (Initial) and the particle locations of the next frame (second frame) is computed and stored in
animation_nodes.particles, at the second frame, the particles that are stored at
animation_nodes.particles are viewed (The ones we computed before) and the particle locations for the next frame (third frame) are computed, and stored in
animation_nodes.particles (Replaces the old data) and so on.
We notice that the particles will keep moving to infinity leaving empty space behind, so, what I am going to do is reset their $y$ location after they exceed their maximum location (Maximum value of the random y value), the reset is easily done using a modulo operator:
We notice that in the gif, the grid is not strictly uniform in its randomness, in other words, there are certain areas that are low in density and others that are high in density, and we can see that the density is almost constant along the y axis. A possible way to achieve such areas of rarefactions is by moving the particles by an amount equal to a smooth low frequency perlin noise, though I believe moving based on the magnitude of its gradient might return better results, but I shall not cover this. Number Wiggle Node can be used to get the values of the perlin noise where the evolution parameter is the x location of the particles, that is, the x random value:
We can add another offset based on a y location based perlin noise creating some nice randomness as the particles move in the y direction:
Now that there is noise involved in computing the initial condition, we should probably cache the result (The initial condition will only be used at frame 1 and it doesn't really change, so we can safely cache it), caching will reduce the execution time from 40ms to just 2ms:
I think this conclude our work on the main part of the particle system, we may come back later to adjust some settings, but for now, we will move on to the second part of the answer, which is adapting this system to avoid obstacles like the sphere we placed in there.
Currently, the particles have no regards to each other or to objects in the scene (Like the sphere we put in there, they are penetrating it like it is not there). So what exactly tells the particles where to move or how to behave? It is of course the velocity field which we defined as constant vector going in the positive $y$ direction. A vector field associates a vector with each point in space, in our case, that vector represents the velocity of the particle, that is, the direction it should move in. And as we said before, the field so far is constant for all points in space. If we want to make the particles avoid the sphere, we have to edit that field such that the particles slip instead of penetrate the sphere.
Now, I want you to open a new blend file to try somethings out. If we want to visualize the velocity field we have, we can create splines from edges that points in the direction of the velocity as follows:
We notice that they all point in the positive $y$ direction and that is indeed that's what our field is doing, that is, move the particle in that direction. We could edit the velocity field such that it points away from the origin simply by normalizing the particle locations:
We could rotate the velocities by 90 degrees obtaining this radial field that will rotate the particles around the origin like a tornado:
Now, I could mix between my original constant vector field and this vector field we just created using a linear combination where the distance from the surface of the sphere is the coefficient:
The vector distance node computes the distance between the particles and the origin, the subtract node is used to compute the distance between the particles and the nearest surface point of the sphere (Check my answer here to see why this work). The rest is the linear combination between both vector fields (Check my answer here to see how this works for Eulers which you can easily translate to vectors). Now imagine some particles moving along this field from bottom to top, if the particle is at the left side of the sphere, it will start moving upward and at some point start moving along the surface of the sphere tangentially as if it was slipping, isn't that exactly what we want?! As for the right side of the sphere, it is a bit of a mess and rather incomprehensible, but since we already have a working left side, lets mirror it. Mirroring can be done by inverting the vectors on the right side of the sphere, in other words, multiplying them by negative one. To get a scalar field with negative one on the right side and one on the other side, we copy the sign to one from the inverse of the $x$ location:
And we can start to see that this is actually the path of the particles, but we are not quite done yet. We will do a spline visualization for the paths of the particle so that we can tune the field even more intuitively. A loop is created to move the particles along the field, append their locations to a list, reassign the particles locations to the new locations and so on some number of times. Then another loop is created to create splines from the appended points and the splines are output:
It is not crucial that you understand this and I know it is not easy to understand, so I shall not explain it further, if you want to know why it works, I will provide more details. Let us now see how the particles move in the current field:
We can see that it is close to how the particles in the reference gif behave around the object, however, there still some work that need to done. The area at which the particles start moving tangentially is rather large, the particles--in the reference gif-- starts to move tangentially when they get pretty close to the surface, so we should reduce it. Reduction can be done by changing the rate at which the distance changes, in other words, multiplying the distance by some scalar:
Now I think it matches the vector field used in the reference gif. So lets go ahead and implement that in our original blend file! The only difference is that we are going to subtract the location of the sphere from the particles locations because in our study, we assumed it was at the center.
One thing we need to take care of is to make sure no particle were inside the sphere in the initial condition, this is easily done by some simple distance based masking:
And finally, we can see in the reference gif that the particles don't move tangentially after they pass the sphere along the $y$ axis, so I am just going to make sure all points with a positive $y$ have a zero coefficient in our linear combination, that is, they have the value of the original constant vector field. To avoid using loops or overly complicated node trees, I am going to use this simple list comprehension:
[factor if Y < 0 else 1 for y,factor in zip(Ys, factors)]
Had we had a vectorized compare and switch nodes, we could have just used them. This produces:
Which when applied on our node tree produces:
We now need to fill the spaces that are left in front of the sphere which appears to be appearing from inside the sphere itself. We will do this by implementing a secondary particle system for this. The secondary is quite trivial to make, if you understood how our main particle system work, then you can do the secondary one on your own. This secondary particle system concludes our work producing a final result:
If rendered with motion blur, it will look something like this:
All you have to do now is tune in the settings to achieved the exact result of the reference gif.