When I was about 7 or 8, my dad got a book on fractals with a CD program to generate them. At the time we had a 486, which was insanely fast compared to the authors 386, so some of the renders the author explained would take hours, we could do in mere minutes. And this was mostly optimised C code running it, not home-brew python that I made up.
It also however contained some optimisations that allowed it to draw fractals without calculating every pixel individually. Optimisations that we just don't need these days (as we have faster computers, but also multi threading, and even transputer type designs where we can outsource the calculations to other chips, other machines etc).
However I always loved one particular design, and I loved it when you left the 'construction' lines on it. The basic premise works like this - we can pretend that a fractal is continuous over a space - i.e. this doesn't work for certain things, and it will loose accuracy where there are 'islands' of results. What we do is we split the render area into 2 boxes with a line straight down the middle. We calculate all the pixels on the border of the left hand box. If they are all the same value, we then fill in that box with that value - if they're not the same value, we split the box in half, and then calculate again, and split and again until we end up with ether boxes of 1x1pixels, or we have a box of the same colours.
When I drew this, I used my basic colour calculations, but for the fill, I divided the value of the colour by 2 - which makes the borders just stand out really brightly.
Making Fractals ... Slowly. As a Performance Art
Part of this though is not the final image, but the process of getting there. So the first thing I did was convert my program from using PIL to using pygame. So previously there was some code in the form of (pseudo python incoming):
for i in range(height): for j in range(width): counter = calc(i,j) pixels[j,i] = colour(counter) show_image()
and instead replacing it inside of a pygame loop along the lines of:
iloc = 0 jloc = 0 while(alive): jloc += 1 if jloc == width: jloc = 0 iloc += 1 if iloc == height finished=True if not finished: counter = calc(iloc,jloc) pixels[iloc,jloc] = color(counter) pygame.display.flip()
Which functionally is just unwrapping the loop and manually keeping track of the locations. It also means there is a display.flip() after each pixel is set. Frame rate is variable - you could include a
clock.tick() call to fix it, but generally we want it to go faster.
calc function itself is a variable run because if it escapes the '2 radius' before the max_limit, then it will return sooner (i.e. for coordinates more then 2 away from (0,0), no iterations are carried out at all.
The code quickly becomes messier when we include scaling factors, and offsets (here I've hidden those inside that mysterious calc function). But what we get is a pixel by pixel art work being generated.
Ok so onto boxes. Now we have an animation method, we can start drawing boxes. Firstly, in the outer loop, I'm keeping a queue of boxes to do, and I generated a class to represent each box: a top left, and bottom right coordinate, a 'direction' flag (so I knew if the sub boxes are to be split horizontally or vertically - alternating the splits is important for stylistic reasons. I guess I could have done this by calculating the ratio of width:height and then splitting, but it was easy doing a flag.
Then added in some flags to do the calculation.
In the main loop, we basically got rid of that
jloc and instead have a coordinate and know which side we're on. Whilst we're on the top side, we calculate and add one, when we reach the right hand side, we stop adding one onto the across, and instead add one to the down - when we reach the bottom, we stop doing that and instead subtract one, etc.
At each pixel, we calculate its colour, update the base image, and check that colour against the first pixel of the box. If it doesn't match, we set the dirty flag, and continue.
At the end of the box, we look at the dirty flag. If the dirty flag is not set, we can fill the box in, otherwise we have to divide the box (by generating 2 sub boxes). For speed reasons, I include a 'minimum box size' and if the boxes are smaller then that, I don't add them to the queue).
Then I pop the next box off the queue, until the queue is empty.
Fun things to do
We have several variables we can now play with:
- The overall formula which we're going to stick with the Julia/Mandlebrot formula for the moment
- The centre point moving up/down, left/right
- The zoom level scale factor, or min-max seen either side of the centre point
- The colours being used - at the moment theres a couple of algorithms I'm using to convert a 'escape level' to a colour, which is worthy of an entire blog post in its own right. Along with this, Max Iterations - has a minor effect on the colour calculation
- Next Box Selection: the choices are, oldest box first (which basically means getting rid of the biggest boxes, usually working in a left to right, top to bottom), last box added (which means diving into all the detail in the bottom left all the way to smallest box, and then out), or randomly.
The random box selection is interesting, because as you divide boxes, you get a lot more small boxes then big boxes, so you can be left with big boxes until quite late. Perhaps by having different queues of different sizes, and weighting the selection (or only taking the box from the 1/3rd mark or something) some interesting render animations could be made.
A quick note - it can be slow because we can be recalculating edges of boxes we've already done (i.e. the right hand side of the left hand box is the left hand side of the right hand box) - an optimisation (which again clutters the code) can be made here by checking if the pixel is already set.
So now we have a set of variables, instead of stopping when we get to the end, we can now do something funky - we know which boxes are 'dirty', so we can decide to zoom in on one of these funky boxes. The question is which of those boxes are 'interesting' to zoom into, and which are boring. With a very primitive 'boring detector' we can get sequences like this (and reset to the original algorithm on a boring find):
Remember each iteration of zoom does not zoom to the smallest box, here we are varying the min-box size, but it might zoom to 'any' dirty box, including some of the large ones.
What we now have is a near-unattended fractal drawing art machine. Now if we slap this into OBS, with a bit of gentle music overlay (from the youtube library), we should be able to make a streaming fractal generation 'performance art' thing. Not sure about the performance art definition - its a one off live thing. And I find it incredibly relaxing to watch.