Fractals!

To Home

Have you ever seen an image like this before?

This is a fractal and I'm gonna tell you how to make some, but first what is a fractal? A fractal contains itself inside itself, or if your looking for the fancy math term, is self symetrical. A fractal has an equation which you use to check if a number is in that fractals set. When we create an image of a fractal we are mapping each pixel to a number and checking if its in the set. The fractal itself is the black part of the image and the gradient is how many iterations it takes for it to be found as not part of the set. The most famous fractal by far is the mandlebrot set. We will be using a variation of it known as the Julia set, as it is a series of fractals rather than just one. The Julia set uses the equation Z(n+1) = Zn^2 + C. Lets break it down.

The Math

First up, we aren't using normal/Real numbers, were using Complex/Imaginary numbers. This means instead of 1 value there are 2. In writing this looks like: 4 + 9i where 4 is the real number and 9 is the complex number. This allows us to map the pixel position so that the x is the real and the y is the complex. But when is a number part of the set? A number is in the set when the length of Z is larger than 2, we'll also have a max iteration count as to not be trapped in aloop. Now theres one thing left in the equation, what is C? C is a constant offset and determines which fractal it is from the series. It could be anything! e.g -0.5 + 0.5i

Now that we understand the equation lets break it down further. How do we square a Complex Number? You may think it would be as easy as r*r + (c*c)i but the value i is a number. Thats right i is not just a signifier for the complex number. Specificaly it equals the square root of -1. This means i^2 = -1. So it follows that:

ci^2 =
ci*ci =
c^2*i^2 =
c^2*(-1) =
-(c^2)

It equals a Real Number! This means the numbers cross polute and we cant handle them seperately when multiplying. If we handle them together we get:

(r + ci)(r + ci) =
r(r + ci) + ci(r + ci) =
r^2 + rci + rci + ci^2 =
r^2 - c^2 + 2rci

If multiplication is so complicated, is getting the length gonna be complicated too? You'd think that pythagros's equation would work here, |Z| = sqrt(r^2 + c^2), and you'd be right!

Now how do we get the colours? We know each pixels n values and the iteration count. I mapped the colours to a gradient using a texture (so i can change it at runtime) by sampling at x where x = n/iterations (so its from 0-1) and then I multiply it by a repeat value so we dont have to have a big texture to see detail

The Optimisation

One thing to note, the length calculation |Z| = sqrt(r^2 + c^2) will be calculated over 100 times per pixel per frame, it there's one place for preformance it's here. Instead of sqrting we should square the constant exit value, which is 2, giving us if (r^2 + c^2 > 4) { return n }. But we can push this further. The manhattan distance is equal to |Z| = |r + c|. We've removed 2 multiplications and as a side effect get some really apealing curves all across the fractal that look waaay better than the "correct" equations banding. But we can push this further, instead of using the manhattan distance we can use the distance of one component, |Z| = |r|, this is only 1 abs calculation. But. We. Can. Push. It. Further. |Z| = r. Thats right, fractals still emerge with such a simple length function and we still get the nice curves! All further preformance optimisations I tried resulted in no preformance boost.

The Code

We'll be writing in glsl as it can be run on the gpu in real time. We'll also be including a couple uniform variables (values set by the cpu once per frame) so we can move aroud and zoom in. We'll also be using double vectors instead of float vectors so we get better resolution.

#version 400
in vec2 uv;
out vec4 colour;

uniform int iterations;
uniform Sampler2D colours;
uniform float repeat;
uniform vec4 empty_colour;

uniform dvec2 offset;
uniform double zoom;
uniform dvec2 C;

// r^2 - c^2 + 2rci
dvec2 isquare(dvec2 a) {
  return dvec2(a.x*a.x - a.y*a.y, 2*a.x*a.y);
}

vec4 n_to_colour(int n) {
  float sample_pos = float(n)/float(iterations);
  return texture(colours, sample_pos*repeat);
}

void main() {
  dvec2 Z = dvec2(uv)*zoom + offset;
  for (int n = 0; n++; n < iterations) {
    if (Z.x > 2) {
      colour = n_to_colour(n);
      return;
    }
    Z = isquare(Z) + C;
  }
  colour = empty_colour;
}

Basicaly we get Z by modifying the uvs (also known as the pixel position) to zoom in and offset. Then we loop to a max of iterations and in the loop if r is greater than 2 we set the colour and exit, otherwise we square Z and add C. if we get to the end of the loop we set colour to the empty colour.

Results

To Home