复杂分形,简单规则:门格海绵世界探秘
连绵的山川、飘浮的云朵、岩石的断裂口、布朗粒子运动的轨迹、树冠、花菜、大脑皮层……这些部分与整体以某种方式相似的形体,可以说,就是“分形”的要义了,也恰恰是这些“不规则的”、“分散的”、“支离破碎的”物体又重新让我们认识了自然。比如,Menger Sponge(Wikipedia),因奥地利数学家卡尔·门格在1926年的描述而得名。它是一个通用曲线,因为它的拓扑维数为一,且任何其它曲线或图都与门格海绵的某个子集同胚。
2017年的夏季学期,金伯顿学校的学生和员工们花费了1000多个小时,建造了一个单人高的3级门格海绵(YouTube 视频)。
下图是纽约数学博物馆提出的展品之一。参观者可以将两块门格海绵分开,发现沿对角线的孔不是正方形,而是六面星形。
“这是一个人们长期研究的众所周知的课题,”博物馆负责人乔治哈特说,“但直到最近,才有人想到用这种有趣的方式来分割它。”
在国外,门格海绵作为分形世界的 “Super Star”,拥有着独特的理性魅力。数学的智慧真是一座开采不尽的宝藏,它促使人类对身处其中的自然世界产生新的探索与发现。看似复杂的分形图形,实际上的规则却是很简单:
- 从一个正方体开始,(第一个图像)把正方体的每一个面分成9个正方形。这将把正方体分成27个小正方体,像魔方一样。
- 把每一面的中间的正方体去掉,把最中心的正方体也去掉,留下20个正方体(第二个图像)。
- 把每一个留下的小正方体都重复第1-3个步骤。
举个栗子,二级海绵的生成过程:先将最初的大立方体分成大小相等的27个小立方体,并对其进行编号。然后,我们只需要去除掉部分小立方体即可。为了更方便地理解,我绘制如下的草图:
比较有趣的是,我在 Google 搜到了一份海绵分形 DIY 的教程。前方高能,手残党绕路。
当在里面装上小灯泡后……
了解了这些,那我们该如何用 Processing 编写一个海绵分形呢?答案在于小盒子的生成。我们可以写一个 generate() 方法,也是核心方法——
ArrayList<Box> generate() {
ArrayList<Box> boxes = new ArrayList<Box>();
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
int sum = abs(x) + abs(y) + abs(z);
float newR = r/3;
if (sum > 1) {
Box b = new Box(pos.x+x*newR, pos.y+ y*newR, pos.z+z*newR, newR);
boxes.add(b);
}
}
}
}
return boxes;
}
比如,编号为“1”的盒子,用(x,y,z)表示,即(-1,-1,-1)。而需要去除的盒子,我们 sum = abs(x) + abs(y) + abs(z) 来找寻它们的共同特征。很明显,他们的共性是 sum<=1。最后,我们返回生成的盒子集。
为了让生成的效果更加炫酷,我给盒子的每一个面贴上我很喜欢的一个电影角色——V。理性、博学、浪漫、绅士。即使屠龙的少年终究已经变成了恶龙。
“I have no tree waiting for me。”
具体实现代码:
/*
* Menger Sponge
* By Hewes
* Further reading: Hewes 的编程艺术(https://zhuanlan.zhihu.com/c_123529691)
*
*/
float a = 0;
ArrayList<Box> sponge;
PImage tex;
void setup() {
size(500, 500, P3D);
noStroke();
textureMode(NORMAL);
tex = loadImage("image.png");
sponge = new ArrayList<Box>();
Box b = new Box(0, 0, 0, 250);
sponge.add(b);
}
void mousePressed() {
// 生成下一个盒子集
ArrayList<Box> next = new ArrayList<Box>();
for (Box b : sponge) {
ArrayList<Box> newBoxes = b.generate();
next.addAll(newBoxes);
}
sponge = next;
}
void draw() {
background(50);
// 光线设置
lights();
pointLight(0, 0, 139, 0, 0, 0);
translate(width/2, height/2);
rotateX(a);
rotateY(a*0.4);
rotateZ(a*0.1);
// 显示每一个盒子
for (Box b : sponge) {
b.show();
}
a += 0.01;
}
void keyPressed(){
saveFrame("file_##.png"); // 按下任意键,保存图片
}
// 盒子类
class Box {
PVector pos; // 盒子的中心位置
float r; // 盒子的大小
Box(float x, float y, float z, float r_) {
pos = new PVector(x, y, z);
r = r_;
}
ArrayList<Box> generate() {
ArrayList<Box> boxes = new ArrayList<Box>();
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
int sum = abs(x) + abs(y) + abs(z);
float newR = r/3;
if (sum > 1) {
Box b = new Box(pos.x+x*newR, pos.y+ y*newR, pos.z+z*newR, newR);
boxes.add(b);
}
}
}
}
return boxes;
}ArrayList<Box> generate() {
ArrayList<Box> boxes = new ArrayList<Box>();
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
int sum = abs(x) + abs(y) + abs(z);
float newR = r/3;
if (sum > 1) {
Box b = new Box(pos.x+x*newR, pos.y+ y*newR, pos.z+z*newR, newR);
boxes.add(b);
}
}
}
}
return boxes;
}
void show() {
pushMatrix();
translate(pos.x, pos.y, pos.z);
box(r);
// 如果贴图已经加载,那我们就可以进行盒子的贴图
//scale(r/2);
//texturedCube(tex);
popMatrix();
}
void texturedCube(PImage tex) {
// 构建盒子的形状,并贴图
beginShape(QUADS);
texture(tex);
// +Z 前面
vertex(-1, -1, 1, 0, 0);
vertex( 1, -1, 1, 1, 0);
vertex( 1, 1, 1, 1, 1);
vertex(-1, 1, 1, 0, 1);
// -Z 后面
vertex( 1, -1, -1, 0, 0);
vertex(-1, -1, -1, 1, 0);
vertex(-1, 1, -1, 1, 1);
vertex( 1, 1, -1, 0, 1);
// +Y 底面
vertex(-1, 1, 1, 0, 0);
vertex( 1, 1, 1, 1, 0);
vertex( 1, 1, -1, 1, 1);
vertex(-1, 1, -1, 0, 1);
// -Y 顶面
vertex(-1, -1, -1, 0, 0);
vertex( 1, -1, -1, 1, 0);
vertex( 1, -1, 1, 1, 1);
vertex(-1, -1, 1, 0, 1);
// +X 右面
vertex( 1, -1, 1, 0, 0);
vertex( 1, -1, -1, 1, 0);
vertex( 1, 1, -1, 1, 1);
vertex( 1, 1, 1, 0, 1);
// -X 左面
vertex(-1, -1, -1, 0, 0);
vertex(-1, -1, 1, 1, 0);
vertex(-1, 1, 1, 1, 1);
vertex(-1, 1, -1, 0, 1);
endShape();
}
}
效果如下:
而就下面的核心代码而言,我们只需要稍加改动…
ArrayList<Box> generate() {
ArrayList<Box> boxes = new ArrayList<Box>();
for (int x = -b; x <=b; x++) {
for (int y = -b; y <=b; y++) {
for (int z = -b; z <=b; z++) {
int sum = abs(x) + abs(y) + abs(z);
float newR = r/c;
if (sum < d) { // 改变 sum 与 d 的大小,会产生意想不到的视觉效果哟!
Box b = new Box(pos.x+x*newR, pos.y+ y*newR, pos.z+z*newR, newR);
boxes.add(b);
}
}
}
}
return boxes;
}
▼
参数赋值: int b=1, c=3, d=3;
大小关系:sum < d
▼
这有些类似于“Jerusalem cube”。
▼
参数赋值: int b=2, c=5, d=6;
大小关系:sum < d
▼
参数赋值: int b=3, c=7, d=6;
大小关系:sum > d
▼
像花椰菜,有木有。
参数赋值: int b=3, c=7, d=6;
大小关系:sum < d
那么,一个漂亮的 Menger Sponge 就这样子完成了。期待后期,我们一起学习如何利用 Processing 渲染这些三维模型。倘若你遇到什么问题,同样欢迎你与我一起讨论。

浙公网安备 33010602011771号