Simulating Kidney Stone Fragmentation in Matter.js
This article explains how to simulate kidney stone fragmentation under ultrasonic shockwaves using the Matter.js 2D physics engine. By modeling kidney stones as breakable clusters of rigid bodies bound by elastic constraints, applying directional high-frequency force pulses to mimic acoustic shockwaves, and dynamically severing bonds once stress limits are breached, you can build an interactive, physically plausible model of ultrasonic lithotripsy directly in the browser.
1. Representing the Kidney Stone Structure
Because Matter.js is a rigid-body physics engine, brittle objects
like kidney stones are best represented as aggregate structures rather
than single deformable meshes. You can model a stone using a
Matter.Composite containing multiple overlapping or tightly
packed convex polygons bound together by stiff
Matter.Constraint instances.
const { Engine, Render, Runner, Bodies, Composite, Constraint, Body, Vector } = Matter;
function createStone(x, y, radius, particleCount) {
const stoneComposite = Composite.create({ label: 'KidneyStone' });
const particles = [];
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * radius;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const part = Bodies.polygon(px, py, 5 + Math.floor(Math.random() * 3), 8, {
density: 0.005,
friction: 0.8,
restitution: 0.05
});
particles.push(part);
Composite.add(stoneComposite, part);
}
// Connect adjacent particles with breakable constraints
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const distance = Vector.magnitude(Vector.sub(particles[i].position, particles[j].position));
if (distance < 20) {
const bond = Constraint.create({
bodyA: particles[i],
bodyB: particles[j],
stiffness: 0.9,
damping: 0.1,
render: { strokeStyle: '#888888', lineWidth: 1 }
});
// Attach custom physical durability limits
bond.tensileStrength = 0.05;
Composite.add(stoneComposite, bond);
}
}
}
return stoneComposite;
}2. Modeling Ultrasonic Shockwaves
Shockwave lithotripsy relies on focused, high-energy acoustic pulses with a rapid compressive phase followed by a tensile negative phase. In a 2D engine, a shockwave can be simulated as an instantaneous, radially decayed force pulse applied to bodies within a specific focal zone.
function applyUltrasonicShockwave(engine, focalPoint, intensity, radius) {
const bodies = Composite.allBodies(engine.world);
bodies.forEach(body => {
const offset = Vector.sub(body.position, focalPoint);
const distance = Vector.magnitude(offset);
if (distance < radius && distance > 0) {
// Inverse-distance force attenuation
const attenuation = 1 - (distance / radius);
const forceMagnitude = (intensity / (distance * distance)) * attenuation;
const forceDirection = Vector.normalise(offset);
const shockForce = Vector.mult(forceDirection, forceMagnitude);
// Apply linear force impulse to particles
Body.applyForce(body, body.position, shockForce);
// Induce slight torque variations to simulate shear stress
body.torque += (Math.random() - 0.5) * intensity * 0.01;
}
});
}3. Implementing Bond Fracture and Fragmentation
After applying the shockwave force, internal stress across each bond
must be evaluated. In Matter.js, the tension of a constraint can be
derived from the difference between the current distance separating the
two linked bodies and the constraint's equilibrium length.
Once the tension surpasses the defined tensileStrength, the
constraint is removed, causing progressive structural failure.
function evaluateBondStress(engine) {
const constraints = Composite.allConstraints(engine.world);
constraints.forEach(constraint => {
if (!constraint.tensileStrength || !constraint.bodyA || !constraint.bodyB) {
return;
}
const currentDistance = Vector.magnitude(
Vector.sub(constraint.bodyA.position, constraint.bodyB.position)
);
const strain = Math.abs(currentDistance - constraint.length);
if (strain > constraint.tensileStrength) {
Composite.remove(engine.world, constraint);
}
});
}4. Running the Simulation Loop
Integrate the stone generation, wave generation, and fracture
verification into the standard Matter.js engine loop. Hooking into the
beforeUpdate event allows real-time stress assessment per
frame.
const engine = Engine.create();
const runner = Runner.create();
const stone = createStone(400, 300, 40, 60);
Composite.add(engine.world, stone);
Matter.Events.on(engine, 'beforeUpdate', () => {
evaluateBondStress(engine);
});
// Trigger an ultrasonic pulse every 500 milliseconds focused at the stone's center
setInterval(() => {
applyUltrasonicShockwave(engine, { x: 400, y: 300 }, 2.5, 120);
}, 500);
Runner.run(runner, engine);5. Refining Realism
To better align the simulation with real-world physical lithotripsy:
- Viscous Damping: Adjust
body.frictionAirto simulate surrounding urine or soft-tissue resistance, preventing fragments from scattering indefinitely. - Surface Erosion: Instead of uniform breaking, set
lower
tensileStrengthvalues on boundary constraints to replicate external spallation before core split. - Debris Cleanup: Monitor fragment sizes and remove
microscopic particles below a threshold (e.g.,
< 2px) to free up memory and simulate successful clearance.