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: