The Mandelbrot Set in WPF

I am fascinated about the Mandelbrot set, since I first saw a video about it at my university. It is so elegant, so beautiful but the cherry on the cake it is that it’s infinitely complex. Throughout my years I implemented the rendering many times. Every time somebody says Math is boring, I flip out my phone to prove them wrong with some images. You can read a lot more about it here.

In WPF there are three main ways to do rendering.

  1. Control composition: You assemble your hierarchy of UI elements (from XAML or code) and you let the engine take care of layouting and rendering for you. Great if you want a lot of user interaction.
  2. Drawing Context:  You use primitives like Lines, Rectangles, Pens and Brushes and you do most of the positioning yourself. It is the basically the GDI rendering from WinForms and involves overriding the OnPaint() method. Great choice when you want to create from scratch a control like a chart.
  3. Image generation: You define the color of each pixel on a surface. This is how game engines do rendering. Great choice when you want to create Heat Maps or Image manipulation

I created a simple WPF application that renders the Mandelbrot Set into a WriteableBitmap.

The main window couldn’t be simpler. Visually it is a borderless full screen window that uses an ImageBrush as a background, which is set to the WriteableBitmap that we create.

<Window x:Class="CpuMandelbrotWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None"
WindowState="Maximized">
	<Window.Background>
		<ImageBrush x:Name="img" />
	</Window.Background>
</Window>

Besides that it contains some code that can help us navigate the set.

  • MouseWheel: increses the zoom level that we have
  • Shift+MouseWheel: increases the maximum number of iterations
  • MouseDown: sets the coordinates as screen center
public partial class MainWindow : Window
{
	WriteableBitmap bmp = new WriteableBitmap(
		(int)SystemParameters.PrimaryScreenWidth, 
		(int)SystemParameters.PrimaryScreenHeight, 
		96, 
		96,
		PixelFormats.Bgr24, 
		null);
	GradientColorGenerator colors = new GradientColorGenerator();
	int depth = 100;
	double step = 0.002;
	double centerX = -0.5;
	double centerY = 0;

	public MainWindow()
	{
		InitializeComponent();
		Render();
		img.ImageSource = bmp;
	}

	protected override void OnMouseDown(MouseButtonEventArgs e)
	{
		var p = e.GetPosition(this);
		centerX += (p.X - bmp.PixelWidth / 2) * step;
		centerY += (p.Y - bmp.PixelHeight / 2) * step;
		Render();

		base.OnMouseDown(e);
	}

	protected override void OnMouseWheel(MouseWheelEventArgs e)
	{
		if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
		{
			depth = (int)(e.Delta > 0 ? 1.1 * depth : 0.9 * depth);
		}
		else
		{
			step *= e.Delta > 0 ? 0.9 : 1.1;
		}

		Render();

		base.OnMouseWheel(e);
	}

	private void Render()
	{
		MandelbrotGenerator.GenerateImage(
			bmp, 
			centerX - (bmp.PixelWidth / 2) * step, 
			centerY - (bmp.PixelHeight / 2) * step, 
			step, 
			depth, 
			colors.GeneratePalette(depth));
	}
}

The following code deserves a bit of explanation since it has to do in part with the inner workings of the WriteableBitmap.

In the first section we lock the image. This tells the engine to not read from it in the rendering phase. This is very useful since we plan to run a bulk of operations on it and we don’t want to update the UI every time. Plus we don’t want any reading operation block our write in the buffer. The most important part is that we can get the address in memory of the image.

The second part is where the calculation happens. It is wrapped around the unsafe keyword since we plan to do direct pointer manipulation (yes you can do this in C# too). For this to work you need to enable unsafe code from project settings. The parallel for loops take advantage of a multi core system to determine in parallel the value for each pixel. For each pixel we determine the amount of iterations until the point escaped.

In the last section we unlock the image. The dirty rectangles are a good old performance optimization trick. There are times when you update only certain parts of your image, so telling the engine which parts need to be redrawn is good. If you updated a large chunk of the image, in this case it is faster to update everything.

public static void GenerateImage(WriteableBitmap bmp, 
	double startX, double startY, double step, int[] palette)
{
	long buffer = 0;
	var stride = 0;
	var width = 0;
	var height = 0;

	bmp.Dispatcher.Invoke(() =>
	{
		bmp.Lock();
		buffer = (long)bmp.BackBuffer;
		stride = bmp.BackBufferStride;
		width = bmp.PixelWidth;
		height = bmp.PixelHeight;
	});

	unsafe
	{
		Parallel.For(0, height, j =>
		{
			Parallel.For(0, width, i =>
			{
				var pixel = buffer + j * stride + i * 3;
				*(int*)pixel = palette[Iterate(
					startX + i * step, 
					startY + j * step, 
					palette.Length)];
			});
		});
	}

	bmp.Dispatcher.Invoke(() =>
	{
		bmp.AddDirtyRect(new Int32Rect(0, 0, width, height));
		bmp.Unlock();
	});
}

static int Iterate(double x, double y, int limit)
{
	var x0 = x;
	var y0 = y;
	for (var i = 0; i < limit; i++)
	{
		if (x * x + y * y >= 4)
			return i;

		var zx = x * x - y * y + x0;
		y = 2 * x * y + y0;
		x = zx;
	}

	return 0;
}

The beauty in this exercise is of course the coloring. For this purpose we use a gradient and we do a linear interpolation between two color values. I didn’t use the built in gradient functions in WPF since i wanted to save time and precalculate the whole palette.

class GradientColorGenerator
{
	private readonly IList<dynamic> _stops = new List<dynamic>
		{
			new {v=0.000f, r=0.000f, g=0.000f, b=0.000f },
			new {v=0.160f, r=0.125f, g=0.420f, b=0.796f },
			new {v=0.420f, r=0.930f, g=1.000f, b=1.000f },
			new {v=0.642f, r=1.000f, g=0.666f, b=0.000f },
			new {v=1.000f, r=0.000f, g=0.000f, b=0.000f },
		};

	public int[] GeneratePalette(int depth)
	{
		var x = new int[depth];
		for (int i = 0; i < depth; i++)
		{
			x[i] = GetColor(i / (float)depth);
		}
		return x;
	}

	private int GetColor(float p)
	{
		if (p > 1) p = 1;
		for (int i = 0; i < _stops.Count; i++)
		{
			if (_stops[i].v <= p && _stops[i + 1].v >= p)
			{
				var s0 = _stops[i];
				var s1 = _stops[i + 1];
				var pos = (p - s0.v) / (s1.v - s0.v);
				var rpos = 1 - pos;
				var r = rpos * s0.r + pos * s1.r;
				var g = rpos * s0.g + pos * s1.g;
				var b = rpos * s0.b + pos * s1.b;
				return 
					Convert.ToByte(r * 255) << 16 | 
					Convert.ToByte(g * 255) << 8 | 
					Convert.ToByte(b * 255);
			}
		}

		return 0; 
	}
}

Try out your own gradient and enjoy!