OpenTechSchool Generative Art Workshop with Processing

Particle system


Home About

Course by Roman for OTS Brussels

Let’s up the level a bit more. Particle system were made possible with hardware being more and more powerful. You can simulate quite complex behavior (fire, smoke, liquids…) or just use them to have fun, which will be our case. It can be pretty process intensive (difficult for your computer to compute it quickly) so don’t start straight with a million particles!

o) What is a particle system?

You’ll never guess. It’s a system with particles inside. Particles are physical objects with a position, speed and acceleration. They can be represented as you see fit (would that be individual shape, trajectory or links between them) and can (or not) interact with each other. Usually you don’t have one or two, but closer to hundreds or even more than millions in complex simulation. In a particle system, the emphasis is not on the individual particles, but on what they create as a whole. This is pretty fitting for Generative Art. You search particle system on youtube, you’ll find quite mesmerizing visualisation. And once you’ve done yours, don’t hesitate to upload it too :D

a) Object, variables, methods & PVector

If before we had dots moving alone a frame, with only x evolving, now we have the whole deal: (x,y). While we could treat each information independently (at the cost of repeted code, implying more energy & time spent, and errors) we can see them as a whole, as a complexe variable, called an object. There are many kind of object, in our case, this one is called a PVector (P for processing, Vector for ... vector). As such, it defines a new type of variable. It can be used to store 2D or 3D data, in our case we'll only take care of 2D vectors.

But a PVector is not a variable type, it's an object type and hence define an object. Objects don't exactly behave as variables. They store some, so you can access them, but on top of that you have inner functions (called methods), in order to act on them. At first, when imagining an object, you can imagine a human. Variables of the human object could be its name (text variable), its age (int variable) and its height (float variable). Its methods could be "breathing", "running" or "screaming". Beside all that, since we handle a complex object and not anymore a simple variable, we need to construct it. This is done by calling a function, called the constructor (which handily has the same name than the type), it handle the whole initialisation of the object.

So, let's review the different possibility of the PVector type. Always keep in mind PVector is not an object itself, but a type. So the inner variables and methods will be of the instance of this type. Let's see some of the possibilities of PVector instances. The upcoming code has no specific aim but to show you how PVector are working.

PVector p1, p2, p3; // As any other type, we declare our object at the root.

void setup() {
  // We define and initialise it by calling the constructor
  p1 = new PVector();     // By default x = 0 and y = 0;
  p2 = new PVector(1,3);  // When precised, the constructor will take x and y values
  p3 = new PVector(10,10);
}

void draw() {

  // Here how you can access each separate Variables.
  p1.x = 5;
  p1.y = p2.y + p3.y;

  // Here are some useful methods.

  // Setting both values at the same time
  p1.set(42, 42);

  // Return the mag of the PVector, its length
  float distance = p3.mag();

  // Set p1 as a random 2D vector with a length of 1
  p1.random2D(); 

  // Unfortunately we can't use classic + - * / operators.
  // Instead, we use the next methods
  p1.add(p2); // Add p2 to p1
  p3.sub(p2); // Substract p2 to p3
  p1.mult(5); // Multiple both x and y by 5
  p3.div(10); // Divide both x and y by 10

}

If you want to learn more about PVector methods, you can check their Processing reference pages: http://processing.org/reference/PVector.html. The reference page - http://processing.org/reference/ - is all in all the best resource to check first or just to read through in order to learn more about the potentials of Processing. If you're wondering why p1.y = p2.y + p3.y; can work while we said we couldn't use +, it's because here we're not handling the PVector but y, one of their inner variable, which is a float, and we can use + on floats.

b) Spread and shine

Let’s look back at the code we wrote in the previous log (i.e. dealing with line, array of variables and random acceleration) and apply it to particles. Try to do it by yourself, or at least to think a bit how you would do that with your new skills. The following code is one way to do so, in which the particles start from the center and evolve toward the exterior of the screen:

PVector[] p, s, a; //The particles position, speed and acceleration
int k; // The number of particles

void setup() {
  size(displayWidth, displayHeight);
  background(0);
  noCursor();
  k = 20; // We want 20 of them apples

  // Particle array size is the number of particles we want
  p = new PVector[k];
  s = new PVector[k];
  a = new PVector[k];

  // Initialise the accelerations, speeds and positions.
  for(int i=0; i<k; i++) {
    p[i] = new PVector(width/2, height/2); // Center screen
    s[i] = new PVector(0,0);
    a[i] = new PVector(0,0);
  }
}


void draw() {
  //Update acceleration, speed and position 
  for(int i=0; i<k; i++) { 
    a[i] = new PVector(random(-0.1,0.1), random(-0.1,0.1));
    s[i].add(a[i]);
    p[i].add(s[i]);
  }

  // Draw the particles
  stroke(255);
  for(int i=0; i<k; i++) { 
      point(p[i].x, p[i].y); // Here we use the point visual primitive
  }
}

Soooooo pretty. Try with many many particles and ... of course, transparency :D

c) Of mice and planets

