26. Simulating Water

In these notes you will learn -

  • About particle systems and their many uses.
  • How to model a drop of water as an object.
  • How to model a flow of water as an ArrayList of objects.

26.1. Introduction

As you’ve already seen, one of the things that we like about object-oriented programming is that it makes it easy to create many copies of object. In this note, we’ll see how to simulate flowing water by observing that water from an animator’s perspective, can be thought of as a collection of drops! We will use a small rectangle to represent a drop, and have hundreds of these drawn on the screen.

This is an example of a technique called particle systems which is often used to simulate things like water, smoke, fire, clouds, and so on.

Note

Particle systems can be used to create many different kinds of special effects. For instance, if you search for “particle system” on YouTube you’ll find impressive demos like this.

26.2. What s Water?

The basic idea we’re going to follow, that will allow us to draw water, is to treat water like a collection of discrete droplets. For simplicity, we’ll represent the droplets as little blue rectangles.

Each droplet is a little particle with a position, velocity, and gravity acting on it. With that in mind, we can outline a Droplet object:

class Droplet {
    float x, y; // position of the droplet.
    float dx, dy; // velocity of the droplet.
    float gravity; // force of gravity on this droplet.

    Droplet() {
        x = 10;
        y = 290;
        dx = 2.86;
        dy = -1.30;
        gravity = 0.1;
    }

    void render() {
        noStroke();
        fill(0, 0, 255);
        pushMatrix();
            translate(x, y);
            rectangle(0, 0, 15, 15);
        popMatrix();
    }

    void update() {
        x += dx;
        y += dy;

        dy += gravity;
    }
}

Note the following:

  • The render() function uses translate() to move the origin of the coordinate system to where we want to draw the droplet. Thus the rectangle function draws the drop at (0, 0).

  • Recall that the pushMatrix() function saves the current coordinate system, and the popMatrix() function restores the coordinate system to the state that it was in before the most recent call to pushMatrix().

  • As we’ve seen before, gravity is simulated as a downward acceleration. For a Droplet object this means that we add a small value (the “gravitational constant” for the droplet) to dy every frame.

  • The constructor for the droplet doesn’t need any input values:

    droplet = new Droplet();
    

    This creates a new Droplet object and calls the Droplet() constructor which in turn sets all of the variables of the droplet to some default values (these were obtained by experimentation).

    This Droplet() constructor is designed for drawing just one particle, so we will have to modify it soon.

26.3. One Droplet of Water

Now that we have the Droplet class,Let’s think about the path a single drop of water takes when it is shot out of a hose. Suppose the particle comes out of the hose on the left side of the screen and moves to the right at an (initially) upward angle. As it moves across the screen, gravity is constantly pulling it down toward the bottom of the screen. Thus the particle traces a curve across the screen: it starts low, climbs up and then descends.

Note

If this were a physics class we would sharpen this description to the point where a particle’s path can be described mathematically. However, we are not interested in the exact equations of movement here. Instead, we want to simulate particle movement well enough to look good for whatever application we are using it for.

So to get this working in code:

// ... Droplet class as above ...

Droplet droplet;

void setup() {
    size(900, 400);

    droplet = new Droplet();
}

void draw() {
    background(255);
    droplet.render();
    droplet.update();
}

Run this program and you will see a small blue square fly across the screen following a curved trajectory.

26.4. Multiple Particles

To simulate a flow of water out of a hose, we need to make some modifications:

  • We needto add many more droplets — enough to make it look somewhat like a stream of water.

  • We’ll add some randomness to the starting position and velocity of the droplets so that they can spread out a bit. If we don’t do this then all the particles will follow exactly the same path.

  • We will want to “recycle” water droplets. With so many Droplet objects being animated at once, our program might start to slow down or eat up a lot of memory.

    So we’ll re-use droplets once they have flown off the screen and are no longer visible. This is a common trick, and it gives the illusion of a continuous flow of water while using a fixed amount of memory.

Here’s the modified program:

