在本文中,将展示大约两个月的工作成果。最初对分形感兴趣,最终完成了一个能够在大约2秒内生成Mandelbrot集的程序。这个项目极大地帮助了学习编程,尤其是在优化和效率方面。
首先,需要使用复数,每个复数都有一个“实部”和“虚部”。这可以写成:
Z = x + i * y
其中x是实部,y是虚部。i对应于虚数中的“i”。由于复数有两个部分(x,y),可以在x-y图上绘制它们。在案例中,将它们绘制在图像上。每一行像素对应一组复数Z,它们都有相同的y值,每个x像素的增量代表x值的一步。
现在,将计算某个x,y值范围内的集合。然后为每个x,y坐标分配一个像素,例如x = 0.3,y = 0.2。这个像素的颜色将使用Mandelbrot方程计算。
方程是:
Z_(n+1) = Z_n^2 + c
其中c是绘制给定像素的坐标,坐标c = x + i * y。n是迭代次数,从n = 0开始,也从Z_0 = 0 + i * 0开始。所以,在n = 0时,Z_(n+1) = Z_1 = Z_0^2 + c = c。所以Z_1 = c。
现在可以将这个Z值放入方程中得到Z_2。在n = 1时,Z_(n+1) = Z_2 = Z_1^2 + c。继续这样迭代,每次迭代生成Z_(n+1)。
但是应该在什么时候停止迭代呢?每次得到一个新的Z_(n+1)值,也会计算一个叫做复模的东西,写成:
| Z_(n+1) |
它是使用勾股定理计算三角形斜边的长度:
| Z_(n+1) | = √(x^2 + y^2)
Mandelbrot告诉,如果| Z_(n+1) | ≤ 2,就停止迭代,否则继续迭代。计算在方程收敛(停止迭代)之前完成的迭代次数n。n值用于表示c = x + i * y处像素的颜色。
集合中有一些部分的n看起来是无限的,或者至少变得如此之大,以至于不可能继续迭代。设置一个最大迭代次数nMax,并总是停止迭代如果n达到nMax。
事实证明nMax相当重要!一个较大的nMax需要更长的时间来计算,但可以显示非常精细的细节,接近Mandelbrot方程收敛(彩色部分)和发散(白色)的地方之间的边界。然而,一个低nMax也可以产生很好的图像,因为颜色尺度可以很好地工作。
请注意:在程序中,k用作迭代计数,kMax用作最大迭代次数。
决定让程序不仅仅是生成主Mandelbrot集,所以它有一些其他特性。添加了一个缩放功能,可以通过点击鼠标来设置想要缩放的量,并渲染新图像。当然,放大得越多,就需要更多的迭代来支持高细节。
能够在“收藏夹”功能中保存Mandelbrot集的一个区域。这会将图像的所有参数保存到一个文本文件中,可以从中读取以加载收藏夹。
每次渲染新图像时,它都会将用于创建图像的参数保存到一个文本文件中。这样,可以使用“撤销”功能返回到图像。
除了显示上次渲染所花费的时间的计时器外,认为保存图像的功能也很不错。图像以PNG格式保存,可以选择一个文件名。
所有保存的文件都存储在C:\Users\%username%\mandelbrot_config中。
该程序是一个Windows表单应用程序。它可以在Microsoft Visual Studio环境中启动。或者,可执行文件可以作为普通的Windows应用程序运行。
用户界面是自解释的,控制允许绘制x,y坐标的范围,允许的最大迭代次数,分辨率(像素步长)和缩放控制。此外,还有其他在本文的“特性”部分提到的控制。
有四个C#类,用途如下:
用于绘制Mandelbrot集的主要代码块如下:
for (double y = yMin; y < yMax; y += xyStep.y) {
int xPix = 0;
for (double x = xMin; x < xMax; x += xyStep.x) {
ComplexPoint c = new ComplexPoint(x, y);
ComplexPoint zk = new ComplexPoint(0, 0);
int k = 0;
do {
zk = zk.doCmplxSqPlusConst(c);
modulusSquared = zk.doMoulusSq();
k++;
}
while ((modulusSquared <= 4.0) && (k < kMax));
if (k < kMax) {
if (k == kLast) {
color = colorLast;
} else {
color = colourTable.GetColour(k);
colorLast = color;
}
if (xyPixelStep == 1) {
if ((xPix < myBitmap.Width) && (yPix >= 0)) {
myBitmap.SetPixel(xPix, yPix, color);
}
} else {
for (int pX = 0; pX < xyPixelStep; pX++) {
for (int pY = 0; pY < xyPixelStep; pY++) {
if (((xPix + pX) < myBitmap.Width) && ((yPix - pY) >= 0)) {
myBitmap.SetPixel(xPix + pX, yPix - pY, color);
}
}
}
}
}
xPix += xyPixelStep;
}
yPix -= xyPixelStep;
}
上面的if...else...语句用于处理分辨率。如果像素步长大于1,分辨率将降低,以便在绘制最终图像时没有空白。如果像素步长为1,则图像将正常绘制,即以尽可能高的最高分辨率绘制。
请注意,上面的循环中y像素计数是递减的,而x是递增的。这是因为绘图区域的原点(x,y = 0, 0)在屏幕的左上角。希望从左下角开始绘制图像,然后向右上角工作。这意味着y从其最大值开始。
正如在引言中提到的,学会了如何优化代码。在写这篇文章之前,用程序渲染Mandelbrot集需要相当长的时间:10分钟。上传的这个版本大约需要2秒(AMD A8),而一台快速的PC可以在不到1秒的时间内完成(AMD FX-8350)。
一个关键的性能改进来自于图像的渲染方式。在这个程序的第一个版本中,使用了System.Drawing,每个Mandelbrot集中的像素都被绘制成一个椭圆。这非常消耗CPU。此外,在高分辨率(小像素步长)下绘制时,每个使用System.Drawing绘制的椭圆都与相邻像素重叠,导致图像略微模糊。上传的这个程序版本使用了bitmap,它克服了这些问题,并且还允许在表单隐藏或最小化时保留图像。
颜色映射用于将迭代值n转换为像素颜色。许多人投入了时间研究不同的颜色映射,大多数使用某种查找表,通常还使用某种插值算法。
解决方案非常简单但也非常有效 - 认为生成的图像和任何其他图像一样好。它的工作原理如下:
for iterationCountN, calculate:
hue = (n/nMax) * α
其中α是一个小数,目前设置为0.2。色调很容易转换为标准RGB,使用固定的饱和度(s)和亮度(l)值,即转换:
n → hsl colour → rgb colour.