Rainbow Cube experiment image.

Creative Coding: Rainbow Cube

August 2023

The first installment of my newly founded Creative Coding series, providing a high-level insight into a variety of expressive computer graphics projects. This particular blog post will explore the ‘Rainbow Cube’.

Note: These blog posts are not written with the intention of being step-by-step tutorials. The aim is to provide readers with a language-agnostic overview of how this project can be created.

Create

Before I begin, it’s important I address the fact that ‘Rainbow Cube’ isn’t a single cube, but a collection of 27 cubes! For the sake of this blog post, I’ll be referring to each of these 27 cubes as ‘cells’, and the collection of those 27 cells as the ‘cube’. I hope that makes sense, anyway…

My initial objective is to create the 3x3x3 cubic structure. So let me start by first defining the parameters of a class for a cell and getting one drawn to the screen. At this stage, all I’m particularly interested in is the size and position of the cell. How the cell is actually drawn will depend on what engine/graphics library you’ve opted to use:

class Cell {
  constructor(_spawnPos, _size) {
    this.m_spawnPos = _spawnPos;
    this.m_size = _size;

    this.createCell() {
      // Create cell geometry, material and mesh
    }
  }
}

Once I’ve drawn a single cell to the screen, I can now start to think about the defining the parameters of a class for my cube. Yet again, all I’m particularly interested in at this stage is the position of the cube, having access to all the cells and how big the cells are:

class Cube {
  constructor(_position, _cellSize) {
    this.m_position = _position;
    this.m_cellSize = _cellSize;
    this.m_cells = new Array();

    this.createCube();
  }
}

Creating the 3x3x3 cubic structure, is now just a case of defining a number of nested loops that instantiate a cell at an offset of its size, across the X, Y and Z axes:

createCube() {
  let min = this.m_position.x - 1;
  let max = this.m_position.x + 1;

  for (let x = min; x <= max; x++) {
    for (let y = min; y <= max; y++) {
      for (let z = min; z <= max; z++) {
        let xPos = this.m_position.x + (x * this.m_cellSize);
        let yPos = this.m_position.y + (y * this.m_cellSize);
        let zPos = this.m_position.z + (z * this.m_cellSize);

        let matrix = new Matrix4();
        matrix.makeTranslation(xPos, yPos, zPos);

        this.m_cells.push(new Cell(matrix, this.m_cellSize));
      }
    }
  }
}

let cube = new Cube(scene, new Vector3(), 1);

Wohoo, I now have a cube - time for the fun stuff!

Transform

There are an infinite number of transformations that can be applied to the cube. However, for the sake of simplicity, the three I’m going to implement are: Face Rotation, Cube Rotation and ‘Explosion’.

I’ll start with Cube Rotation, as that’s the easiest. If I were trying to rotate a single cell, I’d obviously only need to apply a rotation matrix to that single object. However, because the cube is built from 27 cells, I will need to apply the rotation matrix to all cells:

turn() {
  for (let i = 0; i < this.m_cells.length; i++) {
    let cell = this.m_cells[i];
    let rot = new Matrix4();
    // ...
    // Calculate angle, direction and speed of rotation
    // ...
    cell.applyMatrix4(rot);
  }
}

Like I said, nice and easy… Conveniently, if I think about it, I can actually refactor this exact function in order to achieve Face Rotation. Rather than nonchalantly applying the rotation matrix to each cell, I can add a conditional check for each cell, to see if it’s allowed to move or not:

turn() {
  for (let i = 0; i < this.m_cells.length; i++) {
    let cell = this.m_cells[i];
    if (cell.m_allowedToMove) {
      let rot = new Matrix4();
      // ...
      // Calculate angle, direction and speed of rotation
      // ...
      cell.applyMatrix4(rot);
    }
  }
}

Now remember, a Cube Rotation will require all cells pass the conditional check, whereas a Face Rotation will require only the cells across a specific face to pass the conditional check. Before performing a Cube Rotation, be sure to set the boolean value of all cells being allowed to move, to true. Here's a very rudimentary way of implementing this logic to only apply to a specific face of the cube:

turnFace() {
  for (let i = 0; i < this.m_cells.length; i++) {
    let cell = this.m_cells[i];

    // Negative x-axis
    if (cell.position.x < 0) {
      cell.m_allowedToMove = true;
    }
    // Positive x-axis
    else if (cell.position.x > 0) {
      cell.m_allowedToMove = true;
    }
    // ...
    // Calculate other axes
    // ...
  }
}

Things are starting to get pretty cool now, right! Now let me figure out how I can tackle the cube ‘Explosion’. Essentially, all I need to do is move each cell across a vector, based on its position in relation to the center cell. If you’re familiar with mathematical vectors and you conveniently positioned your cube in such a way that the center cell is placed at the origin of your world space (0, 0, 0), this is super easy to do. If you didn’t do this, I would recommend using the local coordinate system relative to your cube and you’ll be fine. Assuming the size of each cell is 1x1x1, I can denote that the instantiated position of each cell lies within a range of (-1, -1, -1) and (1, 1, 1):

This position of instantiation for each cell, conveniently gives me the unit vector I need, determining the direction that each cell must travel to create my ‘Explosion’ effect. Very similarly to how I previously implemented the Cube and Face Rotation transformations, I can do the same here, except rather than applying a rotation matrix to each cell, I will need to move the position of each cell across its bespoke unit vector:

explode() {
  for (let i = 0; i < this.m_cells.length; i++) {
    let cell = this.m_cells[i];
    let move = new Vector3();
    // ...
    // Calculate speed and direction of translation
    // ...
    cell.position.add(move);
  }
}

I’m acutely aware that this will cause the cells to infinitely travel in said direction (which I don’t want), if I don’t implement some kind of max threshold that they should stop at. Rewinding this effect will also require me to implement some kind of max threshold anyway. So as long as I keep track of each cell's position, once this position reaches the max threshold, I will want to invert the direction of each cell's unit vector. Once the cells return to their original position (or min threshold) the effect may then repeat.

Now that I’ve got this nailed, I can start to think about implementing a function that randomly selects which transformation to apply to the cube. I decided to simply generate a random number and pick a transformation function based on the outcome:

transformCube() {
  let move = Math.floor(Math.random() * 100);
  // 10% chance
  if (move < 10) {
    cube.explode();
  }
  // 50% chance
  else if (move >= 10 && move < 60) {
    cube.turnFace();
  }
  // 40% chance
  else {
    cube.turnCube();
  }
}

As I mentioned at the start of this section, there are an infinite number of transformations that can be applied to the cube - so don’t feel compelled to stop here. Get creative!

Embellish

At this point, I’m pretty happy with the functionality of the cube, but am not so happy with how colorless and boring it looks. This is all totally subjective of course… Beauty is in the eye of the beholder and all that. Anyway, I believe this to be the most important and transformative part of the entire process, due to the sheer power of shaders and modern post processing techniques. Just take a look at what a basic fragment shader, bloom and tone mapping gets you:

Thankfully, a lot of post processing effects come out of the box when using libraries such as three.js, otherwise this blog post would be ten times the size and all about shaders. I’ll save that for another time…

In closing, the possibilities here are endless - have fun with it, create something weird and wacky, then show me! I’d love to see what others have come up with. I hope this read was worth your time and provided you with some value. Feel free to share amongst your peers, but also don’t forget to check out the recommended reading material below. This should hopefully fill in all the gaps I missed. Also, if you’d like to play with what I created, check out the live demo below.