已获许可,转载:形形色色的空间填充曲线 和 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,来看下会生成什么图形