Simulate Terminal Velocity in Matter.js
This article explains how to simulate realistic terminal velocity in
Matter.js by dynamically adjusting and clamping the
frictionAir property based on a body's current speed. You
will learn why default air resistance is often insufficient for
realistic speed caps, how to monitor velocity within the simulation
loop, and how to implement a lightweight beforeUpdate
callback that scales drag to maintain a maximum speed threshold without
disrupting the underlying collision physics.
The Challenge with Default Air Friction
In Matter.js, frictionAir provides linear drag on a
rigid body. By default, it is a static coefficient applied uniformly on
every step. Under constant forces such as gravity, a body accelerates
continuously until the linear drag force balances the applied force.
However, standard linear drag either feels unnaturally sluggish at low
speeds or permits excessively high speeds when falling under strong
forces.
Directly clamping the velocity vector (Body.setVelocity)
can resolve the issue, but hard velocity caps often cause visual jitter
and interfere with collision impulse resolution. Modulating
frictionAir dynamically based on current velocity ensures a
smooth deceleration curve toward the terminal velocity limit.
Implementation Strategy
To dynamically clamp frictionAir, calculate the current
speed of a body during each simulation step before physics calculations
resolve. If the speed exceeds a designated threshold, sharply scale the
body's frictionAir up to prevent further acceleration.
Step-by-Step Code Example
const { Engine, Render, Runner, Bodies, Composite, Events, Vector } = Matter;
// 1. Initialize Engine and World
const engine = Engine.create();
const world = engine.world;
// 2. Create the falling body with baseline frictionAir
const terminalSpeed = 12; // Maximum desired speed (pixels per update)
const baseFrictionAir = 0.01;
const maxFrictionAir = 0.25; // Drag applied when exceeding terminal velocity
const fallingObject = Bodies.circle(400, 50, 20, {
frictionAir: baseFrictionAir,
restitution: 0.5
});
Composite.add(world, fallingObject);
// 3. Monitor and clamp via the beforeUpdate event
Events.on(engine, 'beforeUpdate', () => {
// Calculate current scalar speed
const currentSpeed = Vector.magnitude(fallingObject.velocity);
if (currentSpeed > terminalSpeed) {
// Calculate an overshoot factor
const excessRatio = (currentSpeed - terminalSpeed) / terminalSpeed;
// Dynamically scale frictionAir up to its maximum ceiling
fallingObject.frictionAir = Math.min(
maxFrictionAir,
baseFrictionAir + excessRatio * 0.1
);
} else {
// Restore default friction at lower speeds
fallingObject.frictionAir = baseFrictionAir;
}
});How the Dynamic Adjustment Works
- Velocity Sampling:
Vector.magnitude(body.velocity)calculates the absolute speed regardless of trajectory direction. - Threshold Comparison: The engine checks whether
currentSpeedhas crossed theterminalSpeedthreshold. - Proportional Drag Scaling: Rather than jumping
instantly to a rigid value,
excessRatiomeasures how far past the limit the body has traveled. ScalingfrictionAirrelative to this overshoot creates a smooth resistance force that mimics natural aerodynamic drag (which increases quadratically in the real world). - Restoration: Once the body slows below the
limit—such as after bouncing or entering horizontal flight—the property
returns to
baseFrictionAirto preserve expected physics interactions.