Introduction
Cellular automata are fascinating systems where simple rules applied to cells in a grid lead to complex and often mesmerizing patterns. While 2D cellular automata like Conway’s Game of Life are well-known, extending the concept into 3D opens up a whole new dimension of possibilities—literally! In this project, I used p5.js to create an interactive 3D cellular automaton, combining computational elegance with visual appeal.
The Project
- The Grid
The automaton uses a 3D array to represent the cells. Each cell is a small cube, and the entire grid is visualized in a 3D space using p5.js’sWEBGL
mode. - Random Initialization
The grid starts with a random distribution of alive and dead cells, giving each simulation a unique starting point. - Rule Application
At each frame, the automaton calculates the next state of every cell based on its neighbors. The updated grid is then displayed. - Interactivity
Using p5.js’sorbitControl()
, users can rotate and zoom into the 3D grid, exploring the automaton’s patterns from different perspectives.Codelet grid, nextGrid; let cols = 10, rows = 10, layers = 10; // Grid dimensions let cellSize = 20; function setup() { createCanvas(600, 600, WEBGL); grid = create3DArray(cols, rows, layers); nextGrid = create3DArray(cols, rows, layers); randomizeGrid(); } function draw() { background(30); orbitControl(); // Allows rotation and zoom with mouse // Center the grid translate(-cols * cellSize / 2, -rows * cellSize / 2, -layers * cellSize / 2); // Draw cells for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++) { for (let z = 0; z < layers; z++) { if (grid[x][y][z] === 1) { push(); translate(x * cellSize, y * cellSize, z * cellSize); fill(255); noStroke(); box(cellSize * 0.9); // A slightly smaller cube for spacing pop(); } } } } updateGrid(); // Update the grid for the next frame } // Create a 3D array function create3DArray(cols, rows, layers) { let arr = new Array(cols); for (let x = 0; x < cols; x++) { arr[x] = new Array(rows); for (let y = 0; y < rows; y++) { arr[x][y] = new Array(layers).fill(0); } } return arr; } // Randomize the initial state of the grid function randomizeGrid() { for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++) { for (let z = 0; z < layers; z++) { grid[x][y][z] = random() > 0.7 ? 1 : 0; // 30% chance of being alive } } } } // Update the grid based on rules function updateGrid() { for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++) { for (let z = 0; z < layers; z++) { let neighbors = countNeighbors(x, y, z); if (grid[x][y][z] === 1) { // Survival: A live cell stays alive with 4-6 neighbors nextGrid[x][y][z] = neighbors >= 4 && neighbors <= 6 ? 1 : 0; } else { // Birth: A dead cell becomes alive with exactly 5 neighbors nextGrid[x][y][z] = neighbors === 5 ? 1 : 0; } } } } // Swap grids let temp = grid; grid = nextGrid; nextGrid = temp; } // Count the alive neighbors of a cell function countNeighbors(x, y, z) { let count = 0; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { for (let dz = -1; dz <= 1; dz++) { if (dx === 0 && dy === 0 && dz === 0) continue; // Skip the cell itself let nx = (x + dx + cols) % cols; let ny = (y + dy + rows) % rows; let nz = (z + dz + layers) % layers; count += grid[nx][ny][nz]; } } } return count; }
Future Enhancements
- Custom Rules: Experiment with different neighbor conditions to discover new behaviors.
- Larger Grids: Scale up the grid size for more complex patterns (optimize for performance).
- Color Variations: Assign colors based on neighbor count or generation age.
- Save States: Let users save and reload interesting configurations.