CS 2, Winter 2008
Programming for Interactive Digital Arts

Feb 1: Pseudo-physics


To make objects move, we define rules as to how to update their positions each frame. We've seen a random rule -- take a random step in the x direction and a random step in the y direction. We've also seen rules that move in predefined directions -- take a fixed-sized step in both x and y or in angle. We put "bounce" conditions in those rules so that when the motion reached the wall or the full spiral, the direction of the step was reversed.

Today we'll look at a number of other different ways to define rules of motion. We'll stick with our same basic approach for moving objects -- each frame, update the position by some amount in the x direction (x += vx;) and some amount in the y direction (y += vy;). Here we use vx and vy as the variable names for the step sizes, representing the components of the velocity in the x and y directions. We'll develop rules based on the types of equations from physics class, although there's no attempt here to be physically accurate -- we don't worry about masses, exact constants of proportionality, etc.

Gravity

First let's consider gravity. Gravity accelerates an object towards the ground. So at each time step, the velocity in the y direction increases (vy += gravity;). Suppose we use 0.1 as the value for gravity. Then if we start off with vy as 0, after the first frame the object will be moving with a vy of 0.1, after the second frame 0.2, and so forth. It moves faster and faster toward the ground (increasing y) each frame.

sketch1
Ball ball;

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

  ball = new Ball(width/2,height/2); 
}

void draw()
{
  fill(0,20);
  rect(0,0,width,height);
  ball.draw();
  ball.update();
}

void keyPressed()
{
  if (key=='r') { // reset ball
    ball.y = height/2;
    ball.vy = 0;
  }
  else if (key=='g') { // half as much gravity
    ball.gravity *= 0.5;
    println("gravity:"+ball.gravity);
  }
  else if (key=='G') {
    ball.gravity *= 2;
    println("gravity:"+ball.gravity);
  }
}
Ball
class Ball {
  float x, y;          // position
  float vx=0, vy=0;    // velocity in the two directions   
  float r=10;          // radius
  float gravity=0.1;   // the amount of acceleration

  // Initialize a Ball at position (x0,y0)
  Ball(float x0, float y0)
  {
    x = x0; 
    y = y0;
  }

  void draw()
  {
    fill(255);
    ellipse(x,y,r*2,r*2);
  }

  void update()
  {
    // Accelerate according to gravity
    vy += gravity;
    // Move in the appropriate direction by the step size
    x += vx;
    y += vy;
    
    // Bounce   
    if (y > height-r) {
      y = height-r;
      vy = -vy;
    }
  }
}
screenshot[applet]

By varying the gravitational constant ('g' and 'G' keys), we can have the ball bouncing on the moon or on Jupiter.

Friction

You've probably never seen a ball bounce forever, like in that sketch (unless you happened to place a perfectly elastic ball in a perfect vacuum). The ball gets slowed down by various frictional forces -- the air itself slows it down, as does the collision with the ground (i.e., it transfers some energy to the air and to the ground). We can extend our gravity sketch to include a term I call "drag", which slows the ball down every frame (vy *= drag;), along with a term I call "frictY", which slows it down only when it bounces (vy *= frictY; inside the bounce conditional). These terms are multiplicative, as opposed to gravity, which is additive. Gravity constantly accelerates an object, whereas friction just "damps" the velocity proportional to its current magnitude. The faster something is going, the more effect a damping term has. Thus it causes the ball to gradually come to rest, in kind of an "easing"-like fashion.

screenshot[applet]

Try varying the values for the two terms ('d'/'D' for drag, and 'y'/'Y' for frictY); note the 'r' puts the ball back in its initial position. Note that a value of 1 means there is no damping; since the damping term keeps getting multiplied each frame, it has to be fairly close to 1 to avoid immediately halting the object.

Setting the velocity

Gravity constantly increases the velocity, while friction damps it proportionally. We can also (in this not-quite-physical world) just instantaneously set it. The bounce conditionals do that, making the velocity the opposite of what it used to be. We can also imparts an additional increment to it, to act as if we actively hit the object and instantly sped it up.

    // Dribble
    if (abs(x-mouseX) < r && abs(y-mouseY) < 1.25*r) {
      // Close enough to the paddle to bounce;
      // now see if bounce up or down
      if (y < mouseY) { // up
        y = min(y,mouseY-1.25*r);  // make sure above paddle
        if (vy>0) vy=-vy;          // make sure moving up
        vy = max(vy-1,-maxV);      // move up faster, within limit
      }
      else {
        y = max(y,mouseY+1.25*r);  // make sure below paddle
        if (vy<0) vy=-vy;          // make sure moving down
        vy = min(vy+1,maxV);       // move down faster, within limit
      }
    }
