Buddhabrot Buddhabrot
Implement the Buddhabrot technique in Java
Discover the catch

Coming up, a description of the simplest way to implement the technique, in Java.

The first thing we need is the function that returns the famous image. To get a more organized code, we declare the variables at the beginning:

import java.awt.image.*;
...
public static BufferedImage generaBuddhaBrot() {
    float Cr, Ci, Zr, Zi, ZrAux, ZiAux, increm = 4 / 10000f;

    int i, j, n, maxN = 200; 
    float[][] trayectoria = new float[maxN][2];
    int coordX, coordY, lado = 1000, color;
    float[][] pixelsTemp = new float[lado][lado];

    int[] pixels = new int[lado * lado];
    BufferedImage imagen;

Let's remember the Mandelbrot function: Zn = Zn-12 + C

Z and C are complex numbers so they have a real and an imaginary part. We represent them with the Zr, Zi, Cr and Ci variables. ZrAux and ZiAux are just conveniency vars to keep Zn-1.

i, j, and iter are counter vars for the loops. trayectoria contains the Zs that iterating the Mandelbrot function produces.

The remaining vars are there to manage the image -we'll see what each one is for later on.

We have to check a series of Cs on the complex plane, so we'll go through the real axis and the imaginary, both from -2 to 2, at increm intervals. In the example, we're looking at 10000 points on each axis (the 4 dividend corresponds to the axis' length+), which means we're checking 108 Cs on the plane, to see if they belong to the Mandelbrot set (M from now on) or otherwise.

We know a C is outside M if Z, iterating through the aforementioned formula, tends to infinity. And here's where we discover the great catch in this business: to be completely sure C is not part of M, we should calculate Z for n = infinite -and such a thing cannot be done with a computer.

Let's see what has to be done to outwit this problem.

    for (Cr = -2; Cr < 2; Cr += increm) for (Ci = -2; Ci < 2; Ci += increm) {
        ZrAux = ZiAux = 0;    // Z sub 0                   
        for (n = 0; n < maxN; n++) {
            Zr = calculaReal(ZrAux, ZiAux, Cr);
            Zi = calculaImaginaria(ZrAux, ZiAux, Ci);
            trayectoria[n][0] = ZrAux = Zr;
            trayectoria[n][1] = ZiAux = Zi;
            if (Zr * Zr + Zi * Zi > 4) break;
        }

Instead of calculating successive Zs to infinity ( :,D), we iterate up to maxN. We know a Z more than 2 units away from the origin of coordinates (Z0) -or what is the same, Z's square module is larger than 4-, tends to infinity, and C has escaped from M.

So far everything looks just fine, but the problem when limiting n is that we can take points outside M as if they really belonged to the set. We can thus deduce that -assuming n cannot be infinite- no image we could possibly render will ever be accurate; the perfect graphic representation of the Buddhabrot (or the Mandelbrot). However, there's nothing to worry, because we can get images so detailed the human eye wouldn't be able to tell them from the ideal one.

The trayectoria array keeps track of Zs: let's not forget that what we want is to paint the trajectories of Cs outside M.

These are the functions to calculate Zn's real and imaginary parts; arguments are Zn-1 and C:

private float calculaReal(float Zr, float Zi, float Cr) { 
    return Zr * Zr - Zi * Zi + Cr; 
}

private float calculaImaginaria(float Zr, float Zi, float Ci)  { 
    return 2 * Zr * Zi + Ci; 
}

When we break the loop and C escapes M, we have to "paint" the pixels corresponding to each one of the points in the trajectory. Let's imagine the (initialy black) image's pixels as a grid we can superimpose on the complex plane. Every time a Z falls inside one of the cells, we brighten it up a little more.

What pixelsTemp really does is to count the density of Zs that fall in a determined region (which corresponds to a pixel).

        if (n < maxN) 
            for (i = 0; i < n; i++) {
                coordX = (int)(((trayectoria[i][0] + 2) / 4) * lado);
                coordY = (int)(((trayectoria[i][1] + 2) / 4) * lado);
                if (coordX < 0 || coordX >= lado || coordY < 0 || coordY >= lado)
                    continue; 
                pixelsTemp[coordX][coordY]++;
            }
    }

As we can see, coordX and coordY represent the coordinates of the affected pixel.

Now we just have to turn the density array -pixelsTemp- into an image.

We're going to create an sRGB image, which means its primary colors are red, green, and blue; each color is represented with a value between 0 and 255, from brightest to darkest. There's also an additional channel, the alpha, that regulates the image's transparency. We can represent a color with an int value, constructed with another 4 int (alpha, red, green, blue) through bit operators.

    maxColor = encuentraMax(pixelsTemp);
    float factorColor  = 255f / maxColor;
    for (i = 0; i < lado; i++) for (j = 0; j < lado; j++) {
        color = (int)(pixelsTemp[i][j] * factorColor);
        pixels[i * lado + j] = color << 16 | color << 8 | color;
    }
    imagen = new BufferedImage(lado, lado, BufferedImage.TYPE_INT_RGB);
    imagen.setRGB(0, 0, lado, lado, pixels, 0, lado);
    return imagen; 
}

Since we want a gray-scale image, we're setting the same value for the red, green, and blue channels. pixels holds the colors to feed to imagen for it to show the Buddhabrot.

By the way, in order to "scale down" densities to integer values between 0 and 255, and then "translate" them to a color, we need to know the maximum density. We can do that using this simple method:

private float encuentraMax(float[][] arr) {
    float max = 0;
    for (int j, i = 0; i < arr.length; i++) for (j = 0; j < arr[i].length; j++)
        if (arr[i][j] > max) max = arr[i][j];
    return max;
}

If you want to save the image to disk, you can use the following code:

import javax.imageio.*;
...
try {
    BufferedImage imagen = generaImagen();
    ImageIO.write(imagen, "PNG", new File("C:\\BuddhaBrot.png"));
} catch (Exception excepcion) { excepcion.printStackTrace(); }
 
Copyright © Albert Lobo Cusidó 2006-2014