2 创造你的物理世界(1)

2 创造你的物理世界

Nature of Code(Daniel Shiffman)是一本讲述怎样用Processing代码来模拟出我们周围的世界的书。

本文介绍其第一章的内容,使用Processing进行物理世界的抽象。

本文整理自http://natureofcode.com/book/

 

2.1创造物理世界 向量的世界

现在,我们将从基础的物理开始——苹果怎样从树上落下,钟摆怎样在空中摆动,地球环绕太阳运动等等。然而,所有的一切在编程实现时都会用到一个基础的概念——向量(Vector)。我们的故事就由此开始。

向量是一个同时拥有大小和方向的两(相对的是标量,标量只有大小)。

你可以把向量看成是两个点的差,想象你要告诉别人,怎么从一个地方走到另一个地方的时候。

向量通常用一个小箭头标示,箭头方向指示向量的方向,箭头的长度指示向量的大小。

说到这里,相信大家都知道我指的向量是什么东西了。按照书中的正常流程,我应该向大家介绍向量的加减乘除运算了,但是这种东西继续讲的话是在低估大家的智商,坚信大家在高中数学中或者高等数学中都已经学过了,天朝的教育你懂的(不懂的自行百度吧)。所以还是直奔主题吧。

2.1.1小弹球

在我们介绍有关向量的细节之前,让先我们来看几个例子,他们会为我们说明向量为什么这么有用。

Exp 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);

 

}

Exp 1.5 向量求模

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);

}

Exp 1.6 向量单位化

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();

 

Exp 1.7 运动(速度版)

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。

 

Exp 1.8 运动(速度+定加速度版)

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));

 

这里要提到,加速度并不是代表速度的增加和减少,它代表的是速度在大小方向上的变化。这也许显而易见,但是认识到这一点确实很关键的。

 

Exp 1.9 运动(速度+随机加速度版)

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

 

 

posted @ 2013-11-25 11:09  mysunnytime  阅读(2391)  评论(1编辑  收藏  举报