screenshot[applet]

The following "paintball" sketch brings together gravity and friction, following an initial burst of velocity. The x coordinate and velocity now vary. Consequently, we also keep a separate term for friction in the x direction -- skidding across the surface.

sketch4
Ball ball = new Ball();

float shotSpeed=10;
float cannonLength=25;

color shotColor = color(random(255),random(255),random(255),100);

void setup()
{
  size(400,300);
  smooth();
  background(0);
  strokeCap(SQUARE);  // for cannon
}

void draw()
{
  // Erase just the area where the cannon is
  fill(0); stroke(0); strokeWeight(ball.r*2);
  ellipse(0,height,2*cannonLength,2*cannonLength);
 
  // Draw the cannon, pointing toward the mouse 
  // (Code borrowed from the "Coordinates revisited" lecture)
  stroke(shotColor); 
  strokeWeight(ball.r*2);
  pushMatrix();
  translate(0,height);
  rotate(atan2(mouseY-height, mouseX));
  line(0,0,cannonLength,0);
  popMatrix(); 
  noStroke();

  // Handle the ball
  ball.draw();
  ball.update();
}

void keyPressed()
{
  if (key==' ') { // shoot
    // Compute the position at the end of the cannon by cos and sin
    // Compute the initial velocity the same way
    float angle = atan2(mouseY-height,mouseX);
    ball.shoot(cannonLength*cos(angle), height+cannonLength*sin(angle),
               shotSpeed*cos(angle), shotSpeed*sin(angle),
               shotColor);
  }
  else if (key=='c') // color
    shotColor = color(random(255),random(255),random(255),100);
  else if (key=='s') { // shot speed
    shotSpeed++;
    println("shot speed:"+shotSpeed);
  }
  else if (key=='S') {
    if (shotSpeed>1) shotSpeed--;
    println("shot speed:"+shotSpeed);
  }
  else if (key=='g') { // half as much gravity
    ball.gravity *= 0.5;
    println("gravity:"+ball.gravity);
  }
  else if (key=='G') {
    ball.gravity *= 2;
    println("gravity:"+ball.gravity);
  }
  else if (key=='d') { // half as much drag (measured as 1-d)
    ball.drag = 1 - (1-ball.drag)*0.5;
    println("drag:"+ball.drag);
  }
  else if (key=='D') {
    ball.drag = 1 - (1-ball.drag)*2;
    println("drag:"+ball.drag);
  }
  else if (key=='x') { // half as much damping
    ball.frictX = 1 - (1-ball.frictX)*0.5;
    println("x friction:"+ball.frictX);
  }
  else if (key=='X') {
    ball.frictX = 1 - (1-ball.frictX)*2;
    println("x friction:"+ball.frictX);
  }
  else if (key=='y') { // half as much damping
    ball.frictY = 1 - (1-ball.frictY)*0.5;
    println("y friction:"+ball.frictY);
  }
  else if (key=='Y') {
    ball.frictY = 1 - (1-ball.frictY)*2;
    println("y friction:"+ball.frictY);
  }
}
Ball
class Ball {
  float x, y;          // position
  float vx=0, vy=0;    // velocity in the two directions   
  float r=5;           // radius
  float gravity=0.1;   // the amount of acceleration
  float drag=0.99;     // multiplicative factor for velocity
  float frictX=0.75;   // multiplicative factor, only when bounce
  float frictY=0.75;   // multiplicative factor, only when bounce
  color c=shotColor;   // color
  
  // Initial ball is off screen
  Ball()
  {
    x = -2*r; y = -2*r;
  }

  void draw()
  {
    fill(c);
    ellipse(x,y,r*2,r*2);
  }

  void update()
  {
    // Accelerate according to gravity
    vy += gravity;
    // Now damp according to drag
    vx *= drag;
    vy *= drag;
    // Move in the appropriate direction by the step size
    x += vx;
    y += vy;

    // Bounce
    if (x > width-r || x < r) {
      // Frictionless walls
      vx = -vx;
    }
    if (y > height-r) {
      y = height-r;
      vy = -vy;
      vx *= frictX; vy *= frictY;    // damp
    }
  }

