// 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;
}