形形色色的空间填充曲线 和 L-System

 

已获许可,转载:形形色色的空间填充曲线 和 L-System - 邓卓的文章 - 知乎 https://zhuanlan.zhihu.com/p/35358486

注:建议使用 Python 3,本文所有代码虽然可以使用 Python2 运行,但图像实现有些缺陷

使用 Python 可以用简单的逻辑画出一些的空间填充曲线,比如下面 Hilbert 曲线、Dragon 曲线 以及 Gosper 曲线,这些曲线都可以『一笔画』画出

上面 3 种曲线的点生成算法都非常简单,不需要 import 任何库

 

# Hilbert 曲线

def _hilbert(direction, rotation, order):
    if order == 0:
        return

    direction += rotation
    _hilbert(direction, -rotation, order - 1)
    step(direction)

    direction -= rotation
    _hilbert(direction, rotation, order - 1)
    step(direction)
    _hilbert(direction, rotation, order - 1)

    direction -= rotation
    step(direction)
    _hilbert(direction, -rotation, order - 1)

def step(direction):
    next = {0: (1, 0), 1: (0, 1), 2: (-1, 0), 3: (0, -1)}[direction & 0x3]

    global x, y
    x.append(x[-1] + next[0])
    y.append(y[-1] + next[1])

def hilbert(order):
    global x, y
    x = [0,]
    y = [0,]

    _hilbert(0, 1, order)

    return (x, y)

 

当然,要将其画出来,需要额外的库。这里使用的是 Matplotlib,然后简单的使用方式是:

import matplotlib.pyplot as plt
x, y = hilbert(4);plt.plot(x, y); plt.show()

 

稍微复杂点的使用方式

import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 4)   
for i in range(4):
    sub = ax[i]
    sub.axis("off")
    sub.set_aspect("equal")
    x, y = hilbert(i + 1)
    sub.plot(x, y)
plt.show()

Dragon 曲线点生成逻辑

def _dragon_A(direction, order):
    if order == 0:
        return direction

    direction = _dragon_A(direction, order - 1)

    direction += 1
    direction = _dragon_B(direction, order - 1)

    step(direction)
    direction += 1

    return direction

def _dragon_B(direction, order):
    if order == 0:
        return direction

    direction -= 1
    step(direction)
    direction = _dragon_A(direction, order - 1)

    direction -= 1
    direction = _dragon_B(direction, order - 1)

    return direction

def step(direction):
    next = {0: (1, 0), 1: (0, 1), 2: (-1, 0), 3: (0, -1)}[direction & 0x3]

    global x, y
    x.append(x[-1] + next[0])
    y.append(y[-1] + next[1])

def dragon(order):
    global x, y
    x = [0,]
    y = [0,]

    direction = 0
    step(direction)
    _dragon_A(0, order)

    return (x, y)


Gosper 曲线点生成逻辑

def _gosper_A(direction, order):

    if order == 0:
        step(direction)
        return

    _gosper_A(direction, order - 1)

    direction -= 1
    _gosper_B(direction, order - 1)

    direction -= 2
    _gosper_B(direction, order - 1)

    direction += 1
    _gosper_A(direction, order - 1)

    direction += 2
    _gosper_A(direction, order - 1)
    _gosper_A(direction, order - 1)

    direction += 1
    _gosper_B(direction, order - 1)

def _gosper_B(direction, order):
    if order == 0:
        step(direction)
        return

    direction += 1
    _gosper_A(direction, order - 1)

    direction -= 1
    _gosper_B(direction, order - 1)
    _gosper_B(direction, order - 1)

    direction -= 2
    _gosper_B(direction, order - 1)

    direction -= 1
    _gosper_A(direction, order - 1)

    direction += 2
    _gosper_A(direction, order - 1)

    direction += 1
    _gosper_B(direction, order - 1)

cos30 = 3.0 ** 0.5 / 2.0
sin30 = 0.5
nexts = {0: (cos30, sin30), 1: (0, 1), 2: (-cos30, sin30), 3: (-cos30, -sin30), 4: (0, -1), 5: (cos30, -sin30)}
def step(direction):
    next = nexts[direction % 6]

    global x, y
    x.append(x[-1] + next[0])
    y.append(y[-1] + next[1])

