Building a Falling Sand Simulator
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):
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.