final int NUM_DROPLETS = 250;
final int SPEED = 8;

class Droplet {
    float x, y; // position of the droplet.
    float dx, dy; // velocity of the droplet.
    float gravity; // force of gravity on this droplet.

    Droplet() {
        randomReset();
    }

    void randomReset() {
        x = 0;
        y = height;
        dx = SPEED * random(0.24, 0.245);
        dy = SPEED * random(-0.45, -0.35);
        gravity = 2 * random(0.005, 0.015);
    }

    void render() {
        noStroke();
            fill(0, 0, 255);
            pushMatrix();
            translate(x, y);
            rect(0, 0, 15, 15);
        popMatrix();
    }

    void update() {
        x += dx;
        y += dy;

        dy += gravity;

        if (!onScreen()) {
            randomReset();
        }
    }

    boolean onScreen() {
        if ((0 <= x && x <= width) && (0 <= y && y <= height)) {
            return true;
        } else {
            return false;
        }
    }

}

ArrayList<Droplet> droplets;

void setup() {
    size(900, 400);

    droplets = new ArrayList<Droplet>();

    int i = 0;
    while(i < NUM_DROPLETS) {
        Droplet d = new Droplet();
        droplets.add(d);
        i++;
    }
}

void draw() {
    background(255);
    for (Droplet drop : droplets) {
        drop.render();
        drop.update();
    }
}

Notice the following:

  • The constant NUM_DROPLETS is how many droplets of water will be in the simulation. Play around with this value to see the results you get.

    Keep in mind that the bigger the value of NUM_DROPLETS, the more time and memory the program takes to run.

  • The constant SPEED is used as a convenient way to speed-up or slow-down all the particles at once. It’s used in randomReset.

  • The randomReset function in Droplet re-sets the droplet to be at the left side of the screen. Its velocity and gravity are then set randomly.

  • The offScreen function returns true if the droplet is off the screen, and false if it is on the screen.

  • The update function calls randomReset whenever the droplet goes off the screen. In this way particles that are no longer visible get re-used in the animation.

  • Droplet objects are stored in an ArrayList<Droplet> container:

    // ...
    
    ArrayList<Droplet> droplets;  // initialized to null
    
    void setup() {
      // ...
    
      droplets = new ArrayList<Droplet>();  // creates an empty ArrayList
    
      // ...
    }
  • We add Droplet objects to droplets using a while-loop:

    // ...
    
    void setup() {
      //  ...
    
      droplets = new ArrayList<Droplet>();
      int i = 0;
      while (i < NUM_DROPLETS) {
        Droplet d = randomDroplet();
        droplets.add(d);
        ++i;
      }
    }

    The while-loop repeatedly creates a new random droplet, adds it to droplets, and then increments i. Eventually, the condition i < NUM_DROPLETS will become false, thus terminating the loop.

  • The for-each loop in the draw function draws and updates each water droplet:

    void draw() {
      background(255);
      for (Droplet drop : droplets) {
        drop.render();
        drop.update();
      }
    }

The results of this program are quite interesting: it creates an effect that is, at least, suggestive of water. It’s far from perfect, though, and you would need to play around with it some more to get it looking just right.

26.5. Programming Questions

  1. The pre-defined Processing variable frameRate is the approximate number of frames per second that a program is running at. Print the value of frameRate in the upper-left corner of the screen so that it is easy to see what the current frame rate is. This is a good way to get a quick estimate of the performance of your program after you add some new special effect, e.g. some effects like smoothing or using alpha values can have a significant impact on the frame rate.
  2. Modify randomReset so that the size of the droplet is randomly chosen within a min/max range of values of your choosing.
  3. Modify the program so that the source of the water is the location of the mouse pointer. Thus, as you move the pointer around the screen the entire flow of water moves with it.
  4. Modify your answer to the previous question so that if you click on the screen, then the water flow is “pinned” to that location for the rest of the program. That is, the water flow will not follow the mouse after you have clicked a button.