def gosper(order):
    global x, y
    x = [0,]
    y = [0,]

    _gosper_A(0, order)

    return (x, y)

上面 3 种曲线的点生成算法,虽然简单,但是每一次对于一个新的曲线或者图形都要新写的一段逻辑,还是略显麻烦。实际上有一个通用的规则来描述各种曲线和图形,然后对于这个通用规则可以写一个解释器,就可以很容易的描述一个新的曲线和图形。

L-system

L-system,全称 Lindenmayer system,是一位植物学家开发的。在本文中会实现一个简单的 L-system 解释器,因为只使用部分功能,所以这个解释器只是 L-system 的一个子集

L-system 可以使用一个非常形式化的 三元组 [公式] 来定义。但估计大家应该不会对此有兴趣,如果真有兴趣的话可以参考百度百科以及维基百科。

下面我们直接通过示例,来解释 L-system

 

Gosper 曲线的 L-system

{
  "start": "A",
  "rules": {"A": "A-B--B+A++AA+B-", "B": "+A-BB--B-A++A+B"}
  "iterator: 2
}

使用一个字符串来代表 Gosper 曲线的生成过程。根据 start 知道 A 是起始字符串 str,根据 rules 中的替换规则,不断的把 str 中的 A 和 B 替换成箭头右边的字符串,替换次数为预先指定的迭代次数 iterator

第 0 次,起始 A
第 1 次 A => A-B--B+A++AA+B-
第 2 次 A-B--B+A++AA+B- => A-B--B+A++AA+B--+A-BB--B-A++A+B--+A-BB--B-A++A+B+A-B--B+A++AA+B-++A-B--B+A++AA+B-A-B--B+A++AA+B-++A-BB--B-A++A+B-

替换完成后,最后赋予字符串几何意义,代表如下意思 —— 任意设定起点和方向,从左到右扫描字符串,遇到 A / B 就前进一格,遇到 + 向左旋转 60°,遇到 - 向右旋转 60°。如此就可以得到一个 Gosper 曲线

{
  "rotate": 60,
  "actions": {"+": "left", "-": "right", "A": "forward", "B": "forward"}
}

下面我们来实现一个简单的 L-system 解释器,并验证如上的 Gosper 曲线规则

def replacement(str, rules, order):
    for i in range(order):
        dst = ""
        for s in str:
            if s in rules:
                dst += rules[s]
            else:
                dst += s
        str = dst
    return str

def interpretation(str, actions, rotate, angle, x, y):
    import math

    point_x = [x,] 
    point_y = [y,]

    for s in str:
        if s not in actions:
            continue
        if actions[s] == "left":
            angle -= rotate
        elif actions[s] == "right":
            angle += rotate
        elif actions[s] == "forward":
            r = angle / 180.0 * math.pi
            point_x.append(point_x[-1] + math.cos(r))
            point_y.append(point_y[-1] + math.sin(r))
    return point_x, point_y

验证

# Gosper 曲线
grammer = {
  "start": "A",
  "rules": {"A": "A-B--B+A++AA+B-", "B": "+A-BB--B-A++A+B"},
}

geometry = {
  "rotate": 60,
  "actions": {"+": "left", "-": "right", "A": "forward", "B": "forward"}
}

import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 4)   
for i in range(4):
    str = replacement(grammer["start"], grammer["rules"], i + 1)
    x, y = interpretation(str, geometry["actions"], geometry["rotate"], 30, 0, 0)

    sub = ax[i]
    sub.axis("off")
    sub.set_aspect("equal")
    sub.plot(x, y)

plt.show()

有了这个最基本的 L-system 解释器,我们再来画别的曲线就很容易了

# Sierpinski arrowhead 曲线
grammer = {
  "start": "A",
  "rules": {"A": "B-A-B", "B": "A+B+A"},
}

geometry = {
  "rotate": 60,
  "actions": {"+": "left", "-": "right", "A": "forward", "B": "forward"}
}

# Sierpinski triangle
grammer = {
  "start": "F-G-G",
  "rules": {"F": "F-G+F+G-F", "G": "GG"},
}

geometry = {
  "rotate": 120,
  "actions": {"+": "left", "-": "right", "F": "forward", "G": "forward"}
}

