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

  1. Velocity Sampling: Vector.magnitude(body.velocity) calculates the absolute speed regardless of trajectory direction.
  2. Threshold Comparison: The engine checks whether currentSpeed has crossed the terminalSpeed threshold.
  3. Proportional Drag Scaling: Rather than jumping instantly to a rigid value, excessRatio measures how far past the limit the body has traveled. Scaling frictionAir relative to this overshoot creates a smooth resistance force that mimics natural aerodynamic drag (which increases quadratically in the real world).
  4. Restoration: Once the body slows below the limit—such as after bouncing or entering horizontal flight—the property returns to baseFrictionAir to preserve expected physics interactions.