Midterm Project – Saeed

Project Overview

For my project I wanted to replicate the movement of birds between trees and their pathing around obstacles, in this case mountains. To best visualize this I started with a top-down view and then to make the sketch more visually pleasing I used contour lines to replicate a topographic map.

I was first inspired to do a simulation with birds by the garden outside my house in Dubai. Put simply there are a lot of birds, many are pigeons or sometimes crows as far as I can tell. From what I’ve seen most birds travel in packs and follow a similar direction to each other and at our house they often travel from tree to tree within the garden or with the trees lining the walls of my neighbours then they may stay at a tree for a while before moving again but always following a similar path in the air. Similarly, I see some birds stop at our pool to drink water often altogether. I wanted to replicate this behaviour in my project.

In addition, after deciding to do the simulation from a top down view I decided to add contour lines and make it appear to be a topographic map because it’s an idea I have been exploring for a while. I first learned how to make basic contour lines in photoshop using noise and since wanted to find places to use it since I feel it doesn’t get as much use as it should.

Implementation Details

This simulation is built around trees as emitters, birds as moving agents, a flow field for natural motion, mountains as obstacles that affect the vector field, contour lines for terrain, and a UI panel that lets you change parameters in real time.

Bird class

The Bird class represents one moving agent that travels between trees. Each bird stores position, destination, destination tree index, arrival/wait state, size, trail history, and a day-only behavior flag.

  • A destination tree is chosen randomly.
  • The bird travels to its destination.
  • When they arrive, they wait for a random duration.
  • After waiting, they pick a new destination tree at random.
  • Their movement is not only direct-to-target; a vector field is used for more natural movement.
class Bird {

  constructor(originX, originY, destinationX, destinationY, destTreeIndex, diameter = 10) {

    this.pos = createVector(originX, originY);

    this.des = createVector(destinationX, destinationY);

    this.destTreeIndex = destTreeIndex;

    this.arrived = false;

    this.trail = [];

  }

}

 

Tree class

The Tree class is both a visual node and an emitter for birds. Each tree has x and y coordinates, a diameter, and its own bird array.

  • A tree can initialize a number of birds.
  • Each spawned bird gets this tree as origin and a different tree as destination.
  • The tree updates and draws all birds in its own array.
class Tree {

  constructor(x, y, diameter = 40) {

    this.x = x;

    this.y = y;

    this.diameter = diameter;

    this.birdArray = [];

  }

}

The pathing starts with a direct vector from bird position to destination. Then the code samples the flow field at the bird position and then using vector addition it changes the heading of the bird itself.

 

let toDest = p5.Vector.sub(this.des, this.pos);

let desired = toDest.copy().setMag(birdSpeed);
let flow = getFlowAt(this.pos.x, this.pos.y);

let steer = desired.add(flow.mult(0.6));

steer.limit(birdSpeed + 0.5);

this.pos.add(steer);
function updateFlowField() {

  let noiseScale = 0.02;

  let time = frameCount * 0.005;

  for (let y = 0; y < flowFieldRows; y++) {

    for (let x = 0; x < flowFieldCols; x++) {

      let angle = noise(x * noiseScale, y * noiseScale, time) * TWO_PI * 2;

      flowField[y][x] = p5.Vector.fromAngle(angle).mult(0.8);

    }

  }

}

Mountain generation

Mountains are generated by sampling terrain elevation candidates from noise (to make spawning more natural as opposed to complete randomness), sorting candidates by highest elevation first, then placing mountains with spacing constraints.

const candidates = buildMountainCandidates();

for (let i = 0; i < candidates.length && mountains.length < quantity; i++) {

  const c = candidates[i];

  const baseRadius = map(c.elevation, 0, 1, mountainMinRadius, mountainMaxRadius, true);

  const radius = baseRadius * random(0.9, 1.12);

  const x = c.x;

  const y = c.y;

  if (isMountainPlacementValid(x, y, radius, 1, true)) {

    mountains.push({ x, y, radius });

  }

}

Mountains do not rewrite the global flow field grid directly. Instead, there’s an additional tangential force that gets added to the bird’s steer direction, not the vector field.

const repelStrength = map(edgeDistance, 0, 140, 2.8, 0.2, true);

const repel = away.copy().mult(repelStrength);

const tangent = createVector(-away.y, away.x);

if (tangent.dot(desiredDirection) < 0) {

  tangent.mult(-1);

}

 

Contour lines and how they work

Contours are generated from a hybrid elevation function that combines base noise terrain with mountain influence.

  • Build contour levels between minimum and maximum elevation.
  • For each cell, compute corner elevations.
  • Build a case mask and map it to edge segments.
  • Interpolate exact crossing points on edges.
  • Store line segments and then stitch/smooth them into polylines.
  • Draw major and minor contours with different stroke weights and alpha.
if (cell.tl.v >= iso) mask |= 8;

if (cell.tr.v >= iso) mask |= 4;

if (cell.br.v >= iso) mask |= 2;

if (cell.bl.v >= iso) mask |= 1;

const segments = caseToSegments[mask];
const a = getCellEdgePoint(edgeA, cell, iso);

const b = getCellEdgePoint(edgeB, cell, iso);

contourSegmentsByLevel[levelIndex].push({ a, b });

 

How the UI works

The UI is created in setupUIControls() as a fixed bottom panel.

  • Scene and simulation control (refresh noise seed, pause/resume).
  • Canvas preview and scale.
  • Mountains enabled and mountain count.
  • Tree count and birds per tree.
  • Bird speed and trail length.
  • Day/night cycle toggle, manual time selection, and day duration.
  • Contour smoothing iterations.

 

When values change, callback handlers rebuild affected systems. For example:

  • Tree count change regenerates trees and birds, then mountains.
  • Birds per tree change regenerates birds only.
  • Mountain toggle/count updates mountains and rebuilds contours.
  • Contour smooth input rebuilds contour polylines.

 

Example:

ui.birdCountInput.changed(() => {

  birdsPerTree = parseBoundedNumber(ui.birdCountInput.value(), birdsPerTree, 1, 20);

  ui.birdCountInput.value(String(birdsPerTree));

  regenerateBirdsForTrees();

});

States

Last but not least there are multiple states in the program. Day and Night, and with or without mountains. I decided to keep the states simple for this project. Day and night have visual changes and mountains affect the movements of the birds.

Milestones

Version 1

This was one of my first versions of my sketch it consisted of trees that were randomly spawned around the canvas (later I would switch to using random) and birds that travelled inbetween and there is the vector field although it is hard to notice in this image. There are some small details I implemented as well like birds would stop at the edge of the tree and I made the opacity of the background low so that I could see the paths of the birds more clearly.

Version 2

At this point I created the Mountain class, it would be different sizes and I just had it spawn randomly around the canvas at this point and I could change how many would spawn. As you can see at this point I didn’t implement the avoidance as you can see by the trail of one bird at the bottom that phased through the mountain.

Version 3

From my perspective at this point I had overcome the most important technical features. I now had birds that travelled between trees that moved organically and could avoid mountains and I was happy with the effect but I knew there was potential to make it more aesthetically pleasing but I didn’t know what my next step would be.

Version 4 (Daytime and Nighttime)

At this point I had simply implemented two states of it either being day or being night. Later I added a version of birds which would only move during the day and would stay at their trees during the night and a transition effect from day to night.

Version 5 (User Interface)

I took all the features I had before and now I added a user interface in the form of sliders at the bottom of the canvas. Because it changed the canvas in real time it allowed me to see different variations and it led to the idea of making the mountains spawn using noise but only at points of ‘highest elevation’. This later led to the idea of the topographic map.

Version 6

I started with just topographic lines with the noise function alone (not taking into consideration the placement of mountains) then after tweaking the strokeWeight to make it more visible I added the mountains  in the form of the contour lines. Then I added colors based on the elevation opting for a sand color and for the areas of low elevation a blue color to represent water and for the mountains a gray color but I later tweaked the gray color and sand color to make it more prominent.

Version 7

This is the latest version of my sketch. From the last version to this I added a few more elements that can be changed from the UI (not shown in this screenshot) and

(Looks better when viewing it on a larger canvas)

Reflection

I’m happy with the movement of the birds especially the avoidance of the mountains. However, I wasn’t able to get an A3 sized screenshot of the simulation because it is too slow when scaled to be A3 sized. I’m not sure why exactly I haven’t taken the time to sit down and figure it out but I have a suspicion that its because of the size of the vector field and all the vector calculations. I’m just guessing.

I would also want to make improvements on the aesthetics. I thought about shadows and maybe having more detailed models for the birds and trees but I wasn’t sure how to without going against the topographic map aesthetic.

References

  • p5.js library — p5js.org
  • Daniel Shiffman, The Nature of Code: vector movement and flow fields (natureofcode.com)
  • AI disclosure: To assist with the mountain repulsion force implementation and the contour line generation using the Marching Squares algorithm

Leave a Reply

Your email address will not be published. Required fields are marked *