Click here to Skip to main content
Click here to Skip to main content

Mandelbrot Set with PixelShader in WPF

, 23 Jul 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
This article explains how to use pixel shaders for fast generation of Mandelbrot Sets.

Introduction

This article explains how to use pixel shaders for fast generation of Mandelbrot and Julia Sets. With graphics card acceleration, values of pixels can be calculated asynchronously, in real-time. Each unit calculates the pixels assigned to it. Unfortunately, some graphics cards don't support double-precision floating-point numbers (including my card), so we can't get high magnifications. But the speed is an advantage.

Requirements:

  • Visual Studio Express or Standard 2010 - for creating the project
  • DirectX SDK (June 2010) - for compiling HLSL - language for shaders

Background

I learned HLSL from this article. It explains how to manipulate the brightness and contrast with pixel shaders. If you want to know more about the Mandelbrot Set, take a look at Wikipedia.

Writing the shader file

Let's add a text file to our project and name it "mandel.txt". It will be the source code of the shader in HLSL. Now let's write the frame of the shader:

sampler2D input : register(s0);
// Image to be processed, loaded from Sampler Register 0.

float4 main(float2 uv : TEXCOORD) : COLOR
// uv are the coordinates of the pixel to be processed
{
    float4 color = tex2D(input, uv); // Getting pixel uv from input.
    return color; // Returning new color of processed pixel.
}
// As you see HLSL is similar to C.

The shaders are applied to the WPF elements with Effect property. The rendered image of WPF element is passed to the wrapper of shader and it sends compiled shader's program to the graphic card. Wrapper sends also data (with rendered element) to specified registers in graphic card. In WPF registers are assigned by wrapper and can be read by shader. If you want to know more generally about registers, take a look at the Wikipedia.

input is an image loaded from Sampler Register 0. In the future the wrapper (WPF does it automatically) will place the rendered element (e.g., Grid) in this register. Mandelbrot doesn't transform image, it isn't generated from its pixels, so we can delete input in Mandelbrot shader.

Let's go back to the project. We must compile shader file. To do that let's go to Project properties and to Build Events section.

Now let's add the pre-build event command:

"C:\Program Files\Microsoft DirectX SDK (June 2010)\Utilities\bin\x86\fxc" 
    /T ps_3_0 /E main /Fo"$(ProjectDir)mandel.ps" "$(ProjectDir)mandel.txt"

This will compile our shader. Let's now run the project.

It is possible that error might occur during the compilation. It can be a problem with the shader file encoding. FXC compiler supports only ANSI code pages, and Visual Studio by default creates files in UTF-8. To change UTF-8 to ANSI in Visual Studio:

  1. Select mandel.txt in Solution Explorer.
  2. Open File menu and choose Save mandel.txt As...
  3. Don't change the path - the file will be overwritten.
  4. Click Save with Encoding...
  5. Click Yes, when it is overwriting the message.
  6. Select "Central European (Windows) - codepage 1250" and click OK.

If all goes well, you should see a blank window MainWindow. For now, we won't use the shader in our project. Let's add the compiled shader file to a project. The compiled program should be in the project directory as "mandel.ps". Now the file will be overwritten in each rebuild. Let's implement a simple Mandelbrot. To do that, we must have a complex numbers library. I have created this and you can download it from the top of this article. When you have it downloaded, unzip the file and add "complex.txt" to your Mandelbrot project. Add the following line before main in mandel.txt:

#include "complex.txt"

Now we can easily implement a Mandelbrot. Delete all the inner main. Let's then define the variables:

float2 z = float2(uv.x, uv.y); // Complex number z
float i = 0; // Iterations
float maxIter = 32; // Maximal iterations
float2 power = float2(2, 0); // Exponent. For standard Mandelbrot it is (2, 0).
float bailout = 4; // Bailout

Now write the loop:

while (i < maxIter && c_abs(z) <= bailout) // While loop
{
    z = c_add(c_pow(z, power), uv); // Recalculating z
    i++; // Iterations + 1
}

The number of iterations (i) will be represented by colors. The functions c_abs, c_add, c_pow... are functions of the complex library for calculating the absolute value, adding, exponenting... If the absolute value of z ≤ bailout, then the loop ends. In the center of the Mandelbrot, the number of iterations will be infinite, so it's a maxIter variable - it inhibits the formation of infinite loops. The line which calculates the new value of z is in mathematical presenting: z(n+1) = zna + b, where a is power and b is uv.

Now for testing, return red color if i == maxIter else return black color. In future, this will be rendered with a complex palette.

