// Menger Sponge
//
// Iterated cube-based fractal via SDF domain folding. Raymarched in GLSL.
// Camera orbits the sponge. The "menger fold" iteratively removes the
// cross-shaped tunnels at each scale.
export const PARAMS = {
iterations: { value: 4, min: 1, max: 6, step: 1, label: "Fractal Iterations", folder: "Structure" },
twist: { value: 0.0, min: 0, max: 2, step: 0.01, label: "Domain Twist", folder: "Structure" },
glow: { value: 0.5, min: 0, max: 2, step: 0.01, label: "Glow", folder: "Appearance" },
hue: { value: 200, min: 0, max: 360, step: 1, label: "Hue", folder: "Appearance" },
speed: { value: 0.15, min: 0, max: 1.5, step: 0.01, label: "Orbit Speed", folder: "Behavior" },
};
export function shaderUniforms(params) {
return {
u_iterations: { value: params.iterations | 0 },
u_twist: { value: params.twist },
u_glow: { value: params.glow },
u_hue: { value: params.hue },
u_speed: { value: params.speed },
};
}
export function shaderAnimate(uniforms, params) {
uniforms.u_iterations.value = params.iterations | 0;
uniforms.u_twist.value = params.twist;
uniforms.u_glow.value = params.glow;
uniforms.u_hue.value = params.hue;
uniforms.u_speed.value = params.speed;
}
export function fragmentShader() {
return `
uniform float u_time;
uniform vec2 u_resolution;
uniform int u_iterations;
uniform float u_twist;
uniform float u_glow;
uniform float u_hue;
uniform float u_speed;
// ── Menger SDF ──────────────────────────────────────────────────────────
float sdBox(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdMenger(vec3 p) {
// Apply domain twist
float twistAmt = u_twist;
if (twistAmt > 0.0) {
float angle = p.y * twistAmt;
float cosA = cos(angle), sinA = sin(angle);
p.xz = mat2(cosA, -sinA, sinA, cosA) * p.xz;
}
float d = sdBox(p, vec3(1.0));
float s = 1.0;
for (int m = 0; m < 6; m++) {
if (m >= u_iterations) break;
vec3 a = mod(p * s, 2.0) - 1.0;
s *= 3.0;
vec3 r = abs(1.0 - 3.0 * abs(a));
float da = max(r.x, r.y);
float db = max(r.y, r.z);
float dc = max(r.z, r.x);
float c = (min(da, min(db, dc)) - 1.0) / s;
d = max(d, c);
}
return d;
}
// ── Normal ──────────────────────────────────────────────────────────────
vec3 calcNormal(vec3 p) {
const float h = 0.0002;
const vec2 k = vec2(1.0, -1.0);
return normalize(
k.xyy * sdMenger(p + k.xyy * h) +
k.yyx * sdMenger(p + k.yyx * h) +
k.yxy * sdMenger(p + k.yxy * h) +
k.xxx * sdMenger(p + k.xxx * h)
);
}
// ── Palette ─────────────────────────────────────────────────────────────
vec3 palette(float t, float baseHue) {
float h = baseHue / 360.0 + t * 0.35;
vec3 a = vec3(0.5);
vec3 b = vec3(0.5);
vec3 c = vec3(1.0);
vec3 d = vec3(0.00, 0.10, 0.20) + h;
return a + b * cos(6.2831853 * (c * t + d));
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
// Orbiting camera
float t = u_time * u_speed;
vec3 ro = vec3(3.0 * cos(t), 1.2 + 0.8 * sin(t * 0.5), 3.0 * sin(t));
vec3 ta = vec3(0.0);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.5 * ww);
// March
float dist = 0.0;
float steps = 0.0;
bool hit = false;
for (int i = 0; i < 120; i++) {
vec3 p = ro + rd * dist;
float d = sdMenger(p);
if (d < 0.0005) { hit = true; break; }
if (dist > 12.0) break;
dist += d * 0.9;
steps += 1.0;
}
vec3 col = vec3(0.02, 0.02, 0.04);
if (hit) {
vec3 p = ro + rd * dist;
vec3 n = calcNormal(p);
vec3 ld = normalize(vec3(2.0, 4.0, 1.5));
float diff = max(0.0, dot(n, ld));
float spec = pow(max(0.0, dot(reflect(-ld, n), -rd)), 32.0);
col = palette(dist * 0.15 + steps / 120.0, u_hue);
col *= 0.4 + 0.6 * diff;
col += 0.3 * spec;
}
col += u_glow * vec3(steps / 120.0) * palette(steps / 120.0, u_hue) * 0.4;
col = pow(max(col, vec3(0.0)), vec3(1.0 / 2.2));
gl_FragColor = vec4(col, 1.0);
}
`;
}