使用OpenGL进行Mandelbrot集的可视化
Mandelbrot集是哪一集??
Mandelbrot集不是哪一集!!啊不对……
Mandelbrot集是哪一集!!好像也不对……
Mandelbrot集是数集!!所以……他不是一集而是数集??……
所以这个M...dem...集到底是什么啊??
Mandelbrot集是一个数集
Mandelbrot集
也就是说,曼集的元素都是复数,也就是如下的形式:
为什么叫这么复杂的名字??
本华·曼德勃罗特(Benoit B.Mandelbrot)是分形学(Fractals) 的鼻祖,是的,Fractals这个词就是他提出的。
不过,这个集合本身并不是他本人提出的,而是一个叫做阿德里安·杜阿迪的法国数学家提出的,至于为什么以曼德勃罗特命名,根据wiki的说法是提出者为了向曼德勃罗特致敬而放弃用自己的名字命名。
这是个什么样子的集合??
一个复数
其中,
如果把
由于在其中迭代的前一个
这样一来,若
综上描述,有下结论:
式3即曼集判定的充要条件或定义。
为了让问题变得简单,这里可以令初始值
复数??迭代??还收敛??听起来好像很复杂……
这个……是的,这些东西听起来很不直观,以至于我也花了好久看了大量资料才搞清楚是什么意思。
但是,这件事本身的中心思想并不复杂,其无非做了这样的一件事情:
- 给定一个复数
- 计算
( 会成为下一次的 ) - 就这样,反复执行2,直到山穷水尽之时
仍然没有飞升为 ,那- 否则存在某一次
则
山穷水尽之时??那我恐怕等不到那一天了……
不必沮丧,我们甚至没打算去推导出
但是,在有限的,可预见的次数(迭代数)内,进行这样的判定还是可行的并且能得出结果的。
但是这样的话肯定会有纰漏吧
说的对,因为只要迭代数
所幸,在有限的次数内,我们尽管不能立即断言哪些数一定属于曼集,但我们能断言哪些数一定不属于曼集,那正是:
还没上高速公路就要起飞的,那必然是要玩完的
在有限次计算就体现出明显的发散趋势的时候,那基本也就抢救无效了
好吧好吧,那有哪些值是可以放弃治疗的?
根据迭代函数的定义,
根据绝对值三角不等式(这个对复数域同样成立,实际上就是向量绝对值三角不等式的移植),若
由于迭代初始条件
上面的不等数右侧产生了一个很熟悉的形式——幂级数,如果要幂级数
在后面的程序中,我们将使用这个值作为发散判定,意思就是说,如果算出
但是,复数的收敛并不限于模收敛,当复数被整理为辐角形式
哇,断言的这么轻松??何出此言??
其实很容易,找到这样的向量,利用迭代函数平方相加后保持模不变即可,所幸的是,这样的值非常有限,因为首先一个条件就将范围急剧缩减:
是的,只有
也就是说
是随意取得,也就是说幅角就可以是任意值,当然, 的取值决定于 ,也就是 可以和 构造连续的函数映射,那么你根本保证不了 一定能够避开这些值啊你个代数白痴!!
啊是的,但是你很快就会意识到一个事实:
这个丧钟,转不久的!!
什么??
确实,当满足上述两个条件之后,得到的
然后,夹角条件被打破了!!
就像雪崩一样,这个微妙的平衡很快就被打破了,并且马上开始急剧的发散(或收敛)。
如丸走坂
- Ramp Rollerball
这样一来,通过多次的迭代运算,不合格的点一点一点被筛出,如果次数足够多,这个集合最后形成的图形被称为Mandelbrot分形。
大概长出来是这样的:
这个M..d什么b什么的分形有什么特点么??
作为一个分形,它最大的特点之一就是:
德罗斯特效应(自相似性,Droste Effect)
如果对这个图形局部放大,你会看到这个与最开始看到一致的子图性,而且:
放大一次有,一直放大一直有!放大放大再放大!每一个子部都看的清清楚楚!
德罗斯特效应本质是递归式的图形体现,因为迭代函数通过
而具有无限精细的结构也就意味着:它具有有限的面积,而周长却很可能无穷大
当然由于曼集的边缘性质非常复杂,这里就不再详细讨论他的周长是否收敛了(面积收敛是一看就能看出来的),毕竟这篇文章是为了说明如何通过OpenGL实现这个集合的可视化,上面其实已经花了大量的篇幅去推导一个阈值了。
Mandelbrot集的兄弟——Julia集简介
之前是以
— Oh,Julia~
— Oh,Mandelbrot~
— W...What??
同样,因为递归仍然存在,Julia集得到的也会是Julia分形:
迭代函数没变,因此它的发散判定也是和曼集是一致的,这里仅仅介绍一下这个亲戚,并不打算实现这个Julia集的可视化。
OpenGL可视化实现
其实上面的函数都已经推导完毕的情况下,并且也知道集合是如何生成的之后,代码实现就容易多了,绘制曼集的代码如下(只考虑整数点):
基本绘制(二值化)
void DrawMandelbrot(int maxIter) { complex<double> z(0,0); // 位于标准库<complex>头文件中 complex<double> c; complex<double> f(0,0); bool diverge = false; glBegin(GL_POINTS); for (GLint x = -400; x < 400; x++) { diverge = false; for (GLint y = -300; y < 300; y++) { c = { static_cast<double>(x)/150,static_cast<double>(y)/150 }; int iter; for (iter = 0; iter < maxIter; iter++) { z = z*z + c; if ( abs(z) >= 2 ) //发散判定 { diverge = true; break; } } if (!diverge) { glColor3f(0.0f, 0.0f, 0.0f); //集合内黑色 } else { glColor3f(1.0f, 1.0f, 1.0f); //集合外白色 diverge = false; } z = { 0,0 }; glVertex2i(x, y); } } glEnd(); }
效果图:
有问题,如果发散判定值不取2会有什么影响??
问得好,实际上我正想说,其实判定值最好取2,如果实在无法取得的话,也至少请保证它大于2
如果小于2,则有些本来应该在曼集内的点被剔除,这样形成的曼集图形是不完整的。
而如果大于2,当迭代次数非常大的时候,实际上得到的结果与2的时候相差不大(细微的内部可能会有所差别),因为次数非常大的时候,该发散的值一般早都散得没影了,用再大的有限值根本拦不住。但是如果迭代次数非常小的时候,得到的曼集会包含大量冗余的点,因为阈值没能称职地起到约束作用。
迭代着色
当然,无论如何,我们可以让上面的图更加漂亮一些,比如我们以迭代多少次就发散了作为着色的依据,这里对上面的图形进行着色处理。
实际上只需要把集合外的着色代码里面的颜色分量改成与迭代数iter
相关的形式:
//... else { glColor3f(static_cast<float>(iter) / maxIter, (0.5f*iter) / maxIter, 0.15f*iter / maxIter); diverge = false; }
绘制结果:
值得注意的是,由于迭代次数是一个离散的值,因此整个图片的着色显得并不是那般的丝滑和连续,在其他文章中还有提到过对数化将使着色更加柔和漂亮的方法,这里不再阐述。
当指定参数maxIter
的次数不同,形成的图形也不同,并且,随着迭代次数的增大, 形状逐渐趋于稳定,更接近于标准的曼集图样:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签