How to Create an Hourglass in Matter.js

This guide explains how to construct a functional hourglass simulation in Matter.js where granular sand particles reliably flow through a narrow throat without jamming or tunneling. You will learn how to configure the physics engine settings, construct the static funnel geometry using angled bodies, generate hundreds of particle bodies with optimal physical properties, and fine-tune friction and collision parameters to ensure a smooth, realistic granular flow.

1. Engine and World Configuration

Granular simulations involve high-density collisions. To prevent particles from tunneling through the funnel walls or each other, the physics engine requires higher collision solver iterations than the default settings.

const { Engine, Render, Runner, Bodies, Composite, Body } = Matter;

const engine = Engine.create({
  positionIterations: 10,
  velocityIterations: 10
});

const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 600,
    height: 700,
    wireframes: false
  }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

2. Constructing the Funnel Geometry

An hourglass requires an upper chamber, a lower chamber, and a narrow throat. You can build the funnel using angled static rectangular bodies.

The neck width must be carefully calibrated against the particle diameter. In granular physics, if an opening is less than 3 to 4 times the diameter of the grains, spontaneous physical arches form, permanently blocking the flow.

const wallOptions = { isStatic: true, friction: 0.1, render: { fillStyle: '#333' } };
const throatWidth = 24; // Gap size between left and right choke points
const centerX = 300;
const centerY = 350;

const walls = [
  // Upper Funnel (V-Shape)
  Bodies.rectangle(centerX - 80, centerY - 90, 180, 14, { 
    ...wallOptions, 
    angle: Math.PI / 4 
  }),
  Bodies.rectangle(centerX + 80, centerY - 90, 180, 14, { 
    ...wallOptions, 
    angle: -Math.PI / 4 
  }),

  // Lower Chamber (Inverted V-Shape)
  Bodies.rectangle(centerX - 80, centerY + 90, 180, 14, { 
    ...wallOptions, 
    angle: -Math.PI / 4 
  }),
  Bodies.rectangle(centerX + 80, centerY + 90, 180, 14, { 
    ...wallOptions, 
    angle: Math.PI / 4 
  }),

  // Outer Boundary Enclosure
  Bodies.rectangle(centerX, centerY + 220, 300, 20, wallOptions), // Floor
  Bodies.rectangle(centerX - 150, centerY, 20, 440, wallOptions),  // Left wall
  Bodies.rectangle(centerX + 150, centerY, 20, 440, wallOptions),  // Right wall
];

Composite.add(engine.world, walls);

3. Generating Sand Particles

To mimic sand, use small circular rigid bodies. Introduce slight variations in particle radius to avoid crystallization (ordered stacking), which leads to artificial structural locking above the neck.

Keep restitution (bounciness) at zero and keep friction relatively low to facilitate smooth movement through the bottleneck.

const particles = [];
const particleRadius = 3.5;
const rows = 25;
const cols = 20;

for (let i = 0; i < rows; i++) {
  for (let j = 0; j < cols; j++) {
    // Slight random variation in radius disrupts rigid grid formation
    const r = particleRadius + (Math.random() * 0.8 - 0.4);
    
    // Stagger positions to randomize initial contacts
    const x = centerX - 60 + j * 6 + (Math.random() * 2);
    const y = centerY - 260 + i * 6 + (Math.random() * 2);

    const sandGrain = Bodies.circle(x, y, r, {
      friction: 0.05,
      frictionAir: 0.001,
      restitution: 0,
      density: 0.002,
      render: { fillStyle: '#e0c068' }
    });

    particles.push(sandGrain);
  }
}

Composite.add(engine.world, particles);

4. Tuning to Prevent Jamming

If particles clog at the narrow throat, apply the following adjustments: