My answer below tried not to stray too far from EBrown's original. I have improved upon my answer and changed the coloring techniques at this linkimproved upon my answer and changed the coloring techniques at this link.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Original CodeReview Question:
// httphttps://codereview.stackexchange.com/questions/104171/multithreaded-mandelbrot-generator
// Interesting link about coloring:
// http://www.fractalforums.com/programming/newbie-how-to-map-colors-in-the-mandelbrot-set/
// Trusty Wikipedia:
// https://en.wikipedia.org/wiki/Mandelbrot_set
namespace Mandelbrot_Generator
{
public class MandelbrotGenerator
{
// Readonly properties to be set in constructor
public int Width { get; }
public int Height { get; }
public int Iterations { get; }
public short[] Mandelbrot { get; private set; }
// The next properties for Center and scaling are readonly and public, but could very well be private.
public Point Center { get; }
public float ScaleFactor { get; }
public float ScaleFactorSquared { get; }
public SizeF ScaleSize { get; }
// Some get-only public properties.
// In the future, these could possibly be settable but that would require additional validation.
// To prep ahead of time for such future possibilities, these are properties instead of constants or fields.
public int NumberOfSections => 256;
public int NumberOfColors => 32;
public int NumberOfCores => Math.Max(Environment.ProcessorCount - 1, 1);
public MandelbrotGenerator(int width, int height, short iterations)
{
// Use some very basic level limit checking using some arbitrary (but practical) limits.
const int imageLow = 512;
const int imageHigh = 4096 * 8;
const int iterationLow = 128;
const int iterationHigh = (int)short.MaxValue - 1;
CheckLimits(nameof(width), width, imageLow, imageHigh);
CheckLimits(nameof(height), height, imageLow, imageHigh);
CheckLimits(nameof(iterations), iterations, iterationLow, iterationHigh);
Width = width;
Height = height;
Iterations = iterations;
Center = new Point(Width / 2, Height / 2);
// WARNING - WARNING - WARNING
// The next bit of code is not very solid.
// It works best if the Width is at least twice the Height.
// Anything else can produce a truncated or goofy looking Mandelbrot.
// And we'll scale the size so the brot looks "normal", i.e. not stretched or scrunched.
// This scaling depends upon the ratio of the width to height.
// ScaleFactorSquared is a cached value because it is used repeated inside loops.
if (width >= height)
{
ScaleFactor = (float)width / (float)height;
ScaleFactorSquared = ScaleFactor * ScaleFactor;
ScaleSize = new SizeF(Center.X / ScaleFactor, Center.Y);
}
else
{
ScaleFactor = (float)height / (float)width;
ScaleFactorSquared = ScaleFactor * ScaleFactor;
ScaleSize = new SizeF(Center.X, Center.Y / ScaleFactor);
}
}
private void CheckLimits(string name, int value, int inclusiveLow, int inclusiveHigh)
{
if (value < inclusiveLow || value > inclusiveHigh)
{
throw new ArgumentOutOfRangeException(name, $"Argument must be between {inclusiveLow} and {inclusiveHigh} inclusively.");
}
}
public void CreateMandelbrot()
{
Mandelbrot = new short[Width * Height];
var sections = GetHoriztonalSections();
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = NumberOfCores;
Parallel.ForEach(sections, options, section =>
{
var data = GenerateSection(section);
for (var y = section.Start.Y; y < section.End.Y; y++)
{
var brotOffset = y * Width;
var dataOffset = (y - section.Start.Y) * Width;
for (var x = 0; x < Width; x++)
{
Mandelbrot[brotOffset + x] = data[dataOffset + x];
}
}
});
}
public void SaveImage(string filename) => SaveImage(filename, ImageFormat.Png);
public void SaveImage(string filename, ImageFormat imageFormat)
{
if (Mandelbrot == null || Mandelbrot.Length == 0)
{
throw new Exception("You must create the Mandelbrot data set before you can save the image to file.");
}
// Create our colours.
var colors = new Color[NumberOfColors];
for (var i = 0; i < NumberOfColors; i++)
{
colors[i] = Color.FromArgb(255, 0, 0, i * (256 / NumberOfColors));
}
var iterationsPerColor = (int)Math.Ceiling(Iterations / (double)NumberOfColors);
// Create our image.
using (Bitmap image = new Bitmap(Width, Height))
{
for (var y = 0; y < Height; y++)
{
var brotOffset = y * Width;
for (var x = 0; x < Width; x++)
{
image.SetPixel(x, y, colors[Mandelbrot[brotOffset + x] / iterationsPerColor]);
}
}
image.Save(filename, imageFormat);
}
}
private struct Section
{
public Point Start { get; }
public Point End { get; }
public int Height => Math.Abs(End.Y - Start.Y);
public int Width => Math.Abs(End.X - Start.X);
public Section(Point start, Point end)
{
Start = start;
End = end;
}
}
private Section[] GetHoriztonalSections()
{
var sections = new Section[NumberOfSections];
var heightPerSection = (double)Height / (double)NumberOfSections;
for (var i = 0; i < NumberOfSections; i++)
{
sections[i] = new Section(new Point(0, (int)(heightPerSection * i)), new Point(Width, (int)(heightPerSection * (i + 1))));
}
// Note the width is the same per section, namely the image's Width,
// but the very last section's height could be different since
// it's upper rightmost point really should be clamped to the image's boundaries.
var lastIndex = sections.Length - 1;
var lastSection = sections[lastIndex];
if (lastSection.End.Y > Height)
{
sections[lastIndex] = new Section(lastSection.Start, new Point(Width, Height));
}
return sections;
}
private short[] GenerateSection(Section section)
{
var sectionWidth = section.Width;
var data = new short[section.Height * sectionWidth];
for (var y = section.Start.Y; y < section.End.Y; y++)
{
var indexOffset = (y - section.Start.Y) * sectionWidth;
for (var x = section.Start.X; x < section.End.X; x++)
{
// The formula for a mandelbrot is z = z^2 + c, basically. We must relate that in code.
var anchor = new PointF((x - Center.X) / ScaleSize.Width, (y - Center.Y) / ScaleSize.Height);
var iteration = 0;
float xTemp = 0;
float yTemp = 0;
float xSquared = 0;
float ySquared = 0;
for (iteration = 0; iteration < Iterations; iteration++)
{
if (xSquared + ySquared >= ScaleFactorSquared) { break; }
// Important for yTemp to be calculated BEFORE xTemp
// since yTemp depends on older value of xTemp.
yTemp = 2 * xTemp * yTemp + anchor.Y;
xTemp = xSquared - ySquared + anchor.X;
xSquared = xTemp * xTemp;
ySquared = yTemp * yTemp;
}
data[indexOffset + x] = (short)iteration;
}
}
return data;
}
}
}
