CS 467 - Smoke

Due: 2020-04-02 5:00p

Goals

  • Learn about particle systems
  • Use particles and transparency to implement a smoke effect

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer.

Assignment

In this practical, you are going to implement a particle system and implement a simple smoke effect.

Practical 5 Goal

Links to the references for the included functions can be found at the bottom.

Part 1: Emit particles

I've given you the code that is based on the physics system we've been working with. I've simplified it a little bit, but the structure is the same.

The first thing you are going to do is emit particles from the ParticleSystem.

The system should emit particles every time its update() function is called. Think of it like a fountain -- it is just continuously spewing out particles.

Implement this by adding in a for at the top of update(). It only needs to run somewhere between 5-20 times. Inside of the loop, create a new particle and place it at the coordinates of our emitter (the ParticleSystem). Place the particle by updating the x and y components of the position manually rather than creating a new vector.

Push the particle into this.particles so the system can keep track of it.

We want to give the particle a bit of a push, so let's apply a force to it. We are going to fire the particles upward in a narrow cone. You can adjust the spread of the cone by adjusting the random values below (note also that the particle is called p):

const angle = random(255, 285);
const f = p5.Vector.fromAngle(radians(angle), random(1,3));
p.applyForce(f);

DON'T RUN THIS YET

Currently, the system will grow without bound. Wrap your for loop in an if statement and stop emitting if the length of this.particles exceeds 1000.

Now run it. You should see a spray of particles that cuts off after a moment. In truth, you probably see a collection of snakes or hair. Call background to clear the scene in draw() (I left it thi way so you could see another effect).

Part 2: Age your particles

We would like the emitter to stream continuously, and obviously at a certain point we can't see the particles any more. We are going to give each particle a life.

In the constructor of Particle, add a life property. Set this to be a random number between 50 and 200. We randomize this so we don't see waves of particles all being born and dying together.

In the update() function, decrement this.life.

In draw, add a call to fill that sets the transparency based on this.life, so the particle will fade away as it ages.

In ParticleSystem, update update and draw so they only run on particles that have a lifetime that is over 0.

If you run now, the brief spray or particles will fade out after a moment.

Tricky bit: Create a pool of dead particles

Add a pool property to the ParticleSystem and set it to be an empty array. This is where we will store particles that are expired. We won't remove them from the main list, since they are already being skipped, but we need to know which ones we can reuse.

In the update function of ParticleSystem, add a line immediately after running the particle's update that pushes the particle into the pool if it has 'died' (its life has dropped to zero).

Now, in the for loop creating the new particles, before you create a new Particle, check if the pool is empty. if it is, then create the particle and push it on this.particles. If it isn't, pop the last one off, reset its life and multiply its velocity by 0 (to clear its old velocity out). After this point, you will do the same thing to the particle, regardless if it is new or "reborn" (i.e., set is position, pick an angle, and fire it off).

Remove the guard that limited this.particles to 1000 and run it again. You should not see a continuous stream of particles.

Part 3: Add gravity

In setup, add this after you have created the system:

system.addForce({name:'gravity',
  base: createVector(0,0.1),
  calc: function() {return this.base}
});

This is just the simple form of gravity that we saw earlier. Note that we have retained the calc function, but now it doesn't really do much. We got rid of mass, so the force doesn't need to do anything different based on the object it is being applied to.

Try this and you will get a more realistic fountain.

Part 4: Add an Attractor

We are now going to add an attractor and tie it to the mouse location.

Create a new class called Attractor. Its constructor should take a single value: the strength. Inside the constructor, save the strength as a property and add a second property called position, and set it to the vector (0,0).

Write a second method for the class called calc(particle) -- this will return the force we want applied to the particle. As I showed you on the board, this force should be

F=Gv2v^F = \frac{G}{||v|^2}\hat{v}

GG is the strength of the attractor.

vv is the vector from the particle to the attractor. You get this by subtracting the position of the particle from the attractor's position. Use the static form so you don't accidentally move the attractor.

v2||v||^2 is the square of the distance between the particle and the attractor. This is just the magnitude of the vector vv. Fortunately vectors have a handy function, magSq, which computes this for us. We want to be careful, however. If the attractor gets too close to the particle (or vice versa), this number will get very small (and possibly be 0). A 0 will give us a division by zero problem, and getting too close will create a very large force. To alleviate this, we will artificially constrain the values of the distance. We will say that the distance is at least 25 and at most width (at which point the impact of the attractor on the particle should be too small to matter anyway). You will find that we have a function constrain(value, min, max) that will handle this for you.

v^\hat{v} is direction of the vector (or the unit direction). We get this by normalizing the vector with the normalize function.

To summarize,

  • find the vector between the particle and the attractor
  • calculate the square of the magnitude of the vector and constraint it between 25 and width and use this as the distance squared.
  • divide the strength of the attractor by the distance squared
  • normalize the vector between the particle and the attractor and multiply that by the value above

Hooking up the attractor

Create a new global variable to hold your attractor.

In setup, initialize the attractor. Set the strength to something like 50.

In draw, set the position of the attractor to be the position of the mouse:

attractor.position.x = mouseX;
attractor.position.y = mouseY;

You should now be able to affect the flow of particles by getting the mouse near it. Try different strengths. Negative strengths turn the attractor into a repulsor.

Part 5: Blowing smoke

These basic building blocks can be used for a lot of different effects. We are going to use them to emulate smoke.

This effect works best to my eyes with white particles on a black background.

The first thing we will handle is the movement and positioning.

Move the emitter down to the bottom of the page.

We want our smoke to flow upwards, so comment out the line adding in the gravity.

Now, to make it look like he smoke isn't coming from a single point, randomize the x position of new particles. Range over something like 40 pixels -- 20 on each side of the emitters location.

Already this will start looking a little smoke-like -- especially if you interact with he stream with your attractor in repulse mode.

Add a sprite

A very common approach is to replace our basic shapes with images, or sprites. In this case, we are going to use a blurred circle that looks like a little ball of cotton. More to the point, we will make one.

Create a new global variable called smoke.

In setup, use createGraphics to make a small (25,25) offscreen graphics area.

Write a function called drawSmoke(g) and pass it your new graphics region. In it, iterate over every pixel in the graphics context and draw a point that is colored based on the distance from the center. All of the points should be white, but the alpha value should range from 0 to 16 based on the distance from the center. Note that you will probably not be able to see white points with an alpha value of 16 on a black background. We are counting on multiple particles layering on top of one another to form our smoke.

Because we are using transparency in these images, make sure that the first thing you do is call clear on your new graphics context to get rid of any background.

When you are done, replace the call to circle in the Particle with a call to image that displays your smoke image.

Note: If the image isn't working, just try placing a copy in the corner of the canvas for debugging purposes and use a higher alpha value so you can see what you are doing.

Part 6: Play

There are a lot of control knobs you can tweak in your simulation: the rate of particle creation, the initial speed, the color and size of the particle image, the force of the attractor, the spread of the emissions, etc. try some different values and see how they affect the effect.

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.

References

Links to the reference pages for the functions you will be using:

background
clear
constrain
createCanvas
createVector
dist
div
fromAngle
magSq
map
mult
normalize
p5.Vector
point
radians
random
stroke
sub