我尝试过许多曼德博集合的绘制算法,包括幼稚的逃逸时间算法以及优化后的逃逸时间算法。但是,有没有更快的算法可以高效地生成像我们在YouTube上看到的那样深入缩放的图像呢?此外,我想了解如何在C/C++ double
之外增加精度的一些想法。
我尝试过许多曼德博集合的绘制算法,包括幼稚的逃逸时间算法以及优化后的逃逸时间算法。但是,有没有更快的算法可以高效地生成像我们在YouTube上看到的那样深入缩放的图像呢?此外,我想了解如何在C/C++ double
之外增加精度的一些想法。
floats/doubles
不足够.这里有一些相关的问题和答案:
这些可能会让你入门...
其中一种加速方式是,您可以像我在第一个链接中所做的那样使用分数逃逸。它可以在保持最大迭代次数低的同时改善图像质量。
第二个链接将为您提供有关分形哪些部分在内部和外部以及距离有多远的近似值。它不是非常精确,但可以用于避免计算"明确在外面"的部分的迭代。
下一个链接将向您展示如何实现更好的精度。
最后一个链接是关于微扰。其思想是只针对某些参考点使用高精度数学,然后使用低精度数学计算其相邻点,而不会失去精度。我从未使用过它,但看起来很有前途。
最后一步是,一旦您实现了快速渲染,您可能希望瞄准这个:
这里是GLSL中单个值使用3个64位双精度的小例子:
// high precision float (very slow)
dvec3 fnor(dvec3 a)
{
dvec3 c=a;
if (abs(c.x)>1e-5){ c.y+=c.x; c.x=0.0; }
if (abs(c.y)>1e+5){ c.z+=c.y; c.y=0.0; }
return c;
}
double fget(dvec3 a){ return a.x+a.y+a.z; }
dvec3 fset(double a){ return fnor(dvec3(a,0.0,0.0)); }
dvec3 fadd(dvec3 a,double b){ return fnor(a+fset(b)); }
dvec3 fsub(dvec3 a,double b){ return fnor(a-fset(b)); }
dvec3 fmul(dvec3 a,double b){ return fnor(a*b); }
dvec3 fadd(dvec3 a,dvec3 b){ return fnor(a+b); }
dvec3 fsub(dvec3 a,dvec3 b){ return fnor(a-b); }
dvec3 fmul(dvec3 a,dvec3 b)
{
dvec3 c;
c =fnor(a*b.x);
c+=fnor(a*b.y);
c+=fnor(a*b.z);
return fnor(c);
}
dvec3
... fnor中的阈值可以更改为任何范围。您可以将此转换为vec3
和float
。
[Edit1]“快速”C++示例
我想尝试我的新SSD1306驱动程序以及我的AVR32 MCU来计算Mandelbrot,以便我可以与这个Arduino + 3D + Pong + Mandelbrot进行速度比较。我使用带有~66MHz无FPU无GPU和128x64x1bpp显示器的AT32UC3A3256。没有外部存储器,只有内部16 + 32 + 32 KByte。 Naive Mandlebrot 太慢了(每帧约2.5秒),所以我做了一些像这样的事情(利用视图的位置和缩放是连续的特点):
为了腐蚀我的输出,因为它只是B&W
n
更改n
会使上一帧无效,以强制重新计算。我知道这很慢,但它仅在缩放范围之间的转换时发生3次。
缩放上一帧的计数不太好看,因为它不是线性的。
可以使用最后的计数,但是这需要记住用于迭代的复杂变量,并且会占用太多内存。
x,y
屏幕坐标映射到哪个Mandelbrot坐标。所以只需查看#3,#4中的数据,如果我们在上一个和实际帧中有相同的位置(更接近像素大小的一半),则复制像素。并重新计算其余部分。
如果您的视图平滑(因此每帧基础上的位置和缩放不会变化很多),这将极大地提高性能。
//---------------------------------------------------------------------------
//--- Fast Mandelbrot set ver: 1.000 ----------------------------------------
//---------------------------------------------------------------------------
template<int xs,int ys,int sh> void mandelbrot_draw(float mx,float my,float zoom)
{
// xs,ys - screen resolution
// sh - log2(pixel_size) ... dithering pixel size
// mx,my - Mandelbrot position (center of view) <-1.5,+0.5>,<-1.0,+1.0>
// zoom - zoom
// ----------------
// (previous/actual) frame
static U8 p[xs>>sh][ys>>sh]; // intensities (raw Mandelbrot image)
static int n0=0; // max iteraions
static float px[(xs>>sh)+1]={-1000.0}; // pixel x position in Mandlebrot
static float py[(ys>>sh)+1]; // pixel y position in Mandlebrot
// temp variables
U8 shd; // just pattern for dithering
int ix,iy,i,n,jx,jy,kx,ky,sz; // index variables
int nx=xs>>sh,ny=ys>>sh; // real Mandelbrot resolution
float fx,fy,fd; // floating Mandlebrot position and pixel step
float x,y,xx,yy,q; // Mandelbrot iteration stuff (this need to be high precision)
int qx[xs>>sh],qy[ys>>sh]; // maping of pixels between last and actual frame
float px0[xs>>sh],py0[ys>>sh]; // pixel position in Mandlebrot from last frame
// init vars
if (zoom< 10.0) n= 31;
else if (zoom< 100.0) n= 63;
else if (zoom< 1000.0) n=127;
else n=255;
sz=1<<sh;
ix=xs; if (ix>ys) ix=ys; ix/=sz;
fd=2.0/(float(ix-1)*zoom);
mx-=float(xs>>(1+sh))*fd;
my-=float(ys>>(1+sh))*fd;
// init buffers
if ((px[0]<-999.0)||(n0!=n))
{
n0=n;
for (ix=0;ix<nx;ix++) px[ix]=-999.0;
for (iy=0;iy<ny;iy++) py[iy]=-999.0;
for (ix=0;ix<nx;ix++)
for (iy=0;iy<ny;iy++)
p[ix][iy]=0;
}
// store old and compute new float positions of pixels in Mandelbrot to px[],py[],px0[],py0[]
for (fx=mx,ix=0;ix<nx;ix++,fx+=fd){ px0[ix]=px[ix]; px[ix]=fx; qx[ix]=-1; }
for (fy=my,iy=0;iy<ny;iy++,fy+=fd){ py0[iy]=py[iy]; py[iy]=fy; qy[iy]=-1; }
// match old and new x coordinates to qx[]
for (ix=0,jx=0;(ix<nx)&&(jx<nx);)
{
x=px[ix]; y=px0[jx];
xx=(x-y)/fd; if (xx<0.0) xx=-xx;
if (xx<=0.5){ qx[ix]=jx; px[ix]=y; }
if (x<y) ix++; else jx++;
}
// match old and new y coordinates to qy[]
for (ix=0,jx=0;(ix<ny)&&(jx<ny);)
{
x=py[ix]; y=py0[jx];
xx=(x-y)/fd; if (xx<0.0) xx=-xx;
if (xx<=0.5){ qy[ix]=jx; py[ix]=y; }
if (x<y) ix++; else jx++;
}
// remap p[][] by qx[]
for (ix=0,jx=nx-1;ix<nx;ix++,jx--)
{
i=qx[ix]; if ((i>=0)&&(i>=ix)) for (iy=0;iy<ny;iy++) p[ix][iy]=p[i][iy];
i=qx[jx]; if ((i>=0)&&(i<=jx)) for (iy=0;iy<ny;iy++) p[jx][iy]=p[i][iy];
}
// remap p[][] by qy[]
for (iy=0,jy=ny-1;iy<ny;iy++,jy--)
{
i=qy[iy]; if ((i>=0)&&(i>=iy)) for (ix=0;ix<nx;ix++) p[ix][iy]=p[ix][i];
i=qy[jy]; if ((i>=0)&&(i<=jy)) for (ix=0;ix<nx;ix++) p[ix][jy]=p[ix][i];
}
// Mandelbrot
for (iy=0,ky=0,fy=py[iy];iy<ny;iy++,ky+=sz,fy=py[iy]) if ((fy>=-1.0)&&(fy<=+1.0))
for (ix=0,kx=0,fx=px[ix];ix<nx;ix++,kx+=sz,fx=px[ix]) if ((fx>=-1.5)&&(fx<=+0.5))
{
// invalid qx,qy ... recompute Mandelbrot
if ((qx[ix]<0)||(qy[iy]<0))
{
for (x=0.0,y=0.0,xx=0.0,yy=0.0,i=0;(i<n)&&(xx+yy<4.0);i++)
{
q=xx-yy+fx;
y=(2.0*x*y)+fy;
x=q;
xx=x*x;
yy=y*y;
}
i=(16*i)/(n-1); if (i>16) i=16; if (i<0) i=0;
i=16-i; p[ix][iy]=i;
}
// use stored intensity
else i=p[ix][iy];
// render point with intensity i coresponding to ix,iy position in map
for (i<<=3 ,jy=0;jy<sz;jy++)
for (shd=shade8x8[i+(jy&7)],jx=0;jx<sz;jx++)
lcd.pixel(kx+jx,ky+jy,shd&(1<<(jx&7)));
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
这个与lcd
和shade8x8
相关的内容可以在链接的SSD1306 QA中找到。不过你可以忽略它,因为它只是抖动并输出像素,所以你可以直接输出i
(即使没有缩放到<0..16>
)。
预览如下(在PC上,因为我懒得连接相机...):
因此,64x32的曼德博集像素显示为128x64的抖动图像。在我的AVR32上,这可能比朴素方法快8倍(也许是3-4fps)... 代码可能更优化,但请注意曼德博集不是唯一在运行的东西,因为我有一些ISR处理程序在后台处理LCD,还有我的TTS引擎基于此,我自那以来大量升级了它,并用于调试(是的,它可以同时说话和渲染)。另外,我内存不够用,因为我的3D引擎需要占用很多 ~11 KByte的空间(主要是深度缓冲区)。
这是预览代码(在计时器内部):
static float zoom=1.0;
mandelbrot_draw<128,64,1>(+0.37,-0.1,zoom);
zoom*=1.02; if (zoom>100000) zoom=1.0;
对于非 AVR32 C++ 环境,请使用以下内容:
//------------------------------------------------------------------------------------------
#ifndef _AVR32_compiler_h
#define _AVR32_compiler_h
//------------------------------------------------------------------------------------------
typedef int32_t S32;
typedef int16_t S16;
typedef int8_t S8;
typedef uint32_t U32;
typedef uint16_t U16;
typedef uint8_t U8;
//------------------------------------------------------------------------------------------
#endif
//------------------------------------------------------------------------------------------
[编辑2]在GLSL中提高浮点精度
曼德博集合的主要问题是需要将指数差异非常大的数字相加。对于+,-
操作,我们需要对两个操作数的尾数进行对齐,将它们作为整数相加,然后再规范化为科学计数法。然而,如果指数差别很大,那么结果尾数所需的位数就会超过32位浮点数所能容纳的位数,因此只保留24位最高有效位。这样就会产生舍入误差,导致像素化。如果您查看32位float
的二进制表示,您将看到以下内容:
float a=1000.000,b=0.00001,c=a+b;
//012345678901234567890123456789 ... just to easy count bits
a=1111101000b // a=1000
b= 0.00000000000000001010011111000101101011b // b=0.00000999999974737875
c=1111101000.00000000000000001010011111000101101011b // not rounded result
c=1111101000.00000000000000b // c=1000 rounded to 24 bits of mantissa
现在的想法是扩大尾数位数。最简单的方法是使用两个浮点数而不是一个:
//012345678901234567890123 ... just to easy count bits
a=1111101000b // a=1000
//012345678901234567890123 ... just to easy count b= 0.0000000000000000101001111100010110101100b // b=0.00000999999974737875
c=1111101000.00000000000000001010011111000101101011b // not rounded result
c=1111101000.00000000000000b // c=1000 rounded to 24
+ .0000000000000000101001111100010110101100b
//012345678901234567890123 ... just to easy count bits
因此,结果的一部分在一个浮点数中,其余部分在另一个浮点数中... 每个单值中的浮点数越多,尾数就越大。但是,在将大尾数精确分成24位块的情况下,在GLSL中执行此操作将会很复杂且速度慢(如果由于GLSL限制可能根本不可能)。相反,我们可以为每个浮点数选择一些指数范围(就像上面的示例一样)。
因此,在这个例子中,对于每个单值(float),我们得到了3个浮点数(vec3)。每个坐标都代表不同的范围:
abs(x) <= 1e-5
1e-5 < abs(y) <= 1e+5
1e+5 < abs(z)
并且 value = (x+y+z)
,因此我们有三个 24 位尾数,但是范围并不完全匹配 24 位。为此,指数范围应该被分割为:
log10(2^24)=7.2247198959355486851297334733878
不要使用10...,可以考虑使用类似于以下内容的东西:
abs(x) <= 1e-7
1e-7 < abs(y) <= 1e+0
1e+0 < abs(z)
(x0+y0+z0) + (x1+y1+z1) = (x0+x1 + y0+y1 + z0+z1)
(x0+y0+z0) - (x1+y1+z1) = (x0-x1 + y0-y1 + z0-z1)
(x0+y0+z0) * (x1+y1+z1) = x0*(x1+y1+z1) + y0*(x1+y1+z1) + z0*(x1+y1+z1)
不要忘记将值归一化回定义范围。避免添加小和大的(绝对值)值,因此避免 x0+z0
等...
[编辑3]新的win32演示CPU vs.GPU
两个可执行文件都预设到相同的位置和缩放级别,以显示何时开始舍入double
s。我必须稍微升级px,py
坐标的计算方式,因为在此位置(阈值仍可能过大)10^9处,y轴开始偏离。
这里是高倍缩放(n=1600)的CPU与GPU预览:
CPU的实时GIF捕获(n=62 ++,GIF 4倍缩小):
我的最快解决方案是通过跟随轮廓边界和填充来避免迭代相同深度的大区域。可能会有一个惩罚,即可能会剪掉小芽而不是绕过它们,但总的来说,这是为了快速缩放所付出的小代价。
一种可能的效率是,如果缩放使比例翻倍,则已经有1/4的点。
对于动画,我会记录每个帧的值,每次将比例翻倍,并在播放时实时插入中间帧,因此动画每秒钟翻倍一次。 double
类型可以存储超过50个关键帧,从而产生持续超过一分钟(进入和退出)的动画。
实际的迭代是通过手工编写的汇编器完成的,因此一个像素完全在FPU中迭代。
C
(屏幕上的位置)的计算有关(在顶点/几何体着色器和片段着色器之间的阶段)。因为当我在GPU上实现更高的精度时,像素化没有改变一点。我想尝试在CPU端使用高精度,以验证我的怀疑......但这也可能只是我的实现中的某个错误。是的,GPU很平滑。 - Spektrevec2
相比原生浮点数将缩放精度提高到4x
,但此后出现了我所谓的“破碎玻璃”像素化...唯一的缺点是,正如我们之前讨论过的那样,我的速度降低了,我必须减半迭代次数才能恢复以前的速度!感谢您的回答:) 但是,仍然有一些概念上的东西,我个人需要理解...对我来说,最初只是曼德博集的简单渲染,现在却带我去了我从未梦想过的地方! - Vivekanand V