Tutorial 10: Organic Motion

Organic Motion

Examples of organic movement include a leaf falling, an insect walking, a bird flying, a person breathing, a river flowing, and smoke rising. This type of motion is often considered idiosyncratic and stochastic. It is often non-periodic or "almost" periodic.

A Random Walk

The simplest form of organic motion is called a "random walk". The following code shows how we can program this by declaring just two variables to hold the x and y coordinates of a point and then adding a small random amount to the x and y coordinates on every call of the draw() function:

  • Show Sketch
/** @peep sketchcode */
float x; // X-coordinate
float y; // Y-coordinate
 
void setup() {
  size(200, 200);
  background(0);
  x = random(width);
  y = random(height);
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  // Update the position of the point
  x += random(-3, 3); // Add a small amount to x-coordinate
  y += random(-3, 3); // Add a small amount to y-coordinate
  x = constrain(x, 0, width); // Constrain x to width of display
  y = constrain(y, 0, height); // Constrain y to height of display
  // Draw the point
  strokeWeight(4);
  stroke(255);
  point(x, y);
}

Using the above code as a base, use arrays for the x and y coordinates of multiple points moving by random walks. Your sketch should look something like this:

Multiple Random Walks

Try adding colour by creating an array of color values to hold a different colour for each moving point, so that it looks something like this:

Coloured Multiple Random Walks

You might also try practicing write a class, perhaps called RandomWalker or Bug that has variables for its position and colour and updates itself every time a function is called. You would then create an array of your class like we did last week in the lecture, see my notes in my Process portfolio post.

Position and Direction

To produce slightly smoother movement, we can apply small changes to the direction that something is moving in, rather than changing its position directly. First let's start with some code to simulate a single moving point with a direction:

  • Show Sketch
/** @peep sketchcode */
float x; // X-coordinate
float y; // Y-coordinate
float angle; // Direction of motion
float speed; // Speed of motion
 
void setup() {
  size(200, 200);
  background(0);
  x = width/2;
  y = height/2;
  angle = random(TWO_PI);
  speed = 0.5;
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  // Calculate distance to move in x and y
  float dx = cos(angle) * speed;
  float dy = sin(angle) * speed;
  // Update coordinate, constrained to display window
  x = constrain(x + dx, 0, width);
  y = constrain(y + dy, 0, height);
  // Draw the point
  stroke(255);
  strokeWeight(4);
  point(x, y);
}

Now we just need to add a small random amount to the angle each time the draw() function is called:

  • Show Sketch
/** @peep sketchcode */
float x; // X-coordinate
float y; // Y-coordinate
float angle; // Direction of motion
float speed; // Speed of motion
 
void setup() {
  size(200, 200);
  background(0);
  x = width/2;
  y = height/2;
  angle = random(TWO_PI);
  speed = 0.5;
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  // Calculate distance to move in x and y
  float dx = cos(angle) * speed;
  float dy = sin(angle) * speed;
  // Update coordinate, constrained to display window
  x = constrain(x + dx, 0, width);
  y = constrain(y + dy, 0, height);
  // Update direction that the point is going to move in
  angle += random(-0.3, 0.3);
  // Draw the point
  stroke(255);
  strokeWeight(4);
  point(x, y);
}

As with the random walk example from before, extend this code to support multiple trails.

Random Trails

Try creating a multi-coloured version too:

Coloured Random Trails

Of course, we're not limited to drawing simple dots, we can draw whatever we want in it's place. The following simple example shows how we can achieve an interesting effect by just drawing a line across the direction of motion. Notice that in this sketch we use the transformation functions to do the drawing because it is easier that calculating the ends of the lines "manually".

  • Show Sketch
/** @peep sketchcode */
float x; // X-coordinate
float y; // Y-coordinate
float angle; // Direction of motion
float speed; // Speed of motion
 
void setup() {
  size(200, 200);
  background(0);
  x = width/2;
  y = height/2;
  angle = random(TWO_PI);
  speed = 0.5;
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  // Calculate distance to move in x and y
  float dx = cos(angle) * speed;
  float dy = sin(angle) * speed;
  // Update coordinate, constrained to display window
  x = constrain(x + dx, 0, width);
  y = constrain(y + dy, 0, height);
  // Update direction that the point is going to move in
  angle += random(-0.3, 0.3);
  // Draw the point
  translate(x, y);
  rotate(angle);
  stroke(255, 130);
  line(0, -10, 0, 10);
}

