Processing Intermediate Article 3

Physics Simulation

Implement springs, Verlet integration, spatial-grid collision detection, and cloth simulation using both hand-coded physics and the Toxiclibs library.

⏱ 22 min physics springs verlet toxiclibs collision

Realistic-feeling physics does not require a game engine. With a few equations and careful integration you can simulate springs, ropes, cloth, and rigid-body collisions directly in Processing. This tutorial covers the mathematics, then shows how Toxiclibs shortcuts the heavy lifting.

Hooke’s Law: Manual Springs

A spring pulls two points toward each other with a force proportional to how far they are stretched from their rest length:

F = −k × (currentLength − restLength)

k is the spring stiffness. A large k makes a stiff spring; a small k gives a rubbery feel.

class Spring {
  PVector anchorA, anchorB;
  float restLength;
  float k;

  Spring(PVector a, PVector b, float rest, float stiffness) {
    anchorA = a;
    anchorB = b;
    restLength = rest;
    k = stiffness;
  }

  void applyForce(Node nodeA, Node nodeB) {
    PVector delta = PVector.sub(nodeB.pos, nodeA.pos);
    float currentLen = delta.mag();
    float stretch = currentLen - restLength;
    float forceMag = -k * stretch;

    PVector forceDir = delta.normalize(null);  // unit vector A→B
    PVector force = PVector.mult(forceDir, -forceMag);

    nodeA.applyForce(force);
    nodeB.applyForce(PVector.mult(force, -1));
  }
}

Verlet Integration

Standard Euler integration (vel += acc; pos += vel) accumulates errors over time and can “explode” with stiff springs. Verlet integration is more stable because it derives velocity implicitly from the last two positions:

class Node {
  PVector pos;
  PVector prevPos;   // position last frame
  PVector acc;
  float mass;
  boolean pinned;

  Node(float x, float y, float m) {
    pos     = new PVector(x, y);
    prevPos = new PVector(x, y);
    acc     = new PVector(0, 0);
    mass    = m;
    pinned  = false;
  }

  void applyForce(PVector f) {
    acc.add(PVector.div(f, mass));
  }

  void update() {
    if (pinned) { acc.mult(0); return; }

    PVector velocity = PVector.sub(pos, prevPos);
    velocity.mult(0.98);               // damping (simulates air friction)

    PVector nextPos = pos.copy();
    nextPos.add(velocity);
    nextPos.add(PVector.mult(acc, 0.5)); // second-order term

    prevPos = pos.copy();
    pos = nextPos;
    acc.mult(0);
  }

  void display() {
    fill(pinned ? color(255, 80, 80) : 220);
    noStroke();
    ellipse(pos.x, pos.y, 10, 10);
  }
}

Pinned nodes are fixed in space — essential for the top row of a cloth simulation.

Circle-Circle Collision Response

Two circles of radius r1 and r2 overlap when their centre distance is less than r1 + r2. Resolve the overlap by pushing them apart along the collision normal:

void resolveCircleCollision(Node a, Node b, float r) {
  PVector delta = PVector.sub(b.pos, a.pos);
  float dist = delta.mag();
  float minDist = r * 2;

  if (dist < minDist && dist > 0) {
    float overlap = (minDist - dist) * 0.5;
    PVector push = delta.normalize(null).mult(overlap);
    if (!a.pinned) a.pos.sub(push);
    if (!b.pinned) b.pos.add(push);
  }
}

Broad-Phase Spatial Grid

Checking every pair of objects for collision is O(n²) and quickly becomes too slow. A spatial grid divides the canvas into cells. Each cell stores a list of objects whose centre falls inside it. Collision checks then only compare objects sharing the same or neighbouring cells — typically O(n) in practice.

int CELL = 40;
int COLS, ROWS;
ArrayList<Node>[][] grid;

void buildGrid(ArrayList<Node> nodes) {
  COLS = ceil((float) width / CELL);
  ROWS = ceil((float) height / CELL);
  grid = new ArrayList[COLS][ROWS];
  for (int c = 0; c < COLS; c++)
    for (int r = 0; r < ROWS; r++)
      grid[c][r] = new ArrayList<Node>();

  for (Node n : nodes) {
    int col = constrain((int)(n.pos.x / CELL), 0, COLS-1);
    int row = constrain((int)(n.pos.y / CELL), 0, ROWS-1);
    grid[col][row].add(n);
  }
}

Using Toxiclibs Physics

Toxiclibs (toxi.physics2d) provides production-quality 2D physics. Install it via Sketch → Import Library → Add Library and search for “Toxiclibs”.

