本博客中做的五个实验均用Processing完成,
实验内容多半从书籍 The Nature of Code中学习而得,
甚至是在其参考程序的基础上进行拓展修改而成的。
第一个实验我参考的案例是书中的 示例代码 0-6 和练习 0.10
即 二维Perlin噪声 和 噪声地形图
在原案例中,为了实现噪声地形图,代码中使得矩形顶点的Z轴坐标由 noise() 函数决定
z[i][j] = 0.5*map(noise(xoff, yoff,zoff), 0, 1, -120, 120);
在此处,noise() 函数为三维的Perlin噪声,其噪声波形由三个值决定。
其中 xoff 与 yoff 两个值分别为行列的漂移值,这个两个值在地形图坐标循环计算过程中递增,使得不同的矩形高低不同。
但是 xoff 和 yoff 的递增值很小,这样做是为了保证相邻的矩形之间高度相差不会过于大,使得整体地形看起来平滑、和谐。
其中 map() 函数为映射函数,将 noise() 的返回值由(0,1)映射到(-120,120).
在认真分析原案例后,我发现原案例的矩形是只有一种颜色的,看起来过于单调。
所以就有了让地形图呈现彩色的想法。
为了让矩形呈现彩色,那就要分别给矩形的RGB赋值。每个矩形的颜色要不一样,要想让效果变得炫酷,最好颜色是随机变换的。但是随机性又不能太强,如果一个红色矩形四周的八个矩形都是绿色的,那就太丑了。所以相邻矩形的RGB要平滑的随机变化。
Perlin噪声就能很好的满足这个需求。
R = map(noise(xoff, yoff,t_coff), 0, 1, 0, 255);
G = map(noise(xoff, yoff,t_coff+10000), 0, 1, 0, 255);
B = map(noise(xoff, yoff,t_coff+20000), 0, 1, 0, 255);
fill(R,G,B);
上面的代码实现了效果一,相邻矩形之间的灰度不同,且随着时间矩形的灰度也会变化。
但是上述代码却没有实现彩色的效果,分析后发现可能是和 noise() 的参数有关,虽然 RGB 的第三个参数不同,但是前两个参数相同,导致其返回值的差异有限,只能产生灰度图的效果。
R = map(noise(xoff, yoff,t_coff), 0, 1, 0, 255);
G = map(noise(xoff+10000, yoff+10000,t_coff+10000), 0, 1, 0, 255);
B = map(noise(xoff+20000, yoff+20000,t_coff+20000), 0, 1, 0, 255);
fill(R,G,B);
在修改代码后就实现了效果二。
但是地形图中矩形边上还是有黑黑的边框,不是特别美观。将边框隐去有两个方法,一个是让边框颜色和矩形一样,另一个是直接不绘制线框,两个方法都可以,我使用了第一个。
stroke(R,G,B);
noStroke();
第二个实验我参考的案例是书中的 示例代码 1-5 和 示例代码 1-11
即 向量的长度 和 一组同时朝着鼠标加速的运动物体
本实验的规律是蚊虫群与人的交互。
假设一个人身处傍晚时的草坪上,那这个人的头上可能就密密麻麻的飞着非常多的小蚊子。当这个人保持静止或者是走路时,蚊群就会跟着这个人,当你想要挥手去赶这些“烦人”的小东西时,它们就会一哄而散。
在本实验中,当鼠标静止不动或者是运动趋势为远离小球时,小球会受到一个方向为小球到鼠标向量方向的力,即小球接近鼠标,且小球变为蓝色;当鼠标运动趋势为靠近小球时,小球会受到一个方向为鼠标到小球向量方向的力,即小球远离鼠标,且小球变为红色。
小球的颜色也是致敬热力学中的用法,蓝色即为收缩,红色即为膨胀。
代码的关键在于怎么判断鼠标是朝着小球运动还是远离小球/静止不动,只有判断出来之后才能决定小球的运动状态。
在主函数中,使用一个 PVector oldMouse 向量来储存上一帧中鼠标的位置,并且在本帧的 draw() 函数调用时, 将oldMouse作为参数传入每一个小球的 update() 函数中。
void draw() {
background(255);
for (int i = 0; i < movers.length; i++) {
movers[i].update(oldMouse);
movers[i].display();
}
oldMouse = movers[0].updateMouse();
}
PVector updateMouse(){
PVector mouse = new PVector(mouseX,mouseY);
return mouse;
}
在update() 函数中,获取当前鼠标的位置。并且分别计算当前帧小球到鼠标的向量和上一帧小球到鼠标的向量。
在计算出向量后,比较两个向量的长度,若是上一帧长度更长oldPosDif.mag()>nowPosDif.mag()
则表明鼠标是在接近小球,否则小球保持静止或是远离小球
void update(PVector oldMouse) {
PVector mouse = new PVector(mouseX,mouseY);
PVector oldPosDif = PVector.sub(oldMouse,position);
PVector nowPosDif = PVector.sub(mouse,position);
if(oldPosDif.mag()>nowPosDif.mag())
{
//小球做远离鼠标的运动
}else
{
//小球做接近鼠标的运动
}
}
至于运动的计算,则是按照案例中的计算加速度,将加速度加到速度上,再将速度加到位置上,最后更新位置即可。
acceleration = nowPosDif;
acceleration.normalize();
acceleration.mult(0.2);
velocity.sub(acceleration);//若是小球靠近鼠标则是 velocity.add()
velocity.limit(topspeed);//限制小球速度
position.add(velocity);
当然,在实现了效果一之后,若想要稍微看一下小球在前几十帧的运动情况/颜色,那就可以加一个拖影的效果,在 draw() 函数中进行修改
//background(255);
fill(255,15);
rect(-1,-1,width,height);
这两句代码,可以理解为是在上一帧图片的基础上,贴上一层透明纸,透明的程度由 fill(gray,alpha) 中的alpha决定,贴上纸后看过去前几帧的图片就会变得透明,贴的纸越多,就越透明,这样就形成了拖影的效果。
第三个实验我参考的案例是书中的 示例代码 2-5 和 示例代码 2-8
即 流体阻力 和 万有引力
而第三个实验的思路则是来自于练习2.10
所以在本实验中,我将鼠标设置为一个吸引体,吸引力为万有引力,力的大小根据质量和距离变化。而每个小球之间存在万有斥力,斥力大小也根据质量和距离变化。并且小球还受到空气阻力运动,阻力大小与小球速度的平方成正比。
引力
在引力中,我给鼠标一个较大的质量,让其吸引小球的力稍微大一些。
而在引力公式中用到的距离 distance 我也给了相对来说严格的限制
distance = constrain(distance, 25.0, 50.0);
距离的限制是为了不让引力过小,且能够将小球拉到自己周围,这样在最后才能够与斥力有一个较为微妙的平衡。
PVector mouseAttraction()
{
PVector mouse = new PVector(mouseX,mouseY);
PVector force = PVector.sub(mouse,position);
float distance = force.mag();
distance = constrain(distance, 25.0, 50.0);
force.normalize();
float strength = (g* mass * mouseMass) / (distance * distance);
force.mult(strength);
return force;
}
斥力
在万有斥力中,距离的下限很低,因为小球的质量较小,而且小球较多,力的成分复杂,斥力在运动过程中并不是太容易观察。将距离下限设置的很低是为了让两个小球非常接近时明显的表现出斥力。
PVector repulsion(Mover m) {
PVector force = PVector.sub(position, m.position);
float distance = force.mag();
distance = constrain(distance, 0.005, 125.0);
force.normalize();
float strength = -(g * mass * m.mass) / (distance * distance);
force.mult(strength);
return force;
}
空气阻力
空气阻力作用是慢慢降低小球的速度,使其最后能达到一个力的相对平衡,在效果展示的图二中就可以看出,在有万有斥力存在时,小球最后会达到一个相对平衡。
PVector drag() {
float speed = velocity.mag();
//阻力大小与运动速度的平方成正比
float dragMagnitude = c * speed * speed;
// Direction is inverse of velocity
PVector dragForce = velocity.get();
dragForce.mult(-1);
dragForce.normalize();
dragForce.mult(dragMagnitude);
return dragForce;
}
第四个实验我参考的案例是书中的 练习 3.5 和 示例代码 3-11
即 飞船飞行 和 弹簧连接**
这个实验的思路是来自我前段时间看的电影《地心引力》,在电影里有一个桥段是男主和女主在太空中只通过一根绳子连接,而只有男主有喷气背包,当男主每次前进时都会被绳子拉住往回弹一段距离。
根据这个桥段,我就将自身带动力的飞船和一个物体通过弹簧连接了起来。
产生的效果就和电影类似。
飞船飞行
if (keyPressed) {
if (key == CODED && keyCode == LEFT) {
ship.turn(-0.03);
} else if (key == CODED && keyCode == RIGHT) {
ship.turn(0.03);
} else if (key == 'z' || key == 'Z') {
ship.thrust();
}
}
在飞船转向中没有用现成的函数,而是自己设置了参数heading,在经过计算使用。
// Turn changes angle
void turn(float a) {
heading += a;
}
// Apply a thrust force
void thrust() {
// Offset the angle since we drew the ship vertically
float angle = heading - PI/2;
// Polar to cartesian for force vector!
PVector force = new PVector(cos(angle),sin(angle));
force.mult(1.5);
applyForce(force);
// To draw booster
thrusting = true;
}
边界判定
在飞船的边界判定中,因为飞船绑定了弹簧和另一个物体,所以当超过边界时,整体的移动就变得非常困难,花了很长时间也没办法精确的使物体按照移动前的相对位置进行位移,所以只能让飞船老老实实的呆在屏幕范围内,没办法实现无限制移动,这也算是一个遗憾了。
void wrapEdges() {
float buffer = r*2;
if (position.x > width) position.x = width;
else if (position.x < 0) position.x = 0;
if (position.y > height) position.y = height;
else if (position.y < 0) position.y = 0;
}
第五个实验我参考的案例是书中的 练习 4.4 和 示例代码 4-8
即 飞船发射 和 粒子系统图像纹理**
因为感觉圆形的粒子更像是子弹,所以原本在 练习 4.4 中用作模拟工质的红色小球在本实验中则是作为射击时的子弹。
而 示例代码 4-8 中的烟雾特效则是经过我的修改后用于模拟喷气飞船在前进时留下的烟雾。
无论是子弹还是烟雾,其都依赖于粒子系统,而这两种粒子系统的位置在本实验中都与飞船进行了绑定,不同的在于对粒子系统中粒子施加的作用力是变化的。
本次实验使用到了五个类和一个主程序
实验非常全面的使用了面向对象,两个粒子系统类和粒子类都封装在了飞船类Spaceship中,在主函数中的调用就非常的简洁,只需要判断对飞船的操作与更新飞船对象即可。
void draw() {
background(0);
// Update position
ship.update();
// Wrape edges
ship.wrapEdges();
// Draw ship
ship.display();
fill(0);
//text("left right arrows to turn, z to thrust",10,height-5);
// Turn or thrust the ship depending on what key is pressed
if (keyPressed) {
if (key == CODED && keyCode == LEFT) {
ship.turn(-0.03);
} else if (key == CODED && keyCode == RIGHT) {
ship.turn(0.03);
} else if (key == 'z' || key == 'Z') {
ship.thrust(); //按z的时候前进,喷射烟雾
}
if (key == 'x' || key == 'X') {
ship.shot(); //按x的时候射击粒子子弹
}
}
}
在飞船的 update() 中调用了两个粒子系统的 .run()
在 .run()
中又包含了各自粒子的更新和显示
面向对象编程真是简洁而又条理清晰
void update() {
velocity.add(acceleration);
velocity.mult(damping);
velocity.limit(topspeed);
position.add(velocity);
acceleration.mult(0);
ps.run();
bps.run();
}
需要注意的是,在按z键使飞船前进的函数 thrust()
中
每次给烟雾添加粒子时都需要更新粒子系统的位置。
bps.origin = new PVector(position.x,position.y+r);
我将烟雾粒子系统中的粒子生成位置与粒子系统自身的位置 .origin
关联在一起
force.mult(-1);
bps.applyForce(force);
而为了让喷射出的烟雾不只是停留在原处扩散,还需要给烟雾“吹一阵风”,即给添加力。喷射出的粒子方向应该与飞船前进的方向相反。
飞船类中前进函数 thrust()
的代码如下
void thrust() {
// Offset the angle since we drew the ship vertically
float angle = heading - PI/2;
// Polar to cartesian for force vector!
PVector force = PVector.fromAngle(angle);
force.mult(0.1);
applyForce(force);
force.mult(-1);
bps.applyForce(force);
bps.origin = new PVector(position.x,position.y+r);
for (int i = 0; i < 4; i++) {
bps.addParticle();
}
// To draw booster
thrusting = true;
}
本次实验还是花了很多时间的,The Nature of Code的这本书的前五章是一字不拉的看完了,感觉收获还是非常大的。这本书不仅仅是在教读者如何去应用Processing来模拟一些物理效果、数学公式,也是在将面向对象嚼烂了碾碎了给读者看。以前虽然是学过Java和C++这些面向对象语言,也了解面向对象语言所谓的封装、继承、多态三个基本特性,但是从来没有东西像这本书一样一步步的在代码实现中展现出面向对象的用法和优势所在。大概也是因为之前语言没有真正的学好吧,不过感觉这次实验还是收获颇丰的。