变换是计算机描绘图形的基础。像html5中的Canvas,openGL,等图形库均有变换的功能,一般是translate 移动,rotate 旋转, scale 缩放。配合pushMatrix与popMatrix,使得计算机绘图更加的灵活,方便。
但遗憾的是pygame.draw模块没有提供这种特性,这也使得绘图极为不方便。
所以,我决定自己构建,嵌入Painter,使Painter的绘图更加灵活,方便。
Transform组件可被多继承的方式继承,让继承它的类拥有变换的能力。同时为了避免多继承中的陷阱“菱形继承问题(diamond problem)”,component类型的类不能基础任何类。
这里只实现2d的变换,3d与2d原理是一样的。
提供以下几个主要的API函数:
1. translate(*args) -> None
用于位移,*args可以是两个数,tuple,list与vec2,将下一次绘制的位置位移指定的距离。
2. rotate(*args) -> None
用于旋转,*args可以是三个数x, y, a表示绕(x, y)点旋转a度,也可以是一个数a, 默认绕(0, 0)点旋转,也可以是vec2和a,tuple和a, list和a。注意,这里a为弧度制。
3. scale(*args) -> None
用于缩放,*args同位移的一样。
4. push() -> None
将变换的矩阵压入矩阵栈,即保存之前位移,旋转,缩放变换。类似于openGL中的glPushMatrix()。
5. pop() -> None
将栈顶的变换矩阵弹出,移除之前保存的变换。类似于openGL中的glPopMatrix()
6. resetCurrentMat() -> None
将当前进行变换的矩阵置为单位矩阵。
7. resetAll() -> None
将矩阵栈中的所有矩阵置为单位矩阵
其它还有popAll(),getMatStack(), getCurrentMat()等借助名称就可以知道功能的函数。
构造变换矩阵是计算机图形学的基础,它的理论基础来自于线性代数。
首先是旋转变换:
向量(x1, y1),旋转θ度得到向量(x2, y2),(x1, y1)与x轴夹角为φ。
可知 x1 = rcosφ, y1 = rsinφ, x2 = rcos(θ + φ), y2 = rsin(θ + φ)
这里将x2, y2化简,再将x1,y1带入可等到
x2 = x1 * cosθ - y1 * sinθ
y2 = x1 * sinθ + y1 * cosθ
写成矩阵乘法的形式为:
记为1式
所以可以求得旋转矩阵
而这只是绕原点旋转,如果要绕任意点旋转呢?,先来看看位移,然后再回来讨论
位移变换:
向量从(x1, y1)移动至(x2, y2)
可知 x2 = x1+dx
y2 = y1 + dy
改写成矩阵时,需要一个齐次坐标w,这里w一般是1,即:
这个矩阵就是位移矩阵。
再来看看绕任意点的旋转矩阵怎么求?绕任意点旋转其实都可以引申为绕原点旋转。
具体做法是将任意点移动至原点处,旋转,然后再移动至该任意点处。
这里要重新拿回1式改写成含有齐次坐标的形式:
然后可以求出旋转矩阵绕(x, y)点旋转的矩阵M:
上式中因为原点距离(x, y) 为 dx, dy 就是x, y,所以直接写x,y
乘以M,即:
自此旋转矩阵才算真正构造完毕
缩放变换:
向量(x1,y1)x扩大a倍,y扩大b倍成为(x2,y2)
即 x2 = a * x1
y2 = b * y2
同样构造矩阵乘法为:
将这些变换结合起来,只需要让他们相乘即可。
其实,矩阵的乘法就是各种形变而已,不然它的乘法定义为什么那么奇怪。。。
知道了原理,编写代码就非常容易了。
工欲善其事,必先利其器,有了强大的Transform组件为工具,就不怕绘图难了。
L-system(Lindenmayer System)是一位匈牙利裔(好多厉害的人都是匈牙利裔)荷兰Utrecht大学的生物学和植物学家,林登麦伊尔(Aristid Lindenmayer)于1968年提出的有关生长发展中的细胞交互作用的数学模型,尤其被广泛应用于植物生长过程的研究。
主要是利用规则的迭代产生。
一般的规则是:
'+'代表向顺时针旋转一定的角度,'-'代表向逆时针方向旋转一定角度,'['代表存储形变的数据,']'代表删除形变的数据。还有,一般'F'代表画一条直线,或者也可以用其它字母代替。
规则一般有一个初始值,称为axiom,是迭代的源头,然后是规定的角度,迭代的规则。
例如 axiom: A rule: A->AB, B->A
即第一次结果:A
第二次:AB
第三次:ABA
第四次:ABAAB。。。。。。
比如 axiom: F rule:F->F+[+F-F]
第一次结果:F
第二次:F+[+F-F]
第三次:F+[+F-F]+[+F+[+F-F] -F+[+F-F]]。。。。。。
了解了原理,编码就非常简单了,不一定要用sgf-py,同样的,也可以用python的turtle库来实现,原理都是一样的。
以下是用sgf-py实现的几个例子:
axiom:FX rule:X->[-FX]+FX angle:35
axiom:F rule:F->FF+[+F-F-F]-[-F+F+F] angle:35
axiom:F rule:F->F[+FF][-FF]F[-F][+F]F angle:35
axiom: X rule:'X->F[+X]F[-X]+X','F->FF' angle:35
代码:
class LSystemScene(Scene):
"""L-System 分形树,灵感来源于the Coding Train的视频"""
def __init__(self, *args):
super(LSystemScene, self).__init__(*args)
rule_str1_1 = 'F->FF+[+F-F-F]-[-F+F+F]'
rule_str1_2 = 'X->[-FX]+FX'
rule_str1_3 = 'F->F[+FF][-FF]F[-F][+F]F'
# rule_str2_1 = 'X->F+[[X]-X]-F[-FX]+X'
rule_str2_1 = 'X->F[+X]F[-X]+X'
rule_str2_2 = 'F->FF'
self.__rule = self.rule(rule_str1_1)
self.__rule2_1 = self.rule(rule_str2_1)
self.__rule2_2 = self.rule(rule_str2_2)
self.isFill = False
self.caption = 'L-Systems生成树,鼠标点击,观看树的生长'
self.l_system = LSystem([self.__rule], 'F', math.radians(35), self.screen)
self.l_system.len = 10
class rule: # 内部类
def __init__(self, _str):
self.__str = _str
self.a, self.b = _str.split('->')
def __str__(self):
return self.__str
def setup(self):
self.createTextElement('规则:' + str(self.__rule), color=(153, 217, 234))
self.createTextElement('根:' + 'F', color=(153, 217, 234))
def doMouseButtonDownEvent(self, Button):
self.l_system.generate(self.width / 2, self.height)
class LSystem:
def __init__(self, rules, axiom, angle, sur):
self.__axiom = axiom
self.__rules = rules
self.__sentence = self.__axiom
self.__angle = angle
self.len = 10
self.painter = Painter(sur)
def generate(self, x, y):
nextSentence = ''
for i in range(len(self.__sentence)):
current = self.__sentence[i]
found = False
for j in range(len(self.__rules)):
if current == self.__rules[j].a:
found = True
nextSentence += self.__rules[j].b
if not found:
nextSentence += current
self.__sentence = nextSentence
self.__show(x, y)
def getSentence(self):
return self.__sentence
def __show(self, x=0, y=0):
self.painter.resetCurrentMat()
self.painter.translate(x, y)
for i in range(len(self.__sentence)):
current = self.__sentence[i]
if current == 'F':
self.painter.Lines([point2(0, 0), point2(0, -self.len)], (255, 255, 255), 1, 0)
self.painter.translate(0, -self.len)
elif current == '+':
self.painter.rotate(self.__angle)
elif current == '-':
self.painter.rotate(-self.__angle)
elif current == '[':
self.painter.push()
elif current == ']':
self.painter.pop()