# Koch snowflake
grammer = {
  "start": "F",
  "rules": {"F": "F+F--F+F"},
}

geometry = {
  "rotate": 60,
  "actions": {"+": "left", "-": "right", "F": "forward"}
}

# Hilbert 曲线
grammer = {
  "start": "A",
  "rules": {"A": "-BF+AFA+FB-", "B":"+AF-BFB-FA+"},
}

geometry = {
  "rotate": 90,
  "actions": {"+": "left", "-": "right", "F": "forward"}
}

# Dragon 曲线
grammer = {
  "start": "FX",
  "rules": {"X": "X+YF+", "Y": "-FX-Y"},
}

geometry = {
  "rotate": 90,
  "actions": {"+": "left", "-": "right", "F": "forward"}
}

# 不知名的图形
grammer = {
  "start": "F-F-F-F-F",
  "rules": {"F": "F-F++F+F-F-F"},
}

geometry = {
  "rotate": 72,
  "actions": {"-": "left", "+": "right", "F": "forward"}
}

上面这个 L-system 实现的非常简单,只能描述『一笔画』的曲线。现在在几何意义上添加两个动作 —— 入栈 和 出栈

入栈代表将当前的 点和角度 入栈,出栈代表将当前的 点和角度 出栈,新的 L-system 如下(使用 [ 和 ] 表示这两个动作)

def replacement(str, rules, order):
    for i in range(order):
        dst = ""
        for s in str:
            if s in rules:
                dst += rules[s]
            else:
                dst += s
        str = dst
    return str

def interpretation(str, actions, rotate, angle, x, y):
    import math

    point_x = [x,] 
    point_y = [y,]
    coordinates = [[point_x, point_y],]

    stack = []
    for s in str:
        if s not in actions:
            continue
        if actions[s] == "left":
            angle -= rotate
        elif actions[s] == "right":
            angle += rotate
        elif actions[s] == "forward":
            r = angle / 180.0 * math.pi
            point_x.append(point_x[-1] + math.cos(r))
            point_y.append(point_y[-1] + math.sin(r))
        elif actions[s] == "push":
            stack.append((angle, point_x[-1], point_y[-1]))
        elif actions[s] == "pop":
            angle, _x, _y = stack[-1]
            stack = stack[:-1]

            point_x = [_x,]
            point_y = [_y,]
            coordinates.append([point_x, point_y])
            
    return coordinates
现在我们找几个示例来测试下

grammer = {
  "start": "X",
  "rules": {"X": "F+[[X]-X]-F[-FX]+X", "F": "FF"},
}

geometry = {
  "rotate": 25,
  "actions": {"-": "left", "+": "right", "F": "forward", "[": "push", "]": "pop"}
}

import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 4)   

for i in range(4):
    str = replacement(grammer["start"], grammer["rules"], i + 3)
    coordinates = interpretation(str, geometry["actions"], geometry["rotate"], 60, 0, 0)
    print(coordinates)

    sub = ax[i]
    sub.axis("off")
    sub.set_aspect("equal")
    for coordinate in coordinates: 
        x, y = coordinate
        sub.plot(x, y)

plt.show()

# 不知名植物二
grammer = {
  "start": "X",
  "rules": {"X": "F[+X]-X", "F": "FF"},
}

geometry = {
  "rotate": 25,
  "actions": {"-": "left", "+": "right", "X": "forward", "F": "forward", "[": "push", "]": "pop"}
}

# 不知名植物三
grammer = {
  "start": "FX",
  "rules": {"X": "FF+[+F]+[-F]", "F": "FF-[-F+F]+[+F-F]"},
}

geometry = {
  "rotate": 25,
  "actions": {"-": "left", "+": "right", "X": "forward", "F": "forward", "[": "push", "]": "pop"}
}

L-system 的表达能力比上面描述的要更加强大。在实践中,一个完整 L-system 实现有很实际的应用,比如园林设计。在阅读关于空间填充曲线的资料,写了一些曲线实现,后来发现一个简单的 L-system 就有很强的表达能力,觉得非常有意思,于是写了本文。后面举的例子已经不是空间填充曲线了,但同样简单,若有兴趣,可以自定义编写 grammer 和 geometry,来看下会生成什么图形

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值