Soooo pretty, but soooo off the screen. We have the same issue than previously with our lines. And guess what? We'll solve it the same way! Only difference is that now we're in 2D so what we did on x as a float, we'll do on (x,y) as a PVector. This time, let's make the particles attracted by the mouse cursor so that we can play with it. Don't hesitate to put other positions (such as the centre of the screen, as we did for the lines).

  // A full line, but take your time and you'll understand it.
  // So full that it was break on two lines for better readability
  a[i].set( random(-0.1,0.1) + (mouseX - p[i].x)/9000,
            random(-0.1,0.1) + (mouseY - p[i].y)/9000);

In this log, we discover new graphic directions and keep it simple. It doesn't mean that you have to keep it simple too,you can & should explore, for instance you can try: - Color varying with speed - Batch of particles with similar speed, similar color. Launch at the same time different batches. - Try to only plot the particles (not their trajectories, by using background(0); at the beginning of draw). Give them another graphic primitive (ellipse(p[i].x, p[i].y,5,5);). Set it up nice noStroke(); fill(255,10);. And try with many many particles! - Try to not anymore use the mouse as point of attraction but predefined positions, acting as planets. Bonus points if you draw the planets. - And many many others...

e) Connect and reject

One possible exploration was so nice people lobbied to have it included. We can display the particules ... but we can also display their connexions, and that's awesome. For once we won't touch the update part of the code, but the display. We used to draw as:

  // Draw the particles
  stroke(255);
  for(int i=0; i<k; i++) { 
      point(p[i].x, p[i].y); // Here we use the point visual primitive
  }

Now, we'll just draw the particles as ellipse, and lines between each of them. In order to draw the lines, we'll need to go through the particles array twice nested, once for the particle at one end of the line, once for the particle at the other end of the line. For the most acute, you'll see we made a little redundancy in the code below for simplification purpose. (Oh, and don't push up too high the number of particles...)

  background(0);

  // Draw the particles
  noStroke();
  for(int i=0; i<k; i++) { 
      fill(255);
      ellipse(p[i].x, p[i].y, 5, 5);
      fill(255,30);
      ellipse(p[i].x, p[i].y, 30, 30);
  }

  //Draw the links
  stroke(200);
  for(int i=0; i<k; i++) {
    for(int j=0; j<k; j++) { 
      line(p[i].x, p[i].y, p[j].x, p[j].y);
    }
  }

That's nice but ... the lines make it feels more mathematics than organic. Let's not always display the lines, let's do that only if particles are close enough. Ahhh at last, the ultimate computer keyword, summing up all that is a computer : if. Tests. And actions that follows depending on the answer, that's the basic of computing, it was high time we got there!

An if structure allow you to make decisions. At its most complete it is separated in three parts. If it's raining (condition) I'm staying in (action to do if condition is true) else I'm leaving (action to do if condition is false). In code you have it this way:

 if(age < 18) { // Forbidden to enter
    fill(255,0,0);
 } else { // Free to enter
    fill(0,255,0);
 }

 ellipse(width/2, height/2, 100, 100);

In our case, we want to draw the line only if the particles are close enough. Let's just make a test before calling the line function. Remember from previous code that the length of a PVector is accessed by its mag method.

  //Draw the links
  stroke(200);
  for(int i=0; i<k; i++) {
    for(int j=0; j<k; j++) { 

      PVector diffPos = p[i].get(); // the get method return a copy of the vector
      diffPos.sub(p[j]);
      if(diffPos.mag() < 50) {
        line(p[i].x, p[i].y, p[j].x, p[j].y);
      } // We do nothing if condition is not met, so no need for the "else" part

    }
  }

e) Laws of attraction

Enough already with the particles!! .... One last thing? Ok, one last. This time let's focus on the update part, the behavior of the particles. Up to know, they were attracted to the mouse cursor or to static positions. A particle really gets neat when particles interact with each other. We had that with graphism (the lines). Now we'll have it in their behavior by making them attracted by each other. In short, for each particle, you need to apply the calculus we did to with the mouse position, but now to all the other particles' position. Some for loop in the making...

Try to imagine or even code it yourself. But if you're curious, here is one possible solution (you'll see that in it, we separated the update loop in two. One part specially for the acceleration update, and then for both the speed and position). Here, getting the right acceleration is a more complex process, so we start from zero, and add up until we have the right value. Up to you to check back the parameters and see which are the most fit.

  //Updating acceleration
  for(int i=0; i<k; i++) {
    // Initialisation at zero
    a[i].set(0,0);

    // Force between particles
    for(int j=0; j<k; j++) {
      a[i].add( new PVector( (p[j].x - p[i].x)/15000, (p[j].y - p[i].y)/15000) );
    }

    // Random force
    a[i].add( new PVector( random(-0.1,0.1), random(-0.1,0.1) ) );

    // Force that pulls you toward the centre
    a[i].add( new PVector( (width/2 - p[i].x)/9000, (height/2 - p[i].y)/9000 ) );
  }

  //Updating position and speed
  for(int i=0; i<k; i++) { 
    s[i].add(a[i]);
    p[i].add(s[i]);
  }

Ok, time to tell you the truth... It's not working like that in the solar system, or any mass based system... All the forces we applied were following one pattern: the further you were, the stronger you were pulled back. It's not the case with planets. It's more like the opposite. The closer they are, the stronger they are pulled together. For now I'll let you that as an exercise!