Physics Simulation
Implement springs, Verlet integration, spatial-grid collision detection, and cloth simulation using both hand-coded physics and the Toxiclibs library.
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
ArrayListprevents index-shift bugs during removal. - A spatial grid reduces collision detection from O(n²) to near O(n) for large particle counts.
- Toxiclibs
VerletPhysics2Dwraps all of the above in a clean API and handles constraint solving automatically.