// 3D Supershape — Gielis spherical product of two superformulas
//
// r1(θ) = superformula(θ, m1, n1a, n2a, n3a) — latitude ring
// r2(φ) = superformula(φ, m2, n1b, n2b, n3b) — longitude sweep
//
// x = r1(θ) * cos(θ) * r2(φ) * cos(φ)
// y = r1(θ) * sin(θ) * r2(φ) * cos(φ)
// z = r2(φ) * sin(φ)
//
// θ ∈ [-π, π], φ ∈ [-π/2, π/2]
//
// Rebuilt in-place each frame when shape params change; full rebuild when
// `segments` changes (rebuildOnChange).
import * as THREE from 'three';
export const WARMUP = { framesBeforeReady: 60 };
export const PARAMS = {
m1: { value: 8.0, min: 0, max: 16, step: 0.1, label: "θ Symmetry (m1)", folder: "Shape (θ)" },
n1a: { value: 0.5, min: 0.05, max: 5, step: 0.01, label: "θ n1", folder: "Shape (θ)" },
n2a: { value: 0.5, min: 0.05, max: 5, step: 0.01, label: "θ n2", folder: "Shape (θ)" },
n3a: { value: 8.0, min: 0.05, max: 20, step: 0.05, label: "θ n3", folder: "Shape (θ)" },
m2: { value: 6.0, min: 0, max: 16, step: 0.1, label: "φ Symmetry (m2)", folder: "Shape (φ)" },
n1b: { value: 1.0, min: 0.05, max: 5, step: 0.01, label: "φ n1", folder: "Shape (φ)" },
n2b: { value: 0.5, min: 0.05, max: 5, step: 0.01, label: "φ n2", folder: "Shape (φ)" },
n3b: { value: 1.0, min: 0.05, max: 20, step: 0.05, label: "φ n3", folder: "Shape (φ)" },
segments: { value: 80, min: 20, max: 200, step: 5, label: "Segments", folder: "Performance", rebuildOnChange: true },
hue: { value: 30, min: 0, max: 360, step: 1, label: "Hue", folder: "Appearance" },
};
export const SHARE = {
bookmarked: ["m1", "n1a", "n2a", "n3a", "m2", "n1b", "n2b", "n3b", "hue"],
};
// --- Superformula core ---
function superformula(angle, m, n1, n2, n3) {
const t1 = Math.abs(Math.cos(m * angle / 4));
const t2 = Math.abs(Math.sin(m * angle / 4));
const base = Math.pow(t1, n2) + Math.pow(t2, n3);
if (base === 0) return 1;
return Math.pow(base, -1 / n1);
}
// Fill a Float32Array with supershape vertex positions for the given params.
// Array must have length (segments+1)*(segments+1)*3.
function fillPositions(arr, segments, m1, n1a, n2a, n3a, m2, n1b, n2b, n3b) {
let idx = 0;
for (let i = 0; i <= segments; i++) {
const phi = (i / segments) * Math.PI - Math.PI / 2; // -π/2 → π/2
const r2 = superformula(phi, m2, n1b, n2b, n3b);
const r2cosPhi = r2 * Math.cos(phi);
const r2sinPhi = r2 * Math.sin(phi);
for (let j = 0; j <= segments; j++) {
const theta = (j / segments) * Math.PI * 2 - Math.PI; // -π → π
const r1 = superformula(theta, m1, n1a, n2a, n3a);
arr[idx++] = r1 * Math.cos(theta) * r2cosPhi;
arr[idx++] = r1 * Math.sin(theta) * r2cosPhi;
arr[idx++] = r2sinPhi;
}
}
}
// Build a triangulated index array for a (segments+1)*(segments+1) grid.
function buildIndices(segments) {
const indices = [];
for (let i = 0; i < segments; i++) {
for (let j = 0; j < segments; j++) {
const a = i * (segments + 1) + j;
const b = a + 1;
const c = a + (segments + 1);
const d = c + 1;
indices.push(a, c, b);
indices.push(b, c, d);
}
}
return new Uint32Array(indices);
}
export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
scene.background = new THREE.Color(0x07080a);
camera.position.set(0, 0, 3.5);
camera.lookAt(0, 0, 0);
// Lighting
const ambient = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.4);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
const seg = params.segments | 0;
const vertexCount = (seg + 1) * (seg + 1);
const positions = new Float32Array(vertexCount * 3);
fillPositions(
positions, seg,
params.m1, params.n1a, params.n2a, params.n3a,
params.m2, params.n1b, params.n2b, params.n3b,
);
const geo = new THREE.BufferGeometry();
// NOTE: Float32BufferAttribute COPIES its input array. Hold the attribute's
// own .array reference in state so per-frame writes target the buffer the
// GPU actually reads.
const posAttr = new THREE.Float32BufferAttribute(positions, 3);
geo.setAttribute('position', posAttr);
geo.setIndex(new THREE.BufferAttribute(buildIndices(seg), 1));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(params.hue / 360, 0.7, 0.55),
roughness: 0.45,
metalness: 0.15,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
// Use the attribute's actual buffer (Float32BufferAttribute may have copied).
return { mesh, geo, mat, positions: posAttr.array, seg };
}
export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
const { mat } = state;
// Live hue update
mat.color.setHSL(params.hue / 360, 0.7, 0.55);
// Always rebuild positions from current live params. With segments=80 default
// this is ~6500 vertex calculations per frame — cheap and guarantees the mesh
// reflects the current slider values. (segments itself is rebuildOnChange, so
// state.seg matches the live value.)
fillPositions(
state.positions, state.seg,
params.m1, params.n1a, params.n2a, params.n3a,
params.m2, params.n1b, params.n2b, params.n3b,
);
state.geo.attributes.position.needsUpdate = true;
state.geo.computeVertexNormals();
}