About this technique →
cellular-forms/sketch.js
// 3D Cellular Forms — Andy Lomas-style morphogenesis (simplified)
//
// A sphere of cells grows organically. When a cell has 3+ neighbors within
// neighborRadius, it may spawn a new cell at a random offset. The form
// self-organizes into blobby, organic shapes over time.
// Cells are rendered as an InstancedMesh of low-poly spheres for performance.

import * as THREE from 'three';

export const WARMUP = { framesBeforeReady: 480 };

export const PARAMS = {
  maxCells:    { value: 1500, min: 100, max: 4000, step: 50,    label: "Max Cells",       folder: "Structure",   rebuildOnChange: true },
  spawnRadius: { value: 0.18, min: 0.05, max: 0.5, step: 0.01,  label: "Spawn Distance",  folder: "Behavior" },
  neighborR:   { value: 0.35, min: 0.1,  max: 1.0, step: 0.01,  label: "Neighbor Radius", folder: "Behavior" },
  spawnRate:   { value: 0.08, min: 0.005, max: 0.5, step: 0.005, label: "Spawn Probability", folder: "Behavior" },
  cellRadius:  { value: 0.06, min: 0.01, max: 0.2,  step: 0.005, label: "Cell Size",       folder: "Appearance" },
  hue:         { value: 30,   min: 0,    max: 360,  step: 1,     label: "Hue",             folder: "Appearance" },
};

function randomUnitVec3() {
  // Uniform random direction on unit sphere
  const theta = Math.random() * Math.PI * 2;
  const z = Math.random() * 2 - 1;
  const r = Math.sqrt(1 - z * z);
  return new THREE.Vector3(r * Math.cos(theta), r * Math.sin(theta), z);
}

export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
  scene.background = new THREE.Color(0x07080a);
  camera.position.set(0, 0, 4);
  camera.lookAt(0, 0, 0);

  // Lighting
  const ambient = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambient);
  const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
  dirLight.position.set(3, 5, 3);
  scene.add(dirLight);
  const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
  fillLight.position.set(-3, -2, -3);
  scene.add(fillLight);

  const maxCells = params.maxCells | 0;

  const geom = new THREE.SphereGeometry(1, 8, 6);
  const mat = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL(params.hue / 360, 0.65, 0.55),
    roughness: 0.6,
    metalness: 0.1,
  });

  const mesh = new THREE.InstancedMesh(geom, mat, maxCells);
  mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
  // Start with count=0, we'll grow it
  mesh.count = 0;
  scene.add(mesh);

  // Seed: ~50 cells in a small spherical cluster
  const positions = [];
  const seedCount = Math.min(50, maxCells);
  for (let i = 0; i < seedCount; i++) {
    const dir = randomUnitVec3();
    positions.push(dir.multiplyScalar(Math.random() * 0.3));
  }

  return { mesh, mat, positions, maxCells };
}

export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
  const { mesh, mat, positions } = state;
  const maxCells = params.maxCells | 0;

  // Update material color
  mat.color.setHSL(params.hue / 360, 0.65, 0.55);

  // Each frame: check a random subset of cells for spawning
  const count = positions.length;
  const checkCount = Math.min(50, count);
  const neighborR2 = params.neighborR * params.neighborR;

  if (count < maxCells) {
    const newCells = [];

    for (let k = 0; k < checkCount; k++) {
      const i = Math.floor(Math.random() * count);
      const p = positions[i];

      // Count neighbors within neighborRadius
      let neighborCount = 0;
      for (let j = 0; j < count; j++) {
        if (i === j) continue;
        const q = positions[j];
        const dx = p.x - q.x;
        const dy = p.y - q.y;
        const dz = p.z - q.z;
        if (dx * dx + dy * dy + dz * dz < neighborR2) {
          neighborCount++;
          if (neighborCount >= 3) break; // Early exit
        }
      }

      if (neighborCount >= 3 && Math.random() < params.spawnRate) {
        if (count + newCells.length < maxCells) {
          const dir = randomUnitVec3();
          newCells.push(new THREE.Vector3(
            p.x + dir.x * params.spawnRadius,
            p.y + dir.y * params.spawnRadius,
            p.z + dir.z * params.spawnRadius,
          ));
        }
      }
    }

    for (const c of newCells) positions.push(c);
  }

  // Update InstancedMesh
  const cellR = params.cellRadius;
  const activeCount = Math.min(positions.length, maxCells);
  mesh.count = activeCount;

  const dummy = new THREE.Object3D();
  for (let i = 0; i < activeCount; i++) {
    const pos = positions[i];
    dummy.position.set(pos.x, pos.y, pos.z);
    dummy.scale.setScalar(cellR);
    dummy.updateMatrix();
    mesh.setMatrixAt(i, dummy.matrix);
  }
  mesh.instanceMatrix.needsUpdate = true;
}