CS 2, Winter 2009
Programming for Interactive Digital Arts

Feb 9: Particle systems


Reusing

Arrays let us have lots of objects doing things simultaneously. But when we create an array, we have to tell Processing the number of objects it could contain. What if we don't know? Sometimes, we can just create way more objects than we need. But if the sketch runs long enough, we might have underestimated. Shiffman 10-9 shows how to expand an array. But what if most of the objects have made their appearance and then left the screen? Then we really don't want or need to have them all still around, and rather than creating a much bigger aray than we really need, and then making it bigger and bigger as time goes on, we can simply reuse or recycle existing objects.

One example, inspired by Reas & Fry 43-12, places ripples in a pond, expanding from each mouse press. Each Ring object keeps track of its center (where the mouse was pressed) and its current diameter. It grows until it reaches a fixed diameter; after that point, it also won't be displayed. We draw a ring by looping from the diameter inward, drawing concentric circles. (We also make them more and more transparent as we work inwards.) Note that since the Ring class doesn't have a constructor, the fields all take their default values of 0.

At the beginning, we create an array of rings. A variable "current" keeps track of the next one we'll use. When the mouse is pressed, the next Ring object in the array is started at the mouse position. When we've used up all the objects in the array, we just wrap around and reuse the zeroth one. As long as we have enough rings so that they finish expanding before we have to reuse them, it'll look like we have an infinite supply of them.

sketch1
Ring[] rings = new Ring[50];  // at most 50 at a time
int current = 0;  // for next mouse press

void setup() 
{
  smooth();
  for (int i=0; i<rings.length; i++) {
    rings[i] = new Ring();
  }
}

void draw() 
{
  background(0);
  for (int i=0; i<rings.length; i++) rings[i].display();
  for (int i=0; i<rings.length; i++) rings[i].grow();
}

// Click to create a new Ring
void mousePressed() 
{
  rings[current].start(mouseX, mouseY);
  current++;
  if (current == rings.length) {
    // wrap around -- reuse rings that presumably are done by now
    current = 0;
  }
}
Ring
class Ring {
  float x, y;
  float diameter;
  boolean on = false;  // whether or not it's being shown

  // Turn ring on, starting to expand from (x0,y0)
  void start(float x0, float y0) 
  {
    x = x0; y = y0;
    on = true;
    diameter = 1;
  }

  void grow() 
  {
    if (on) {
      diameter += 0.5;
      if (diameter > 400) {  // too big -- stop it
        on = false;
      }
    }
  }

  // CBK variation
  void display()
  {
    if (on) {
      noFill();
      int d = round(diameter);
      int a = 200;
      // More opaque at larger diameter
      while (d > 0 && a > 0) {
        stroke(150, a);
        ellipse(x, y, d, d);
        d -= 10;
        a -= 20;
      }
    }
  }
}
screenshot[applet]

In terms of the reuse concept, the following example works exactly the same way -- notice the variable "current" and the wrapping around calculation in addChip (done here with modulus rather than if/then). We're assuming that by the time we've put enough chips up, we can replace some of the old ones with new ones.

The sketch differs more visibly in how the objects are displayed. Instead of representing a set of rings, now an object represents a "chip" in a kaleidoscope. When a chip is displayed, the kaleidoscope also makes us see symmetric partners. Thus instead of looping over the diameter of the concentric rings, we loop over the angle separating the symmetry-mates. If there are 3 symmetry partners, we draw them at 0, 120, 240 degrees, and so forth. A global angle is added in, in order to keep the kaleidoscope spinning. We put to good use the angular calculations and transformations from earlier this term.

sketch2
int symm=3;      // degree of symmetry
int sz=20;       // size of chips
int rand=75;     // control of random chip generation
float a,da=0.01; // global rotation and increment

// The chips and the current one to be added
Chip[] chips = new Chip[100];
int current = 0;

void setup()
{
  size(400,400);
  noStroke();
  background(0);
  rectMode(CENTER);
  
  for (int i=0; i<chips.length; i++) chips[i] = new Chip();
}

void draw()
{
  background(0);
  translate(width/2, height/2);
  
  // Draw them
  for (int i=0; i<chips.length; i++) chips[i].draw(a);

  // The probability of a random chip is 1/rand
  if (random(rand) < 1) 
    addChip(random(width),random(height));
  
  // Rotate everyone
  a += da;
}

