“数学是上帝描写自然的语言”伽利略说,爱因斯坦也说过,“纯数学让我们发现这些概念和联系这些概念之间的规律,这些概念和规律给了我们理解自然现象的钥匙“。数学一词在西方源于古希腊语,意思是通过学习获得知识的意思,因此早期的数学涵盖的范围比我们今天讲的数学要广得多,和人类的生活以更接近一些。但是和任何事物一样,数学也在不断的演化,这个发展过程使得数学变得高深起来,渐渐大家头脑里想到的数学只剩下数字、符号、公式和定理了。就这样数学看上去和生活、和美似乎渐行渐远了,但其实不然。

 

Jacob Joaquin-Sine Waves Selfie

 

感谢Jacob Joaquin大佬用P5.js写了SineWaves(见下图),用代码很好的展现了数学之美中的三角函数之美。这也很契合我的一个想法,“代码和文字、音乐、图片、画作都一样,都可以用来表现这个世界的智慧,这个世界的美”。

 

SineWaves

 

接下来首先我会为大家介绍一下三角函、随机数几种常见的分布、HSB颜色模式,然后讲解如何用Processing实现SinWaves,之后会介绍我在SinWaves的基础之上实现的SinWavesPlus(如下图)。在最后的拓展部分,我会大概介绍一下三角函数在生活中的应用:用余弦定理进行新闻分类。

 

SineWaves

 

希望大家能够喜欢!当然心急的同学可以先看看以上两个作品的在线展示和代码:

Jacob Joaquin的SinWaves:https://www.openprocessing.org/sketch/617085

我的SinWavesPlus:https://www.openprocessing.org/sketch/711256

我的github:https://github.com/pearmini/processing/tree/master/SineWaves

 

分析SinWaves发现,可以将作品看成由一系列的格子构成,而每一个格子里面有一个图案。其中,每一个图案处处相似,却又都不尽相同。相似是因为这些图案都是用正弦函数来绘制的,不同是因为函数设定的参数不同,而这些参数都是随机产生的。所以在准备阶段,我们先来大概了解一下正弦函数和常用的几种随机数的分布。

 

1.三角函数

“ Trigonometry is a sine of the times.”

相信大家对三角函数都不陌生:正弦、余弦、正切函数等,而今天我们的主角是:正弦函数。下面我们先来看一个最简单的正弦函数的图形: 

我们看到正弦函数输出的是介于-1和1之间的平滑曲线。这种类型的图形我们称之为“波动”。两点之间的周期性运动。吉他琴弦的运动,钟摆的运动,弹簧的运动等这些都属于波形运动。

但是这样的波动看上去难免单调,那接下来我们就看看如何来自定义一个与众不同的波动。

 

2.振幅

首先我们可以改变波动的高度,也就是函数的振幅:。大家可以比较一下当A分别等于0.5, 1, 2的时候的函数图像。

观察可得:当A=0.5时,正弦函数输出的是介于-0.5和0.5之间的平滑曲线;当A=0.5时,正弦函数输出的是介于-2和2之间的平滑曲线。也就是说A的绝对值越大,波动越高。

 

3.周期

然后我们可以改变波动的宽度,也就是函数的周期:。大家可以比较一下当T分别等于0.5, 1, 2的时候的函数图像。

观察可得:当T=0.5时,正弦函数输出的平滑曲线每过4π重复一次,周期为 4π;当T=2时,正弦函数输出的平滑曲线每过0.5π重复一次,周期为0.5π。也就是说$的绝对值越大,波动越窄。

 

4.初相

最后我们可以改变波动在水平方向的位置,这个可以通过改变初相实现:。大家可以比较一下当P分别等于0.5π,0,-0.5π的时候的函数图像。

观察可得:当P=0.5π时,正弦函数输出的平滑曲线相对于 向左平移了0.5π个单位,初相为0.5π;当P=-0.5π时,正弦函数输出的平滑曲线相对于 向右平移了-0.5π个单位,初相为-0.5π。也就是我们可以通过增加或者减小P来控制波动向左或者向右移动。

 

目前我们对正弦函数有了一个初步的了解,知道了决定波动图像形状和位置的主要参数。那么接下来我们就来看看设计其这些参数大小的基本思想。

 

1.随机数的分布

观察作品可以发现里面有100个波动图形,而显我们然不可能亲自设置每一个波动图案对应的正弦函数的参数。一种很好的办法就是采用随机的方式构建我们的波动系统,但是随机数的分布却大有学问。下面就让我们来看一下几种比较常用的随机数分布方式。

 

均匀分布

最常用的随机数分布方式应该是均匀分布了,在取值范围内获得每个值的概率都是相等的。

在Processing中我们可以用random函数来实现该效果。每一次调用random函数的时候,都会随机返回一个在指定范围里面的值。如果我们只是传入一个参数,会返回一个在0和这个参数之间的浮点数;如果我们传入两个参数,会返回一个在这两个参数之间的浮点数。

