Devlog #94 – Wave 73 WebGL/GLSL Special: Mandelbulb, Menger Sponge, Wormhole 3D, Supershape & GPGPU Particles

Wave 73 is entirely GPU-powered — every simulation runs as GLSL shader code on the graphics card. Raymarching a 3D fractal, folding space into a sponge, bending spacetime around a wormhole throat, sculpting alien geometry from a superformula, and simulating a quarter-million particles simultaneously. This wave pushed the limits of what a browser tab can render.

Wave 73 — 5 simulations added

All five simulations in Wave 73 require WebGL2

Each simulation uses custom GLSL fragment shaders, raymarching, signed distance fields, or GPGPU ping-pong render targets — techniques that run thousands of parallel calculations per frame on the GPU. A WebGL2-capable browser (Chrome, Firefox, Edge, Safari 15+) is required. The simulations automatically detect WebGL2 support and display a fallback message if unavailable.

638
Total simulations
5
New this wave
73
Wave number
94
Devlog #

GPU Technologies Used in This Wave

Sphere-tracing / Raymarching
March a ray through space, stepping by the SDF distance until hit or max steps reached
Signed Distance Fields
Scalar function d(p) = signed distance from point p to surface; negative inside, positive outside
Distance Estimators
For fractals: approximate lower bound on distance to the set, enabling safe step sizes
GPGPU Ping-Pong
Two pairs of WebGLRenderTargets alternated each frame: read from A, write to B, swap
Three.js BufferGeometry
Dynamic mesh with per-frame position update in JavaScript; custom vertex+fragment shaders
Finite-Diff Normals
Gradient of SDF estimated via 6 nearby samples; cheap alternative to analytical normals

New Simulations

🌌

Mandelbulb GLSL Raymarching

GLSL fragment shader raymarching 3D fractal. Triplex power n, distance estimator de=0.5·log(r)·r/dr. 96 steps + finite-diff normals + soft shadows + AO. Power slider 2–12. 4 palettes.

🧊

Menger Sponge GLSL SDF

Iterated SDF folding: fold to unit cube, subtract cross cavity, scale ×3, repeat. 3 modes: Solid/Wireframe/X-ray. Hausdorff dimension log20/log3 ≈ 2.7268. Surface area → 0 as iterations → ∞.

🌀

Wormhole 3D GLSL Shader

Morris-Thorne traversable wormhole. Flamm's paraboloid r(ℓ)=√(b₀²+ℓ²). GLSL sky blending from two universes. 3 modes: embedding/FPV/cross-section. Throat pulse animation.

🌸

Supershape Three.js Shader

Gielis superformula r(φ)=(|cos(mφ/4)/a|^n2+|sin(mφ/4)/b|^n3)^(−1/n1) as spherical product. Live 300×300 BufferGeometry. 8 presets. Matte/metallic, wireframe, Save PNG.

GPGPU Particles WebGL2 GPGPU

Ping-pong GPGPU: 2×2 WebGLRenderTargets (RGBA FloatType) for positions+velocities. Compute fragment shader. 4 behaviors: curl noise/N-body/boids/Lorenz. Up to 250,000 particles.

Mandelbulb: Raymarching a 3D Fractal

The Mandelbulb is the most successful 3D analogue of the Mandelbrot set. It uses "triplex" algebra — a way of multiplying 3D vectors using spherical coordinate exponentiation — to generalise the 2D iteration z → z^n + c into three dimensions.

Triplex Power and Distance Estimator

The iteration at power n maps a point (r, θ, φ) in spherical coordinates to:

r_new    = r^n
theta_new = n * theta
phi_new   = n * phi
x_new    = r_new * sin(theta_new) * cos(phi_new) + cx
y_new    = r_new * cos(theta_new) + cy
z_new    = r_new * sin(theta_new) * sin(phi_new) + cz

The distance estimator uses the derivative magnitude dr, accumulated alongside r during iteration: dr = n·r^(n-1)·dr + 1. The lower bound on distance to the set is de = 0.5 · |log(r)| · r / dr. This safe step size lets the raymarcher advance quickly through empty space and slow down near the surface — sphere tracing rather than fixed-step marching.

Shading: Normals, Shadows, and Ambient Occlusion

Normals are estimated by finite differences: evaluate the SDE at six nearby points (±ε on each axis) and take the gradient. Soft shadows are approximated by marching a secondary ray toward the light and tracking the minimum ratio of step size to distance along the way. Ambient occlusion samples five distances along the surface normal and computes how much the surface curves back on itself — concave crevices are darker.

Menger Sponge: Iterated SDF Folding

The Menger sponge is defined by recursive subtraction: take a cube, divide it into 3×3×3 = 27 sub-cubes, remove the 7 central cross pieces (face centres and the body centre), and repeat on each remaining sub-cube. After infinite iterations, the result has Hausdorff dimension log(20)/log(3) ≈ 2.7268 — less than 3D but more than 2D.

GLSL SDF via Iterated Folding

The signed distance function for the sponge is computed entirely inside the fragment shader using an elegant iterative fold technique:

for (int i = 0; i < iterations; i++) {
    p = abs(p);                    // fold to positive octant
    if (p.x < p.y) p.xy = p.yx;  // sort axes
    if (p.x < p.z) p.xz = p.zx;
    if (p.y < p.z) p.yz = p.zy;
    p = p * 3.0 - 2.0;            // scale and offset
    if (p.z < -1.0) p.z += 2.0;  // fold back
}
float d = (length(max(abs(p) - 1.0, 0.0))) / pow(3.0, float(iterations));

Each outer loop doubles the effective iteration depth. The resulting SDF can be sphere-traced at interactive frame rates for up to 6 iterations of the sponge.

Three Rendering Modes and Mathematical Properties

The mathematical properties panel displays live values: total surface area (6·(5/9)^n·20^n → 0 as n → ∞ — the surface shrinks at each step), and the Hausdorff dimension formula log(N_retained)/log(scale_factor) = log(20)/log(3).

Wormhole 3D: Spacetime Embedding Shader

A Morris-Thorne traversable wormhole is a hypothetical solution to Einstein's field equations that connects two distant regions of spacetime via a "throat." It cannot be created without exotic matter (negative energy density), but it makes a spectacular visualisation and a genuine lesson in differential geometry.

Flamm's Paraboloid Embedding

The spatial geometry of a wormhole at a moment in time is described by the Flamm paraboloid. In the equatorial plane, the proper radial distance ℓ maps to Euclidean embedding radius r(ℓ) = √(b₀² + ℓ²) and height z(ℓ) = b₀ · arcsinh(ℓ/b₀), where b₀ is the throat radius. This gives two flared funnels connected at r = b₀.

GLSL Shader: Two-Universe Sky and Throat

The fragment shader determines which side of the throat a ray ends up on by integrating the geodesic path through the embedding. The "sky" of each universe is a discrete set of coloured stars — the two universes have different colour temperatures. As b₀ decreases (tighter throat), more of each universe's sky is visible through the aperture. The Doppler-like blueshift gradient near the throat gives a visual sense of the gravitational depth. Three modes:

Supershape: Gielis Superformula Geometry

Johan Gielis' 2003 superformula is a single parametric equation that generates an extraordinary variety of shapes — from platonic solids to flowers to alien geometries — by varying six parameters. Applied as a spherical product (once for latitude θ, once for longitude φ), it creates 3D surfaces with remarkable richness.

The Superformula and Spherical Product

The polar radius in 2D is:

r(φ) = (|cos(m·φ/4) / a|^n2 + |sin(m·φ/4) / b|^n3)^(-1/n1)

For the 3D spherical product, evaluate r₁(θ) and r₂(φ) independently. The 3D surface point is:

x = r1(θ) · cos(θ) · r2(φ) · cos(φ)
y = r1(θ) · sin(θ) · r2(φ) · cos(φ)
z = r2(φ) · sin(φ)

The mesh is a 300×300 grid of UV points, each evaluated independently and uploaded to Three.js BufferGeometry. Parameter changes rebuild the geometry in a single JavaScript pass at ~20ms.

Eight Presets and Material System

GPGPU Particles: 250,000 Particles on the GPU

General-Purpose GPU computing (GPGPU) repurposes the graphics pipeline as a massively parallel computing engine. Each "pixel" of a floating-point render texture stores the state of one particle. The fragment shader that "draws" this texture actually computes the new particle state — and by reading from one texture while writing to another (ping-pong), we can simulate hundreds of thousands of particles at interactive frame rates entirely on the GPU.

Ping-Pong Architecture

The simulation maintains two pairs of RGBA FloatType WebGLRenderTargets: one for positions (x, y, z, age) and one for velocities (vx, vy, vz, mass). Each frame:

  1. Bind "compute" shader with current position/velocity textures as uniforms
  2. Render a full-screen quad into the next position/velocity textures
  3. Swap current and next textures (ping ↔ pong)
  4. Render particles as GL_POINTS, sampling position from the current texture in the vertex shader

No data ever leaves the GPU between frames — the CPU only sets initial conditions and tweaks uniforms.

Four Particle Behaviours

Engineering Notes: WebGL2 and Floating Point Textures

Getting 250,000 particles working correctly required solving several WebGL2-specific challenges:

Float Texture Precision

WebGL2 supports OES_texture_float (32-bit RGBA float textures) as a core feature, removing the extension dance required in WebGL1. However, linear filtering of float textures still requires OES_texture_float_linear — the GPGPU particle sim uses nearest-neighbour sampling since each particle maps to exactly one texel.

MRT and Simultaneous Position/Velocity Update

Updating position and velocity simultaneously without using the previous frame's velocity would require reading and writing the same texture — impossible in WebGL. The solution is separate position and velocity textures, each in their own ping-pong pair. The compute shader reads both from the "current" set and writes both to the "next" set, keeping the full state consistent.

Particle Indexing

Each particle's UV coordinate in the texture is computed from its index: uv = vec2(float(index % WIDTH), float(index / WIDTH)) / float(WIDTH). The vertex shader reads this UV from the position texture and uses the result as gl_Position. With 250,000 particles in a 500×500 texture, every texel is a particle.

Up Next

That's Wave 73 — all 5 simulations, all GPU-native. With 638 simulations now live across all categories, we're pausing to consolidate: fixing mobile layouts, adding missing keyboard shortcuts, and expanding the search index before starting Wave 74. Check the blog archive for all previous devlogs.

← Devlog #93 All posts →