Modeling Angle of Repose in Matter.js

This article explains how to simulate the angle of repose for stacked granular materials using the 2D physics engine Matter.js. You will learn which physical properties directly govern pile stability, how to overcome the limitation of rolling circular bodies, how to configure the engine for particle stability, and how to write the code required to generate a stable, natural granular slope.

What Governs the Angle of Repose in Matter.js

The angle of repose is the steepest angle at which a sloping surface of loose material remains stable without sliding down. In real-world physics, this angle depends on particle shape, surface roughness, interlocking, and moisture.

Because Matter.js is a rigid-body physics engine rather than a discrete element method (DEM) simulator, granular materials must be represented by many small, interacting bodies. Achieving a specific angle of repose requires tuning several key body properties:

Overcoming the Rolling Problem

If you model sand grains purely as perfect circles (Bodies.circle), the pile will collapse and spread wide because circles exhibit zero rolling friction. To achieve a realistic angle of repose, use one of the following approaches:

1. Polydisperse Polygons

Instead of circles, generate regular or irregular polygons with varying vertex counts (e.g., pentagons, hexagons, and septagons). Flat facets interlock naturally, dramatically increasing the resting angle. Varying particle sizes (polydispersity) also prevents uniform crystallization patterns that cause unnatural structural cleavage.

2. Angular Damping / Artificial Rolling Friction

If circles are required for performance reasons, you can simulate rolling friction by capping or damping angular velocity. Matter.js does not have a native rolling friction parameter, but you can increase inertia or manually damp rotation each tick:

Matter.Events.on(engine, 'beforeUpdate', () => {
    particles.forEach(body => {
        // Dampen angular velocity to mimic rolling resistance
        Matter.Body.setAngularVelocity(body, body.angularVelocity * 0.85);
    });
});

Alternatively, setting inertia: Infinity on circular bodies completely disables rotation, forcing them to interact entirely via linear surface friction.

For dry, sand-like materials, configure each particle with the following parameters:

const particleOptions = {
    friction: 0.8,         // High surface friction
    frictionStatic: 1.0,   // High static threshold
    restitution: 0.05,     // Minimal bounciness
    density: 0.002,        // Realistic weight distribution
    frictionAir: 0.01      // Standard atmospheric drag
};

Engine Solver Configuration

Large stacks of granular bodies create deep contact chains that place heavy demands on the constraint solver. If the solver settings are too low, the pile will jitter, sink into itself, or act like a fluid.

Increase the solver fidelity in your Engine.create() instance:

const engine = Matter.Engine.create({
    positionIterations: 10, // Default is 6; higher values stop sinking/jittering
    velocityIterations: 8   // Default is 4; improves contact resolution
});

Using a fixed, sub-stepped delta time in your runner ensures consistent accumulation without spontaneous explosions from overlapping particles:

Matter.Runner.run(runner, engine);

Implementation Example

The following script sets up an emitter dropping varied polygonal particles onto a flat surface to form a natural pile:

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

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

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

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

// Floor
const ground = Bodies.rectangle(400, 580, 810, 40, { isStatic: true, friction: 1.0 });
Composite.add(world, ground);

const particles = [];

// Drop a granular particle every few frames
let frameCount = 0;
Events.on(engine, 'afterUpdate', () => {
    frameCount++;
    if (frameCount % 5 === 0 && particles.length < 350) {
        const x = 400 + (Math.random() - 0.5) * 20;
        const radius = 6 + Math.random() * 4;
        const sides = Math.floor(Math.random() * 3) + 5; // 5 to 7 sides for interlocking

        const particle = Bodies.polygon(x, 50, sides, radius, {
            friction: 0.9,
            frictionStatic: 1.2,
            restitution: 0.0,
            density: 0.005
        });

        particles.push(particle);
        Composite.add(world, particle);
    }
});

By substituting smooth circles with irregular polygons, setting restitution near zero, and tuning static and dynamic friction, you can precisely control the resulting slope angle and build stable granular piles in Matter.js.