if (i == maxIter)
    return float4(1.0, 0.0, 0.0, 1.0); //float4(red, green, blue, alpha)
else
    return float4(0.0, 0.0, 0.0, 1.0);

Now your code should be like this:

#include "complex.txt"

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float2 z = float2(uv.x, uv.y);
    float i = 0;
    float maxIter = 32;
    float2 power = float2(2, 0);
    float bailout = 4;

    while (i < maxIter && c_abs(z) <= bailout)
    {
        z = c_add(c_pow(z, power), uv);
        i++;
    }

    if (i == maxIter)
        return float4(1.0, 0.0, 0.0, 1.0);
    else
        return float4(0.0, 0.0, 0.0, 1.0);
}

You can recompile your project. All should be good.

Wrapper for the shader

Our compiled shader should be added to the project. If we want the wrapper to be able to load the shader, we must set the shader file Build Action to Resource.

Now add the new class to the project and name it "MandelbrotEffect". It will be our wrapper. Add the following using statements to the class:

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

Our class must inherit from ShaderEffect. Let's load the shader:

PixelShader m_shader = new PixelShader() { UriSource = 
            new Uri(@"mandel.ps", UriKind.RelativeOrAbsolute) };

This line must be added into the MandelbrotEffect class. Now add the constructor:

public MandelbrotEffect()
{
    PixelShader = m_shader;
}

Constructor sets shader source to the m_shader. We don't have to implement input, because we don't use it in shader. If you'd like in future to create an effect, which requires input, take a look here.

Now you can recompile your project.

Using the shader

Let's test our shader now. Go to MainPage.xaml. It should look like this:

<Window x:Class="ShaderMandelbrot.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        <!-- HERE -->

        Title="MainWindow" Height="350" Width="525">

Add this line where there is a comment "HERE":

xmlns:my="clr-namespace:ShaderMandelbrot"

and replace ShaderMandelbrot with the namespace that contains your MandelbrotEffect. Now set the background of the Grid to any visible color (e.g., Red) because there are only visible pixels transformed by the pixel shader. So let's add MandelbrotEffect to the Grid:

<Grid.Effect>
    <my:MandelbrotEffect />
</Grid.Effect>

Now rebuild the project and Run!

Adding parameters

For changing the maximal iterations, power, or bailout, we must modify the pixel shader file. Let's change these to parameters. We will modify the pixel shader. First, delete the following lines from main:

float maxIter = 32;
float2 power = float2(2, 0);
float bailout = 4;

Second, add these lines at the top of the file, after input:

float maxIter : register(c0);
float2 power : register(c1);
float bailout : register(c2);

Now the parameters are loaded from the Pixel Shader Constant Registers.

Third, implement these parameters in the wrapper:

public static readonly DependencyProperty MaxIterProperty = 
  DependencyProperty.Register("MaxIter", typeof(double), typeof(MandelbrotEffect), 
  new UIPropertyMetadata(32.0, PixelShaderConstantCallback(0))); // register(c0)
public double MaxIter
{
    get { return (double)GetValue(MaxIterProperty); }
    set { SetValue(MaxIterProperty, value); }
}

public static readonly DependencyProperty PowerProperty = 
  DependencyProperty.Register("Power", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(2, 0), 
  PixelShaderConstantCallback(1))); // register(c1)
public Point Power
{
    get { return (Point)GetValue(PowerProperty); }
    set { SetValue(PowerProperty, value); }
}

public static readonly DependencyProperty BailoutProperty = 
  DependencyProperty.Register("Bailout", typeof(double), 
  typeof(MandelbrotEffect), 
  new UIPropertyMetadata(4.0, PixelShaderConstantCallback(2))); // register(c2)
public double Bailout
{
    get { return (double)GetValue(BailoutProperty); }
    set { SetValue(BailoutProperty, value); }
}

Fourth, update the shader values - add these lines into the MandelbrotEffect constructor:

UpdateShaderValue(MaxIterProperty);
UpdateShaderValue(PowerProperty);
UpdateShaderValue(BailoutProperty);

Done. Now rebuild the project and you can set the parameters in the XAML editor.

<my:MandelbrotEffect Power="6,0" />

That creates a mutant of Mandelbrot.

Offset and size

For now, we can see only a part of a fractal. Let's rescale it. Add the offset and size parameters into the shader:

float2 offset : register(c3);
float2 size : register(c4);

Implement the parameters in a wrapper:

public static readonly DependencyProperty OffsetProperty = 
  DependencyProperty.Register("Offset", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(-3, -2), 
  PixelShaderConstantCallback(3)));
public Point Offset
{
    get { return (Point)GetValue(OffsetProperty); }
    set { SetValue(OffsetProperty, value); }
}

public static readonly DependencyProperty SizeProperty = 
  DependencyProperty.Register("Size", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(0.25, 0.25), 
  PixelShaderConstantCallback(4)));
public Point Size
{
    get { return (Point)GetValue(SizeProperty); }
    set { SetValue(SizeProperty, value); }
}

And update the shader values:

UpdateShaderValue(OffsetProperty);
UpdateShaderValue(SizeProperty);

Now we must rescale uv in the shader:

float2 xy = float2(uv.x / size.x + offset.x, uv.y / size.y + offset.y);

Change all uv to xy in the shader and Run.

Palette

Our Mandelbrot Set isn't very colorized. To get beautiful colors, we have to create a palette. Here is the code for a simple palette, but very good:

float4 getColor(float i)
{
    float k = 1.0 / 3.0;
    float k2 = 2.0 / 3.0;
    float cr = 0.0;
    float cg = 0.0;
    float cb = 0.0;
    if (i >= k2)
    {
        cr = i - k2;
        cg = (k-1) - cr;
    }
    else if (i >= k)
    {
        cg = i - k;
        cb = (k-1) - cg;
    }
    else
    {
        cb = i;
    }
    return float4(cr * 3, cg * 3, cb * 3, 1.0);
}

Paste it after the "include line" in the shader. Now replace this:

if (i == maxIter)
    return float4(1.0, 0.0, 0.0, 1.0);
else
    return float4(0.0, 0.0, 0.0, 1.0);

with this:

if (i < maxIter)
    return getColor(i / maxIter);
else
    return float4(0.0, 0.0, 0.0, 1.0);

This is the result:

Continuous (smooth) coloring

The Normalized Iteration Count algorithm allows to remove unsightly color thresholds. We can easily implement this as follows. Replace this:

return getColor(i / maxIter);

with this:

{
    i -= log(log(c_abs(z))) / log(c_abs(power));
    return getColor(i / maxIter);
}

That's it! Our Mandelbrot shader file is completed. That is the effect:

Mandelbrot screenshots

Julia

Julia's formula is similar to Mandelbrot. There is one change. This is the formula for Julia: z(n+1) = zna + b, where a is power, and b isn't uv - in Julia, that's seed. The easiest way to select it is by switching from the Mandelbrot - in the example application, I do it that way.

This is the code of the Julia shader:

float maxIter : register(c0);
float2 power : register(c1);
float bailout : register(c2);
float2 offset : register(c3);
float2 size : register(c4);
float2 seed : register(c5);

#include "complex.txt"

float4 getColor(float i)
{
    float k = 1.0 / 3.0;
    float k2 = 2.0 / 3.0;
    float cr = 0.0;
    float cg = 0.0;
    float cb = 0.0;
    if (i >= k2)
    {
        cr = i - k2;
        cg = (k-1) - cr;
    }
    else if (i >= k)
    {
        cg = i - k;
        cb = (k-1) - cg;
    }
    else
    {
        cb = i;
    }
    return float4(cr * 3, cg * 3, cb * 3, 1.0);
}

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float2 xy = float2(uv.x / size.x + offset.x, uv.y / size.y + offset.y);
    float2 z = float2(xy.x, xy.y);
    float i = 0;

    while (i < maxIter && c_abs(z) <= bailout)
    {
        z = c_add(c_pow(z, power), seed);
        i++;
    }
    if (i < maxIter)
    {
        i -= log(log(c_abs(z))) / log(c_abs(power));
        return getColor(i / maxIter);
    }
    else
        return float4(0.0, 0.0, 0.0, 1.0);
}

You must implement the wrapper by yourself or download the source code. Remember: you must add the compilation line into the pre-build commands.

Julia screenshots

Other features

In the example application (and its source), I applied some other features in MainWindow. These include moving, scaling fractal with mouse, switching to Julia, and sliders for regulating parameters.

What next?

You can try to implement other fractals yourself.

Conclusion

Mandelbrot Set is very easy to implement with complex numbers. Julia's formula is similar to Mandelbrot's formula. With pixel shaders, we can get fast fractals, but because there are problems with double-precision numbers, we can't get high magnifications. Pixel Shaders are a good solution for fast image processing, and with WPF and HLSL, it is very easy.

History

  • 2011-07-19 - corrections, added Julia.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

TeapotDev

Poland Poland

My homepage
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmembertoantvo24-Jul-11 17:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141220.1 | Last Updated 23 Jul 2011
Article Copyright 2011 by TeapotDev
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid