2 创造你的物理世界(1)
2 创造你的物理世界
Nature of Code(Daniel Shiffman)是一本讲述怎样用Processing代码来模拟出我们周围的世界的书。
本文介绍其第一章的内容,使用Processing进行物理世界的抽象。
本文整理自http://natureofcode.com/book/
2.1创造物理世界 向量的世界
现在,我们将从基础的物理开始——苹果怎样从树上落下,钟摆怎样在空中摆动,地球环绕太阳运动等等。然而,所有的一切在编程实现时都会用到一个基础的概念——向量(Vector)。我们的故事就由此开始。
向量是一个同时拥有大小和方向的两(相对的是标量,标量只有大小)。
你可以把向量看成是两个点的差,想象你要告诉别人,怎么从一个地方走到另一个地方的时候。
向量通常用一个小箭头标示,箭头方向指示向量的方向,箭头的长度指示向量的大小。
说到这里,相信大家都知道我指的向量是什么东西了。按照书中的正常流程,我应该向大家介绍向量的加减乘除运算了,但是这种东西继续讲的话是在低估大家的智商,坚信大家在高中数学中或者高等数学中都已经学过了,天朝的教育你懂的(不懂的自行百度吧)。所以还是直奔主题吧。
2.1.1小弹球
在我们介绍有关向量的细节之前,让先我们来看几个例子,他们会为我们说明向量为什么这么有用。
float x = 100;
float y = 100;
float xspeed = 1;
float yspeed = 3.3;
void setup() {
size(640,360);
background(255);
}
void draw() {
background(255);
x = x + xspeed;
y = y + yspeed;
if ((x > width) || (x < 0)) {
xspeed = xspeed * -1;
}
if ((y > height) || (y < 0)) {
yspeed = yspeed * -1;
}
stroke(0);
fill(175);
ellipse(x,y,16,16);
}
在这个例子中,我们创造了一个非常简单的世界——一个小球在白画布上弹弹弹。
这个小球有一些属性:
位置 x和y
速度 xspeed和yspeed
我们能想象,在更复杂的sketch中,还可能用到:
加速度 xacceleration和yacceleration
目标位置 xtarget和ytarget
风力 xwind和ywind
……
清楚了吧?在我们构造的世界里面,几乎每个东西都有两个维度,我们需要两个变量。当然这只是在我们构造的二维世界里,如果是三维世界,我们需要三个变量,比如x,y,z,xspeed,yspeed,zspeed……
如果我们能将这两个或者三个变量简化成一个变量,是不是方便很多呢?
所以,我们将
float x;
float y;
float xspeed;
float yspeed;
改为:
Vector location;
Vector speed;
当然,我们使用向量不只是因为它能将多个变量集成到一个,还因为它能提供各种数学运算操作,让我们更方便的操控这些变量。
所以,在我们更进一步之前,我们先来看一下Processing为我们提供的一个向量类——PVector。
2.1.2PVector
PVector的结构可以简单描述为:
class
PVector{
float x;
float y;
PVector(float x_,float y_){
x = x_;
y = y_;
}
}
使用一个PVector,要在全局中声明,并且在调用前实例化。如:
PVector p;
void setup(){
p = new PVector(0,0);
}
void draw(){
p.set(1, 1);
}
在之前的小球例子中,我们指定每一帧中,小球的位置都在横向和纵向上增加一定的量。
用物理的思维:
我们说向量是两个点之间的差,应用到这里来,速度就是一个向量,是新位置和老位置之间的差,位置也是可以一个向量,是原点到该点的差。
那么,顺理成章地:
float x =
100;
float y =
100;
float xspeed =
1;
float yspeed =
3.3;
变成了
PVector location =
new PVector(100,100);
PVector velocity =
new PVector(1,3.3);
有了这两个向量后,我们就可以在每帧中实现"新位置 = 受到速度影响后的老位置"的逻辑了:
x = x + xspeed;
y = y + yspeed;
将变成:
location = location + velocity;
然而,PVector中没有"+"这个操作符,而是提供add()方法来实现向量的加法。
void
add(PVector v)
{
y = y + v.y;
x = x + v.x;
}
我们要将这两个向量相加,要这样写:
location.add(velocity);
于是现在我们可以重写一下刚刚的小弹球程序了:
Exp 1.2 小弹球(使用向量!)
PVector location;
PVector velocity;
void setup() {
size(640,360);
location = new PVector(100,100);
velocity = new PVector(2.5,5);
}
void draw() {
background(255);
location.add(velocity);
if ((location.x > width) || (location.x < 0)) {
velocity.x *= -1;
}
if ((location.y > height) || (location.y < 0)) {
velocity.y *= -1;
}
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
什么?你说这段代码明明变得更复杂了?(扶眼镜)好像是啊,阿拉不要在意这些细节啦~
是的,由于我们很多时候我们不能直接使用对象,必需要将向量的一方面的维度值取出来,,所以局部的代码会看起来很复杂(比如,例子中用到的画圆的ellipse函数,我们不能说"ellipse(location,16,16);",而是要说"ellipse(location.x,location.y,16,16);")。但是不要着急,从长远来看,PVector类为我们提供了更多的方法(接下来会介绍),可以让我们的代码变得更加简明可读。小弹球只是我们的第一步,向量的优势会渐渐显示出来。
下面是更多的PVector类的方法:
add() — 加
sub() — 减
mult() — 数乘
div() — 额,数除?
mag() — 取模
setMag() – 设置模。。。
normalize() — 单位化(就是变为该方向上的单位向量。。)
limit() — 限制模的大小
下面的高级用法请大家自己阅读
heading() — the 2D heading of a vector expressed as an angle
rotate() — rotate a 2D vector by an angle
lerp() — linear interpolate to another vector
dist() — the Euclidean distance between two vectors (considered as points)
angleBetween() — find the angle between two vectors
dot() — the dot product of two vectors
cross() — the cross product of two vectors (only relevant in three dimensions)
random2D() - make a random 2D vector
random3D() - make a random 3D vector
下面是几个使用示例帮助大家了解用法,对函数查询和使用方法比较了解的童鞋请酌情忽略之:
Exp 1.3 向量减法
void setup() {
size(640,360);
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
PVector subtraction!
mouse.sub(center);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
Exp 1.4 向量数乘
void setup() {
size(640,360);
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
mouse.mult(0.5);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
void setup() {
size(640,360);
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
float m = mouse.mag();
fill(0);
rect(0,0,m,10);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
mouse.normalize();
mouse.mult(50);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
2.1.3Mover类
好了,对PVector类了解的够多了,现在我们要回到我们的物理世界来~
我们在小弹球中已经用PVector小试牛刀,我们了解到,屏幕上的一个物体有着它的位置和速度,位置说明了这一刻它将在哪个位置显示,速度说明了下一刻它的位置将改变多少。
每一帧中,位置向量将依据速度向量累加:
location.add(velocity);
然后,我们在该位置画出物体:
ellipse(location.x,location.y,16,16);
这是就我们的一个运动模式。
1、将位置加上速度;
2、在该位置画出物体。
在小弹球的例子中,所有的行为都发生在Processing主tab中的setup函数和draw函数中。明显是一个面向过程的程序。现在我们要做的,就是将这个运动的小球(将它所有的运动逻辑)封装起来,成为一个类,一个表示运动的物体的类Mover。
为了构建这个类,我们要思考两个问题:
1、Mover类有什么数据成员?
2、Mover类有什么行为?
我们刚刚的总结帮我们回答了这两个问题。一个Mover类包含两个数据:位置和速度;而Mover的行为,则是更新位置(将位置加上速度),并且画出自己(在该位置画出物体)
class
Mover
{
PVector location;
PVector velocity;
void
update()
{
}
void
display()
{
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
}
当然,由于成员变量里有PVector,我们需要在Mover类进行构造时也对它进行实例化。我们将这个操作写在Mover的构造函数里(这一次,我们为Mover设定一个随机的位置和速度):
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
最后,我们需要实现一个checkEdges函数,来检查我们的Mover是否到了画面的边缘,如果到了边上,我们要让它反弹回来:
void checkEdges() {
if (location.x > width) {
location.x = 0;
}
else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
}
else if (location.y < 0) {
location.y = height;
}
}
至此,我们完成了我们的Mover类,那么怎么使用这个类呢?
首先,我们要在全局中声明一个Mover变量:
Mover mover;
然后在setup函数中将它实例化:
mover = new Mover();
最后在draw函数中调用它的几个方法:
mover.update();
mover.checkEdges();
mover.display();
Mover mover;
void setup() {
size(640,360);
Create Mover object.
mover = new Mover();
}
void draw() {
background(255);
mover.update();
mover.checkEdges();
mover.display();
}
class Mover {
PVector location;
PVector velocity;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
void update() {
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
}
2.1.4加速度来了
好了。现在我们应该明白了向量是什么,还有我们怎么用它来实现一个运动Mover类。这真是令人激动的第一步,请大家为自己鼓掌(papapapa~)。但是在我们庆祝之前,我们还要再百尺竿头,更进一步,毕竟,只是看着一个小弹球看起来还是太无聊了——碰碰撞撞,无限循环,小球从不会加速或者减速,也不会转弯。为了有更有意思的效果,为了更加真实地模拟出真实世界,我们需要再给Mover类加一个向量——加速度acceleration。
很容易理解,加速度就是单位时间内速度的变化。很熟悉吧,加速度至于速度就好比速度之于位置。所以我直接把下面的代码甩给大家,大家也应该很容易理解了:
velocity.add(acceleration);
location.add(velocity);
所以我们要做的改动就是:
给Mover增加一个数据成员:
class Mover {
PVector location;
PVector velocity;
PVector acceleration;
在update函数里增加加速度的部分:
void update() {
velocity.add(acceleration);
location.add(velocity);
}
快搞定了。现在就剩下在构造函数中实例化这些向量。
我们现在让小球一开始出现在画面中央,并且初始的速度为0(现在我们完全不用担心速度了,我们将它全然交给加速度来控制),那加速度呢?现在我们要尝试几种不同的加速度:
1、一个不变的加速度;
2、一个完全随机的加速度;
3、一个朝向鼠标的加速度。
定加速度
给Mover一个定价速度不怎么有趣,但是是最简单的方法。让我们从简单的开始。我们这次将加速度设为一个定值:
Mover() {
location = new PVector(width/2,height/2);
velocity = new PVector(0,0);
acceleration = new PVector(-0.001,0.01);
}
也许你会想"我去,这个加速度也太小了吧,要不要改大一点啊!"是,这个加速度是小,但是你要明白,加速度在速度上累加的频率是draw函数调用的频率,一般每秒10-30次(取决于这个sketch的帧频率frame rete,Processing用内置变量framerate来记录,你可以使用函数framerate()对它自定义),这样一累加,速度的值就很大了,速度过大出现的后果,就是小球还没到屏幕上来,就到屏幕外去了(请大家自行理清这句话的逻辑)。所以,不仅不能让加速度过大(会很快增大速度值),而且还要对速度值作一个限制。我们用limit函数来做到这一点:
velocity.limit(10);
这个函数要做的是:看看velocity的模有没有超过10。没有?那就好;如果超过了10,就将velocity减到10。
class Mover {
PVector location;
PVector velocity;
PVector acceleration;
float topspeed;
Mover() {
location = new PVector(width/2,height/2);
velocity = new PVector(0,0);
acceleration = new PVector(-0.001,0.01);
topspeed = 10;
}
void update() {
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
//display() is the same.
void display() {}
//checkEdges() is the same.
void checkEdges() {}
}
大家将主tab补充完整,运行一下看看。。。
随机加速度
现在我们来看第二种加速度——完全随机的加速度。也就是说,我们每次(比如,在update函数中)都要选一个随机的加速度。
void
update()
{
acceleration = PVector.random2D();
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
这里使用的random2D函数是返回一个长度为1,并且指向一个随机的方向的向量。
由于random2D给我们的是一个单位向量,所以我们还要试着给它赋一个模值:
(1)赋一个固定的模值
acceleration = PVector.random2D();
acceleration.mult(0.5);
(2)赋一个随机的模值
acceleration = PVector.random2D();
acceleration.mult(random(3));
这里要提到,加速度并不是代表速度的增加和减少,它代表的是速度在大小和方向上的变化。这也许显而易见,但是认识到这一点确实很关键的。
voidupdate() {
acceleration = PVector.random2D();
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
静态方法和非静态方法
在我们尝试第三种加速度(指向鼠标的加速度)之前,我们要再了解一个关于PVector的重要的东西——静态(static)方法和非静态(non-static)方法。
先不看向量,让我们先看看这组代码:
float x = 0;
float y = 5;
x = x + y;
很简单吧?x开始为0,然后我们将y加到它上面,所以现在它应该是5。对应到向量上,我们也能很轻松地用我们刚刚的知识实现这种逻辑:
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
v.add(u);
向量v值为(0, 0),然后我们把u加到它上面去了,它就成了(4, 5),so easy对吧?
那么来看下一组例子:
float x = 0;
float y = 5;
float z = x + y;
x一开始为0,我们将它和y相加,然后把结果存在一个新变量z里面。注意X的值没有改变(y也没有)!这看起来是个不足为提的问题,非常显而易见,但是,我们转到PVector试试:
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
PVector w = v.add(u);
糟糕。。我们的add并不支持这种用法,来看看这个add函数的定义:
void add(PVector v) {
x = x + v.x;
y = y + v.y;
}
可以看到,这里的add函数是个无返回值的void函数,并且它会改变去访问这个函数的PVector的x和y值,它不能帮助我们达成目的。
为了将两个PVector相加,并且返回一个新的PVector,我们必须使用它的兄弟——静态的add方法。
使用类名调用的方法叫做静态方法(static function),相对的,使用对象名调用的方法则是非静态方法(non-static function)。举个例子:
PVector.add(v,u); //这就是一个静态方法
v.add(u); //这是一个非静态方法
静态的add方法的定义是这样的:
static PVector add(PVector v1, PVector v2) {
PVector v3 = new PVector(v1.x + v2.x, v1.y + v2.y);
return v3;
}
它与它的兄弟不同的地方在于:
1、在定义的开始有一个static关键字;
2、返回值不是void,而是一个PVector;
3、它创建一个新的PVector来保存要相加的两个向量的和,最后将它返回。
当你调用静态函数的时候,你不用使用一个具体的对象(如某个向量u,v或者w),而是直接使用类名调用:
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
PVector w = v.add(u);
PVector w = PVector.add(v,u);
PVector类中,add(),sub(),mult()和div()都有其对应的静态函数。
交互式加速度
最后我们要来完成一个稍微复杂一点点,但是好玩得多的效果:我们要根据鼠标的位置,动态地计算Mover的加速度——指向鼠标的加速度。
每当我们要考虑加速度的时候,我们都要考虑两方面的内容:加速度的大小和加速度的方向。
让我们从方向开始。
显然,加速度应该从Mover的位置指向鼠标的位置。我们假定Mover的位置是在点(x,y),而鼠标的位置是在(mouseX,mouseY)。
dx = mouseX - x
dy = mouseY - y
我们用PVector将这点表示出来:
PVector mouse = new PVector(mouseX,mouseY);
PVector dir = PVector.sub(mouse,location);
现在我们就得到了一个从Mover的位置指向鼠标的位置的向量dir。但是如果我们现在就使用这个向量作为Mover的新加速度,那么Mover会立即(在下一帧)跑到鼠标的位置。这不是我们想要的效果。我们需要来确定一下这个加速度的大小。
为了设置我们的加速度大小,我们需要将这个dir向量 ? 一下。你说对了,"单位化"。我们将这个向量单位化,然后再乘上我们想要的值,就得到我们想要的大小的向量了,当然,这个向量也是我们想要的方向。
float anything = ?????
dir.normalize();
dir.mult(anything);
总结一下,我们经历了这四个步骤:
1、得到一个从物体位置指向目标位置的向量;
2、单位化这个向量;
3、乘以某个值,让它变成我们想要的长度;
4、将这个向量赋给加速度。
Exp 1.10 运动(前进吧!向着鼠标)
void update() {
PVector mouse = new PVector(mouseX,mouseY);
//Step 1: Compute direction
PVector dir = PVector.sub(mouse,location);
//Step 2: Normalize
dir.normalize();
//Step 3: Scale
dir.mult(0.5);
//Step 4: Accelerate
acceleration = dir;
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
大功告成,赶紧运行试试吧!
2.1.5来一堆小球怎么样?
所谓生命不息,创新不止。如果你觉得一个小球也无聊了,那么一堆小球怎么样?我们辛辛苦苦构建了一个Mover class,如果只使用一个Mover对象,着实有点屈才了。
下面这个程序用array创建了一大群小球:
Exp 1.11 向着鼠标运动的mover数组
Mover[] movers =
new Mover[20];
void setup() {
size(640,360);
background(255);
for (int i = 0; i < movers.length; i++) {
movers[i] = new Mover();
}
}
void draw() {
background(255);
for (int i = 0; i < movers.length; i++) {
movers[i].update();
movers[i].checkEdges();
movers[i].display();
}
}
class Mover {
PVector location;
PVector velocity;
PVector acceleration;
float topspeed;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(0,0);
topspeed = 4;
}
void update() {
PVector mouse = new PVector(mouseX,mouseY);
PVector dir = PVector.sub(mouse,location);
dir.normalize();
dir.mult(0.5);
acceleration = dir;
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
}
else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
}
else if (location.y < 0) {
location.y = height;
}
}
}
Reference
1 Daniel Shiffman, THE NATURE OF CODE, http://natureofcode.com/book/chapter-1-vectors, 2012