How to Create Zero-Gravity Zones in Matter.js

This guide explains how to implement localized zero-gravity chambers in Matter.js simulations that otherwise use standard global gravity. By utilizing sensor bodies, collision detection events, and per-step upward force compensation, you can dynamically neutralize gravity for specific objects entering designated zones without affecting the rest of the simulation.

The Core Concept

Matter.js defines gravity at the engine level (engine.gravity), applying a uniform downward acceleration to all dynamic bodies in the world. Because Matter.js does not support native per-region gravity settings, you must create a zero-gravity effect by applying an equal and opposite counter-force to any body occupying the designated zero-gravity volume.

The force of gravity exerted on any body in Matter.js is:

\[\text{Force}_{\text{gravity}} = \text{mass} \times (\text{gravity.y} \times \text{gravity.scale})\]

To make a body appear weightless, apply a counter-force in the negative direction on every physics step while the body resides inside the chamber.

Step 1: Define the Chamber Sensor

Create a body to act as the chamber boundary. Set isSensor: true so other bodies can enter and pass through it without physical collision resistance.

const zeroGChamber = Matter.Bodies.rectangle(400, 300, 200, 200, {
  isSensor: true,
  isStatic: true,
  render: {
    fillStyle: 'rgba(0, 150, 255, 0.2)',
    strokeStyle: '#0096ff',
    lineWidth: 2
  }
});

Matter.Composite.add(engine.world, zeroGChamber);

Step 2: Track Active Bodies

Maintain a Set to store references to any bodies currently inside the chamber. Use Matter.js collision events to add bodies when they enter and remove them when they leave.

const floatingBodies = new Set();

Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA === zeroGChamber && !pair.bodyB.isStatic) {
      floatingBodies.add(pair.bodyB);
    } else if (pair.bodyB === zeroGChamber && !pair.bodyA.isStatic) {
      floatingBodies.add(pair.bodyA);
    }
  });
});

Matter.Events.on(engine, 'collisionEnd', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA === zeroGChamber) {
      floatingBodies.delete(pair.bodyB);
    } else if (pair.bodyB === zeroGChamber) {
      floatingBodies.delete(pair.bodyA);
    }
  });
});

Step 3: Apply the Counter-Force

Hook into the beforeUpdate event on the engine. For every registered body inside the chamber, compute the exact upward force needed to cancel out the global gravity setting and apply it to the body's center of mass.

Matter.Events.on(engine, 'beforeUpdate', () => {
  const gravity = engine.gravity;
  const gravityScale = gravity.scale;
  
  floatingBodies.forEach((body) => {
    // Calculate opposite force components
    const counterForceY = -body.mass * (gravity.y * gravityScale);
    const counterForceX = -body.mass * (gravity.x * gravityScale);

    Matter.Body.applyForce(body, body.position, {
      x: counterForceX,
      y: counterForceY
    });
  });
});

Step 4: Fine-Tune the Environment

Objects entering a zero-gravity chamber retain their existing velocity. To simulate a dense fluid or realistic space-like damping inside the chamber, apply linear and angular drag inside the update loop:

Matter.Events.on(engine, 'beforeUpdate', () => {
  const dragFactor = 0.98; // Reduces speed by 2% per tick

  floatingBodies.forEach((body) => {
    Matter.Body.setVelocity(body, {
      x: body.velocity.x * dragFactor,
      y: body.velocity.y * dragFactor
    });
    Matter.Body.setAngularVelocity(body, body.angularVelocity * dragFactor);
  });
});

This approach leaves global gravity fully functional across the rest of the canvas while providing an isolated, frictionless, or damped zero-gravity experience inside the bounded area.