连绵的山川、飘浮的云朵、岩石的断裂口、布朗粒子运动的轨迹、树冠、花菜、大脑皮层……这些部分与整体以某种方式相似的形体,可以说,就是“分形”的要义了,也恰恰是这些“不规则的”、“分散的”、“支离破碎的”物体又重新让我们认识了自然。比如,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 渲染这些三维模型。倘若你遇到什么问题,同样欢迎你与我一起讨论。