p5.js: How to build a Menger sponge

According to Wikipedia, a Menger sponge is:

In mathematics, the Menger sponge (also known as the Menger universal curve) is a fractal curve. It is a three-dimensional generalization of the Cantor set and Sierpinski carpet, though it is slightly different from a Sierpinski sponge. It was first described by Karl Menger in 1926, in his studies of the concept of topological dimension.

A Menger sponge is essentially a block that is recursively subdivided into smaller blocks where the central block on each side is removed. Here is an animation showing what the Menger sponge looks like after four iterations:

This code uses a class called Box to create the boxes and it has constructor(), generate(), and show() functions. The constructor uses the p5.js createVector(x, y, z) method.

According to the p5js.org reference page, createVector():

Creates a new p5.Vector (the datatype for storing vectors). This provides a two or three dimensional vector, specifically a Euclidean (also known as geometric) vector. A vector is an entity that has both magnitude and direction.

createVector(x, y, z)

The generate() function uses nested for loops to create all of the sub boxes, stored in an array.

There is a mousePressed() function (the name of the function also happens to be a built-in for the mousePressed event, which means the function is triggered when the mouse is pressed!) which uses the generate() function to further sub-divide the box and create new boxes recursively.

Here is the complete p5.js code for creating this:

var a = 0;
var sponge = [];

class Box {
  constructor(x, y, z, r) {
    this.pos = createVector(x, y, z);
    this.r = r;
  }

  generate() {
    var boxes = [];
    for (var x = -1; x < 2; x++) {
      for (var y = -1; y < 2; y++) {
        for (var z = -1; z < 2; z++) {
          var sum = abs(x) + abs(y) + abs(z);
          var newR = this.r / 3;
          if (sum > 1) {
            var b = new Box(this.pos.x + x * newR, this.pos.y + y * newR, this.pos.z + z * newR, newR);
            boxes.push(b);
          }
        }
      }
    }
    return boxes;
  }

  show() {
    push();
    translate(this.pos.x, this.pos.y, this.pos.z);
    stroke(255);
    noStroke();
    noFill();
    fill("#00ff00");
    pointLight(255, width/2, height/2, 0);
    box(this.r);
    pop();
  }
}

function setup() { 
  createCanvas(400, 400, WEBGL);
  
  // An array of Box objects
  // Start with one
  var b = new Box(0, 0, 0, 200);
  sponge.push(b);
} 

function mousePressed() {
  // Generate the next set of boxes
  var next = [];
  for (var i = 0; i < sponge.length; i++) {
    var b = sponge[i];
    var newBoxes = b.generate();
    next = next.concat(newBoxes);
  }
  sponge = next;
}

function draw() { 
  background(51);
  rotateX(a);
  rotateY(a * 0.4);
  rotateZ(a * 0.1);
  
  for (var i = 0; i < sponge.length; i++) {
    sponge[i].show();    
  }
  
  a += 0.01;
}

Here is what the final result looks like: (Click the box only twice. Otherwise it will slow to a crawl.)

The code in this article is based on The Coding Train – “Coding Challenge #2: Menger Sponge Fractal”. This video was coded by Daniel Shiffman in Processing. As I followed along, I translated it into p5.js. Then I double checked my code against the p5.js code that was posted on github. My code does have some small differences.