前言
近日拜读了Daniel Shiffman先生的著作——《代码本色:用编程模拟自然系统》,决定做一组习作,来对书中提到的随机行为及牛顿运动学进行理解,并对一些实例进行拓展学习,从而提升自己相关方面的知识水平和实践能力。
《代码本色》第0章概述
在第0章中,作者向我们介绍了一系列的随机数算法,以及面向对象编程思想以及面向对象编程思想的应用——建立了一个随机游走类Walker,并进行了很多实践。
随后,作者向我们介绍了Perlin噪声——这一噪声在各领域都有广泛的应用。随后,进行了一维和二维Perlin噪声的实现。
在这一章中,我印象比较深的点是如何实现自定义分布的随机数,因为这将非常实用。其次,Perlin噪声的各种应用也令我印象深刻。下面,我将介绍我根据本章内容而创作的Processing编程习作。
习作
让我们先看看创作的成品:
交互:点击鼠标可以添加新点。
在本次创作中,我使用了Perlin噪声来更新每个点的坐标,并利用ArrayList类和mouseClicked()函数进行点的增加。同时,使用了蒙特卡洛算法进行步长的计算。实现过程将在下面详细介绍。
创作过程
利用Perlin噪声进行随机游走
首先介绍一下什么是Perlin噪声:
一个好的随机数生成器能产生互不关联且毫无规律的随机数。跟我们前面看到的一样,一定程度的随机性有利于有机体和生命活动的建模。然而,单独把随机性作为唯一指导原则是不够的,它并不完全符合自然界的特征。有个算法叫“Perlin噪声”,它就将这一点考虑在内了,该算法是以Ken Perlin命名的。20世纪80年代初,Ken Perlin曾参与电影《电子世界争霸战》(Tron)的制作,在此期间他发明了Perlin噪声算法,用于生成纹理特效。1997年,Perlin因此获得了奥斯卡技术成就奖。Perlin噪声算法可用于生成各种自然特效,包括云层、地形和大理石的纹理。
Processing内置了Perlin噪声算法的实现:noise()函数。noise()函数可以有1~3个参
数,分别代表一维、二维和三维的随机数。
在使用Perlin函数时我们需要注意,noise(x)的参数x表示在x的位置处的随机数值。所以,如果想要得到不同的值,x的取值必须发生变化。
在书中介绍的示例中。作者使用了noise函数分别对一个小球x和y方向的坐标进行了随机。但是由于Perlin函数的取值是平滑的曲线,所以只要x的间隔足够小,小球看上去就是平滑的移动。以下是我参考示例编写的代码:
void walk() {
position.x = map(noise(noff.x),0,1,0,width);
position.y = map(noise(noff.y),0,1,0,height);
noff.add(0.005,0.005,0);
}
这里要提一下map函数的应用,map函数拥有“映射”的功能,能让一个数从一个取值范围线性地映射到另一个取值范围:
上面代码中的Map函数就将一个取值范围为(0,1)的随机数映射到了(0,屏幕宽度或屏幕长度)的取值范围上,确保了小球在屏幕范围内移动。这也就是我们之前提到的,如何实现自定义分布的随机数的方法。
下面放出Walker类的代码:
class Walker {
PVector position;
PVector noff;
Walker() {
position = new PVector(width/2, height/2);
noff = new PVector(random(1000),random(1000));
}
void display() {
strokeWeight(2);
fill(255);
stroke(0);
ellipse(position.x, position.y, 10, 10);
}
void walk() {
position.x = map(noise(noff.x),0,1,0,width);
position.y = map(noise(noff.y),0,1,0,height);
noff.add(0.005,0.005,0);
}
}
可以看到,我将walk()函数里的步长缩小到了0.005,这样一来就可以实现更平滑的移动。
多个点的生成——ArrayList类的使用
在作者提供的实例中,仅仅定义了一个Walker类的对象w,然后对他进行操作
原代码:
Walker w;
void setup() {
size(800, 200);
frameRate(30);
// Create a walker object
w = new Walker();
}
void draw() {
background(255);
w.walk();
w.display();
}
如果我想要定义两个或者多个对象,该怎么办呢?Walker w1;Walker w2;Walker w3;…………
这样未免太笨拙了,所以我使用了ArrayList类来定义,存储,访问一系列的Walker对象。让我们首先来看看ArrayList的定义和常用的函数:
An ArrayList stores a variable number of objects. This is similar to making an array of objects, but with an ArrayList, items can be easily added and removed from the ArrayList and it is resized dynamically. This can be very convenient, but it's slower than making an array of objects when using many elements. Note that for resizable lists of integers, floats, and Strings, you can use the Processing classes IntList, FloatList, and StringList.
An ArrayList is a resizable-array implementation of the Java List interface. It has many methods used to control and search its contents. For example, the length of the ArrayList is returned by its size() method, which is an integer value for the total number of elements in the list. An element is added to an ArrayList with the add() method and is deleted with the remove() method. The get() method returns the element at the specified position in the list. (See the above example for context.)
语法:ArrayList<Type>()
事不宜迟,我们就来用一用这个ArrayList,首先,定义一个列表:
ArrayList<Walker> Walkers=new ArrayList<Walker>();
注意!一定要给这个列表开辟内存,不然会产生NullPointerException的错误!!
申请了一个Walker类型的列表后,我们就可以愉快地加东西啦~
for(int i=0;i<10;i++)
{
Walker newWalker=new Walker();
Walkers.add(newWalker);
}
用add函数,就是这么方便。
随后,在Draw()函数中定义他们的行为。
void draw() {
background(0);
for(int i=0;i<Walkers.size();i++)
{
Walkers.get(i).walk();
Walkers.get(i).display();
}
这样一来,我们就生成了10个Walker的对象,让我们看看效果:
紧接着,为他们连线,这里,我编写了一个二层循环,让每一个点和所有点都进行连线,代码如下:
for(int i=0;i<Walkers.size();i++)
{
stroke(126);
for(int j=0;j<Walkers.size();j++)
{
line(Walkers.get(i).position.x,Walkers.get(i).position.y,Walkers.get(j).position.x,Walkers.get(j).position.y);
}
}
让我们看看效果:
效果非常不错。
下一步,我添加了鼠标交互添加新点的功能,使用了void mouseClicked() 函数,代码如下:
void mouseClicked()
{
Walker newWalker=new Walker();
Walkers.add(newWalker);
}
非常简单,但有效:
蒙特卡洛随机数的使用
完成了上面的工作之后,我对目前作品的感受是,缺乏了点灵动性。
究其原因,是因为我的步长一直不变,这些点有些缺乏“生机”,那么该如何让这些点的移动看起来更有灵动性呢?我决定让我们的步长不再固定,为此,我使用了蒙特卡洛随机数。
让我们先来看看什么是蒙特卡洛随机数。
生成两个随机数,而不是只生成一个随机数。第一个随机数只是
一个普通的随机数。第二个随机数我们称作“资格随机数”,用来决定第一个随机数的取
舍。那些资格更高的随机数被选中的概率更大,而资格更低的随机数被选中的概率更小。
下面是计算步骤(只考虑位于0~1的随机数):
1.选择一个随机数R1;
2.计算R1被选中的资格概率P,假设P = R1;
3.选择另一个随机数R2;
4.如果R2小于P,那么R1就是我们要的随机数;
5.如果R2大于P,回到第(1)步重新开始。
在本例中,一个随机数被选中的资格概率的大小等于其本身。假如我们选中的R1是0.1,
这意味着R1被最后选中的概率是10%。如果R1是0.83,那么它有83%的概率被最后选中。
数字越大,最后被选择的概率也越大。
该算法称为蒙特卡洛算法,以蒙特卡洛大赌场命名
我对该算法进行了改写,让r1和r2的随机数范围可以自定义,并将其写入了Walker类,代码如下:
float montecarlo(float a ,float b){
while (true)
{
float r1 = random(a);
float probability = r1;
float r2 = random(b);
if (r2 < probability)
{
return r1;
}
}
}
随后,将之前固定的步长修改成调用这个函数:
void walk() {
position.x = map(noise(noff.x),0,1,0,width);
position.y = map(noise(noff.y),0,1,0,height);
noff.add(montecarlo(0.01,0.02),montecarlo(0.01,0.02),0);
}
可以看到,这样一来,步长就变成了利用蒙特卡洛算法生成的随机数,增加了随机性,让我们来看看效果:
很明显,点的移动更加具有灵动性了。
下面,附上所有代码:
Walker类:
// The Nature of Code
// Daniel Shiffman
// http://natureofcode.com
// A random walker class!
class Walker {
PVector position;
PVector noff;
Walker() {
position = new PVector(width/2, height/2);
noff = new PVector(random(1000),random(1000));
}
void display() {
strokeWeight(2);
fill(255);
stroke(0);
ellipse(position.x, position.y, 10, 10);
}
// Randomly move up, down, left, right, or stay in one place
void walk() {
position.x = map(noise(noff.x),0,1,0,width);
position.y = map(noise(noff.y),0,1,0,height);
noff.add(montecarlo(0.01,0.02),montecarlo(0.01,0.02),0);
}
float montecarlo(float a ,float b){
while (true)
{
float r1 = random(a);
float probability = r1;
float r2 = random(b);
if (r2 < probability)
{
return r1;
}
}
}
}
主函数:
ArrayList<Walker> Walkers=new ArrayList<Walker>();
void setup() {
size(800, 800);
frameRate(30);
// Create a walker object
for(int i=0;i<3;i++)
{
Walker newWalker=new Walker();
Walkers.add(newWalker);
}
}
void draw() {
background(0);
// Run the walker object
for(int i=0;i<Walkers.size();i++)
{
Walkers.get(i).walk();
Walkers.get(i).display();
}
for(int i=0;i<Walkers.size();i++)
{
stroke(126);
for(int j=0;j<Walkers.size();j++)
{
line(Walkers.get(i).position.x,Walkers.get(i).position.y,Walkers.get(j).position.x,Walkers.get(j).position.y);
}
}
}
void mouseClicked()
{
Walker newWalker=new Walker();
Walkers.add(newWalker);
}
总结
本文介绍了《代码本色:用编程模拟自然系统》第0章的主要内容,并在示例的基础上进行了拓展性的创作。