2023-12-06
The Mandelbrot set is the set of
complex numbers, c
, for which an infinite sequence of numbers
z = [z0, z1, z2, ...]
remains bounded.
Bounded means that there is a limit that the magnitude of the complex number never exceeds. It must be at least 2.
The sequence is given by the recursive formula:
Below is a VERY SLOW visualization of the Mandelbrot set.
Click to zoom in. Shift-click to zoom out.
To generate the image, we iterate over each pixel in the canvas and calculate a color for a corresponding location in the Mandelbrot set.
for ; y < canvas.height; ++y
imageData, 0, 0;
To calculate the color, we iterate up to some maximum iteration count calculating the z(i) value for that location in the set.
The green-ness of a pixel is how close to the maximum we got before be exceeded the bound.
;
Surprisingly, we color locations that never exceed the bound black, so the actual members of the Mandelbrot set are the negative space in the visualization.
If we leave it at that, we get bands of color, because we're coloring based on integer iteration counts. We can normalize the iteration counts to smooth out the coloring with the following formula:
Which looks like this:
What this is doing is using f(i) = 2^i
to approximate z(i)
.
Using powers of 2, log2(x)
is the value of i
that results in x
, so:
log2(bound)
is the approximate value of i
that exactly breaks the bound
,
we'll call it boundI
.log2(z)
is the approximate value of i
that caused us to break the bound
,
zI
.zI/boundI
is the percentage of boundI
that zI
is. In our approximation,
this will be from 1 if z = bound
to 2 if z = 2bound
.log2(percentage)
will be 0 if z = bound
to 1 if z = 2bound
. This is how
much we would need to subtract from i
to equal bound
in the approximation.The key thing to remember here is z(i-1) < bound <= z(i)
, so we need to
subtract a value from i
where 0 <= value < 1
to get z(i-value) = bound
.
This approximation means that when i
is already correct we don't change it,
and when it needs adjusting, for each doubling of the bound
we back off i
a
whole iteration.
At least, the best I can understand what the formula is doing. Fortunately, it's quite easy to code:
I'm pretty sure you could use whatever logarithm you want.
The biggest problem with the visualization above is that it's extremely slow. This is because we loop over each pixel and calculate its color one at a time.
Because the color of each pixel is independent of the other pixels, we can calculate them in parallel, but we can't do that in plain JavaScript. Fortunately, your GPU is great a doing massively parallel calculations, and we can write code for it with WebGL!
Converting what we have to WebGL yields dramatic improvements:
The problem you'll find with this visualization is when you zoom in too far it becomes "pixelated".
This is caused by errors in the floating-point math. WebGL is restricted to single-precision floating-point numbers, so we run into these errors much faster than with JavaScript's double-precision floats.