import toxi.physics2d.*;
import toxi.physics2d.behaviors.*;
import toxi.geom.*;

VerletPhysics2D physics;

void setup() {
  size(800, 600);
  physics = new VerletPhysics2D();
  physics.setWorldBounds(new Rect(0, 0, width, height));
  physics.addBehavior(new GravityBehavior(new Vec2D(0, 0.5)));
}

Add particles and connect them with springs:

VerletParticle2D a = new VerletParticle2D(200, 100);
VerletParticle2D b = new VerletParticle2D(260, 100);
physics.addParticle(a);
physics.addParticle(b);
physics.addSpring(new VerletSpring2D(a, b, 60, 0.01));
a.lock();  // equivalent to pinned

Full Example: Cloth Simulation

import toxi.physics2d.*;
import toxi.physics2d.behaviors.*;
import toxi.geom.*;

int COLS = 20;
int ROWS = 14;
float SPACING = 28;
float STIFFNESS = 0.08;

VerletPhysics2D physics;
VerletParticle2D[][] cloth;

void setup() {
  size(800, 600);
  physics = new VerletPhysics2D();
  physics.setWorldBounds(new Rect(0, 0, width, height));
  physics.addBehavior(new GravityBehavior(new Vec2D(0, 0.4)));

  cloth = new VerletParticle2D[COLS][ROWS];
  float startX = (width  - (COLS - 1) * SPACING) / 2;
  float startY = 60;

  // create particles
  for (int c = 0; c < COLS; c++) {
    for (int r = 0; r < ROWS; r++) {
      float x = startX + c * SPACING;
      float y = startY + r * SPACING;
      cloth[c][r] = new VerletParticle2D(x, y);
      physics.addParticle(cloth[c][r]);
      // pin the top row
      if (r == 0) cloth[c][r].lock();
    }
  }

  // connect with horizontal and vertical springs
  for (int c = 0; c < COLS; c++) {
    for (int r = 0; r < ROWS; r++) {
      if (c < COLS - 1) {
        physics.addSpring(new VerletSpring2D(
          cloth[c][r], cloth[c+1][r], SPACING, STIFFNESS));
      }
      if (r < ROWS - 1) {
        physics.addSpring(new VerletSpring2D(
          cloth[c][r], cloth[c][r+1], SPACING, STIFFNESS));
      }
    }
  }
}

void draw() {
  background(30, 30, 50);

  // optional wind force based on mouse
  float windX = map(mouseX, 0, width, -0.3, 0.3);
  physics.addBehavior(new GravityBehavior(new Vec2D(windX, 0.4)));
  physics.update();

  // draw cloth as a mesh of lines
  stroke(180, 200, 230, 200);
  strokeWeight(1);
  noFill();

  for (int c = 0; c < COLS; c++) {
    for (int r = 0; r < ROWS; r++) {
      Vec2D p = cloth[c][r];
      if (c < COLS - 1) {
        Vec2D right = cloth[c+1][r];
        line(p.x, p.y, right.x, right.y);
      }
      if (r < ROWS - 1) {
        Vec2D below = cloth[c][r+1];
        line(p.x, p.y, below.x, below.y);
      }
    }
  }

  // draw pin points
  fill(255, 80, 80);
  noStroke();
  for (int c = 0; c < COLS; c++) {
    Vec2D p = cloth[c][0];
    ellipse(p.x, p.y, 8, 8);
  }
}

// click and drag to grab cloth
VerletParticle2D grabbed = null;

void mousePressed() {
  float nearest = 30;
  for (int c = 0; c < COLS; c++) {
    for (int r = 1; r < ROWS; r++) {
      Vec2D p = cloth[c][r];
      float d = dist(mouseX, mouseY, p.x, p.y);
      if (d < nearest) { nearest = d; grabbed = cloth[c][r]; }
    }
  }
}

void mouseDragged() {
  if (grabbed != null) grabbed.set(mouseX, mouseY);
}

void mouseReleased() { grabbed = null; }

Move the mouse to apply wind. Click and drag any part of the cloth to pull it.

Key Takeaways

  • Hooke’s law (F = −k × stretch) drives spring force; apply equal and opposite forces to both endpoints.
  • Verlet integration is more stable than Euler for stiff constraints because it limits error accumulation.
  • Iterating backwards through ArrayList prevents index-shift bugs during removal.
  • A spatial grid reduces collision detection from O(n²) to near O(n) for large particle counts.
  • Toxiclibs VerletPhysics2D wraps all of the above in a clean API and handles constraint solving automatically.