add animation, beautify the scene 

import java.util.concurrent.*;
import controlP5.*;

final int MAX_SIZE = 128;
int currentSize = 96;
boolean[][][] grid = new boolean[MAX_SIZE][MAX_SIZE][MAX_SIZE];
boolean[][][] nextGrid = new boolean[MAX_SIZE][MAX_SIZE][MAX_SIZE];
int[][][] cellAge = new int[MAX_SIZE][MAX_SIZE][MAX_SIZE];

float rotX, rotY;
float zoom = 1.0; // Zoom level
int cellSize = 3;
boolean paused = false;
int framesPerGeneration = 5;
int frameCounter = 0;

PShader cellShader;
ThreadPoolExecutor executor;

// Rule set
int minSurvive = 4;
int maxSurvive = 5;
int birthCount = 5;

PShape cellShape;
ControlP5 cp5; // ControlP5 instance

void setup() {
  size(1200, 800, P3D); // Increased canvas size

  // Initialize ControlP5
  cp5 = new ControlP5(this);
  setupControls();

  randomizeGrid();

  // Initialize thread pool
  int processors = Runtime.getRuntime().availableProcessors();
  executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(processors);

  // Load shader
  cellShader = loadShader("cellFrag.glsl", "cellVert.glsl");

  // Initialize cell age
  for (int x = 0; x < MAX_SIZE; x++) {
    for (int y = 0; y < MAX_SIZE; y++) {
      for (int z = 0; z < MAX_SIZE; z++) {
        cellAge[x][y][z] = 0;
      }
    }
  }

  // Create cell shape for instanced rendering
  cellShape = createShape(BOX, cellSize);
  cellShape.setStroke(false);
}

void draw() {
  background(30);

  // 3D transformations and rendering
  pushMatrix(); // Start of 3D scene
  lights();
  translate(width / 2, height / 2, 0);
  scale(zoom); // Apply zoom
  translate(0, 0, -currentSize * cellSize / 2); // Adjust Z position based on zoom
  rotateX(rotX);
  rotateY(rotY);
  translate(-currentSize * cellSize / 2, -currentSize * cellSize / 2, -currentSize * cellSize / 2);

  // Update grid
  if (!paused && frameCounter % framesPerGeneration == 0) {
    updateGrid();
  }
  frameCounter++;

  // Use shader
  shader(cellShader);

  // Prepare positions for instanced rendering
  cellShader.set("cellSize", cellSize);
  cellShader.set("gridSize", currentSize);

  beginShape(POINTS);
  for (int x = 0; x < currentSize; x++) {
    for (int y = 0; y < currentSize; y++) {
      for (int z = 0; z < currentSize; z++) {
        if (grid[x][y][z]) {
          vertex(x, y, z);
        }
      }
    }
  }
  endShape();

  resetShader();
  popMatrix(); // End of 3D scene

  // Reset transformations and coordinate system
  camera();
  // Disable depth test to ensure GUI is on top
  hint(DISABLE_DEPTH_TEST);

  // Draw GUI controls
  cp5.draw();

  // Re-enable depth test if you plan to draw more 3D content afterward
  hint(ENABLE_DEPTH_TEST);
}

