Model Cloud Droplet Coalescence in Matter.js
This article explains how to simulate cloud droplet coalescence into falling raindrops using Matter.js particle bodies. By leveraging the engine's rigid-body physics, collision events, and dynamic body modifications, you can model the transition from suspended microscopic cloud droplets to falling precipitation. The following sections break down the physics principles, collision detection strategies, body merging calculations, and drag adjustments necessary for an accurate simulation.
1. Conceptual Physics Model
Cloud droplets remain suspended due to updrafts and high surface-area-to-mass ratios, leading to low terminal velocities governed by Stokes' Law. When droplets collide, they often coalesce into a single, larger drop rather than bouncing apart. As droplets grow, their mass increases proportionally to the cube of their radius (\(r^3\)), while cross-sectional area increases by the square (\(r^2\)). This causes larger drops to overcome upward air resistance and fall as rain.
In Matter.js, this process requires:
- Generating lightweight, high-drag circular particles to represent cloud droplets.
- Intercepting collisions before elastic resolution occurs to prevent bouncing.
- Combining two colliding bodies into a single body conserving mass, momentum, and volume.
- Adjusting air resistance (
frictionAir) dynamically as the droplet grows.
2. Spawning Suspended Droplets
Begin by configuring the Matter.js engine with reduced gravity to
mimic a cloud updraft. Droplets are created using
Bodies.circle with low mass and high air friction.
const { Engine, Render, Runner, Bodies, Composite, Events, Body } = Matter;
const engine = Engine.create({
gravity: { x: 0, y: 0.05, scale: 0.001 } // Mild downward pull
});
function createDroplet(x, y, radius = 2) {
return Bodies.circle(x, y, radius, {
restitution: 0,
friction: 0,
frictionAir: 0.08, // High drag keeps droplets floating
label: 'droplet',
render: {
fillStyle: 'rgba(200, 220, 255, 0.6)'
}
});
}3. Detecting Droplet Collisions
Matter.js resolves collisions elastically by default. To simulate
coalescence, attach an event listener to collisionStart.
Identify colliding droplet pairs and queue them for merging during the
next engine tick to avoid altering the physics state mid-step.
const mergeQueue = [];
Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
const { bodyA, bodyB } = pair;
if (bodyA.label === 'droplet' && bodyB.label === 'droplet') {
// Determine dominant drop and absorbed drop
const primary = bodyA.circleRadius >= bodyB.circleRadius ? bodyA : bodyB;
const secondary = bodyA === primary ? bodyB : bodyA;
// Prevent processing the same drop multiple times
if (!mergeQueue.some(item => item.secondary === secondary || item.primary === secondary)) {
mergeQueue.push({ primary, secondary });
}
}
});
});4. Executing Coalescence Mechanics
After detecting contact, execute the coalescence process using an
afterUpdate or beforeUpdate hook:
- Volume and Radius Conservation: Assuming 3D volume preservation mapped to 2D scale: \[r_{\text{new}} = \sqrt[3]{r_1^3 + r_2^3}\]
- Momentum Conservation: \[\vec{v}_{\text{new}} = \frac{m_1 \vec{v}_1 + m_2 \vec{v}_2}{m_1 + m_2}\]
- Dynamic Drag Reduction: Decrease
frictionAirrelative to drop size so larger drops fall faster.
Events.on(engine, 'afterUpdate', () => {
while (mergeQueue.length > 0) {
const { primary, secondary } = mergeQueue.shift();
// Check if secondary still exists in the world
if (!Composite.allBodies(engine.world).includes(secondary)) continue;
const r1 = primary.circleRadius;
const r2 = secondary.circleRadius;
const m1 = primary.mass;
const m2 = secondary.mass;
// Calculate new radius and scale factor
const newRadius = Math.cbrt(Math.pow(r1, 3) + Math.pow(r2, 3));
const scaleFactor = newRadius / r1;
// Conserve momentum
const newVelocity = {
x: (primary.velocity.x * m1 + secondary.velocity.x * m2) / (m1 + m2),
y: (primary.velocity.y * m1 + secondary.velocity.y * m2) / (m1 + m2)
};
// Remove the absorbed drop
Composite.remove(engine.world, secondary);
// Update primary drop geometry, velocity, and properties
Body.scale(primary, scaleFactor, scaleFactor);
Body.setVelocity(primary, newVelocity);
// As radius increases, reduce air friction toward standard falling speeds
primary.frictionAir = Math.max(0.005, 0.08 - (newRadius * 0.005));
// Update visual styling to signify a denser raindrop
if (newRadius > 6) {
primary.render.fillStyle = 'rgba(100, 150, 240, 0.9)';
}
}
});5. Managing Lifecycles and Boundaries
As droplets coalesce and fall through the bottom of the simulation bounds, remove them from the composite using world boundary checks to prevent memory leaks. This setup creates a continuous, natural cycle where cloud droplets remain suspended until Brownian motion or micro-currents induce collisions, producing accelerating raindrops that fall out of the cloud layer.