Created: 6 Oct 2016, last rebuilt: 25 May 2017

Menger sponge

I was inspired by FractalLab to try and make my own Menger Sponge animation.

My first attempt was in Three.js, by assembling the three-iteration sponge from smaller cubes. Pretty simple, but it got very slow very fast (at n=3 it’s basically unusable). See here for the result.

It became obvious that this should be done with a shader. Many great tutorials exist to kick it off. I went through multiple iterations, starting with much worse performance than three.js:

  • assembling the cube from “atoms”: slow (~1 fps for 3 iterations)
  • knocking out empty spaces in all dimensions: slow (~1 fps for 3 iterations)
  • using a repeating pattern (mod) to knock out empty spaces

The last one shows promise, but I’m sure it can be optimized much further. A project for another rainy day :)

Below’s the basic GLSL code for the last one

float basic_box(vec3 pos, vec3 b){
    vec3 d = abs(pos) - b;
    return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0));
}

float map_menger5(vec3 p){

    float main_width_b = 4.0;
    float inf = 50.0;

    float hole_x, hole_y, hole_z;
    float hole_width_b = main_width_b / 3.0;
    
    float menger = basic_box(p, vec3(main_width_b));
    
    for (int iter=0; iter<5; iter++){

        float hole_distance = hole_width_b * 6.0;
 
        vec3 c = vec3(hole_distance);
        vec3 q = mod(p + vec3(hole_width_b), c) - vec3(hole_width_b);

        hole_x = basic_box(q, vec3(inf, hole_width_b, hole_width_b));
        hole_y = basic_box(q, vec3(hole_width_b, inf, hole_width_b));
        hole_z = basic_box(q, vec3(hole_width_b, hole_width_b, inf));

        hole_width_b = hole_width_b / 3.0;        // reduce hole size for next iter
        menger = max(max(max(menger, -hole_x), -hole_y), -hole_z); // subtract

    }
    return menger;
}

float trace(vec3 origin, vec3 ray){
    
    float t = 0.0;
    for (int i=0; i<32; ++i){
        vec3 p = origin + ray * t;
        float d = map_menger5(p);
        t += d;
    }
    return t;
}

mat2 rotate(float theta){
    return mat2(cos(theta), -sin(theta), sin(theta), cos(theta));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {

    vec2 uv = fragCoord.xy / iResolution.xy;  // normalize coords
    uv = uv * 2.0 - 1.0;                      // convert coords from 0,1 to -1,1
    uv.x *= iResolution.x/iResolution.y;      // fix aspect
    
    vec3 ray = normalize(vec3(uv.x, uv.y, 1.0));
    vec3 origin = vec3(0.0,0.0,-4.0 -5.2*sin(iGlobalTime * 0.20));

    float theta;
    theta = iGlobalTime / 5.0;
    ray.yz *= rotate(theta);
    ray.xy *= rotate(theta);
    origin.yz *= rotate(theta);
    origin.xy *= rotate(theta);
    
    float t = trace(origin, ray);    
    float fog = 1.0 / (1.0 + t * t * 0.05);
    vec3 fc = vec3(fog) * vec3(0.8+0.2*sin(iGlobalTime * 0.1), 0.8+0.2*sin(iGlobalTime * 1.0),0.9+0.1*cos(iGlobalTime*1.0));
    fragColor = vec4(fc,1.0);
}