void mousePressed()
{
  addChip(mouseX, mouseY);
}

// Place the next available chip at (x,y)
void addChip(float x, float y)
{
  // Angle from center to (x,y)
  float dx = x - width/2;
  float dy = y - height/2;
  float ca = atan2(dy, dx) - a; // subtract out the current angle,
                                // since it'll be added right back in

  // Distance from center to (x,y)
  // This will be the new x coordinate upon rotation
  // (The new y coordinate will be 0)
  float cr = dist(x, y, width/2, height/2);

  // Add chip, and advance counter (wrapping around)
  chips[current].place(ca, cr, sz, symm);
  
  current = (current+1)%chips.length;
}

void keyPressed()
{
  if (key=='Z') {        // Bigger
    sz+=5;
  }
  else if (key=='z') {
    if (sz>5) sz-=5;
    if (sz<0) sz = 0;
  }
  else if (key=='S') {   // Higher symmetry
    symm++;
    println(symm+"-fold");
  }
  else if (key=='s') {
    if (symm>3) symm--;
    println(symm+"-fold");
  }
  else if (key=='R') {    // More random chips
    if (rand>5) rand-=5;
  }
  else if (key=='r') {
    rand+=5;
  }
  else if (key=='c') {    // Clear
    for (int i=0; i<chips.length; i++) chips[i].on = false;
    current = 0;
  }
}
Chip
class Chip {
  float angle, radius;  // position of the first chip, from center of window
  float sz;  // how big the chip is
  color clr;
  int symm;  // how many symmetry partners to display
  boolean on;  // display or not?

  // Turn on the chip, at angle a and radius r, of size z, and including s symmetry partners
  void place(float a, float r, int z, int s)
  {
    on = true;
    angle = a; 
    radius = r;
    sz = z;
    symm = s;
    clr = color(random(100,255),random(100,255),random(100,255),100);
  }

  // Draw the chip and its symmetry partners, rotated by the global angle.
  void draw(float globalAngle)
  {
    if (on) {
      fill(clr);
      pushMatrix();
      rotate(angle + globalAngle);
      // Draw main chip and symm copies, at equal rotations.
      for (int d=0; d<symm; d++) {
        rect(radius,0,sz,sz);
        rotate(TWO_PI/symm);
      }
      popMatrix();
    }
  }
}
screenshot[applet]

Recycling

The previous examples reused objects when necessary, because we'd run out of new ones in the array. Another practice is to recycle them immediately -- once they're off-screen (or in some way not useful), set them back. We can use this, for example, to create an everlasting sparkler, based on an example from Reas and Fry (50-08) and one that comes with Processing (Topics | Simulate | SimpleParticleSystem).

The key idea is to detect when a Particle is no longer visible (off the screen or too transparent), and then re-initialize it back at the center. One other change avoids having the sparkler start out with a "bang" (like our earlier example when all the balls shoot out of the center together), by staggering the first draw/update of the particles. A variable "curr" keeps track of how many particles have been released--one more each frame until they all are.

sketch3
int num = 100;  // total number of particles
int curr = 1;   // how many are active
Particle[] particles = new Particle[num];

void setup()
{
  size(200,200);
  smooth();
  noStroke();
  background(0);
  
  // Create the particles
  for (int i=0; i<num; i++)
    particles[i] = new Particle(width/2, height/2); 
}

void draw()
{
  fill(0,10);
  rect(0,0,width,height);
  // Draw and update however many have been allowed to enter
  // (one per frame, till all have entered)
  for (int i=0; i<curr; i++) particles[i].draw();
  for (int i=0; i<curr; i++) particles[i].update();
  if (curr<num) curr++;  // bring in the next
}
Particle
class Particle {
  float ox, oy;  // original position
  float x, y;    // current position
  float vx, vy;  // velocity
  float t;       // transparency
  float dt = 3;  // transparency step
  float g = 0.1; // gravity
  float r = 3;   // radius
  
  Particle(float x0, float y0)
  {
    ox = x0; oy = y0;
    initialize();
  }

  // Put at original position, fully opaque, with random velocity
  void initialize()
  {
    x = ox; y = oy;
    // More vertical than horizontal
    vx = random(-3,3); vy = random(-5,-1);
    t = 255;
  }
  
