thecodingidiot.com

The InfinitePlay It

Play It

Add the Burning Ship fractal, run the tester, and explore the renderer. This page completes the chapter with one final demonstration: a single-line change to the iteration formula produces a completely different geometry.


The Burning Ship

The Burning Ship fractal[1] modifies the Mandelbrot iteration with one operation added before each squaring: take the absolute value of both components of z.

zn+1=(Re(zn)+iIm(zn))2+cz_{n+1} = \bigl(|\operatorname{Re}(z_n)| + i\,|\operatorname{Im}(z_n)|\bigr)^2 + c

What absolute value does to the iteration. The Mandelbrot iteration treats the complex plane symmetrically above and below the real axis: (re, im) and (re, −im) produce conjugate orbits, which is why the Mandelbrot set is perfectly symmetric top-to-bottom. Forcing re|re| and im|im| before each squaring step folds both half-planes to the positive quadrant. Every orbit is reflected into the upper-right quarter before each step. This fold breaks the conjugate symmetry — the Burning Ship is not symmetric — and changes which orbits escape and which stay bounded. The asymmetric ship silhouette visible at the bottom of the set is a direct consequence of how that fold interacts with the squaring.

Add burning_ship to mandelbrot.c. The mandelbrot.h declaration was already updated on the previous page.

int burning_ship(double c_re, double c_im, int max_iter, double *mag_out)
{
    double re;
    double im;
    double new_re;
    double sq;
    int    i;
 
    re = 0.0;   /* z starts at zero, same as Mandelbrot */
    im = 0.0;
    i = 0;
    while (i < max_iter) {
        sq = re * re + im * im;
        if (sq > 4.0) {             /* |z|² > 4: point has escaped */
            if (mag_out)
                *mag_out = sq;      /* squared magnitude for smooth colouring */
            return (i);
        }
        if (re < 0.0)
            re = -re;               /* |Re(z)| — fold negative real to positive */
        if (im < 0.0)
            im = -im;               /* |Im(z)| — fold negative imaginary to positive */
        new_re = re * re - im * im + c_re; /* real part of (|z|)² + c */
        im = 2.0 * re * im + c_im;         /* imaginary part — uses folded re */
        re = new_re;
        i++;
    }
    if (mag_out)
        *mag_out = re * re + im * im;
    return (max_iter);              /* never escaped: point is in the set */
}

The fold step. The two if blocks appear after the escape check and before the squaring — between these two positions the fold has its intended effect. They use conditional negation (if (x < 0) x = -x) rather than fabs() for a deliberate reason: fabs() requires <math.h>, and the platform separation rule keeps mandelbrot.c free of math-library includes. Conditional negation is exactly what fabs() does for a single double value. After the fold, the squaring and addition proceed identically to mandelbrot. Everything else — initialisation, escape condition, mag_out interface, return value — is unchanged.

burning_ship was declared in mandelbrot.h from the Set page and is already wired into the else branch of the render_frame dispatch from the previous page.

make re && ./infinite

Press B for Burning Ship mode. The set looks nothing like Mandelbrot — an asymmetric, spiky structure with a region at the bottom that genuinely resembles a burning ship or city skyline. One fold, one extra pair of conditionals, completely different geometry. The formula IS the visual.

Zoom into the ship region at the bottom (roughly centre (−0.5, −0.5), zoomed to around 0.3× the default scale). The detail is as rich as the Mandelbrot set — spirals and miniature copies are present, but their shape is distorted by the fold.


Run the tester

git clone https://github.com/thecodingidiot-com/c04-the-infinite.git
cp c04-the-infinite/test/test.sh ~/c04-practice/
cp c04-the-infinite/test/test_mandelbrot.c ~/c04-practice/
cp c04-the-infinite/test/test_view.c ~/c04-practice/
cd ~/c04-practice
bash test.sh

The tester compiles mandelbrot.c and view.c directly into a test binary — no SDL2, no renderer, no window. This is only possible because those files have no SDL2 includes: the platform separation rule, which kept them free of SDL2 throughout the chapter, is what makes automated testing possible. The same code that runs in the renderer compiles into a standalone binary.

Suite 1 — escape time. Known (re, im) inputs with precomputed expected iteration counts for mandelbrot(), julia(), and burning_ship(). The tests use the strict escape condition sq > 4.0 (strictly greater than, not greater-than-or-equal). If you wrote >= 4.0, some counts will be off by one — the output identifies which test failed and what value was expected.

Suite 2 — viewport mapping. Given a view_t with known centre and scale, verifies that pixel_to_re(WIDTH/2, &v) returns center_re and pixel_to_im(HEIGHT/2, &v) returns center_im. Also verifies that one view_zoom(&v, 0.9) step scales the view correctly.

If the tester fails with a compile error naming an SDL2 header, the platform separation rule has been violated. Check the #include list in mandelbrot.c, julia.c, view.c, and colour.c — none should include SDL2/SDL.h or any SDL2 header.


What the three fractals taught

The chapter built three fractals in sequence.

Mandelbrot. The base case. c = pixel, z0=0z_0 = 0. Generates the canonical shape: cardioid, bulbs, infinite boundary.

Julia. Same formula. c fixed by mouse click, z0z_0 = pixel. Every point in the Mandelbrot plane produces a different Julia set. Points inside the Mandelbrot set produce connected Julia sets; points outside produce dust. The Mandelbrot set is the map of all Julia sets.

Burning Ship. Same loop. |re| and |im| taken before each squaring. Completely different geometry. The proof that the formula IS the visual.

Each step changed exactly one thing in the iteration. Each step produced a completely different image. The lesson is not specific to fractals — it is about what a mathematical formula contains. The structure visible on screen is not drawn. It falls out of arithmetic.


What comes next

The renderer is complete. It renders three fractals in colour, zooms and pans interactively, and responds to mouse clicks. It runs on one CPU core and slows measurably at deep zoom.

That slowdown is load-bearing. Every pixel is independent: computing the colour of pixel (100, 200) has no dependency on pixel (101, 200). That independence is called embarrassingly parallel[2] — the ideal workload for a GPU, which runs thousands of small threads simultaneously. The GPU was built to run exactly this computation, not just for fractals but for every pixel in every triangle in every frame of every real-time 3D game.

The r-tier returns to this. When the renderer moves to hardware-accelerated targets, the pixel loop written here is the one the GPU was designed to run in parallel. What you experienced as latency becomes measured in microseconds.

Before that, the curriculum builds more of the systems stack: c05 teaches processes, c06 tackles sorting, and c07 builds a shell that brings it together. The graphical side returns in g01b — the quiz game from g01a rebuilt with SDL2 graphics using the event loop and pixel buffer you just wrote.

Footnotes

  1. Burning Ship fractal - Wikipedia

  2. Embarrassingly parallel - Wikipedia

up next

Who Wants to Be a Game Developer? Graphical

Who Wants to Be a Game Developer? Graphical