Skip to content
Sample of Menger Sponge, 2D Carpet edition — recursive 3x3 / centre-removal

shader_legends_2 · MengerSponge2D

Menger Sponge, 2D Carpet edition — recursive 3x3 / centre-removal

The Menger sponge (1926) is a 3D fractal formed by sub-dividing each cube into 3x3x3 and removing the centre plus the six face-centres. The 2D version (Menger carpet) is the shader community's standard recursive-fractal primitive, used in BigWings's iconic Mandelbox/Sponge shaders. Each level divides each surviving cell into 3x3 and removes only the centre, leaving 8/9 per cell with self-similar boundary structure. This formula renders the per-pixel 'kill level' (which iteration first cut the pixel), giving the carpet a depth-cued hue gradient.

Original: https://www.shadertoy.com/view/Mdf3zX

▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.

Family
shader_legends_2
Source
shader legends II
Evolution
fractal
Render
scalar_field
Complexity
low
Agents
0
Year
2010
Author
Karl Menger (1926); shader adaptation after BigWings

Math essence

Iterate levels times: subdivide p into 3x3 sub-cells; mark (sub_x == 1 && sub_y == 1) as killed at this level. Survivors (kill == levels) are kept; killed pixels are recorded with the level that cut them. The render maps each kill level to a different hue band via level_hue[level] * hue_shift, modulated by an IQ cosine palette. A per-cell hash tints survivors; the sub-square boundary distance is multiplied by edge_softness to mark outlines.

Parameters

Name Type Default Range Description
dim int 360 120–720 Square output size in pixels.
levels int 4 1–6 Number of subdivision iterations. 4 -> 81x81 cells; 5 -> 243x243.
edge_softness float 2 0.5–20 Brightness multiplier for the in-sub-square edge falloff.
hue_shift float 0.18 0–1 Hue spacing between adjacent kill levels.
palette_offset float 0.5 0–1 Base phase offset for the IQ cosine palette.
contrast float 1 0.4–3 Post-render contrast multiplier.
time_speed float 0.04 -0.3–0.3 Drift speed of the palette phase per step.

Reference

First 60 lines of reference.py. Full source: library_external/shader_legends_2/MengerSponge2D/reference.py

from __future__ import annotations

from typing import ClassVar

import numpy as np


def _hash11(x: np.ndarray) -> np.ndarray:
    p = 127.1 * x
    return (np.sin(p) * 43758.5453123 - np.floor(np.sin(p) * 43758.5453123)).astype(np.float32)


def _hash12(x: np.ndarray, y: np.ndarray) -> np.ndarray:
    p = 127.1 * x + 311.7 * y
    return (np.sin(p) * 43758.5453123 - np.floor(np.sin(p) * 43758.5453123)).astype(np.float32)


def _depth_levels(
    xv: np.ndarray,
    yv: np.ndarray,
    levels: int,
) -> np.ndarray:
    """Per-pixel *kill depth* of the 2D Menger sponge (carpet).

    Returns the integer level at which the pixel gets cut.  Pixels
    outside any cut are capped at `levels` (survivors).
    """
    kill = np.full(xv.shape, levels, dtype=np.int32)
    size = 1.0
    cx = 0.0
    cy = 0.0
    s = 1.0
    for lev in range(levels):
        x = xv * s + cx
        y = yv * s + cy
        ix = np.floor(x / size).astype(np.int64)
        iy = np.floor(y / size).astype(np.int64)
        fx = (x / size) - ix.astype(np.float32)
        fy = (y / size) - iy.astype(np.float32)
        sub_x = np.floor(fx * 3.0).astype(np.int64)
        sub_y = np.floor(fy * 3.0).astype(np.int64)
        centre_hit = (sub_x == 1) & (sub_y == 1)
        # Update: pixels that get hit at this level and haven't
        # been assigned an earlier kill level get lev.
        first_hit = centre_hit & (kill == levels)
        kill = np.where(first_hit, lev, kill)
        s *= 0.3333333
        cx = cx * 0.3333333
        cy = cy * 0.3333333
    return kill


def _sponge_mask(
    xv: np.ndarray,
    yv: np.ndarray,
    levels: int,
) -> np.ndarray:
    """Boolean survival mask for the Menger carpet: True => pixel
    is *in* the fractal (survives all levels)."""
    kill = _depth_levels(xv, yv, levels)

Try running it locally

The reference implementation is self-contained Python with numpy. Install the package and run:

python library_external/shader_legends_2/MengerSponge2D/reference.py

Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.

More from shader legends II

Sample of Hex Grid Tiling — axial-coord tessellation with fBM-filled cells

Hex Grid Tiling — axial-coord tessellation with fBM-filled cells

shader_legends_2 · HexGridTiling

A hexagonal grid is the canonical 6-fold-symmetric alternative to the square-grid hash tiling.

View →
Sample of IQ Domain Warping — fbm composed with itself at 2/3 levels

IQ Domain Warping — fbm composed with itself at 2/3 levels

shader_legends_2 · IQDomainWarping

Inigo Quilez's domain warping trick: feed a noise function *itself* as input coordinates, producing organic flowing patterns that no plain noise function can match.

View →
Sample of IQ Mandelbox (2D edition) — iterated box-fold / ball-fold / scale / translate

IQ Mandelbox (2D edition) — iterated box-fold / ball-fold / scale / translate

shader_legends_2 · IQMandelbox2D

The Mandelbox is a 3D fractal defined by the iterate v := scale * box_fold(ball_fold(v)) + c, parametrised by a 'scale' fold factor and the (r_min, r_fixed) ball-fold thresholds.

View →
Sample of IQ Voronoi (cellular noise) — F1 / F2 distance field

IQ Voronoi (cellular noise) — F1 / F2 distance field

shader_legends_2 · IQVoronoi

Inigo Quilez's classical 2D Voronoi / cellular noise: for each integer cell of the plane a single feature point is placed at cell_centre + jitter*(hash-0.5).

View →
Sample of Lorenz Attractor (3D ODE) — strange-attractor trajectory with depth shading

Lorenz Attractor (3D ODE) — strange-attractor trajectory with depth shading

shader_legends_2 · LorenzAttractor3D

The Lorenz system (1963) is the canonical strange-attractor: x' = sigma(y - x); y' = x(rho - z) - y; z' = xy - beta*z.

View →

See all shader_legends_2 artworks →