  void draw()
  {
    fill(255,t);
    ellipse(x,y,2*r,2*r);
  }
  
  void update()
  {
    t -= dt;    // decay the transparency
    // Usual gravity stuff
    vy += g;
    x += vx;
    y += vy;
    // When exit screen or totally transparent, re-initialize
    if (t < 0 || x>width+r || x<r || y>height+r || y<r) 
      initialize();
  }
}
screenshot[applet]

And where there's fire, there must be smoke. Let's alter the sparkler sketch into a smoke sketch, based on the Processing example Topics | Simulate | SmokeParticleSystem. The main changes are to slow down the frame rate, completely erase every time, get rid of gravity, and have the velocities be primarily vertical.

sketch4
int num = 100;  // total number of particles
int curr = 1;   // how many are active
Particle[] particles = new Particle[num];

void setup()
{
  size(200,200);
  smooth();
  noStroke();
  background(0);
  frameRate(30);
  
  // Create the particles
  for (int i=0; i<num; i++)
    particles[i] = new Particle(width/2, height); 
}

void draw()
{
  background(0);

  // Draw and update however many have been allowed to enter
  // (one per frame, till all have entered)
  for (int i=0; i<curr; i++) particles[i].draw();
  for (int i=0; i<curr; i++) particles[i].update();
  if (curr<num) curr++;  // bring in the next
}
Particle
class Particle {
  float ox, oy;  // original position
  float x, y;    // current position
  float vx, vy;  // velocity
  float t;       // transparency
  float r = 5;   // radius
  float dt = 3;  // transparency step
  
  Particle(float x0, float y0)
  {
    ox = x0; oy = y0; 
    initialize();
  }

  // Put at original position, fully opaque, with random velocity
  void initialize()
  {
    x = ox; y = oy;
    t = 255;
    // Mostly vertical
    vx = random(-0.3,0.3); 
    vy = random(-0.3,0.3)-1;
  }
  
  void draw()
  {
    fill(255,t);
    ellipse(x,y,2*r,2*r);
  }
  
  void update()
  {
    t -= dt;    // decay the transparency
    x += vx;
    y += vy;
    // Re-initialize when decayed away
    if (t < 0) initialize();
  }
}
screenshot[applet]

To make it more smoke-like, we do two things that we haven't seen before, and one that we have. The one we've seen is to introduce some wind; this is just an additional increment to x each time. One that we haven't seen is a different way of generating random numbers. The random() function we've been using generates numbers uniformly in a range. Smoke looks more realistic if the numbers are concentrated toward the center, like a Gaussian (Normal) distribution. The Random class has a function to generate a random Gaussian number. Finally, rather than uniform looking ellipses, we want to draw the smoke particles as kind of fuzzy blobs. We do that with an image (we'll go into images much more pretty soon, but don't worry about the details now).

fuzzy particle
sketch5
int num = 100;  // total number of particles
int curr = 1;   // how many are active
Particle[] particles = new Particle[num];
Random generator = new Random();

float wind = 0; // additional x increment

void setup()
{
  size(200,200);
  smooth();
  background(0);
  frameRate(30);
 
  // From the referenced Processing example
  // Create an alpha masked image to be applied as the particle's texture
  PImage msk = loadImage("texture.gif");
  PImage img = new PImage(msk.width,msk.height);
  for (int i = 0; i < img.pixels.length; i++) img.pixels[i] = color(255);
  img.mask(msk);
  
  // Create the particles
  for (int i=0; i<num; i++)
    particles[i] = new Particle(width/2, height, img); 
}

void draw()
{
  background(0);

  // Compute the amount of wind, between -1 and 1
  wind = 2.0*mouseX/width - 1.0;
  // Draw an arrow in the direction of the wind,
  // with length between -50 and 50
  stroke(255);
  pushMatrix();
  translate(width/2,10);
  if (wind<0) rotate(PI);
  float len = 50*abs(wind);
  line(0,0,len,0);
  line(len,0,len-4,2);
  line(len,0,len-4,-2);
  popMatrix();

  // Draw and update however many have been allowed to enter
  // (one per frame, till all have entered)
  for (int i=0; i<curr; i++) particles[i].draw();
  for (int i=0; i<curr; i++) particles[i].update(wind);
  if (curr<num) curr++;  // bring in the next
}
Particle
class Particle {
  float ox, oy;  // original position
  float x, y;    // current position
  float vx, vy;  // velocity
  float t;       // tint
  float dt = 3;  // tint step
  PImage img;
  
