Simulating a Quadcopter Drone in Matter.js

This article explains how to build a stable quadcopter drone simulation in Matter.js featuring multi-point motor thrust and active gyroscopic self-leveling. You will learn how to construct the drone's physical body, calculate local-to-world force vectors representing individual motor outputs, and implement a proportional-derivative (PD) controller that mimics an onboard flight controller to keep the craft balanced in a 2D physics environment.

1. Representing the Drone Body

In a 2D rigid-body engine like Matter.js, a quadcopter is modeled along its cross-section. The four physical rotors are represented as two thrust axes—left and right—where each side accounts for a pair of motors (front-left/rear-left and front-right/rear-right).

Create the drone chassis as a single rectangular body with an explicit mass and moment of inertia to ensure predictable rotational physics:

const { Engine, Render, Runner, Bodies, Composite, Body, Vector } = Matter;

const droneWidth = 120;
const droneHeight = 15;

const drone = Bodies.rectangle(400, 300, droneWidth, droneHeight, {
    mass: 1.5,
    inertia: 1500, // Explicit inertia provides resistance to erratic spinning
    frictionAir: 0.02
});

Composite.add(engine.world, drone);

2. Calculating Offset Thrust Points

To induce rotation, thrust must be applied at specific offsets along the drone's lateral axis rather than at the center of mass. Because the drone rotates, these offset points must be translated from local body coordinates to world coordinates on every frame.

The local offsets for the left and right motor mounts are positioned toward the ends of the chassis:

const motorOffset = droneWidth / 2 - 5; // Distance from center to motor mount

function getMotorWorldPositions(body) {
    const angle = body.angle;
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);

    // Left motor position in world space
    const leftPos = {
        x: body.position.x - motorOffset * cos,
        y: body.position.y - motorOffset * sin
    };

    // Right motor position in world space
    const rightPos = {
        x: body.position.x + motorOffset * cos,
        y: body.position.y + motorOffset * sin
    };

    return { leftPos, rightPos };
}

3. Directional Thrust Vectors

Rotors generate thrust perpendicular to the drone's frame. To ensure thrust pushes the craft relative to its current orientation, derive the upward normal vector using the drone's current rotation angle:

function getThrustVector(body, thrustMagnitude) {
    // Normal vector pointing "up" relative to the drone's top surface
    return {
        x: thrustMagnitude * Math.sin(body.angle),
        y: -thrustMagnitude * Math.cos(body.angle)
    };
}

4. Gyro Leveling with a PD Controller

A real drone uses an Inertial Measurement Unit (IMU) running a Proportional-Integral-Derivative (PID) algorithm to maintain level flight. In Matter.js, an effective gyro stabilizer can be achieved using a Proportional-Derivative (PD) controller:

const Kp = 0.08; // Proportional gain: strength of tilt correction
const Kd = 0.45; // Derivative gain: rotational damping

function calculateStabilization(body, targetAngle = 0) {
    const angleError = targetAngle - body.angle;
    const angularVelocity = body.angularVelocity;

    // Control output for differential thrust
    return (Kp * angleError) - (Kd * angularVelocity);
}

5. Running the Physics Update Loop

Integrate the stabilization logic directly into the engine's update cycle via the beforeUpdate event. Calculate the base thrust required to counteract gravity, modify each motor's output using the stabilization control signal, and apply the resulting forces at the motor positions.

const gravity = engine.gravity.y * engine.gravity.scale;
const hoverThrustPerMotor = (drone.mass * gravity) / 2;

Matter.Events.on(engine, 'beforeUpdate', () => {
    const correction = calculateStabilization(drone, 0);
    const { leftPos, rightPos } = getMotorWorldPositions(drone);

    // Adjust individual thrust based on gyro output
    // Tilting clockwise requires more left thrust; counter-clockwise requires more right thrust
    const leftThrustMag = hoverThrustPerMotor - correction;
    const rightThrustMag = hoverThrustPerMotor + correction;

    const leftForce = getThrustVector(drone, Math.max(0, leftThrustMag));
    const rightForce = getThrustVector(drone, Math.max(0, rightThrustMag));

    // Apply forces to the physical body
    Body.applyForce(drone, leftPos, leftForce);
    Body.applyForce(drone, rightPos, rightForce);
});

6. Tuning and Flight Control

To control lateral movement and altitude: