Simulate Soil Compaction in Matter.js
Simulating soil compaction in Matter.js involves modeling soil as a collection of discrete granular particles and using physical forces, polydispersity, and vibration to drive them into a higher packing density. This article covers how to configure particle attributes such as friction and restitution, prevent unnatural crystalline lattice formation, and apply mechanical compression and vibration to achieve realistic compaction dynamics in a 2D physics environment.
1. Representing Soil as Granular Particles
Soil is best represented in a 2D rigid-body engine like Matter.js as an ensemble of circular bodies. To achieve realistic soil behavior, particles must have varied sizes (polydispersity). Identical circles naturally align into a hexagonal crystal lattice, which produces unrealistic shear planes and artificially locks packing density.
const { Engine, Render, Runner, Bodies, Composite, Body } = Matter;
const engine = Engine.create();
const world = engine.world;
const particles = [];
const particleCount = 300;
for (let i = 0; i < particleCount; i++) {
// Vary radii between 4px and 8px to disrupt lattice formation
const radius = 4 + Math.random() * 4;
const x = 200 + (Math.random() * 200 - 100);
const y = 100 + (Math.random() * 200 - 100);
const particle = Bodies.circle(x, y, radius, {
restitution: 0.05, // Low bounciness absorbs kinetic energy
friction: 0.4, // Surface friction between grains
frictionStatic: 0.7, // Resistance to initial sliding
density: 0.002 // Standard mass density
});
particles.push(particle);
}
Composite.add(world, particles);2. Tuning Particle Physics for Compaction
Compaction requires particles to overcome friction and rearrange into interstitial voids without bouncing excessively:
- Restitution (0.0 to 0.1): Real soil deforms inelastically. A low restitution ensures that impact energy is dissipated quickly, mimicking plastic deformation.
- Friction and Static Friction: High static friction creates "force chains" that bridge gaps and create void spaces. During active compaction, temporarily lowering friction or applying forces that exceed static friction allows particles to roll and slip into empty spaces.
- Collision Iterations: Increase
engine.positionIterationsandengine.velocityIterations(e.g., from default values to 10 or 12) to minimize soft-body overlap and maintain stable contacts under high pressure.
3. Applying External Compactive Effort
Real-world soil compaction relies on mechanical energy via static weight, kneading, or impact. In Matter.js, this is achieved by introducing a compressive plate (piston) directly above the granular aggregate.
// Create a heavy tamper/plate
const plate = Bodies.rectangle(200, 50, 220, 20, {
density: 0.05, // Significantly heavier than individual grains
friction: 0.5
});
Composite.add(world, plate);
// Apply continuous downward force during compaction phases
Matter.Events.on(engine, 'beforeUpdate', () => {
Body.applyForce(plate, plate.position, { x: 0, y: 0.05 });
});4. Overcoming Jamming with Dynamic Vibration
Under pure vertical loading, granular assemblies often experience "shear jamming," where force chains support the load and prevent further densification. To simulate dynamic vibratory compaction (like a vibratory roller or plate compactor), apply periodic horizontal or vertical impulses to the soil bed:
let frame = 0;
Matter.Events.on(engine, 'beforeUpdate', () => {
frame++;
// Apply a micro-oscillation to simulate vibratory shaking
if (frame % 4 === 0) {
particles.forEach(p => {
const jitter = (Math.random() - 0.5) * 0.0005;
Body.applyForce(p, p.position, { x: jitter, y: 0 });
});
}
});The small oscillating disturbances break temporary force chains, enabling the heavier static load to push particles into smaller voids and substantially increasing the final packing fraction.
5. Calculating the Packing Density
To quantify the degree of compaction, measure the packing density (volume fraction) over time. This is the ratio of total particle area to the area of the bounding polygon containing the soil.
function calculatePackingDensity(particles, containerWidth, bottomY) {
const totalParticleArea = particles.reduce((sum, p) => {
return sum + Math.PI * Math.pow(p.circleRadius, 2);
}, 0);
// Determine the current height of the aggregate
const topY = Math.min(...particles.map(p => p.position.y));
const currentHeight = bottomY - topY;
const totalBoundingArea = containerWidth * currentHeight;
return totalParticleArea / totalBoundingArea;
}As the vibratory load drives the top surface downward, the computed packing density will scale from a loose random packing configuration (around 0.75–0.80 in 2D) toward the theoretical limit of dense random packing (roughly 0.84–0.88 for polydisperse disks).