复杂分形,简单规则:门格海绵世界探秘

连绵的山川、飘浮的云朵、岩石的断裂口、布朗粒子运动的轨迹、树冠、花菜、大脑皮层……这些部分与整体以某种方式相似的形体,可以说,就是“分形”的要义了,也恰恰是这些“不规则的”、“分散的”、“支离破碎的”物体又重新让我们认识了自然。比如,Menger Sponge(Wikipedia),因奥地利数学家卡尔·门格在1926年的描述而得名。它是一个通用曲线,因为它的拓扑维数为一,且任何其它曲线或图都与门格海绵的某个子集同胚。

2017年的夏季学期,金伯顿学校的学生和员工们花费了1000多个小时,建造了一个单人高的3级门格海绵(YouTube 视频)。
Kimbolton School's Level 3 Menger Sponge
下图是纽约数学博物馆提出的展品之一。参观者可以将两块门格海绵分开,发现沿对角线的孔不是正方形,而是六面星形。
“这是一个人们长期研究的众所周知的课题,”博物馆负责人乔治哈特说,“但直到最近,才有人想到用这种有趣的方式来分割它。”
门格海绵在切开时的外观
在国外,门格海绵作为分形世界的 “Super Star”,拥有着独特的理性魅力。数学的智慧真是一座开采不尽的宝藏,它促使人类对身处其中的自然世界产生新的探索与发现。看似复杂的分形图形,实际上的规则却是很简单:

  1. 从一个正方体开始,(第一个图像)把正方体的每一个面分成9个正方形。这将把正方体分成27个小正方体,像魔方一样。
  2. 把每一面的中间的正方体去掉,把最中心的正方体也去掉,留下20个正方体(第二个图像)。
  3. 把每一个留下的小正方体都重复第1-3个步骤。
    门格海绵的迭代过程。图源:Wikipedia
    举个栗子,二级海绵的生成过程:先将最初的大立方体分成大小相等的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 渲染这些三维模型。倘若你遇到什么问题,同样欢迎你与我一起讨论。

posted @ 2020-01-12 10:00  升卿  阅读(692)  评论(0)    收藏  举报