Design your own organic motion based on this sketch. You might try extending one of your earlier sketches involving multiple moving things and draw something between things that are close together, like we did in the Process example before.

Noise-Based Motion

Another way to achieve apparently organic motion is to use the noise() function. The simplest way to do this is simply to use two calls to the noise() function, one for the x and one for the y coordinates of a point we want to move about the display.

  • Show Sketch
/** @peep sketchcode */
void setup() {
  size(200, 200);
  background(0);
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  // Calculate position based on the noise function
  float x = width * noise(frameCount * 0.017);
  float y = height * noise(frameCount * 0.013);
  // Draw the point
  stroke(255);
  strokeWeight(4);
  point(x, y);
}

We can easily extend this approach to draw several trails at the same time, creating a teaming ball of noise:

  • Show Sketch
/** @peep sketchcode */
void setup() {
  size(200, 200);
  background(0);
}
 
void draw() {
  // Fade the background
  if (frameCount % 10 == 0) {
    noStroke();
    fill(0, 2);
    rect(0, 0, width, height);
  }
  for (int i = 0; i < 10; i++) {
    // Calculate position based on the noise function
    float x = width * noise((frameCount + i * 1000) * 0.017);
    float y = height * noise((frameCount + i * 1000) * 0.013);
    // Draw the point
    stroke(255);
    strokeWeight(4);
    point(x, y);
  }
}

Flocking

Dan Shiffman provides a great introduction to steering behaviours and how to use them in Processing to produce flocking. (These and many other great examples of using Processing can be found on his Nature of Code web site that is a great resource for anyone interested in exploring how to code with Processing and soon to be released in an expanded form as a book.) Steering behaviours are a way of controlling organic motion that can be used to simulate the movement of birds, fish and other animals that move in groups. This powerful technique was first introduce by Craig Reynolds and is used extensively in games and movies.

I'm not going to repeat Dan Shiffman's introduction here, you should definitely go and read it. I've provided a version of his flocking code that has been converted to run on Processing.js and has been simplified somewhat.

  • Show Code
/** @peep sketch */
// Flocking based on example code by Dan Shiffman
// See http://www.shiffman.net/teaching/nature/steering/
 
Flock flock;
 
void setup() {
  size(400,400);
  colorMode(RGB,255,255,255,100);
  flock = new Flock(10, 50);
  for (int i = 0; i < 10; i++) {
    flock.addBoid(width/2, height/2);
  }
}
 
void draw() {
  background(100);
  flock.run();
}
 
// Add a new boid into the System
void mousePressed() {
  flock.addBoid(mouseX, mouseY);
}
 
class Flock {
  ArrayList boids; // An arraylist for all the boids
  float maxspeed = 2.0;
  float maxforce = 0.05;
  float desiredseparation = 10.0;
  float neighbordist = 50.0;
 
  Flock(float _desiredseparation, float _neighbordist) {
    desiredseparation = _desiredseparation;
    neighbordist = _neighbordist;
    boids = new ArrayList(); // Initialize the arraylist
  }
 
  void run() {
    for (int i = 0; i < boids.size(); i++) {
      Boid b = (Boid) boids.get(i);  
      b.update();
      b.render();
    }
  }
 
  void addBoid(float x, float y) {
    Boid b = new Boid(new PVector(x, y), this);
    boids.add(b);
  }
}
 
 
class Boid {
  PVector loc;
  PVector vel;
  PVector acc;
  float radius;
  Flock flock;
 
  Boid(PVector _location, Flock _flock) {
    acc = new PVector(0,0);
    vel = new PVector(random(-1,1),random(-1,1));
    loc = new PVector(_location.x, _location.y);
    radius = 2.0f;
    flock = _flock;
  }
 