  Particle(float x0, float y0, PImage img)
  {
    ox = x0; oy = y0; 
    this.img = img;  // distinguish parameter from field
    initialize();
  }

  // Put at original position, fully opaque, with random velocity
  void initialize()
  {
    x = ox; y = oy;
    t = 255;
    // Mostly vertical
    vx = (float)generator.nextGaussian()*0.3;
    vy = (float)generator.nextGaussian()*0.3 - 1;
  }
  
  void draw()
  {
    imageMode(CORNER);
    tint(255,t);
    image(img, x-img.width/2, y-img.height/2);
  }
  
  void update(float wind)
  {
    t -= dt;    // decay the transparency
    x += vx+wind;
    y += vy;
    // Re-initialize when decayed away
    if (t < 0) initialize();
  }
}
screenshot[applet]

Arrays of objects with arrays

The fields of an object can hold anything -- even an array. This lets an object collect a bunch of related objects. For example, we can set off a bunch of fireworks, where each firework has a bunch of particles shooting out. In the following sketch, the main array holds Firework objects, each of which has an array of Particle objects. The Firework constructor creates all the Particles for that one Firework. We employ the "reuse" approach from above to handle explosions of multiple Fireworks up to a given number.

The Particles for a Firework are shot out in random angles from the center; we use sine and cosine to convert the angle and an overall speed to velocities in the x and y directions. Particles then move in the usual way, with gravity pulling them to the ground.

sketch6
// Max number of simultaneous fireworks; which one is next to explode
int numFireworks=10, currFirework=0;
Firework[] fireworks = new Firework[numFireworks];
// How many particles for each firework
int numParticles=100;

void setup()
{
  size(400,300);
  smooth();
  noStroke();
  background(0);

  // Create the fireworks (but don't explode them yet)
  for (int i=0; i<fireworks.length; i++)
    fireworks[i] = new Firework(numParticles); 
}

void draw()
{
  fill(0,10);
  rect(0,0,width,height);

  // Draw each, then update each
  for (int i=0; i<fireworks.length; i++) fireworks[i].draw();
  for (int i=0; i<fireworks.length; i++) fireworks[i].update();
}

void mousePressed()
{
  // Explode a firework from the current position
  fireworks[currFirework].explode(mouseX, mouseY);
  currFirework = (currFirework+1) % numFireworks;
}
Firework
class Firework {
  Particle[] particles;        // the individual bits exploding
  color c;                     // common color
  float transparency, dt=3;    // transparency decays over time
  
  Firework(int numParticles)
  {
    // Create the particles for the firework
    particles = new Particle[numParticles];
    for (int i=0; i<numParticles; i++) particles[i] = new Particle();
  }
  
  void draw()
  {
    // Draw the individual particles with the shared color & transparency
    fill(c,transparency);
    for (int i=0; i<particles.length; i++) particles[i].draw();
  }

  void update()
  {
    // Decay the transparency
    if (transparency > dt) transparency -= dt;
    // Update the individual particles
    for (int i=0; i<particles.length; i++) particles[i].update();
  }
  
  // Start a new explosion at (x,y)
  void explode(float x, float y)
  {
    c = color(random(255), random(255), random(255));
    transparency = 255;
    for (int i=0; i<particles.length; i++) particles[i].initialize(x, y);
  }
}
Particle
class Particle {
  float x, y;     // position
  float vx, vy;   // velocity
  float g = 0.02; // gravity
  float r = 3;    // radius
 
  void initialize(float x0, float y0)
  {
    x = x0; y = y0;
    // Random direction and velocity according to circle
    float angle = random(0, TWO_PI);
    float speed = random(1,2);
    vx = cos(angle)*speed; vy = sin(angle)*speed;
  }

  void draw()
  {
    ellipse(x,y,2*r,2*r);
  }
  
  void update()
  {
    vy += g;  // accelerate
    x += vx;  // move
    y += vy;
  }
}
screenshot[applet]

Programming notes

This
this refers to the particular object whose method (or constructor) has been called. For example, if we call "ball.update()", then in the body of the update method, "this" is the same as "ball". This is useful, for example, when we want to distinguish a field of an object from another variable (or parameter) with the same name.