// Metaballs — Implicit blobs via MarchingCubes
//
// Each ball contributes a falloff field: f(p) = Σ strength / |p - bᵢ|²
// The isosurface where f = threshold yields organic blobs that fuse when
// balls approach and separate when they move apart.
//
// Three.js's MarchingCubes is designed exactly for this; uses the built-in
// addBall() API which was designed for metaballs.
import { MarchingCubes } from 'three/addons/objects/MarchingCubes.js';
import * as THREE from 'three';
export const WARMUP = { framesBeforeReady: 60 };
export const PARAMS = {
resolution: { value: 50, min: 20, max: 80, step: 2, label: "Grid Resolution", folder: "Structure", rebuildOnChange: true },
ballCount: { value: 6, min: 2, max: 12, step: 1, label: "Ball Count", folder: "Structure" },
ballStrength: { value: 0.5, min: 0.1, max: 2, step: 0.01, label: "Ball Strength", folder: "Behavior" },
ballSubtract: { value: 12, min: 1, max: 30, step: 0.5, label: "Subtract", folder: "Behavior" },
speed: { value: 0.5, min: 0, max: 2, step: 0.01, label: "Speed", folder: "Behavior" },
hue: { value: 200, min: 0, max: 360, step: 1, label: "Hue", folder: "Appearance" },
};
export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
scene.background = new THREE.Color(0x07080a);
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
const dirA = new THREE.DirectionalLight(0xffffff, 1.5);
dirA.position.set(5, 5, 5);
scene.add(dirA);
const dirB = new THREE.DirectionalLight(0x4466ff, 0.5);
dirB.position.set(-5, -3, -5);
scene.add(dirB);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(params.hue / 360, 0.7, 0.55),
roughness: 0.3,
metalness: 0.2,
});
const resolution = params.resolution | 0;
const mc = new MarchingCubes(resolution, mat, true, true, 100000);
mc.position.set(0, 0, 0);
mc.scale.set(2.5, 2.5, 2.5);
scene.add(mc);
return { mc, mat };
}
export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
const { mc, mat } = state;
mat.color.setHSL(params.hue / 360, 0.7, 0.55);
const t = time * params.speed;
const count = params.ballCount | 0;
mc.reset();
// Distribute balls in Lissajous-like orbits for varied, non-repeating motion
for (let i = 0; i < count; i++) {
const phase = (i / count) * Math.PI * 2;
const freq1 = 1.0 + i * 0.23;
const freq2 = 1.3 + i * 0.17;
const freq3 = 0.7 + i * 0.31;
// MarchingCubes expects coords in [0,1]
const x = 0.5 + Math.sin(t * freq1 + phase) * 0.25;
const y = 0.5 + Math.cos(t * freq2 + phase * 1.3) * 0.25;
const z = 0.5 + Math.sin(t * freq3 + phase * 0.7) * 0.25;
mc.addBall(x, y, z, params.ballStrength, params.ballSubtract);
}
mc.update();
}