17
3D Game of Life: Bays 5766
drag to orbit ยท click to reseed
idle
177 lines ยท three
view source
// 3D Game of Life โ Bays's 5766 rule (Carter Bays, 1987).
//
// Rule notation B/S = 6/5,6,7 on a 26-cell Moore neighborhood:
// - a dead cell with exactly 6 alive neighbors becomes alive
// - an alive cell with 5, 6, or 7 alive neighbors survives
// This is one of the very few 3D Life rules that admits long-lived
// dynamics โ most random rules either die instantly or explode to a
// stable hash. Bays found it by exhaustive search.
//
// Rendering: a single InstancedMesh of cubes, one instance per grid cell.
// Dead cells get scaled to zero so they disappear from rasterization.
// Cells inherit color from their world position so the cube has a soft
// gradient texture that makes oscillators legible.
const N = 22; // grid side length (22ยณ โ 10.6k cells)
const STEP_MS = 150; // CA update period
const BIRTH = new Set([6]);
const SURVIVE = new Set([5, 6, 7]);
const CELL = 1.0; // world units per cell
const GAP = 0.12; // visual gap between cubes
let grid, next; // Uint8Array of size N*N*N
let mesh; // THREE.InstancedMesh
let dummy; // reusable Object3D for setMatrixAt
let colorAttr; // for per-instance colors
let stepAcc = 0; // ms accumulator for CA steps
let popCount = 0;
let stableTicks = 0; // consecutive steps with no change
let lastPop = -1;
let ambient, dirLight;
let userYaw = 0.6, userPitch = 0.35;
let isDragging = false;
let lastMouseX = 0, lastMouseY = 0;
let idleTime = 0;
function idx(x, y, z) { return x + N * (y + N * z); }
function countNeighbors(g, x, y, z) {
let c = 0;
for (let dz = -1; dz <= 1; dz++) {
const zz = z + dz; if (zz < 0 || zz >= N) continue;
for (let dy = -1; dy <= 1; dy++) {
const yy = y + dy; if (yy < 0 || yy >= N) continue;
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0 && dz === 0) continue;
const xx = x + dx; if (xx < 0 || xx >= N) continue;
c += g[idx(xx, yy, zz)];
}
}
}
return c;
}
function stepCA() {
let alive = 0;
for (let z = 0; z < N; z++) {
for (let y = 0; y < N; y++) {
for (let x = 0; x < N; x++) {
const n = countNeighbors(grid, x, y, z);
const i = idx(x, y, z);
const was = grid[i];
let now;
if (was) now = SURVIVE.has(n) ? 1 : 0;
else now = BIRTH.has(n) ? 1 : 0;
next[i] = now;
if (now) alive++;
}
}
}
const tmp = grid; grid = next; next = tmp;
if (alive === lastPop) stableTicks++; else stableTicks = 0;
lastPop = alive;
popCount = alive;
}
function seedRandomSoup(densityFrac, region) {
// region: optional {x0,y0,z0,x1,y1,z1}; if null, full grid
const r = region || { x0: 0, y0: 0, z0: 0, x1: N, y1: N, z1: N };
for (let z = r.z0; z < r.z1; z++) {
for (let y = r.y0; y < r.y1; y++) {
for (let x = r.x0; x < r.x1; x++) {
grid[idx(x, y, z)] = Math.random() < densityFrac ? 1 : 0;
}
}
}
}
function fullReseed() {
grid.fill(0);
// Seed a chunky cluster in the middle โ Bays 5766 likes ~30-40%
// density in a ~10ยณ pocket; uniform random over the full grid
// tends to either die or run away.
const half = (N / 2) | 0;
const r = 5;
seedRandomSoup(0.35, {
x0: half - r, y0: half - r, z0: half - r,
x1: half + r, y1: half + r, z1: half + r,
});
stableTicks = 0;
lastPop = -1;
}
function rebuildInstances() {
// Place every cell once; visibility is encoded by scale.
const offset = -(N - 1) * CELL / 2;
const color = new THREE.Color();
for (let z = 0; z < N; z++) {
for (let y = 0; y < N; y++) {
for (let x = 0; x < N; x++) {
const i = idx(x, y, z);
dummy.position.set(
offset + x * CELL,
offset + y * CELL,
offset + z * CELL,
);
const alive = grid[i];
const s = alive ? (CELL - GAP) : 0.0001;
dummy.scale.set(s, s, s);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
// Hue blends through position so the lattice reads as 3D.
const h = ((x + y + z) / (3 * N)) % 1;
color.setHSL(h, 0.7, 0.55);
mesh.setColorAt(i, color);
}
}
}
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
}
function init({ scene, camera, renderer, width, height }) {
renderer.setClearColor(0x06080d, 1);
const total = N * N * N;
grid = new Uint8Array(total);
next = new Uint8Array(total);
fullReseed();
ambient = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambient);
dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
dirLight.position.set(20, 30, 25);
scene.add(dirLight);
const fill = new THREE.DirectionalLight(0x88aaff, 0.35);
fill.position.set(-25, -10, -20);
scene.add(fill);
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshLambertMaterial({ color: 0xffffff });
mesh = new THREE.InstancedMesh(geo, mat, total);
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
// Per-instance color buffer.
colorAttr = new THREE.InstancedBufferAttribute(new Float32Array(total * 3), 3);
mesh.instanceColor = colorAttr;
dummy = new THREE.Object3D();
scene.add(mesh);
rebuildInstances();
const dist = N * 1.9;
camera.position.set(dist * 0.6, dist * 0.4, dist * 0.7);
camera.lookAt(0, 0, 0);
return { scene, camera };
}
function tick({ dt, scene, camera, renderer, width, height, input }) {
// Input: drag = orbit, click = reseed.
const clicks = input.consumeClicks();
if (clicks > 0) fullReseed();
if (input.mouseDown) {
if (!isDragging) {
isDragging = true;
lastMouseX = input.mouseX;
lastMouseY = input.mouseY;
idleTime = 0;
} else {
const dx = input.mouseX - lastMouseX;
const dy = input.mouseY - lastMouseY;
userYaw -= dx * 0.006;
userPitch -= dy * 0.006;
userPitch = Math.max(-1.2, Math.min(1.2, userPitch));
lastMouseX = input.mouseX;
lastMouseY = input.mouseY;
idleTime = 0;
}
} else {
isDragging = false;
idleTime += dt;
if (idleTime > 0.4) userYaw += dt * 0.18;
}
const r = N * 2.0;
const cy = Math.cos(userYaw), sy = Math.sin(userYaw);
const cp = Math.cos(userPitch), sp = Math.sin(userPitch);
camera.position.set(r * cp * sy, r * sp, r * cp * cy);
camera.lookAt(0, 0, 0);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
// CA step on a fixed wall-clock cadence, not per frame.
stepAcc += dt * 1000;
if (stepAcc >= STEP_MS) {
stepAcc = 0;
stepCA();
// Auto-reseed on death, gridlock, or runaway fill.
const total = N * N * N;
if (popCount === 0
|| popCount > total * 0.6
|| stableTicks > 30) {
fullReseed();
}
rebuildInstances();
}
renderer.render(scene, camera);
}
Comments (0)
Log in to comment.