The Nature of Code:Introduction

Introduction

The Nature of Code:Introduction

nature-of-code/noc-examples-processing: Repository for example code from The Nature of Code book (github.com)

本章将会从最简单的运动仿真——随机游走(random walk)开始。

随机游走(Random Walks)

随机算法是一种并不复杂的算法,随机算法能够用来为世界中的许多现象建模。比如:气体中分子的运动、赌徒在赌场一天的行为。

以随机游走开始介绍这本书,需要记住以下三点:

  1. 本书的核心编程思想是——面向对象编程(object-oriented programming)。随机游走器将作为如何使用面向对象设计处理窗口中移动物体的模板;
  2. 随机游走引出两个贯穿全书的问题:
    • 如何定义控制对象行为的规则;
    • 如何在Processing中应用这些规则;
  3. 本书中,需要对随机性、概率和Perlin噪声有基本的了解。随机游走和它们都有关系;

随机游走类(The Random Walker Class)

面向对象编程基本概念:

  • Object

    Processing中的对象(Object)是一个具有数据和功能的实体;

  • Class

    类(Class)是用来创建对象的模板;

P.S: processing中类介绍

basics on the Processing website

概率和非正态分布(Probability and Non-Uniform Distributions)

计算机图形系统中,产生一个随机数是最很容易的事。然后,我们的目的是使建立的模型更加符合自然界中的特定现象,简单的随机数在这种场景中并不合适,需要调整随机数的使用方法。尤其是在涉及到进化或仿自然仿真(natural-looking simulation)中。仅仅需要一点小技巧就能改变random()方法来产生非正态分布。比如,在进化模型中,该如何选择哪个个体的DNA遗传给下一代?根据达尔文的进化理论——“适者生存”,一个猴群中强壮的、速度快的个体有90%的概率得到繁殖的机会,其余个体只有10%的机会。

首先对概率(probability)的基本概念进行介绍。在独立事件中,概率就是一个事件发生的可能性(likehood)。一个有限可能结果的系统中,某特定事件的概率等于符合该事件的结果数除以所有可能结果的总数。抛硬币就是一个简单的例子,正面朝上及背面朝上的两种结果看来概率相同,每个的概率都是1/2,也就是正面朝上及背面朝上的概率各有50%。多个独立事件发生的概率等于每个事件的概率相乘。

比如连续抛掷三次硬币,都是正面朝上的概率表示为:

有几种方法可以在代码中使用带有概率的 random() 函数。第一种是数组方式,将有限结果事件全部存储在一个数组中,然后调用random()产生事件指针,根据数组输出结果,这种实现方式中,事件发生的概率由数组中的内容决定。

1
2
3
4
5
6
7
8
int [] stuff = new int[5];
stuff[0] = 1;
stuff[1] = 1;
stuff[2] = 2;
stuff[3] = 3;
stuff[4] = 4;
int index = int(random(stuff.length));
int outcome = stuff[index];

运行上面的代码,有outcome为1的概率是2/5,2的概率为1/5,3的概率是1/5,4的概率为1/5;

另外一种方式是生成一个随机数(浮点型0~1),输出结果有随机数的范围决定。

1
2
3
4
5
6
7
8
9
10
11
12
float num = random(1);

If random number is less than 0.6
if (num < 0.6) {
println("Outcome A");
Between 0.6 and 0.7
} else if (num < 0.7) {
println("Outcome B");
Greater than 0.7
} else {
println("Outcome C");
}

随机正态分布( A Normal Distribution of Random Numbers)

自然界中的很多现象能使用正态分布来描述,比如人的身高分布,大部分的人身高在平均身高附近,很高或者很矮的人所占的比例很少,random()函数生成的随机数不能描述这种现象。

正态分布指的是数值分布聚集在均值附近。也叫高斯分布(Gaussian distribution)。将分布绘制成图就是下面这种钟形图。

上面的曲线是由一个数学函数生成,该函数将任何给定值出现的概率定义为均值(μ)和标准(σ)的函数。对于上面的曲线,均值决定最高点的位置,方差决定了最高点的值,右边的方差比左边的方差值更大,表示分布离散度更高。高斯分布特点:

  • 在μ±σ范围的值占比:68%;
  • μ±2σ范围的值占比:98%;
  • μ±3σ范围的值占比:99.7%;

均值和方差计算

给定以下数值:
85, 82, 88, 86, 85, 93, 98, 40, 73, 83
均值:81.3
标准差:15.13

Processing提供了Random类用以处理随机数。

  1. 随机数生成器申明和定义;

    1
    2
    3
    4
    5
    6
    Random generator;
    void setup()
    {
    size(400,400);
    generator=new Random();
    }
  2. 生成服从高斯分布的随机数

    1
    2
    3
    4
    5
    6
    void draw()
    {
    // nextGaussian输出的是double类型数据
    float new_value=(float)generator.nextGaussian();
    }

nextGaussian()函数生成的数服从标准正态分布(均值为0,方差为1)。不同场景下,比如需要绘制均值为200,标准差为60的分布图,只需要将nextGaussian() 生成的数乘标准差,然后再加上均值即可。

1
2
3
4
5
6
7
8
void draw()
{
float sd = 60;
float mean_v = 200;

float num = (float)generator.nextGaussian();
float x = sd*num+mean_v;
}

自定义随机分布(A Custom Distribution of Random Numbers)

很多时候,我们真正需要的并不是一个正态分布随机数,比如Gaussian。比如,想象一下一个随机游走的寻找食物的过程。在一个范围内随机寻找是一个常用的技巧,毕竟在找到食物之前并不知道它们存放在那里。但是,随机寻找存在一个问题,那就是会回到之前已经到过的地方(过采样,oversampling)。为了避免这种问题,一个经常使用的策略就是每次走很大一步,这允许步行者在特定位置周围随机觅食,同时定期跳得很远以减少过采样。随机游走(称为 Lévy flight)的这种变化需要一组自定义的概率。 虽然不是 Lévy flight的精确实现,但我们可以将概率分布表述如下:步长越长,被选中的可能性越小; 步骤越短,可能性越大。

在前面序言中,介绍了可以通过用值填充数组(一些值是重复的以便更高频率选择到)或通过比较random() 的结果来生成自定义概率分布。 比如有通过有 1% 的机会迈出一大步来实现 Lévy flight。

1
2
3
4
5
6
7
8
9
10
11
float r=random();
if (r<0.01)
{
xstep=random(-100,100);
ystep=random(-100,100);
}
else
{
xstep = random(-1,1);
ystep = random(-1,1);
}

但是,这会将概率降低到固定数量的选项。如果想制定一个更普遍的规则——数字越大,被选中的可能性就越大呢? 3.145 比 3.144 更有可能被选中,即使这种可能性只是稍微大一点。 换句话说,如果 x 是随机数,我们可以用 y = x 在 y 轴上映射概率。

如果我们能够根据上图弄清楚如何生成随机数的分布,那么我们将能够将相同的方法应用于任何曲线。

一种解决方案是选择两个随机数而不是一个。 第一个随机数作为待比较随机数,第二个是“比较随机值”。 它决定使用第一个还是舍弃并选择另一个。较容易获得资格的号码将被更频繁地挑选,而很少符合资格的号码将不经常被挑选。 以下是步骤(现在,我们只考虑 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% 的机会获得资格。 数字越高,我们实际使用它的可能性就越大。 这是一个实现上述算法的函数(以蒙特卡洛方法命名,以蒙特卡洛赌场命名),返回一个介于 0 和1。

1
2
3
4
5
6
7
8
9
10
11
12
13
float montecarlo()
{
while(true)
{
float r1=random(1);
float probility=r1;
float r2=random(1);
if (r2<probility)
{
return r1;
}
}
}

Perlin噪声(Perlin Noise (A Smoother Approach))

一个好的随机数生成器产生的数字之间没有关系,也没有明显的模式。正如我们开始看到的,在编写有机的、栩栩如生的行为时,一点点随机性可能是一件好事。然而,作为单一指导原则的随机性并不一定是自然的。一种被称为“Perlin 噪声”的算法(以其发明者 Ken Perlin 命名)考虑了这一点。 Perlin 在 1980 年代初制作原始 Tron 电影时开发了噪声功能;它旨在为计算机生成的效果创建程序纹理。 1997 年,佩林因这项工作获得了奥斯卡技术成就奖。 Perlin 噪声可用于生成具有自然品质的各种效果,例如云、风景和大理石等图案纹理。 Perlin 噪声具有更有机的外观,因为它产生自然有序(“平滑”)的伪随机数序列。左下图显示了 Perlin 噪声随时间的变化,x 轴代表时间;注意曲线的平滑度。右图显示随时间变化的纯随机数。

Processing 有一个 Perlin 噪声算法的内置实现:函数noise()noise() 函数可以输入一个、两个或三个参数,因为噪声是在一维、二维或三个维度上计算的。从一维噪声开始。

Processing中noise()是在几个“八度音阶”上计算的。 调用 noiseDetail() 函数将改变八度音阶的数量及其相对于彼此的重要性。 这反过来又改变了噪声函数的行为方式。

Perlin Noise Detail

noise()函数理解。假如我们需要生成一个在某一范围内的随机数,random()函数输入一个参数,表示生成一个0-参数值范围内的随机数。但是这种方法并不是noise()函数的正确使用方法。如果像random()一样, 给noise()输入一个参数,得到的结果将是一个0-1范围之内的固定值。实际上,Perlin噪声可以认为是一个随时间线性变化的一系列值。例如:

Time Noise
0 0.365
1 0.363
2 0.363
3 0.364
4 0.366

因此在Processing中为了获取特定的噪声值,需要给noise()函数输入一个特定的”时间参数”。

1
2
3
4
5
6
float t_value = 3;
void draw()
{
float n = noise(t_value);
println(n);
}

上面的代码会持续输出固定值。

1
2
3
4
5
6
7
float t_value=0;
void draw()
{
float n = noise(t_value);
t_value +=1;
println(n);
}

上面的代码会输出不同的随机值。

t_value 变化快慢会影响噪声的光滑度;如果我们在时间上有很大的跳跃,那么我们就会向前跳跃,并且值会更加随机。

噪声值映射(Mapping Noise)

noise()函数生成的值范围是0~1,使用map()函数能将这些值映射到任意的范围,map()函数需要五个参数。

1
2
3
4
5
6
7
8
9
10
float t = 0;

void draw() {
float n = noise(t);
//Using map() to customize the range of Perlin noise
float x = map(n,0,1,0,width);
ellipse(x,180,16,16);

t += 0.01;
}

二维噪声(Two-Dimensional Noise)

Processing中Perlin噪声是通过noise()函数实现的,相比较于另外一个随机数生成函数random()noise()最大的不同点是:调用此函数生成的噪声曲线更光滑,这是因为其相邻数值变化更小。对于一维Perlin噪声,某个数有两个与它直接相邻(左图)。对于二维Perlin噪声,其相邻数据是在网格中一个格子周边的所有单个格子中的数据(如右图)。

从概念上看,二维噪声和一维噪声的实现方式相同。不同之处在于其相邻点的存在方式和个数,一位是一条线相邻的两点,二维是网格周边所有的单个格子。给定值将的相邻点在其:上方、下方、右侧、左侧以及沿任何对角线。如果您要将这张方格纸可视化,并将每个值映射到颜色的亮度,会得到看起来像云的东西。 白色旁边是浅灰色,灰色旁边是灰色,深灰色旁边是黑色,黑色旁边是深灰色,等等。

这就是最初发明噪音的原因。可以稍微调整参数或使用颜色,以使生成的图像看起来更像大理石或木材或任何其他有机纹理。

二维噪声的实现:如果想为窗口的每个像素随机着色,可以通过一个嵌套循环来实现,这样能访问每个像素点并选择随机亮度。

1
2
3
4
5
6
7
8
9
10
loadPixels();
for(int x=0;x<width;x++)
{
for(int y=0;y<height;y++)
{
float brightness = random(255);
pixels[x+y*width]=color(brightness);
}
}
updatePixels();

如果使用noise()调整每个像素点的颜色,其实现方式也很简单,只需要把random()替换为noise()即可。

1
float bright = map(noise(x,y),0,1,0,255);

2D Perlin噪声的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float xoff = 0.0;

for (int x = 0; x < width; x++) {
//For every xoff, start yoff at 0.
float yoff = 0.0;

for (int y = 0; y < height; y++) {
//Use xoff and yoff for noise().
float bright = map(noise(xoff,yoff),0,1,0,255);
//Use x and y for pixel location.
pixels[x+y*width] = color(bright);
//Increment yoff.
yoff += 0.01;
}
//Increment xoff.
xoff += 0.01;
}


random函数生成二维随机颜色


noise函数生成二维随机颜色

在本节中,介绍了 Perlin 噪声的几种典型用法。 对于一维噪声,使用平滑值来设置对象的位置,从而呈现出徘徊的外观。 使用二维噪声,在像素平面上创建了具有平滑值的多云图案。 然而,Perlin 噪声值就是特定的数值,它们本质上与像素位置或颜色无关,只是对其值的进行使用。本书中任何具有变量的示例都可以通过 Perlin 噪声进行控制。比如对风力建模时,它的强度可以由Perlin噪声控制。分形树模式中分支之间的角度或流场模拟中沿网格移动的对象的速度和方向也是如此。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020-2023 Wh
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信