  // Shoot the Ball from position (x0,y0), moving with velocity (vx0,vy0),
  // and with the given color 
  void shoot(float x0, float y0, float vx0, float vy0, color c0)
  {
    x = x0; y = y0; 
    vx = vx0; vy = vy0;
    c = c0;
  }
}
screenshot[applet]

Springs

The force applied by a spring is proportional to how far it has been stretched or compressed. Thus for a vertical spring, we take the difference between the current y and some center (rest position) y. If the difference positive, the spring is stretched and the force is negative, to make it shorter. If it's negative, the spring is compressed and the force is positive, to make it longer. Thus we negate the difference, and scale it by some amount, called the spring constant, to indicate how hard to try to restore the length (the "easing" type of concept again). We can also include a damping term, for the friction in the spring.

sketch5
Spring spring;

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

  spring = new Spring(width/2,height/2); 
}

void draw()
{
  background(0);
  spring.draw();
  spring.update();
}

void keyPressed()
{
  if (key=='k') { // half as much spring constant
    spring.k *= 0.5;
    println("spring:"+spring.k);
  }
  else if (key=='K') {
    if (spring.k < 0.5) {
      spring.k *= 2;
      println("spring:"+spring.k);
    }
  }
  else if (key=='d') { // half as much damping (measured as 1-d)
    spring.d = 1 - (1-spring.d)*0.5;
    println("damping:"+spring.d);
  }
  else if (key=='D') {
    spring.d = 1 - (1-spring.d)*2;
    println("damping:"+spring.d);
  }
}
Spring
class Spring {
  float cx, cy;        // rest position
  float x, y;          // current position
  float vx=0, vy=0;    // velocity in the two directions   
  float r = 10;        // radius
  float k = 0.1;       // spring constant
  float d = 0.95;      // damping
  boolean grab = false;  // is the mouse pressed in it?
  
  // Initialize a Spring at rest position (x0,y0)
  Spring(float x0, float y0)
  {
    cx = x0; cy = x0;
    x = x0; y = y0;
  }

  void draw()
  {
    if (grab) fill(128);  // gray when grabbed
    else fill(255);
    ellipse(x,y,r*2,r*2);
  }

  void update()
  {
    if (mousePressed && dist(mouseX,mouseY,x,y) < r) {
      // When grabbed, follow mouse
      grab = true;
      y = mouseY;
    }
    else {
      // When not grabbed, spring
      grab = false;
      // Apply spring force
      vy -= k * (y-cy);
      // Damp
      vy *= d;
      // Update position
      y += vy;
    }
  }
}
screenshot[applet]

Try varying the constants ('k'/'K' and 'd'/'D').

We can have a two-dimensional spring that is restored in both x and y directions. This sketch is inspired by an example in the code for chapter 4 of Greenberg.

Spring
class Spring {
  float cx, cy;        // rest position
  float x, y;          // current position
  float vx=0, vy=0;    // velocity in the two directions   
  float r = 10;        // radius
  float k = 0.1;       // spring constant
  float d = 0.95;      // damping
  
  // Initialize a Spring at rest position (x0,y0)
  Spring(float x0, float y0)
  {
    cx = x0; cy = x0;
    x = x0; y = y0;
  }

  void draw()
  {
    // Set color from stretchiness
    float col = 128+128*dist(x,y, cx,cy)/max(width/2,height/2); 
    fill(col); stroke(col);  
    // Attach to wall 
    line(width/2,0,x,y);
    line(width/2,height,x,y);
    ellipse(x,y,r*2,r*2);
  }

  void update()
  {
    if (mousePressed && dist(mouseX,mouseY,x,y) < r) {
      x = mouseX; y = mouseY;
    }
    else {
      // Spring equations for both x and y
      vx -= k * (x-cx);
      vx *= d;
      x += vx;
      vy -= k * (y-cy);
      vy *= d;
      y += vy;
    }
  }
}
screenshot[applet]

Practice problems

  1. Make gravity pull the ball to the ceiling rather than the floor. [hints]
  2. Have different "zones" of drag (indicated with different colored rectangles), so that the ball slows down more in one than in another. [hints]
  3. Make a hot air balloon -- very little gravity, a bit of "current" in the x direction (perhaps different amount at different y coordinates or zones), a keypress to give a little upward boost, etc. [hints]
  4. Untether the spring -- allow the center position to move, pulling the ball along. [hints]