How Did I Build The Rubik's Cube

The Challenge
Building a 3D Rubik's Cube that can rotate realistically is more complex than it seems. The cube has possible combinations, making it one of the most fascinating puzzles ever created.
In this post, I'll show you how I built an interactive Rubik's Cube using Three.js, breaking down the process into simple steps.
Step 1: Building a Single Cube
First, let's create one colorful cube. Each face needs its own color based on the Rubik's Cube color scheme.
// Create geometry and apply vertex colors
function makeSingleCube(x, y, z) {
const geometry = new BoxGeometry().toNonIndexed();
const faceCount = geometry.getAttribute("position").count / 6;
const colors = [];
// Color each face based on position
for (let face = 0; face < faceCount; face++) {
const color = new Color(getColor(x, y, z, face));
// Each face has 6 vertices (2 triangles)
for (let v = 0; v < 6; v++) {
colors.push(color.r, color.g, color.b);
}
}
geometry.setAttribute("color", new Float32BufferAttribute(colors, 3));
return geometry;
}
The color scheme follows the standard Rubik's Cube:
- Right: Green
- Left: Blue
- Top: Yellow
- Bottom: White
- Front: Red
- Back: Orange
Step 2: Assembling the 3×3×3 Cube
Now we'll create 27 cubes arranged in a 3×3×3 grid. The key is maintaining proper spacing and storing references for rotation.
const CUBE_MARGIN = 0.1; // Gap between cubes
const material = new MeshBasicMaterial({ vertexColors: true });
function makeRubik() {
// Initialize 3D array to store cube references
const cubes = Array(3).fill().map(() =>
Array(3).fill().map(() => Array(3))
);
// Create and position each cube
for (let x = 0; x < 3; x++) {
for (let y = 0; y < 3; y++) {
for (let z = 0; z < 3; z++) {
const geometry = makeSingleCube(x, y, z);
const cube = new Mesh(geometry, material);
// Position with gap
cube.position.set(
x * (1 + CUBE_MARGIN),
y * (1 + CUBE_MARGIN),
z * (1 + CUBE_MARGIN)
);
cubes[x][y][z] = cube;
scene.add(cube);
}
}
}
return cubes;
}
Step 3: The Rotation System
The trickiest part is implementing realistic rotations. A Rubik's Cube doesn't rotate individual pieces - entire layers move together.
graph LR A[Select Layer] --> B[Group Cubes] B --> C[Rotate Group] C --> D[Ungroup] D --> E[Update Positions]
The Rotation Algorithm
Here's how the rotation works:
// Create pivot at cube center
const pivot = new Object3D();
const k = ((cubeNum - 1) / 2) * (1 + CUBE_MARGIN);
pivot.position.set(k, k, k);
function startMove(face, depth, magnitude) {
// 1. Group cubes in the selected layer
for (let x = 0; x < cubeNum; x++) {
for (let y = 0; y < cubeNum; y++) {
for (let z = 0; z < cubeNum; z++) {
if (isInFace(x, y, z, face, depth)) {
pivot.attach(cubes[x][y][z]);
}
}
}
}
// 2. Calculate rotation target
let target = { x: 0, y: 0, z: 0 };
const angle = (Math.PI / 2) * magnitude;
if (face === FACE_LEFT || face === FACE_RIGHT) {
target.x = angle;
} else if (face === FACE_TOP || face === FACE_BOTTOM) {
target.y = angle;
} else {
target.z = angle;
}
// 3. Animate rotation
animate({
targets: pivot.rotation,
...target,
duration: 600,
easing: eases.linear,
complete: cleanUpAfterMove
});
}
function cleanUpAfterMove() {
// 4. Ungroup and update positions
const newCubes = Array.from(cubes);
for (let i = pivot.children.length - 1; i >= 0; i--) {
const cube = pivot.children[i];
scene.attach(cube);
// Get world position and snap to grid
const pos = cube.getWorldPosition(new Vector3());
const x = Math.round(pos.x / (1 + CUBE_MARGIN));
const y = Math.round(pos.y / (1 + CUBE_MARGIN));
const z = Math.round(pos.z / (1 + CUBE_MARGIN));
// Update position and array reference
cube.position.set(
x * (1 + CUBE_MARGIN),
y * (1 + CUBE_MARGIN),
z * (1 + CUBE_MARGIN)
);
newCubes[x][y][z] = cube;
}
cubes = newCubes;
pivot.rotation.set(0, 0, 0);
}
See it in action
Step 4: Adding Variations
Once the core mechanics work, you can experiment with different features:
Different Cube Sizes
The same logic works for any NxNxN cube. Here's a 5x5x5 cube with playful easing animations:
Key Features Added
- Auto-rotation: Cubes rotate randomly when idle
- Smooth animations: Using anime.js for fluid movements
- Dynamic easing: Random easing functions for variety
- Camera controls: OrbitControls for user interaction
Complete Implementation
Learn more
Here is a Rust implementation of this scene. It is a bit more complex than this one but the idea still remains. See it live at here (/rubik-rs)