I write visual, interactive essays about code, creative coding, and software engineering. Each post aims to explain concepts from the ground up with hands-on demos.

← Back

Building a Falling Sand Simulator

3 min readcreative-coding

In this post, we'll build a simple but fun falling sand simulator. Over the years, there have been many projects focusing on building systems of particle materials that interact with one another - "Powder Game" written in Java, Sandspiel, and the roguelike game Noita which is built entirely on particle materials.

Demo

Here's what we'll build (drag on the canvas):

Demo yükleniyor...

Left click to add sand, right click to clear.


Drawing Pixels to the Screen

First, we need a way to draw pixels and track their positions. We're using p5.js for this.

We can build a Grid class to represent our pixels:

class Grid {
  initialize(width, height) {
    this.width = width;
    this.height = height;
    this.grid = new Array(width * height).fill(0);
  }

  // Clear the canvas
  clear() {
    this.grid = new Array(this.width * this.height).fill(0);
  }

  // Set a specific particle
  set(x, y, color) {
    this.grid[y * this.width + x] = color;
  }

  // Swap two particles (or space)
  swap(a, b) {
    const temp = this.grid[a];
    this.grid[a] = this.grid[b];
    this.grid[b] = temp;
  }

  // Check if a space is empty
  isEmpty(index) {
    return this.grid[index] === 0;
  }
}

Adding Gravity

Our first rule: gravity.

The rule is: if the space below a particle is empty, swap it with the empty space.

updatePixel(i) {
  const below = i + this.width;
  // If there's nothing below, move down
  if (this.isEmpty(below)) {
    this.swap(i, below);
  }
}

update() {
  // Go through each pixel and apply the rule
  for (let i = this.grid.length - this.width - 1; i > 0; i--) {
    this.updatePixel(i);
  }
}

Important: We iterate from the end of the array, not the beginning. Otherwise, a particle could fall all the way down in a single frame!

Settling Behavior

Sand doesn't just fall straight down - it rolls to the side if there's space.

updatePixel(i) {
  const below = i + this.width;
  const belowLeft = below - 1;
  const belowRight = below + 1;

  if (this.isEmpty(below)) {
    this.swap(i, below);
  } else if (this.isEmpty(belowLeft)) {
    this.swap(i, belowLeft);
  } else if (this.isEmpty(belowRight)) {
    this.swap(i, belowRight);
  }
}

Final Touches

To improve the experience, we draw a filled circle instead of a single grain:

setCircle(x, y, colorFn, radius = 2, probability = 0.5) {
  const radiusSq = radius * radius;
  for (let y1 = -radius; y1 <= radius; y1++) {
    for (let x1 = -radius; x1 <= radius; x1++) {
      if (x1 * x1 + y1 * y1 <= radiusSq && Math.random() < probability) {
        this.set(x + x1, y + y1, colorFn());
      }
    }
  }
}

The probability parameter creates a more natural look by only drawing some particles.


Conclusion

That's it! We've built a simple falling sand simulation. From here you could:

  • Add water (different flow behavior)
  • Add smoke (moves upward)
  • Add fire (spreads and burns)
  • Optimize performance

The code uses React and p5.js, so you can easily integrate it into your own projects.