I suppose you could say I had a deprived coding childhood; I already wrote about how I never calculated pi and now I am about to tell you that until this afternoon, I had never drawn the Mandelbrot set. I don’t know why I never took the time to do so, but as with the pi calculator, when I decided to plot the Mandelbrot set, I found a very interesting coding challenge waiting for me.

The first step in the process was to fix the notoriously slow Bitmap.SetPixel() method. I haven’t decompiled System.Drawing to look at .NET’s implementation but I assume it involves some sort of Marshal.Copy()ing of unmanaged bitmap data to a managed byte array, modifications to pixels in the managed byte array, and recopying the array to unmanaged memory. That’s well and good for setting three pixels. We’re going to render Mandelbrot sets of an arbitrary size; 2800×1600 is about 4.5 million pixels. Bitmap.SetPixel() would make this process way, way longer than necessary.

I created a class with a readable, writable bitmap as well as a method to set any of its pixels using unsafe code.

public class FastBitmap { public FastBitmap(int width, int height) { this.Bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); } public unsafe void SetPixel(int x, int y, Color color) { BitmapData data = this.Bitmap.LockBits(new Rectangle(0, 0, this.Bitmap.Width, this.Bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); IntPtr scan0 = data.Scan0; byte* imagePointer = (byte*)scan0.ToPointer(); // Pointer to first pixel of image int offset = (y * data.Stride) + (3 * x); // 3x because we have 24bits/px = 3bytes/px byte* px = (imagePointer + offset); // pointer to the pixel we want px[0] = color.B; // Red component px[1] = color.G; // Green component px[2] = color.R; // Blue component this.Bitmap.UnlockBits(data); // Set the data again } public Bitmap Bitmap { get; set; } } |

This class isn’t very useful outside of this program; it would need some modifications to use pixel formats other than 24bpp RGB. However, it gets the job done! The SetPixel method is what makes this class useful. It locks the provided Bitmap’s pixels, then gets a pointer to where the first color byte of the first pixel is in memory (Scan0.ToPointer()). It then figures out what the offset is to the pixel we want to modify. Finally, it creates a pointer to the pixel we want. It sets the blue, green and red to the specified color’s blue, green and red, then finally unlocks the bitmap data. When the data gets unlocked, it is saved in memory.

The SetPixel method presented here does not provide tremendous performance. It does, however, let you work with a bitmap fast enough that you won’t get bored. Most Mandelbrot rendering time is in calculations, anyway.

Now that we have an image class, we can get down to the math. The Mandelbrot fractal is based on the following formula:

The formula is given a starting value, C, that lies somewhere in the complex plane. If the formula (using C) does not tend to infinity, then C is part of the Mandelbrot set. If C tends to infinity, it is part of the Mandelbrot set. As a quick programmatic note, if the absolute value of Z sub N (the value of the formula after N iterations) ever becomes greater than two, the formula will tend to infinity.

Knowing that, we can start coming up with some pseudo-code:

for x on the screen: for y on the screen: scale (x, y) to the complex plane to figure out where we are c = where we are on the screen z = 0 + 0i b = a bitmap for i = 0 to however many iterations: z = z^2 + c if |z| > 2: plot (x, y) on b as some color that indicates how much "infinite tendency" there is at C break

If you read the Wikipedia article about rendering the Mandelbrot set, you will notice that it uses two real numbers to accomplish the same thing as one complex one. C# has a complex number class, which we will use in our code. However, before we talk about the actual rendering, we need a color palette. The members of the Mandelbrot set will be rendered black (or actually, based on the flow of the pseudo-code, not rendered at all), while the rest of the set will be rendered some color based on how many iterations it takes for the absolute value of Z to eclipse two. The more iterations it takes, the whiter the pixel. The fewer iterations, the bluer the pixel. It all comes together in the following (very simple) method:

public static List<Color> GenerateColorPalette() { List<Color> retVal = new List<Color>(); for (int i = 0; i <= 255; i++) { retVal.Add(Color.FromArgb(255, i, i, 255)); } return retVal; } |

Okay, we can finally translate the pseudo-code into a real Mandelbrot-rendering method:

public static Bitmap DrawMandelbrot(int width, int height, double rMin, double rMax, double iMin, double iMax) { List<Color> Palette = GenerateColorPalette(); FastBitmap img = new FastBitmap(width, height); // Bitmap to contain the set double rScale = (Math.Abs(rMin) + Math.Abs(rMax)) / width; // Amount to move each pixel in the real numbers double iScale = (Math.Abs(iMin) + Math.Abs(iMax)) / height; // Amount to move each pixel in the imaginary numbers for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { Complex c = new Complex(x * rScale + rMin, y * iScale + iMin); // Scaled complex number Complex z = c; for (int i = 0; i < Palette.Count; i++) // 255 iterations with the method we already wrote { if (z.Magnitude >= 2.0) { img.SetPixel(x, y, Palette[i]); // Set the pixel if the magnitude is greater than two break; // We're done with this loop } else { z = c + Complex.Pow(z, 2); // Z = Zlast^2 + C } } } } return img.Bitmap; } |

Some notable points about this method:

- The rScale and iScale variables represent the change in real or imaginary value in the complex plane of each pixel on the screen.
- Some good parameters to pass to this method are rMin = -2.5, rMax = 1.0, iMin = -1.0 and iMax (no copyright infringement intended!) = 1.0.
- This method could be significantly improved on multi-core machines by dividing up the x-values to render and running a thread on each core. However, any rendering would have to be done after all the threads finished to avoid any cross-thread bit-locking issues. Luckily, the part that takes a while is the iterating, not the rendering.

That’s all it takes to draw a pretty good-looking Mandelbrot fractal. Here’s a fairly large one that you can give to your math teacher. I promise it will make him or her proud. I’m totally giving one to my math team coach as a New Year’s present.