下面是返回10个均匀分布的随机数,它们在-5和5之间。

 

float min = -5, max = 5;

int cnt = 10;

float [] nums = new float[cnt];

for(int i = 0; i < cnt; i++){

  nums[i] = random(min, max);

}

 

在我们希望获得随机数没有任何规律和模式的时候可以选择这个方法。下面是个简单的小例子,每次均匀随机选择一个矩形增长高度。

 

正态分布

假如我们要描述真实世界中生物的高度,random函数显然就不恰当了。

 

在北京人行道中,随便挑一个路人,他的身高可能是随机的。但是这种随机性和random()函数产生的随机选并不一样。人们的身高并不是均匀分布的,拥有平均身高的人数总是比特别高或特别矮的人多的多,当然也存在特别高和特别矮的。

 

像这种随机数聚集在平均值附近的情况,我们称之为“正态分布”,也被称作“高斯分布”(以数学家高斯命名),在法国则被称为“拉普拉斯分布”。这两位数学家同时在19世纪初定义了这种分布情况。

 

把这种情况用图形画出来,我们就会得到类似下面的形状,称之为贝尔曲线:

 

这条曲线由一个数学函数产生,它描述了在给定平均值(通常用希腊字母μ表示)和标准差(用希腊字母σ表示)下的概率分布情况。

 

μ的值很好理解,以猴子为例,它们的身高为200~300,那么身高的平均值为250。那么,如果我说标准差为3或者15,你能理解两者之间的区别吗?上面的两张图已经给出了非常直观的答案,左图表示标准差比较小的情况——数据聚集在平均值附近,右图表示标准差比较大的情况——数据相对分散的落在平均值两边。

 

在Processing中,我们调用randomGaussian()从一系列均值为0,标准差为1满足正态分布的浮点数中返回一个浮点数。

 

下面这个例子,我们获得10个满足正态分布均值为320,标准差为60的浮点数。

 

float sd = 60, mean = 320;

int cnt = 10;

float [] nums = new float[cnt];

for(int i = 0; i < cnt; i++){

float x = randomGaussian();

nums[i] = sd * x + mean;

}

 

在我们希望获得的随机数大多数在某个值附近聚集,少部分出现极端情况下时可以用这种方法。在SineWavesPlus中我们会用到它。下面是一个简单的例子,按照正态分布分布随机选择一个位置画圆。

 

 

Perlin噪声

就像我们前面人的身高的例子,一定程度的随机性有利于我们对真实世界的行为模式进行编程建模。然而把随机性当作唯一的指导原则是不符合自然特征的。有个算法——“Perlin 噪声”(以其发明者“Ken Perlin”命名),把这个因素考虑进去了。

 

Perlin噪声比随机数展现出更符合“自然”的外观形象,因为它能生成符合自然排序(平滑)的伪随机数。下面左边的图片显示了经过“Perlin噪声”处理的效果,右图则展示了原始的随机数效果。

 

 

在Processing中已经内置了Perlin噪声算法的实现:noise()函数。noise()函数可以有1~3个参数,分别对应一维、二维、三维的计算,简单起见,我们这里就以一维为例。

 

获得一个范围在0到1之间noise随机数只用给noise函数传入任意的值t,然后将t值增加,再次传入noise函数,我们就可以获得下一个noise随机数。传入的t值不同,一般情况下获得的noise随机数不同。

 

t值递增的速度会影响噪声的平滑度。如果我们让t发生很大的跳跃,那么很多中间值会被跳过,得到的值也更随机。见下图。

 

下面是一个简单的例子,获得10个范围在0到10的noise随机数。

 

float t = 1;

float min = 0, max = 10;

int cnt = 10;

float [] nums = new float[cnt];

for(int i = 0; i < cnt; i++){

float x = noise(t);

t += 0.01;

nums[i] = map(x, 0, 1 , min, max);

}

 

当我们希望获得随机数具有某种特定的模式的时候我们可以选择这种方式。下面是使用了noise随机数模拟的自然界中随风摇曳的树和随波逐流的小舟。

 

自定义分布

如果以上Processing内置的随机数生成器都不能满足我们的需求呢?没问题,我们可以DIY。

 

固定概率

在概率是个常数的时候,定义一个随机数生成器很简单。比如我希望获得一个在0到100间的浮点数。其中,这个数在90到100之间的概率是百分之九十,在0到90之间的概率是百分之十,那么我们就可以如下定义:

 

float randomGenerator(){

float probability = 0.1;

if(random(1) < probability){

return random(90, 100);

}else{

return random(0, 90);

}

}

 

下面个这个例子获得以上10个自定义分布的随机数。

 

int cnt = 10;

float [] nums = new float[cnt];

for(int i = 0; i < cnt; i++){

nums[i] = randomGenerator();

}