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:
- Kinetic Friction (
friction): Controls the resistance between sliding surfaces. Higher values increase shear strength, allowing steeper piles. - Static Friction (
frictionStatic): Determines the force required to initiate sliding from a resting state. This property must be higher thanfrictionto stabilize the internal core of the pile. - Restitution (
restitution): Represents bounciness. For granular materials like sand, soil, or gravel, restitution should be close to0to prevent particles from scattering upon contact. - Rolling Resistance: Standard circular bodies roll freely in Matter.js regardless of high friction values, leading to an artificially flat pile. You must suppress pure rolling to mimic particle interlocking.
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.
Recommended Particle Settings
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
};- Steeper Angle: Increase
friction(e.g.,0.9to1.0), reduce roundness, or clamp angular velocity entirely. - Shallower Angle: Decrease
friction(e.g.,0.1to0.3) and allow particles to roll freely.
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.