void updateGrid() {
  CountDownLatch latch = new CountDownLatch(currentSize);
  for (int x = 0; x < currentSize; x++) {
    final int fx = x;
    executor.execute(() -> {
      for (int y = 0; y < currentSize; y++) {
        for (int z = 0; z < currentSize; z++) {
          int neighbors = countNeighbors(fx, y, z);
          if (grid[fx][y][z]) {
            nextGrid[fx][y][z] = (neighbors >= minSurvive && neighbors <= maxSurvive);
            if (nextGrid[fx][y][z]) {
              cellAge[fx][y][z] = min(cellAge[fx][y][z] + 1, 255);
            }
          } else {
            nextGrid[fx][y][z] = (neighbors == birthCount);
            if (nextGrid[fx][y][z]) {
              cellAge[fx][y][z] = 0;
            }
          }
        }
      }
      latch.countDown();
    });
  }
  try {
    latch.await();
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  boolean[][][] temp = grid;
  grid = nextGrid;
  nextGrid = temp;
}

int countNeighbors(int x, int y, int z) {
  int count = 0;
  for (int dx = -1; dx <= 1; dx++) {
    int nx = x + dx;
    if (nx < 0 || nx >= currentSize) continue;
    for (int dy = -1; dy <= 1; dy++) {
      int ny = y + dy;
      if (ny < 0 || ny >= currentSize) continue;
      for (int dz = -1; dz <= 1; dz++) {
        int nz = z + dz;
        if (nz < 0 || nz >= currentSize) continue;
        if (dx == 0 && dy == 0 && dz == 0) continue;
        if (grid[nx][ny][nz]) count++;
      }
    }
  }
  return count;
}

void randomizeGrid() {
  for (int x = 0; x < currentSize; x++) {
    for (int y = 0; y < currentSize; y++) {
      for (int z = 0; z < currentSize; z++) {
        grid[x][y][z] = random(1) < 0.1;
        cellAge[x][y][z] = 0;
      }
    }
  }
}

void mouseDragged() {
  // Prevent rotation when interacting with GUI
  if (!cp5.isMouseOver()) {
    rotY += (mouseX - pmouseX) * 0.01;
    rotX += (mouseY - pmouseY) * 0.01;
  }
}

void mouseWheel(MouseEvent event) {
  float e = event.getCount();
  zoom += e * 0.05;
  zoom = constrain(zoom, 0.2, 5.0); // Limit zoom level
}

// Add or remove cells by clicking
void mouseClicked() {
  // Avoid interference when clicking on GUI
  if (!cp5.isMouseOver()) {
    PVector pos = screenToWorld(mouseX, mouseY);
    int x = int(pos.x / cellSize);
    int y = int(pos.y / cellSize);
    int z = int(pos.z / cellSize);
    if (x >= 0 && x < currentSize && y >= 0 && y < currentSize && z >= 0 && z < currentSize) {
      grid[x][y][z] = !grid[x][y][z];
      cellAge[x][y][z] = 0;
    }
  }
}

PVector screenToWorld(float screenX, float screenY) {
  PGraphics3D p3d = (PGraphics3D)g;

  // Get inverse of modelview-projection matrix
  PMatrix3D modelViewProj = new PMatrix3D();
  modelViewProj.apply(p3d.projection);
  modelViewProj.apply(p3d.modelview);
  modelViewProj.invert();

  // Map screen coordinates to NDC
  float ndcX = map(screenX, 0, width, -1, 1);
  float ndcY = map(screenY, 0, height, 1, -1); // Note the Y inversion

  // Far and near points
  PVector nearPoint = new PVector(ndcX, ndcY, -1);
  PVector farPoint = new PVector(ndcX, ndcY, 1);

  // Transform to world coordinates
  PVector nearPointWorld = modelViewProj.mult(nearPoint, null);
  PVector farPointWorld = modelViewProj.mult(farPoint, null);

  // Direction from near to far point
  PVector direction = PVector.sub(farPointWorld, nearPointWorld).normalize();

  // Compute intersection with Z=0 plane
  float t = -nearPointWorld.z / direction.z;
  PVector point = PVector.add(nearPointWorld, PVector.mult(direction, t));

  return point;
}

void keyPressed() {
  if (key == ' ') paused = !paused;
  if (key == 'r') randomizeGrid();
  if (key == 's') saveState();
  if (key == 'l') loadState();
}

void saveState() {
  JSONArray json = new JSONArray();
  for (int x = 0; x < currentSize; x++) {
    for (int y = 0; y < currentSize; y++) {
      for (int z = 0; z < currentSize; z++) {
        if (grid[x][y][z]) {
          JSONObject cell = new JSONObject();
          cell.setInt("x", x);
          cell.setInt("y", y);
          cell.setInt("z", z);
          cell.setInt("age", cellAge[x][y][z]);
          json.append(cell);
        }
      }
    }
  }
  saveJSONArray(json, "data/saved_state.json");
}

void loadState() {
  JSONArray json = loadJSONArray("data/saved_state.json");
  if (json == null) return;

  // Clear the current grid
  for (int x = 0; x < currentSize; x++) {
    for (int y = 0; y < currentSize; y++) {
      for (int z = 0; z < currentSize; z++) {
        grid[x][y][z] = false;
        cellAge[x][y][z] = 0;
      }
    }
  }

  // Load saved cells
  for (int i = 0; i < json.size(); i++) {
    JSONObject cell = json.getJSONObject(i);
    int x = cell.getInt("x");
    int y = cell.getInt("y");
    int z = cell.getInt("z");
    int age = cell.getInt("age");
    if (x < currentSize && y < currentSize && z < currentSize) {
      grid[x][y][z] = true;
      cellAge[x][y][z] = age;
    }
  }
}

void setupControls() {
  int guiY = 20; // Starting Y position for controls
  int guiSpacing = 30; // Spacing between controls

  // Slider for minSurvive
  cp5.addSlider("minSurvive")
     .setPosition(20, guiY)
     .setSize(200, 20)
     .setRange(0, 26)
     .setValue(minSurvive);

  // Slider for maxSurvive
  cp5.addSlider("maxSurvive")
     .setPosition(20, guiY + guiSpacing)
     .setSize(200, 20)
     .setRange(0, 26)
     .setValue(maxSurvive);

  // Slider for birthCount
  cp5.addSlider("birthCount")
     .setPosition(20, guiY + guiSpacing * 2)
     .setSize(200, 20)
     .setRange(0, 26)
     .setValue(birthCount);

  // Slider for framesPerGeneration
  cp5.addSlider("framesPerGeneration")
     .setPosition(20, guiY + guiSpacing * 3)
     .setSize(200, 20)
     .setRange(1, 60)
     .setValue(framesPerGeneration);

  // Buttons for grid size
  cp5.addButton("Grid Size: 64")
     .setPosition(20, guiY + guiSpacing * 5)
     .setSize(200, 20)
     .onClick(e -> setGridSize(64));

  cp5.addButton("Grid Size: 96")
     .setPosition(20, guiY + guiSpacing * 6)
     .setSize(200, 20)
     .onClick(e -> setGridSize(96));

  cp5.addButton("Grid Size: 128")
     .setPosition(20, guiY + guiSpacing * 7)
     .setSize(200, 20)
     .onClick(e -> setGridSize(128));

  // Button for randomize grid
  cp5.addButton("Randomize Grid")
     .setPosition(20, guiY + guiSpacing * 9)
     .setSize(200, 20)
     .onClick(e -> randomizeGrid());

  // Button for pause/resume
  cp5.addToggle("Pause")
     .setPosition(20, guiY + guiSpacing * 10)
     .setSize(50, 20)
     .setValue(paused)
     .onChange(e -> paused = e.getController().getValue() == 1);

  // Set theme (optional)
  cp5.setColorForeground(color(100, 100, 140));
  cp5.setColorBackground(color(60, 60, 90));
  cp5.setColorCaptionLabel(color(255));
  cp5.setColorValueLabel(color(200));
}

void setGridSize(int newSize) {
  currentSize = newSize;
  randomizeGrid();
}

void exit() {
  executor.shutdown();
  super.exit();
}



gellFrag
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

uniform float cellAge;
uniform vec3 lightDir;

varying vec3 vNormal;
varying vec3 vPosition;

void main() {
  // Basic lighting
  vec3 normal = normalize(vNormal);
  float diff = max(dot(normal, lightDir), 0.0);

  // Color based on age
  vec3 youngColor = vec3(1.0, 0.5, 0.0);  // Orange for young cells
  vec3 oldColor = vec3(0.0, 0.5, 1.0);    // Blue for old cells

  vec3 cellColor = mix(youngColor, oldColor, cellAge);

  vec3 ambient = 0.2 * cellColor;
  vec3 diffuse = diff * cellColor;

  gl_FragColor = vec4(ambient + diffuse, 1.0);
}

cellVert
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

attribute vec4 position;
uniform mat4 modelview;
uniform mat4 projection;
uniform float cellSize;
uniform int gridSize;

varying vec3 vNormal;
varying vec3 vPosition;

void main() {
  vec3 pos = position.xyz * cellSize;
  vPosition = pos;
  vNormal = normalize(position.xyz - vec3(0.5));

  gl_Position = projection * modelview * vec4(pos, 1.0);
}