Realistic Billiards Pocketing in Matter.js

Simulating realistic ball-pocketing mechanics on a digital billiards table requires more than simply deleting a ball when it touches a pocket. Because Matter.js is a rigid-body 2D physics engine, it does not naturally account for the 3D drop, slate beveling, or pocket shelf dynamics of a real pool table. This guide demonstrates how to achieve realistic pocketing by combining sensor bodies, localized gravitational pull, visual scaling, and collision filtering to mimic depth and momentum loss.

1. Define Pockets as Sensors

In Matter.js, rigid cushions should deflect the ball, but the pocket opening itself needs to detect balls without generating a physical bounce. Create circular bodies for each pocket with isSensor: true.

const pocketRadius = 25; // Adjusted to table scale
const pocket = Matter.Bodies.circle(x, y, pocketRadius, {
  isStatic: true,
  isSensor: true,
  label: 'pocket'
});
Matter.Composite.add(engine.world, pocket);

Place rounded cushion corner vertices flanking each pocket to simulate the pocket "jaws." A ball that strikes the corner should rebound, while a ball passing between them enters the sensor zone.

2. Track Ball-to-Pocket Interactions

Use the beforeUpdate or collisionActive events to evaluate balls entering the pocket sensor. Simply removing the ball immediately looks abrupt; instead, flag the ball as "sinking" and store the target pocket's coordinates.

Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    const { bodyA, bodyB } = pair;
    if (bodyA.label === 'pocket' && bodyB.label === 'ball') {
      startPocketing(bodyB, bodyA);
    } else if (bodyB.label === 'pocket' && bodyA.label === 'ball') {
      startPocketing(bodyA, bodyB);
    }
  });
});

3. Apply a Radial Inward Force

To mimic the sloped bevel of the slate and the drop into the net, apply a continuous force directing the ball toward the pocket center during each engine update.

function applyPocketGravity(ball, pocket) {
  const dx = pocket.position.x - ball.position.x;
  const dy = pocket.position.y - ball.position.y;
  const distance = Math.hypot(dx, dy);

  if (distance > 0) {
    const forceMagnitude = 0.0005 * ball.mass;
    Matter.Body.applyForce(ball, ball.position, {
      x: (dx / distance) * forceMagnitude,
      y: (dy / distance) * forceMagnitude
    });
  }
}

4. Dampen Velocity and Disable Table Collisions

Once a ball enters the pocket threshold, prevent it from bouncing back out onto the table due to unwanted collisions with other active balls:

  1. Modify Collision Filtering: Set the ball's collisionFilter.group = -1 or change its collisionFilter.mask = 0 so it no longer collides with rails or incoming balls.
  2. Apply Friction/Damping: Artificially increase linear damping (ball.frictionAir = 0.1) to represent energy absorption by the pocket leather and netting.

5. Simulate 3D Depth via Visual Scaling

Because Matter.js operates in 2D, simulate vertical descent by scaling the ball down as it approaches the pocket center.

In your custom render loop or update step:

function updateSinkingBall(ball, pocket) {
  const dx = pocket.position.x - ball.position.x;
  const dy = pocket.position.y - ball.position.y;
  const distance = Math.hypot(dx, dy);

  // Shrink the ball as it gets closer to the center
  const progress = Math.max(0, distance / pocketRadius);
  const targetScale = 0.5 + 0.5 * progress;

  // Render scale adjustment (custom render property)
  ball.renderScale = targetScale;

  // Remove completely when near the center or moving sufficiently slow
  if (distance < 5 || ball.speed < 0.1) {
    Matter.Composite.remove(engine.world, ball);
  }
}

6. Rim Roll and Pocket Rejection

To capture true physical realism, assess the ball's velocity upon pocket entry. If a ball hits the pocket jaws at an acute angle with high velocity, it should "rattle" or lip out: