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.
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; } }
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; } } } }
[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.
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; } }
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(); } } }
[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.
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 }
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(); } }
[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.
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 }
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(); } }
[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).
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 }
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(); } }
[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.
// 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; }
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); } }
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; } }
[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.