  void update() {
    acc.set(0,0,0); // Reset accelertion to 0 each cycle
    // We accumulate a new acceleration each time based on three rules
    PVector sep = separate();   // Separation
    PVector ali = align();      // Alignment
    PVector coh = cohesion();   // Cohesion
    // Arbitrarily weight these forces
    sep.mult(2.0f);
    ali.mult(1.0f);
    coh.mult(1.0f);
    // Add the force vectors to acceleration
    acc.add(sep);
    acc.add(ali);
    acc.add(coh);
 
    vel.add(acc); // Update velocity
    vel.limit(flock.maxspeed); // Limit speed
    loc.add(vel); // Update location
 
    // Wraparound borders
    if (loc.x < -radius) loc.x = width+radius;
    if (loc.y < -radius) loc.y = height+radius;
    if (loc.x > width+radius) loc.x = -radius;
    if (loc.y > height+radius) loc.y = -radius;
  }
 
  void render() {
    // Draw a triangle rotated in the direction of velocity
    float theta = atan2(vel.y, vel.x);
    fill(200);
    stroke(255);
    pushMatrix();
    translate(loc.x,loc.y);
    rotate(theta);
    triangle(radius*2, 0, -radius*2, radius, -radius*2, -radius);
    popMatrix();
  }
 
  // Separation
  // Method checks for nearby boids and steers away
  PVector separate() {
    PVector sum = new PVector(0, 0, 0);
    PVector diff = new PVector(0, 0, 0);
    int count = 0;
    // For every boid in the system, check if it's too close
    for (int i = 0 ; i < flock.boids.size(); i++) {
      Boid other = (Boid) flock.boids.get(i);
      if (this != other) {
        float d = PVector.dist(loc, other.loc);
        // If the distance is greater than 0 and less than an arbitrary amount
        if ((d > 0) && (d < flock.desiredseparation)) {
          // Calculate vector pointing away from neighbor
          diff.set(loc.x, loc.y, loc.z);
          diff.sub(other.loc);
          diff.normalize();
          diff.div(d);        // Weight by distance
          sum.add(diff);
          count++;            // Keep track of how many
        }
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      sum.div(count);
    }
    return sum;
  }
 
  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  PVector align() {
    PVector sum = new PVector(0,0,0);
    int count = 0;
    for (int i = 0 ; i < flock.boids.size(); i++) {
      Boid other = (Boid) flock.boids.get(i);
      if (this != other) {
        float d = PVector.dist(loc, other.loc);
        if ((d > 0) && (d < flock.neighbordist)) {
          sum.add(other.vel);
          count++;
        }
      }
    }
    if (count > 0) {
      sum.div(count);
      sum.limit(flock.maxforce);
    }
    return sum;
  }
 
  // Cohesion
  // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
  PVector cohesion() {
    PVector sum = new PVector(0,0,0);   // Start with empty vector to accumulate all locations
    int count = 0;
    for (int i = 0 ; i < flock.boids.size(); i++) {
      Boid other = (Boid) flock.boids.get(i);
      float d = PVector.dist(loc, other.loc);
      if ((d > 0) && (d < flock.neighbordist)) {
        sum.add(other.loc); // Add location
        count++;
      }
    }
    if (count > 0) {
      sum.div((float)count);
      return steer(sum,false);  // Steer towards the location
    }
    return sum;
  }
 
  // A method that calculates a steering vector towards a target
  // Takes a second argument, if true, it slows down as it approaches the target
  PVector steer(PVector target, boolean slowdown) {
    PVector steer = PVector.sub(target,loc);  // A vector pointing from the location to the target
    float d = steer.mag(); // Distance from the target is the magnitude of the vector
    // If the distance is greater than 0, calc steering (otherwise return zero vector)
    if (d > 0) {
      // Normalize desired
      steer.normalize();
      // Two options for desired vector magnitude (1 -- based on distance, 2 -- maxspeed)
      if ((slowdown) && (d < 100.0f)) steer.mult(maxspeed*(d/100.0f)); // This damping is somewhat arbitrary
      else steer.mult(flock.maxspeed);
      // Steering = Desired minus Velocity
      steer.sub(vel);
      steer.limit(flock.maxforce);  // Limit to maximum steering force
    } else {
      steer.set(0,0);
    }
    return steer;
  }
}

Note: To add new members to the flock click on the sketch with the mouse.

Experiment with the effect of changing the values for desiredseparation and neighbordist.

Can you adapt the flocking code to produce multiple flocks with different behaviours, i.e., different values for desiredseparation and neighbordist?

Can you change the code for rendering a Boid to draw them differently, or to support different colours, either for the Flock or for individual Boids?

Comments

Nobody has said anything yet.