DEV Community

Cover image for Fractals and Non-Euclidean Geometry in the Browser
Ajay D
Ajay D

Posted on

Fractals and Non-Euclidean Geometry in the Browser

I have been watching Corridor Digital's videos for a while now. Their breakdowns of how impossible spaces work in games and film got me thinking about whether I could build something similar that runs entirely in a browser tab. No game engine, no desktop app. Just a Next.js site and a GPU.

So I built two things for my portfolio: an infinite-zoom Mandelbrot explorer and a non-Euclidean corridor that loops forever. Both use GLSL fragment shaders and React Three Fiber. This post is about the GPU techniques that make them possible.

Why the GPU

The Mandelbrot formula is dead simple. For each pixel, run z = z^2 + c until it escapes. The problem is scale. A 1080p canvas has 2 million pixels. Each one needs hundreds of iterations. On the CPU that is a single-threaded loop and you get maybe 3 FPS. Completely unusable.

GPUs run the same small program on every pixel at the same time. Write the iteration loop in GLSL instead of JavaScript and suddenly all 2 million pixels compute in parallel, every frame, at 60fps.

Smooth Coloring

Most Mandelbrot renderers use integer iteration counts for color, which gives you hard bands. One line fixes that:

float iter = float(i) - log2(log2(dot(z,z)) * 0.5);
Enter fullscreen mode Exit fullscreen mode

This gives a fractional iteration count. Mix it with a cosine palette function and you get the smooth gradients instead of staircase banding.

Mandelbrot

Going Past the Precision Wall

GPU floats are 32-bit. Around zoom 10^4 the coordinates lose precision and the fractal pixelates. This is where most WebGL implementations stop.

I used Perturbation Theory to get around this. The idea: use the CPU to calculate one high-precision reference orbit at screen center (using decimal.js for arbitrary precision), upload it to the GPU as a Data Texture, then have the shader calculate only the tiny difference between each pixel and that reference. Since the difference is small, 32-bit floats handle it fine.

vec2 Z = texelFetch(uOrbitTex, ivec2(tx, ty), 0).rg;
vec2 z = Z + D;
D = 2.0 * complexMul(Z, D) + complexSq(D) + deltaPT;
Enter fullscreen mode Exit fullscreen mode

The CPU does the heavy precision work once per view change. The GPU handles per-pixel rendering at 60fps. You can zoom past 10^14 and keep going.

non euclidean corridor

Non-Euclidean Corridor

The corridor is an infinite hallway where you walk forward, turn right, and keep going forever. Two techniques make it work.

Treadmill looping. Only a handful of corridor chunks exist. As one moves behind the camera, it teleports to the front. The camera barely moves. Memory stays constant.

Clipping planes. The right wall needs to vanish at the corner to reveal the turn. I defined a THREE.Plane at the corner threshold. Three.js applies it at the WebGL level and the GPU discards any fragment on the wrong side. No geometry swaps. The wall just has a mathematically perfect hole in it, exactly where the turn should be.

Stack

React Three Fiber wires everything together. The important architectural choice was keeping all render-loop state in refs instead of React state. React re-renders are way too slow for 60fps GPU work. The component tree handles the UI. Refs handle the render loop.

Both experiments are live:

Top comments (0)