Four chrome spheres orbit and overlap to simulate merging liquid metaballs.
three @react-three/fiber @react-three/drei<LiquidMetaballs /> inside your own <Canvas>.import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { MeshDistortMaterial } from '@react-three/drei';
import type { Group } from 'three';
// Safe fallback: 4 orbiting distorted spheres that overlap to simulate metaball merging.
// MarchingCubes from drei crashes in the test-renderer environment.
const ORBIT_CONFIG = [
{ radius: 0.7, speed: 1.0, phase: 0, y: 0.2, size: 0.55 },
{ radius: 0.55, speed: -1.3, phase: Math.PI / 2, y: -0.15, size: 0.5 },
{ radius: 0.8, speed: 0.8, phase: Math.PI, y: 0.05, size: 0.45 },
{ radius: 0.45, speed: 1.6, phase: (3 * Math.PI) / 2, y: 0.3, size: 0.42 },
];
export default function LiquidMetaballs({ color = '#22e0ff', scale = 1 }: { color?: string; scale?: number }) {
const groupRef = useRef<Group>(null);
useFrame((state) => {
const group = groupRef.current;
if (!group) return;
const t = state.clock.elapsedTime;
ORBIT_CONFIG.forEach((cfg, i) => {
const child = group.children[i];
if (!child) return;
const angle = t * cfg.speed + cfg.phase;
child.position.x = Math.cos(angle) * cfg.radius;
child.position.z = Math.sin(angle) * cfg.radius;
child.position.y = cfg.y + Math.sin(t * 0.7 + cfg.phase) * 0.15;
});
});
return (
<group ref={groupRef} scale={scale}>
{ORBIT_CONFIG.map((cfg, i) => (
<mesh key={i}>
<sphereGeometry args={[cfg.size, 32, 32]} />
<MeshDistortMaterial
color={color}
metalness={1}
roughness={0.05}
distort={0.3}
speed={2}
emissive={color}
emissiveIntensity={0.2}
/>
</mesh>
))}
</group>
);
}