创意编程一词,很难去给到它一个定义。
因为人的创意是无限的,用于编程的方法将创意表达出来,这可能就是创意编程的本质。而processing和p5.js是这方面的先驱,它们来源于同一个组织,p5.js是我见过的最棒的js库。
processing网址:https://processing.org
p5.js网址:https://p5js.org
在使用这两款软件的时候,我就在想,sgf-py能不能也可以实现创意编程。
这里要介绍,创意编程可以干什么?
创意编程应该是真正计算机艺术,美妙的图形,不断变化的声音,音乐与图形的交互,输入设备与图形,音乐的交互等。创意编程为人们提供工具,让每个人利用代码,创造独一无二的艺术作品。
同时,创意编程无所不包,大到对3D世界的描述,和小到对一个像素点和一个音调的操作,真实世界所存在的和不存在的一切都可以由创意来实现。
在游戏开发中(我觉得游戏仅仅只是创意编程的一小部分),利用创意编程,主要有两个作用:
1. 使游戏有趣,提高可玩度:
美妙的视觉艺术,和动态的可交互的图形与音乐,绚丽的粒子,无疑将会吸引更多人来游玩。
2. 模拟真实的世界
一般的游戏会有模拟的系统,因为,游戏是来源于人的生活,一切要以符合人的常识为前提。例如,人不会飞,白云一直飘在天空,如果游戏中有不自然或不符合自然逻辑的情况,显然会给玩家或多或少带来“不舒适”的感觉,当然也会有猎奇的游戏出现,但其内在的逻辑大多还是符合常理的。
人不会飞,白云飘在空中,这种是比较好实现的。但还有不那么容易实现,但又相当重要的。比如,如何画出一朵自然的云,如何展现熊熊燃烧的火焰以及川流不息的河流,大理石的纹理怎么显示,连绵不断的山峦怎么显示更加自然......
如果将这些全部交由美工来做,先不说能不能实现,一个海岸线的场景,就能花费大半年的时间了吧。做出来后还无法交互,例如,角色踩到沙滩上,脚印应该是什么样的,浪花拍打海岸,海岸被浸湿的面积是多少等等。
大自然中没有完全相同的两样东西,因为游戏是实时交互的,不像电影只能观赏。通常在一些高质量的3d游戏中,实时渲染(Real-time Rendering)的概念被应用。例如,沙滩上的脚印应该与人的体重,姿态,与沙子的密度......有关系,安照这些实时产生的变量来确定渲染的参数,再利用计算机进行绘图。现在基于物理的渲染(PBR,Physically Based Rendering)与光线追踪(Ray Tracing, RTX系列显卡中RT的含义)技术都是基于这样的思路,来源于计算机图形学(CG,Computer Graphics)的基础。而计算机图形学就是控制计算机绘制图形。
但使用这样技术,有依然无法解决的问题,如连绵起伏的山脉,熊熊燃烧的火焰,大理石,树干的纹理,这样看似以用一般的变量难以解决的问题。山脉的形状是成千上万年的雨水冲刷,地质变化,等引起的,如果按这种思路来解决,那计算量太大,游戏只要一个山脉的样子,没必要付出这么大的代价。同时这个计算的过程存在着“蝴蝶效应(The Butterfly Effect)”,即“对初值的敏感性大”,我们无论如何也不可能构造出一个真正的现实世界的山脉。树叶,树干,大理石纹理,水和火等等同理。
既然已真实世界为基础不行,我们只能换个思路,通过模拟的方法。我们只有条曲线的形状像山的形状就行,我们只有有一副纹理图像大自然中的纹理就行了。模拟在解决“像”的问题,而不是“是”的问题。
幸运的是伟大的先驱者们已经有一套成熟的模拟方法来解决这些问题,例如噪声函数,这其中柏林噪声(Perlin noise)最具代表性。分形(fractal),分形 fractal这个词是分形之父本华.曼德勃罗(Benoit B.Mandelbrot)创造的取自拉丁语的“破碎”之意,分形被称为里程碑式的发现,它不同于传统欧式几何(以欧几里得为中心)研究的理想情况,而是注重于我们的大自然,研究现实世界的情况。感兴趣的话可以去了解一下“混沌与分形”。在这其中,L-系统(L-system, Lindenmayer-system)能最真实的模拟植物生长。并且计算量不是很大。(所以要求计算机配置高的游戏不一定图形表现好,也有可能是他们程序员的实力不强)
这些是图形方面的,还有音乐,比如恐怖游戏中,玩家身边的光越暗,脚步声越大。在竞技游戏Dota2中,双方玩家开始战斗,音乐变的高亢激昂,等等这些表现,都可以大大增强游戏的沉浸感。
这所有的一切归根结底都是创意编程,将自己的灵感,用计算机实现,不是为了实现某种服务来编程。
2d游戏,如果使用上述思想可以使得美工减少工作量的同时,获得更逼真,更有趣的效果。事实上现在大部分操作像是2d的游戏都是采用3d的方式来开发的,例如:奥日与黑暗森林,超级马里奥系列,绝地奇兵等。
其实创意编程在以前,绝非易事。关键是要知道一系列的理论,知道图形库,计算机处理图像程序的逻辑,计算机处理声音信号的方式,图形文件,声音文件(波形文件)的处理,mp3,mp4文件的解码等等。
现在有了processing与p5.js,他们很好的解决了这一点,只通过简单的代码就可以展现出很棒的作品,虽然依然需要知道向量,坐标,的概念,但这也大大减少了以前的学习量。
如何使用sgf-py进行创意编程?很简单,在Scene中,将代码写在draw函数中就可以了,同sgf-py提供向量,矩阵,行列式等用算工具,可以很好的表现出想要的作品。
1. 波纹,该示例来自于p5.js的范例,我试着翻译成sgf-py框架支持的python代码
它可以根据鼠标的位置来调整转动的幅度
代码:
class createWave(Scene):
"""创建波纹
这个例子展示了Scene内置对象 screen。
试着将isFill设置为True或删去,或将sceneCanvas相关的方法移动到初始化方法里,看看结果。
"""
def __init__(self, *args):
super(createWave, self).__init__(*args)
self.isFill = False
self.t = 0.0
self.sceneCanvas = self.screen.copy()
self.caption = '测试场景:波纹'
def draw(self):
self.sceneCanvas.fill((0, 0, 0))
self.sceneCanvas.set_alpha(20)
# 创建圆
for i in range(0, 800, 30):
for j in range(0, 600, 30):
# 每个圆的初始位置取决于鼠标位置
ax = mapping(self.mouseX, 0, 800, -4 * PI, 4 * PI)
ay = mapping(self.mouseY, 0, 600, -4 * PI, 4 * PI)
# 根据圆的位置获取角度
angle = ax * (i / 800) + ay * (j / 600)
# 每个圆做圆周运动
x = i + 20 * cos(2 * PI * self.t + angle)
y = j + 20 * sin(2 * PI * self.t + angle)
Painter(self.sceneCanvas).Circle(Circle(x, y, 10), (0, 255, 0), 0)
self.t += 0.01 # 更新时间
self.screen.blit(self.sceneCanvas, (0, 0))
2. 万花筒,同样,该示例来自于p5.js的范例,我试着翻译成sgf-py框架支持的python代码
代码:
class kaleidoscope(Scene):
"""一个很有趣的万华筒,改写自p5.js的范例 按q键清空屏幕
万花筒是一个光学仪器,具有两个或多个互相倾斜的反射面。 此范例尝试模仿万花筒的效果。 通过 symmetry 变量设定反射的数量,并开始在屏幕上绘制
"""
def __init__(self, *args):
super(kaleidoscope, self).__init__(*args)
self.symmetry = 6
self.angle = math.radians(360 / self.symmetry)
self.isFill = False
self.v = vec2(self.width / 2, self.height / 2)
self.caption = '测试场景:万花筒 按下鼠标按键进行绘制,按q键清屏'
def draw(self):
if 0 < self.mouseX < self.width and 0 < self.mouseY < self.height:
mx = self.mouseX - self.width / 2
my = self.mouseY - self.height / 2
pmx = self.lastMousePos[0] - self.width / 2
pmy = self.lastMousePos[1] - self.height / 2
v1_n = vec2(self.width / 2 + mx, self.height / 2 + my)
v1_l = vec2(self.width / 2 + pmx, self.height / 2 + pmy)
v2_n = vec2(self.width / 2 + mx, self.height / 2 - my)
v2_l = vec2(self.width / 2 + pmx, self.height / 2 - pmy)
if self.mousePressed:
for i in range(0, self.symmetry):
v1_n = self.__rotateBy(v1_n, self.width / 2, self.height / 2)
v1_l = self.__rotateBy(v1_l, self.width / 2, self.height / 2)
v2_n = self.__rotateBy(v2_n, self.width / 2, self.height / 2)
v2_l = self.__rotateBy(v2_l, self.width / 2, self.height / 2)
Painter(self.screen).Lines([v1_n, v1_l], (255, 255, 255), 1, 0, 1)
Painter(self.screen).Lines([v2_n, v2_l], (255, 255, 255), 1, 0, 1)
# 将v以(x, y)点为中心进行旋转
def __rotateBy(self, v, x, y):
v = vec2(v.x - x, v.y - y)
v = v.rotate(self.angle)
v = vec2(v.x + x, v.y + y)
return v
def doKeyEvent(self, Key, Mod, Type, Unicode=None):
if Key == 113:
self.screen.fill((0, 0, 0))
3. 美丽的参数方程:
代码:
class paramEquation(Scene):
"""这个是参数方程的例子, 改写自p5.js范例,灵感来源于Alexander Miller的视频"""
def __init__(self, *args):
super(paramEquation, self).__init__(*args)
self.t = 0 # x 和 y 所依靠的参数通常被视为 t
self.caption = '数学之美:参数方程'
def draw(self):
for i in range(0, 100):
s_point = point2(self.width / 2 + self.__x1(i), self.height / 2 + self.__y1(i))
e_point = point2(self.width / 2 + self.__x2(i) + 20, self.height / 2 + self.__y2(i) + 20)
Painter(self.screen).Lines([s_point, e_point], (255, 255, 255), 1, 0, 1)
self.t += 0.15
# 改变直线的初始 x 坐标
def __x1(self, i):
return sin((self.t + i) / 10) * 125 + sin((self.t + i) / 20) * 125 + sin((self.t + i) / 30) * 125
# 改变直线的初始 y 坐标
def __y1(self, i):
return cos((self.t + i) / 10) * 125 + cos((self.t + i) / 20) * 125 + cos((self.t + i) / 30) * 125
# 改变直线的最终 x 坐标
def __x2(self, i):
return sin((self.t + i) / 15) * 125 + sin((self.t + i) / 25) * 125 + sin((self.t + i) / 35) * 125
# 改变直线的最终 y 坐标
def __y2(self, i):
return cos((self.t + i) / 15) * 125 + cos((self.t + i) / 25) * 125 + cos((self.t + i) / 35) * 125
4. 分形:曼德勃罗集(Mandelbrot Set),分形学的源头,函数y=x^2 + c 迭代的运算在复数域上的图形
代码:
class MandelbrotSet(Scene):
"""分形(fractal)模拟: 曼德勃罗集 Mandelbrot set"""
def __init__(self, *args):
super(MandelbrotSet, self).__init__(*args)
self.caption = '测试场景: MandelbrotSet'
self.width, self.height = 600, 600
self.canvas = pygame.Surface((self.width, self.height))
self.max_iteration = 100
for x in range(self.width):
for y in range(self.height):
a = mapping(x, 0, self.width, -1.5, 1.5)
b = mapping(y, 0, self.height, -1.5, 1.5)
ca, cb = a, b
n = 0
while n < self.max_iteration: # 迭代
aa, bb = a * a - b * b, 2 * a * b
a, b = aa + ca, bb + cb
if abs(a + b) > 16:
break
n += 1
bright = mapping(n, 0, self.max_iteration, 0, 1)
bright = mapping(math.sqrt(bright), 0, 1, 0, 255)
if n is self.max_iteration:
bright = 0
self.canvas.set_at([x, y], (bright, bright, bright))
def draw(self):
self.screen.blit(self.canvas, self.canvas.get_rect())
5. 茱莉亚集(Julia Set)函数y=x^2 + c,c不变在复数域上的图像:
c为复数 -0.70176 -0.3842i
c为复数:0.285 + 0.01i
c为复数:-0.8 + 0.156i
代码:
class JuliaSet(Scene):
"""分形(fractal)模拟: 茱莉亚集 JuliaSet set"""
def __init__(self, *args):
super(JuliaSet, self).__init__(*args)
self.caption = '测试场景: JuliaSet'
self.real = 0.285 # -0.70176 # -0.8 # 实部
self.imaginary = 0.01 # -0.3842 # 0.156 # 虚部
self.width, self.height = 600, 600
self.canvas = pygame.Surface((self.width, self.height))
self.w = 5
self.h = (self.w * self.height) / self.width
self.x_min = - self.w / 2
self.y_min = - self.h / 2
self.x_max = self.x_min + self.w
self.y_max = self.y_min + self.h
self.dx = (self.x_max - self.x_min) / self.width
self.dy = (self.y_max - self.y_min) / self.height
self.max_iteration = 100
self.y = self.y_min
for i in range(self.width):
self.x = self.x_min
for j in range(self.height):
a = self.y
b = self.x
n = 0
while n < self.max_iteration: # 迭代
aa, bb = a * a, b * b
ab_double = 2 * a * b
a, b = aa - bb + self.real, ab_double + self.imaginary
if aa * aa + bb * bb > 16:
break
n += 1
if n == self.max_iteration:
self.canvas.set_at([i, j], (0, 0, 0))
else:
norm = mapping(n, 0, self.max_iteration, 0, 1)
norm = mapping(math.sqrt(norm), 0, 1, 0, 255)
self.canvas.set_at([i, j], (norm, norm, norm))
self.x += self.dx
self.y += self.dy
def draw(self):
self.screen.blit(self.canvas, self.canvas.get_rect())