Have you ever seen an image like this before?
A beautiful picture that contains itself within itself. This is a fractal and I'm gonna tell you how to make some.
I modeled my fractals using the equation Z(n+1) = Zn^2 + C a common fractal equation that produces whats known as the julia set. Lets break it down.
First up, we aren't using Real Numbers (if you don't know what that is, just think of them as regular numbers), were using Complex 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. Secondly this equation is a loop. We can tell because Z has a little n that means index and it sets the following index, n + 1. So what does Z start at? Z0 equals the pixels position where x is the real number and y is the complex. But when does the loop end? We exit when the length of Z is larger than 2 or when the index is to high (so we aren't stuck in the loop), this answers another question, how do we get that gradient? well we simply return the index, dividing n by the maximum iterations gives the graient in the above image. now theres one thing left in the equation, what is C? C is a constant offset and determines the shape of the fractal. it could be anything! e.g
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 means something. Thats right i is not just a signifier for the complex number it also has a numerical value. Specificaly it equals the square root of -1. This means i*i = -1. So it follows that:
ci*ci =
c^2*(-1) =
-c^2
It equals a Real Number! This means the number 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| = (r^2 + c^2)sqrt, and you'd be right! Few things to note, this calculation (aswellas the squaring) will be calculated iterations (most likely set to 200-1000) 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. 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, though this may be the same speed as addition as computers are unintitive and weird but nonetheless I was simply using this as a stepping stone because. We. Can. Push. It. Further. |Z| = r. Thats right, fractals still emerge with such a simple length function and we still get the nice fractal curves!
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 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 gpu 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 140
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;
dvec2 isquare(dvec2 a) {
return dvec2(a.x*a.x - a.y*a.y, 2*a.x*a.y); //r^2 - c^2 2rci
}
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 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.