February 25, 2025

Fractals

I thought I would try and put together a little fractal visualization. Specifically, the Mandelbrot set, since it is so well-known and simple to implement. And there's a whole wikipedia page devoted to Plotting Algorithms for the Mandelbrot Set. I just wanted to do something quick and simple to learn a bit about how these visualizations work.

The Mandelbrot set is defined as the set of points in the complex plane where the series z_n = z_(n-1)^2 + c remains localized and does not diverge. Since we're squaring, if z^2 > 1, then we're diverging. But how in general do we decide if a point is going to diverge? Well, one of the reasons I love numerical methods is their simplicity. We just grind through a whole bunch of iterations and see if we ever cross that frontier. If we're looking at a complex number z = x + iy, we can do something like:

def escape_steps(real, imag):
    steps = 0
    max_steps = 1000
    x = 0
    y = 0
    
    while steps < max_steps and (x*x + y*y <= 4):
        x, y = (x*x - y*y + real), (2*x*y + imag)
        steps += 1
    return steps

This simple function tells us how many iterations before the point 'escapes'. We can then use this to scale some sort of color gradient. To make a nice visualization, we just need to do this for every pixel. I started to build such a visualization with Raylib, but I realized that applying the same transformation for each pixel is exactly what shaders are designed to do. I don't know anything about shaders, but this seemed like a great opportunity to learn something. So I went to Shadertoy and tried to implement my naive little visualization.

int escape(float x0, float y0) {
    int steps = 0;
    
    float x = 0.0f;
    float y = 0.0f;
    
    while (x*x + y*y <= 4.0f && steps < 1000) {
         float temp = x*x - y*y + x0;
         y = 2.0f*x*y + y0;
         x = temp;
         steps += 1;
    }
    return steps;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float scaling =  2.0f;
    float x_offset = 0.5f;
    float y_offset = 0.5f;
    
    vec2 uv = fragCoord/iResolution.xy;
    float x0 = (uv.x -x_offset) * scaling;
    float y0 = (uv.y -y_offset) * scaling;

    int steps = escape(x0, y0);
    float scale = float(steps) / 256.0;

    vec3 col = vec3(scale, scale, scale);

    // Output to screen
    fragColor = vec4(col,1.0);
}

And behold, this produces a very basic image: image

After a little more fiddling with the scaling and offsets and learning how shadertoy gives you time and frame data, we can animate it a bit. As we zoom in, we are actually seeing fractal patterns repeating at different length scales. Neat!

image