Smoke Dispersal and Buoyancy in Matter.js

This article explains how to simulate realistic smoke particle dispersal and buoyancy inside confined room geometry using the Matter.js 2D physics engine. By configuring static boundary bodies for walls, disabling particle-to-particle collisions via collision filters, and applying upward buoyancy alongside randomized horizontal turbulence in the engine update loop, you can achieve a lightweight, convincing smoke effect that naturally gathers against ceilings and escapes through room openings.

1. Creating the Confined Room Geometry

A confined environment requires static rigid bodies that prevent smoke particles from escaping while allowing them to interact naturally with obstacles. Define the floor, ceiling, and walls using Matter.Bodies.rectangle with the isStatic property set to true:

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

const engine = Engine.create();
const world = engine.world;

// Define boundaries: floor, ceiling, left wall, right wall
const wallOptions = { isStatic: true, friction: 0 };
const ground = Bodies.rectangle(400, 600, 810, 30, wallOptions);
const ceiling = Bodies.rectangle(400, 0, 810, 30, wallOptions);
const leftWall = Bodies.rectangle(0, 300, 30, 600, wallOptions);
const rightWall = Bodies.rectangle(800, 300, 30, 600, wallOptions);

// Add an internal partition to test smoke movement around obstacles
const partition = Bodies.rectangle(400, 400, 20, 300, wallOptions);

Composite.add(world, [ground, ceiling, leftWall, rightWall, partition]);

2. Configuring Collision Filtering for Particles

Simulating hundreds of individual smoke particles colliding with each other quickly degrades performance. To optimize computation, assign collision filters so particles only register collisions with static boundaries, passing freely through one another:

const SMOKE_CATEGORY = 0x0002;
const WALL_CATEGORY = 0x0001;

// Update wall collision masks
[ground, ceiling, leftWall, rightWall, partition].forEach(body => {
  body.collisionFilter.category = WALL_CATEGORY;
  body.collisionFilter.mask = SMOKE_CATEGORY;
});

const smokeParticleOptions = {
  frictionAir: 0.05, // Mimics air resistance to prevent runaway acceleration
  restitution: 0.1,  // Low bounce against walls
  collisionFilter: {
    category: SMOKE_CATEGORY,
    mask: WALL_CATEGORY // Only collide with walls, ignore other smoke
  }
};

3. Implementing Buoyancy and Dispersal Forces

Matter.js applies downward gravity by default. While you can set engine.gravity.y = -1, doing so inverts gravity for the entire scene. A more robust solution is leaving scene gravity neutral or standard and applying discrete upward (buoyant) and lateral (dispersive) forces directly to active smoke particles on every frame.

Listen to the beforeUpdate event to modify particle forces:

const particles = [];

function emitSmoke(x, y) {
  const particle = Bodies.circle(x, y, 6, smokeParticleOptions);
  particle.lifeSpan = 300; // Total frames before despawning
  particle.age = 0;
  
  particles.push(particle);
  Composite.add(world, particle);
}

Events.on(engine, 'beforeUpdate', () => {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.age++;

    // Remove expired particles
    if (p.age >= p.lifeSpan) {
      Composite.remove(world, p);
      particles.splice(i, 1);
      continue;
    }

    // Upward buoyancy force (counteracting gravity or driving upward drift)
    const buoyancy = -0.0004 * p.mass;

    // Horizontal dispersal using random micro-forces (Brownian motion)
    const dispersion = (Math.random() - 0.5) * 0.0002;

    Body.applyForce(p, p.position, {
      x: dispersion,
      y: buoyancy
    });

    // Expand particle size slightly as it ages
    Body.scale(p, 1.002, 1.002);
  }
});

4. Simulating Gas Expansion and Ceilings Trapping

In a confined space, warm smoke flows vertically until it collides with the ceiling, where vertical velocity translates into lateral movement. To make this behavior realistic: