Part III

Part III: The FractalView Window

This is a bit more complex, as there are a few issues to handle. The FractalView window manages the specific generator instance, handles user input, elaborates and displays output and performs a few other tasks.

It is a window that contains three controls: a top menu, a bottom status bar and a PictureBox set to Dock = Fill to fill the whole free window area. The following code shows the contructor of the window:

public FractalView(string title, IGenerator generator, Options options)
{
    this.options = options;
    this
.generator = generator;

    this.traceSel = false;
    this
.selecting = false;

    this.history = new List<Image>(options.historySize);
    this
.currentImage = -1;

    InitializeComponent();

    Text = title;

    this.ClientSize = new Size(options.xRes, options.yRes + this.statusBar.Height + this.tsMenu.Height);
}

It is quite clear what that code does. The traceSel and selecting members are used by the area selection routine. The history member is initialized with the user specified size.
When the Fractal is generated for the first time, or when the user selects a plane area and wants to zoom into it, a domain must be computed so that the generator knows what portion of a potentially infinite plane should be considered.
The default value is algorithm dependant and is used for the first image. After that, the new domain is computed as follows:

private Domain ComputeDomain()
{
    Domain
domain = this.generator.GetDomain();

    Domain newDomain = new Domain();
    newDomain.X = domain.X + (domain.Width /
this.options.xRes) * this.zoomArea.X;
    newDomain.Y = domain.Y - (domain.Height /
this.options.yRes) * this.zoomArea.Y;
    newDomain.Width = domain.Width / (
this.options.xRes / Math.Max(this.zoomArea.Width, 1));
    newDomain.Height = newDomain.Width * (domain.Height / domain.Width);
//Keep aspect ratio

    return newDomain;
}

The Domain structure is a simplified version of the RectangleF structure available in C#, the main difference being that Domain uses doubles instead than floats.
The zoomArea variable is nothing but the currently selected area of the image. For details about how I handle the selection please look at the code.

DrawFractal() method

The most important method is DrawFractal(). It deserves special attention and so it will be split in a few parts:

private void DrawFractal(Domain domain)
{
    DateTime t1 = DateTime.Now;

    this.generator.SetDomain(domain);

    Cursor.Current = Cursors.WaitCursor;

    //Split work in tasks
    RenderTask
[] tasks = new RenderTask[this.options.nThreads];
    Thread
[] threads = new Thread[this.options.nThreads];

    double dy = domain.Height / this.options.nThreads;
    int
dyres = this.options.yRes / this.options.nThreads;

    for (int i = 0; i < this.options.nThreads; ++i)
    {
        Domain dt = new Domain();
        dt.X = domain.X;
        dt.Y = domain.Y - (i * dy);
        dt.Width = domain.Width;
        dt.Height = dy;

        dyres = i < (this.options.nThreads - 1) ? dyres : this.options.yRes - dyres * i;

        tasks[i] = new RenderTask(this.generator, dt, this.options.xRes * this.options.AA, dyres * this.options.AA);
        threads[i] =
new Thread(new ThreadStart(tasks[i].DoWork));
        threads[i].Start();
    }

    //Wait for the rendering tasks
    for
(int i = 0; i < this.options.nThreads; ++i)
    threads[i].Join();

A few years ago multiprocessor systems were so rare that building single threaded application was not a shame. Today CPU features more than one computational unit, and adding cores seems the main trick to improve computers speed. This means that when you write a computationally intensive algorithm you need to carefully consider how to split the work among more threads. Avoiding that might become a crime soon or later :-)
We are lucky: many interesting CG algorithms (like raytracing) are easy to multithread. As you can see from the code, I create as many instances of the RenderTask class as defined by the nThreads variable in the options member. RenderTask will be explained later. The trivial way to split the work is to split the domain and execute the Fractal routine on those partial domains at the same time.

After the threads are executed, the join() method is called on each of them so that the main thread waits for them to be completed.

Important architectural consideration: I could have decided to create several generator instances, one for each thread. This would have been a better idea perhaps, but the code would have been slightly more complex. As a consequence of this choice, every generator implementation must be thread safe. For example, the drawing routine should not write on member variables, but only on variables defined inside the method itself, unless it carefully handles syncronization and so on in order to avoid data corruption.

    //Compose the sections in one image
    Bitmap img = new Bitmap(this.options.xRes * this.options.AA, this.options.yRes * this.options.AA);
    Graphics
gx = Graphics.FromImage(img);
    for
(int i = 0; i < this.options.nThreads; ++i)
    {
        gx.DrawImage(tasks[i].Output, new Rectangle(0, dyres * i * this.options.AA, this.options.xRes * this.options.AA, dyres * this.options.AA));
        tasks[i].Output.Dispose();
    }

    if(this.options.AA > 1)
    {
        Bitmap res = new Bitmap(this.options.xRes, this.options.yRes);
        gx =
Graphics.FromImage(res);
        gx.InterpolationMode = System.Drawing.Drawing2D.
InterpolationMode.High;

        gx.DrawImage(img, new Rectangle(0, 0, this.options.xRes, this.options.yRes));

        img.Dispose();
        img = res;
    }

Once all threads have completed their tasks, the partial images can be found in RenderTask.Output. A new bitmap is created with the resolution of the full frame. If we compute the Fractal function only once for each pixel, the resulting image will be severely affected by ugly aliasing artifacts. Antialiasing itself is a huge topic: aliasing naturally appears as soon as we try to sample a function using too few samples. It does not matter if we are considering image functions (pictures, 3D scenes) or acoustic functions (a sound) or any other type.

In our case the function is the Fractal equation and the samples are the pixels. There is no need, for this tutorial, to make things unnecessarily complex, so I simply supersample the function, that is, increase the resolution by n times, where n is a user defined value. Once the image is ready, I reduce it using one of the filtering options available in .Net.

The last thing to do is gamma correction:

    //Apply gamma correction

    for(int x = 0; x < img.Width; ++x)
        for
(int y = 0; y < img.Height; ++y)
        {
            Color color = img.GetPixel(x, y);

            double r = (float)color.R / 255;
            double
g = (float)color.G / 255;
            double
b = (float)color.B / 255;

            if (r > 0.0031308) r = 1.055 * (Math.Pow(r, (1 / 2.2f))) - 0.055;
            else
r = 12.92 * r;

            if (g > 0.0031308) g = 1.055 * (Math.Pow(g, (1 / 2.2f))) - 0.055;
            else
g = 12.92 * g;

            if (b > 0.0031308) b = 1.055 * (Math.Pow(b, (1 / 2.2f))) - 0.055;
            else
b = 12.92 * b;

            img.SetPixel(x, y, Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255)));
        }

    DateTime t2 = DateTime.Now;

    this.tsslTime.Text = string.Format("Rendering: {0} s", t2.Subtract(t1).TotalSeconds);
    this
.tsslDomain.Text = string.Format("Domain: {0}", this.generator.GetDomain().ToString());

    if (this.history.Count >= this.history.Capacity)
    this
.history.RemoveAt(0);

    this.history.Add(img);
    this
.currentImage = this.history.Count - 1;
    this
.tsslIndex.Text = string.Format("Image {0}/{1}", this.currentImage + 1, this.history.Count);
    this
.pbxImage.Image = img;
    this
.traceSel = false;

    Cursor.Current = Cursors.Default;
}

For those who never heard about that (or did and always wondered what it was) I provide here a short explanation. It is a useful thing to know for anyone, and expecially important for graphic programmers and digital artists.
Back in the days when we still were using CRT displays, the intensity value of a screen pixel was related to the voltage applied to the circuits. The voltage was modulated by the value of that pixel in the image data. If the image pixel was two times bigger than another, the voltage applied to that screen pixel was two times bigger as well.

It happened though that the brightness of the screen pixel was not linear with the voltage applied, and this meant that a pixel that was supposed to be two times brighter than another actually was darker than what expected. For this reason they decided to change the pixel values with a non-linear function so that the resulting screen intensities were correct. This process got the name of Gamma Correction.

Old games such as Quake featured the option to change the parameters of that function because different displays could have different responses. Todays LCD technology does not suffer from the same problem, but vendors decided to introduce that behaviour anyway to keep compatibility with older data and software. A standard color space named sRGB has been formulated, and it is the one used for the Web. The code shown performs that transformation on the Fractal output.

Remember that every time a synthetic image is generated in linear space, it must be gamma corrected in order to look correct once displayed. Usually images stored on disk are already gamma corrected. A notably exception are High Dynamic Range images (hdr, exr) which are in linear space.

The rest of the code simply prints informations about the current image: time required to be generated, domain and index.

The RenderTask class

As anticipated, here is the RenderTask code:

class RenderTask
{
    private
IGenerator generator;
    public
Domain Domain { get; set; }
    public
int ResX {get; set;}
    public
int ResY {get; set; }
    public
Image Output { get; set; }

    protected RenderTask()
    {}

    public RenderTask(IGenerator generator, Domain d, int rx, int ry)
    {
        this
.generator = generator;
        Domain = d;
        ResX = rx;
        ResY = ry;
    }

    public void DoWork()
    {
        Output =
this.generator.Draw(Domain, ResX, ResY);
    }
}

Not much, really. When the thread starts it executes the specified method, DoWork(). It simply forwards the call to IGenerator.Draw() with the specified domain and resolution.

The FractalView form does a few more things. A button is provided to allow image saving. In addition, a list of generated images is kept. That list has a maximum size defined by the user. Two more buttons allow the user to switch through the buffered images.