Modular Fractal Viewer
Wed 29 December 2021How to Make a Fractal
Here's a quick tutorial on how to make the Mandelbrot
set. Let's take a complex number and call it c
. Then we will add it to
a complex number z
that is initially zero. Then, we will square our z
,
and add c
again and again. For example, if we start with c = 1 + 0i
, our
z
will go through the values 0, 1, 2, 5, 26, ...
. As you can see, this number
gets out of control very fast. What if we pick a number like c = 0 + 1i
? Then
our z
looks like: 0, i, -1 + i, -i, 1 - i, -i, ...
. This value gets stuck
in a cycle, and doesn't get very large. The numbers that grow very fast are colored
based on how fast the sum of the magnitude of their real and imaginary components
reach the value 16. For example, our c = 1 + 0i
became greater than 16 in five
iterations, so we will color it along with all the other points that reached 16 in
five iterations an arbitrary color. The points that iterate forever and never reach
16, we color black, as they are part of the mandelbrot set. Let a computer do this
for all the pixels on screen, and you get an image that looks like this:
Read until the end and you'll get to see some cool animations where I modify our definition of the Mandelbrot to make some cool visuals.
But how would we put this idea into code? For some reason, I thought that doing operations on complex numbers was going to be difficult because computers don't normally deal with them, but then after I started playing around, I quickly learned that complex numbers are really just two real numbers, but one of the numbers has some funky properties, and the complex number as a whole can have some funky things done to it.
I first started coding this fractal viewer in Java, as a product of my boredom in my Computer Science class. You can read about it in this article. I later ported it to C, because I am much more comfortable with optimizing it there. So let's start the journey from when I ported it to C.
Defining Imaginary Numbers
The start of my journey began with a simple struct:
/* A complex number. */
typedef struct
{
double aReal;
double aImag;
}complex_t;
With this, I could start writing some functions to do operations on complex numbers, such as squaring and adding.
/* Squares a complex number. */
void sqr_complex( complex_t *spVal )
{
/* Binomial expansion. */
double real = ( spVal->aReal * spVal->aReal ) - ( spVal->aImag * spVal ->aImag );
spVal->aImag = 2 * spVal->aReal * spVal->aImag;
spVal->aReal = real;
}
/* Adds two complex numbers. */
void add_complex( complex_t *spVal, complex_t sAdd )
{
spVal->aReal += sAdd.aReal;
spVal->aImag += sAdd.aImag;
}
A Quick Image Format
I quickly wrote an image struct that is basically just a pointer to the pixel data, and dimensions. I also wrote some functions to create the images and free them.
The Render/Calculations
Doing the calculations and rendering it to the screen was
pretty straight forward. I just made a function named calc_mandel
that takes in
some transformation arguments and an image. You can supply a real number offset, and
an imaginary number offset, as well as a zoom level. This way, I can zoom into my
Mandelbrot to see all its complex (haha) beauty. I do some math to assign each pixel
a complex c
value. I divide the width and height by two, add the offset, add the
index of pixel, and divide the whole thing by the zoom. Then, for each pixel, I made
a loop that performs the squaring and adding, for an arbitrary amount of iterations.
Finally, based on the number of iterations required to reach the magnitude of 16,
I color the pixel.
I can't actually remember how exactly I did it at first
because since then, I multithreaded it to squeeze more performance out of the program.
I have twelve threads on my CPU, so I will use all of them. I did some math to
split the coordinates into twelve horizontal pieces, and then ran a function
that did the calculations for a select region of the zoom in a pthread
to render
the fractal twelve times faster. It almost worked. For some reason the very first thread
has trouble getting the correct region, and often renders a region that is assigned to
another thread. I have no idea why this happens, but when it's apparent when I am zooming
into the fractal, and there is a band on the top of the render that appears frozen, and
only updates occasionally.
/* Iterates a complex number. */
pixel_t iterate_mandel( complex_t c )
{
u32 iterations = 0;
complex_t z = { 0.f, 0.f };
for ( u32 i = 0; i < gMaxIterations; ++i )
{
pow_complex( &z, gBrotDimension );
if ( mag_complex( z ) > 16 )
break;
++iterations;
add_complex( &z, c );
}
/* Is part of the set. */
if ( iterations == gMaxIterations )
return 0x000000;
/* Give it a cool grey color if there is just nothing to show. */
else if ( iterations == 1 )
return 0x111111;
else
return gpColors[ iterations % gColorCount ];
}
/* Threaded calculate function. */
void *threaded_calc_mandel( void *spArgs )
{
/* I don't know why this is named test_t, lol. */
test_t args = *( test_t* )spArgs;
for ( int y = args.y0; y < args.yf; ++y )
{
for ( int x = 0; x < args.apImage->aWidth; ++x )
{
complex_t c = {
( -args.apImage->aWidth / 2.f + x + args.aRealOff * args.aZoom ) / args.aZoom,
( -args.apImage->aHeight / 2.f + y + args.aImagOff * args.aZoom ) / args.aZoom
};
args.apImage->apData[ y * args.apImage->aWidth + x ] = iterate_mandel( c );
}
}
}
You may be wondering what the pow_complex
function does.
After playing with the simple squaring function, I wanted to improve it to be
able to operate on any real exponent, so I can see what a Mandelbrot with the equation
z = z^2.1 + c
looks like. Currently, it doesn't work for odd exponents, and I wish I knew
why, but it works well for anything else. Later, I will add more operations on the complex
numbers such as trigonometric ones, so I can do some other weird fractals like
z = sin(z^2) + c
Here's a Reward for Reading This Far
Normally, when you look up visualizations of the Mandelbrot set,
you see zooms into the set. However with my program, you can make some other animations,
such as changing the exponent on the z
in the Mandelbrot equation, or changing
the threshold for a point to be in the mandelbrot set. Here's a video showing some of these
animations: