In part one and two, we got our bearings working with ChatGPT to solve a basic problem in Blender. Now it's time to move on to cellular automata.
A cellular automaton is a mathematical model used to simulate the behavior of complex systems. It is made up of a grid of cells, each of which can be in one of a finite number of states, such as "on" or "off". These cells interact with each other according to a set of rules, which determine how their state will evolve over time. This simple yet powerful approach has been used to model a wide range of phenomena, from the spread of diseases to the formation of patterns in nature. It has also been applied in fields as diverse as physics and economics, offering insights into the collective behavior of large groups of individuals.
The Game of Life, also known as Life, is a specific example of 2-dimensional cellular automaton created by the mathematician John Horton Conway in 1970.
The rules for the Game of Life are shockingly simple:
A cellular automaton is a mathematical model used to simulate the behavior of complex systems. It is made up of a grid of cells, each of which can be in one of a finite number of states, such as "on" or "off". These cells interact with each other according to a set of rules, which determine how their state will evolve over time. This simple yet powerful approach has been used to model a wide range of phenomena, from the spread of diseases to the formation of patterns in nature. It has also been applied in fields as diverse as physics and economics, offering insights into the collective behavior of large groups of individuals.
The Game of Life, also known as Life, is a specific example of 2-dimensional cellular automaton created by the mathematician John Horton Conway in 1970.
The rules for the Game of Life are shockingly simple:
- Any live cell with fewer than two live neighbors dies, as if by underpopulation.
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
*Life is equivalent to a cell being "on", death is equivalent to a cell being "off"
These rules are applied to every cell in the grid simultaneously, with the state of each cell at the next step being determined by the current state of the cell and the states of its eight neighbors (the cells directly adjacent to it and the cells diagonally adjacent to it). This process is repeated over time, with the state of the grid changing at each step according to the rules.
Perhaps the easiest way to understand these rules is to play the game... try it here.
A quick Google search reveals that there is evidence to suggest that research has already been conducted to determine if it is possible to create a 3D Game of Life. It appears that, with some modifications, it is possible to achieve this. However, I still feel compelled to re-invent the wheel with this project as an exercise in problem-solving. As such, I will avoid looking at solutions that have already been discovered, and begin by mapping and tweaking Conway's simple rules to the three dimensional space.
...
...
As a first pass at solving this problem, I have decided to use Three.js instead of Blender. The reason being that, Three.js is less complex than Blender, and in theory will provide me with a slightly quicker working solution. Despite this, I have no experience with Javascript (other than college projects) or Three.js. So, I'll get some more practice leveraging ChatGPT.
Let's get started...
Listed below are a list of boilerplate tasks I needed to solve. Google and Three.js documentation were the most helpful at detailing how to do these steps. Though ChatGPT served as a helpful guide for illuminating what these abstractions were in the problem space.
1. Import Three.js into a project.
2. Create a scene and a camera
3. Import an "OrbitalControls" object into the scene
4. Create a grid as a visual guide of space.
5. Create an animation loops that allows controlling the camera
a. Compute 3 second intervals within the animation loop
The code for this looks as follows:
import * as THREE from 'three';
import { OrbitControls } from '../node_modules/three/examples/jsm/controls/OrbitControls.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 100, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
camera.position.z = 5;
camera.position.y = 5;
camera.position.x = 0;
const controls = new OrbitControls( camera, renderer.domElement );
const size = 10;
const divisions = 10;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
var last = 0; // timestamp of the last render() call
function animate(now) {
controls.update();
if(now - last >= 3*1000) {
last = now;
}
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();Cool.
Next, the goal will be to compute N positions in space and create a cub with a random color at each of these positions.
ChatGPT was very helpful here and give me the following code:
function generatePositions(n) {
const positions = [];
while (positions.length < n) {
const x = Math.round((Math.random() * 2) - 1);
const y = Math.round((Math.random() * 2));
const z = Math.round((Math.random() * 2) - 1);
positions.push(`${x}_${y}_${z}`);
}
return [...new Set(positions)];
}I ended up making slight modifications to only provide unique values and return a more constrained random space. ChatGPT was very helpful here by introducing me to the Javascript API functions for these math operations and sugary things like [...new Set(positions)]
And for creating a cube with a random color and add it to the scene:
function createCube(position) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const color = Math.random() * 0xffffff;
const material = new THREE.MeshBasicMaterial({ color });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(position.x, position.y, position.z);
scene.add(cube);
}And running these together:
const alivePositions = generatePositions(6);
for (let livePosition of alivePositions) {
const livePositionAsObj = convertStringToObject(livePosition);
createCube(livePositionAsObj);
}I created the helper convertStringToObject because I know that I am going to need to index the positions for lookups and uniqueness checks, so converting to and from string will be needed.
The results:
Okay. We're getting somewhere.
Now that I have these initial cubes set to an array, alivePositions, we will need to think about the problem.
What do we care about? At each iteration of the game, what is being considered? The answer to this question will be the crux of our computation.
We care about, 1) alive positions and 2) their neighbors. All of these spaces will be considered for evaluation. An "active" positions is "in play", meaning if the positions is alive, it can stay alive or die, or if the position is dead, it can become alive.
Thus, for each alive cube in the starting space we need to compute their neighbors and add it to an array of "active positions". The set of alive positions and their neighbors are in play.
ChatGPT was very helpful with this calculation:
A relatively simple calculation, but I'm a lazy human, so ChatGPT to the rescue.
...
Despite my laziness, I actually wanted to solve the crux of this problem myself for fun. It's all quite simple.. which is kind of the point of Cellular Automata. The following code was written myself, leveraging ChatGPT or Google to gain insight about specific Javascript syntax.
function playGame() {
const alivePositions = Array.from(cubeMap.keys());
const allNeighborPositions = getAllNeighborPositions(alivePositions);
const setOfAllActivePositions = new Set([...alivePositions, ...allNeighborPositions]);
for (const activePosition of setOfAllActivePositions.values()) {
const aliveNeighbors = aliveNeighborsFrom(activePosition, alivePositions);
if (positionIsAlive(activePosition, alivePositions)) {
if (aliveNeighbors.length <= 2) {
// death - solitude
removeCube(activePosition);
} else if (aliveNeighbors.length >= 8) {
// death - overpopulation
removeCube(activePosition);
}
} else {
if (aliveNeighbors.length == 5) {
// birth
createCube(convertStringToObject(activePosition));
}
}
}
}Hopefully my code is self documenting enough to get the general gist of what is going on.
I plugged the game into the animation like so:
var last = 0; // timestamp of the last render() call
function animate(now) {
controls.update();
if(now - last >= 3*1000) {
last = now;
playGame(); // <--------
}
requestAnimationFrame(animate);
renderer.render(scene, camera);
};I played around with the rules until I found something interesting. I'm sure there are more interesting combinations of rules... but that will be a follow up project perhaps... these rules are interesting in that some particular starting positions appears to run indefinitely. Here it is:
It's really quite amazing.
Most starting positions either die out or lay stable and unchanging... but some configurations last for many iterations, and some possibly run forever. Some really cool symmetries emerge, such as this one:
Or becoming a huge blob:
Maybe if this runs long enough it will recreate the universe...
But, one thing is for certain, my computer definitely can't handle this, let alone run this with Javascript in my web browser. I might need to head back to Blender.
Next up, let's see if we can make these automata evolve.