The Nature of Code:Particle System


粒子系统(Particle Systems)

Click Here Jump To Source Page—The Nature of Code

​ 粒子系统是由许多单个粒子的集合,所有这些单个粒子的集合用来表示一个模糊的物体。一段时间内,粒子在系统中生成、移动、变化最后消失。粒子系统是一种在计算机图形中非常常见且有用的技术。

​ 自 1980 年代初以来,粒子系统已被用于无数视频游戏、动画、数字艺术作品和装置中,用以模拟各种不规则类型的自然现象,例如火、烟、瀑布、雾、草、气泡等。

​ 本章重点介绍粒子系统编码的实现。包括:

  • 如何组织代码;
  • 在哪里存储单个粒子相关信息而不是整个粒子系统的信息;

​ 虽然本章内容为粒子使用简单的形状和最基本的行为(重力),但是基于此框架应用更复杂的方式构建粒子和行为,可以实现各种不同的效果。

为什么需要粒子系统?

  1. 使用粒子系统能够对例如烟、火、瀑布等已经列出来的自然现象进行建模;

  2. 更为抽象的意义就是:使用粒子系统的思路来处理需要面对的大量(many)对象编码过程;之前章节在处理大量对象的过程中使用的是数组的方式来实现,粒子系统能够提供一种超越数组的方式‘;

  3. 编写多个类,以及用以保存其他类实例的类编程思想;

    粒子系统中,需要更加灵活的处理元素数量的方法,除了编写单个粒子的类之外还要实现一个描述粒子集合的类—粒子系统本身。本章的目标是能够实现如下的程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ParticleSystem ps;

    void setup() {
    size(640,360);
    ps = new ParticleSystem();
    }

    void draw() {
    background(255);
    ps.run();
    }

    上面的代码中,没有对单个粒子类进行显示引用,但是能够实现整个粒子系统。这是一种非常有用的面向对象编程方式—编写多个类,以及用以保存其他类实例的类。

  4. 使用另外两种高级面向对象编程(object-oriented programming)的方法:继承(inheritance )和多态(polymorphism)。

    通过继承(和多态),我们将学习一种方便的方法来存储包含不同类型对象的单个列表。 这样,粒子系统不仅需要是单一类型粒子的系统。

单个粒子

​ 粒子系统是由多个粒子组成的,开始实现系统之前,先实现描述单个粒子的类。第二章中的Mover类可以作为一个很好的模板进行使用。实际上粒子是一个能在屏幕上运动的物体。它具有location、velocity、acceleration,一个用以初始化上述变量的结构体,一个用以显示和更新的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Particle {
//[full] A “Particle” object is just another name for our “Mover.” It has location, velocity, and acceleration.
PVector location;
PVector velocity;
PVector acceleration;
//[end]

Particle(PVector l) {
location = l.get();
acceleration = new PVector();
velocity = new PVector();
}

void update() {
velocity.add(acceleration);
location.add(velocity);
}

void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,8,8);
}

​ 上面为一个最简单的单粒子定义,此外可以添加一个applyfoce()函数控制粒子的运动。添加变量来描述粒子的颜色和形状,或者引用 PImage 来绘制粒子。接下来重点关注一个附加项:生命周期(lifespan)

​ 典型的粒子系统涉及一种称为发射器(emitter)的东西。发射器是粒子的来源,并控制粒子、位置、速度等的初始设置。发射器可能会发射单个粒子爆发,或连续的粒子流,或两者兼而有之。关键是对于这样的典型实现,粒子在发射器处诞生,但不会永远存在。如果它要永远存在,我们的 Processing 草图最终会随着时间的推移随着粒子数量增加到一个难以处理的数字而停止。随着新粒子的诞生,我们需要旧粒子死亡。这会产生无限粒子流的错觉,并且我们的程序的性能不会受到影响。有许多不同的方法可以决定粒子何时死亡。例如,它可能会接触到另一个物体,或者它可能只是离开屏幕。然而,对于我们的第一个粒子类,我们只是要添加一个寿命变量。计时器将从 255 开始并倒计时至 0,此时粒子将被视为“死亡”。所以我们扩展粒子类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Particle {
PVector location;
PVector velocity;
PVector acceleration;
// A new variable to keep track of how long the particle
// has been “alive”
float lifespan; //[bold]

Particle(PVector l) {
location = l.get();
acceleration = new PVector();
velocity = new PVector();
// We start at 255 and count down for convenience
lifespan = 255; //[bold]
}

void update() {
velocity.add(acceleration);
location.add(velocity);
// Lifespan decreases
lifespan -= 2.0; //[bold]
}

void display() {
//[full] Since our life ranges from 255 to 0 we can use it for alpha
stroke(0, lifespan); //[bold]
fill(175, lifespan); //[bold]
//[end]
ellipse(location.x, location.y, 8, 8);
}

// 查询函数
boolean isDead() {
//[full] Is the particle still alive?
if (lifespan < 0.0) {
return true;
} else {
return false;
}
//[end]
}
}

我们选择从 255 开始生命周期并倒计时到 0 的原因是为了方便。 有了这些值,我们也可以指定lifespan来充当椭圆的Alpha 透明度。 当粒子“死”时,它也会在屏幕上消失。

​ 加上 lifespan 变量,我们还需要一个额外的函数——一个可以查询(判断真假答案)粒子是”活”还是”死”的函数。 这在我们编写 ParticleSystem 类时会派上用场,该类的任务是管理粒子列表本身。 编写这个函数很容易; 我们只需要检查lifespan的值是否小于0。如果是我们返回true,否则返回false

验证单粒子运行

Particlesystem.pde

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// import
Particle singleParticle;

void setup()
{
size(500, 500);
// initialization single particle
PVector initialPos = new PVector(random(width/2-50, width/2+50), 0);
singleParticle = new Particle(initialPos);
}

void draw()
{
background(255);
singleParticle.run();
if (singleParticle.isDead()) {
println("Particle dead!");
}
}

Particle.pde

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Particle {
PVector location;
PVector velocity;
PVector acceleration;
// A new variable to keep track of how long the particle
// has been “alive”
float lifespan; //

Particle(PVector l) {
location = l.get();
acceleration = new PVector(0, 0.05);
velocity = new PVector(random(-1, 1), random(-2, 0));
// We start at 255 and count down for convenience
lifespan = 255; //
}

void update() {
velocity.add(acceleration);
location.add(velocity);
// Lifespan decreases
lifespan -= 2.0; //
}

void display() {
//[full] Since our life ranges from 255 to 0 we can use it for alpha
stroke(0, lifespan); //
fill(175, lifespan); //
ellipse(location.x, location.y, 8, 8);
}

// 查询函数
boolean isDead() {
//[full] Is the particle still alive?
if (lifespan < 0.0) {
return true;
} else {
return false;
}
//[end]
}

void run()
{
update();
display();
}
}

现在我们有了一个描述单个粒子的类,我们已经准备好迈出下一步了。 当我们无法确定在任何给定时间我们可能拥有多少粒子时,我们如何跟踪许多粒子?

动态数组

动态数组

​ 实际上,可以使用数组(Array)来管理粒子系统中的多个粒子对象。对于粒子数量固定的粒子系统来说,使用数组是非常有效的方法。Processing同时也提供了,expand()、contract()、subset()、splice()函数来调整数组大小。但是,这章中将会使用Java自带类ArrayList(ArrayList (Java Platform SE 6) (oracle.com))来代替Array管理粒子。

使用ArrayList和使用Array存储粒子对象的思想是一样的,只是语法不同。

如下代码分别使用ArrayList和Array实现的效果是一致的;

数组方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int total = 10;
Particle[] parray = new Particle[total];

void setup() {
for (int i = 0; i < parray.length; i++) {
parray[i] = new Particle();
}
}

void draw() {
for (int i = 0; i < parray.length; i++) {
Particle p = parray[i];
p.run();
}
}

ArrayList实现

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

ArrayList<Particle> plist = new ArrayList<Particle>();
void setup() {
for (int i = 0; i < total; i++) {
// An object is added to an ArrayList with add().
plist.add(new Particle());
}
}

void draw() {
// The size of the ArrayList is returned by size().
for (int i = 0; i < plist.size(); i++) {
// An object is accessed from the ArrayList with get().
Particle p = plist.get(i);
p.run();
}
}

ArrayList中的加强for循环,’:’操作符。

加强for循环操作符’:’

对于ArrayList\ 类可以使用’:’操作符号替换for,使得for迭代更加优雅、简洁。

1
2
3
4
5
6
7
8
9
10
11
// 标准for循环
for(int i=0;i<plist.size();i--)
{
Particle p=plist.get(i);
p.run();
}
// ‘:’操作符号
for (Particle p : plist)
{
p.run();
}

动态调整数组大小

​ 要求:在粒子系统中,实现每个draw循环周期新增一个粒子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ArrayList<Particle> particles;

void setup() {
size(640,360);
particles = new ArrayList<Particle>();
}

void draw() {
background(255);
// A new Particle object is added to the ArrayList every cycle through draw().
particles.add(new Particle(new PVector(width/2,50)));

for (int i = 0; i < particles.size(); i++) {
Particle p = particles.get(i);
p.run();
}
}

上面的代码能够满足要求,但是如果持续运行一段时间,会因为内存消耗过大导致程序崩溃,因为每个draw循环周期向着ArrayList中持续添加元素,ArrayList的大小会不断增大。

对应上述问题,通常我们会想到的方法是,减小ArrayList的元素个数,然后出现如下代码:

1
2
3
4
5
6
7
8
for (int i = 0; i < particles.size(); i++) {
Particle p = particles.get(i);
p.run();
//If the particle is “dead,” we can go ahead and delete it from the list.
if (p.isDead()) {
particles.remove(i);
}
}

上述代码尽管能够消除ArrayList中元素过多,导致内存占用过大的问题,但是它存在一个很不容易发现的隐患。

通过堆栈的方式来对上面代码进行分析,ArrayList在增加和删除元素的过程中,应该是这样一个过程,

for循环中,通过指针i的变化,索引到不同的元素:

  1. i=0 :arrow_right: 检查粒子A是否消失 :arrow_right: 保留粒子A
  2. i=1:arrow_right: 检查粒子B是否消失 :arrow_right: 保留粒子B
  3. i=2 :arrow_right: 检查粒子C是否消失 :arrow_right: 删除粒子C
  4. (系统自动)将粒子D和粒子E移动到原来粒子C和粒子E所占的内存地址。

但是在实现的过程中,由于删除完C时候,i=2,但是D又移动到原来C的位置上,所以这种简单的for循环实现方式并不能对D进行检查,存在漏检问题。针对上面一种情况,有两种解决方法:一种向后(backwards)迭代(iterate)ArrayList、另外一种是使用Jave提供的一种特殊类Iterator。

Iterator 能够实现:

  • 给出下一个元素;
  • 保证迭代过程中,即使出现移除元素也不会出现元素漏掉或二次出现;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Iterator;

ArrayList<Particle> particles;

void setup() {
size(640, 360);
particles = new ArrayList<Particle>();
}

void draw() {
background(255);

particles.add(new Particle(new PVector(width/2, 50)));

Iterator<Particle> it = particles.iterator();
//Using an Iterator object instead of counting with int i
while (it.hasNext()) {
Particle p = it.next();
p.run();
if (p.isDead()) {
it.remove();
}
}
}

粒子系统类(The Particle System Class)

​ 前面的章节已经实现了单个粒子类、使用ArrayList来管理多个粒子对象。现在需要实现一个ParticleSystem类,用来描述粒子系统本身的行为和属性。对粒子系统抽象成一个类,能够避免在main标签中冗杂的for循环,以及允许一个main标签中定义多个粒子系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Iterator;

class ParticleSysSelf
{
ArrayList<Particle> particles;

// constructor
ParticleSysSelf()
{
particles = new ArrayList<Particle>();
}

// +
void addParticle() {
particles.add(new Particle());
}
// operator
void run()
{
Iterator<Particle> it = particles.iterator();
while (it.hasNext())
{
Particle p = it.next();
p.run();
if (p.isDead())
{
it.remove();
}
}
}
}

⏳ 2022-05-27 16:11

系统的系统

系统是由一部分基本元素构成的,多个粒子(Particle)对象组成粒子系统(system of particle),一个粒子系统就是多个粒子对象的集合。当然,粒子系统也可以看作是一个组成更大系统的元素,多个粒子系统的集合就叫粒子系统的系统(system of particle system)。

继承和多态性:介绍

面向对象(object-oriented )编程理论三个基本要素:继承(Inheritance )、多态(Polymorphism)、封装(encapsulation);

考虑如下的情景,使用processing给你的朋友做一个生日贺卡,生日贺卡是由不同的彩色碎纸组成,这些碎纸有不同的颜色、形状、动作等属性。你可能会想到的解决方案是,先分别实现不同的碎纸类,然后再将这些类组成碎纸系统;

1
2
3
4
5
6
7
8
9
10
11
12
// 定义不同类型的碎纸类
class HappyConfetti {

}

class FunConfetti {

}

class WackyConfetti {

}

接下来实现一个彩色碎纸系统,碎纸系统构造函数中引用不同的碎纸类:

1
2
3
4
5
6
7
8
9
10
11
12
class ParticleSystem {
ParticleSystem(int num) {
particles = new ArrayList();
for (int i = 0; i < num; i++) {
float r = random(1);
Randomly picking a "kind" of particle

if (r < 0.33) { particles.add(new HappyConfetti()); }
else if (r < 0.67) { particles.add(new FunConfetti()); }
else { particles.add(new WackyConfetti()); }
}
}

上述能够实现想要的功能,但是存在两个问题:

  1. 实现过程中,会拷贝\粘贴许多重复的代码;
  2. ArrayList不会知道具体是新增的哪个类;

要解决问题2可以通过申明不同的类型ArrayList来实现,

1
2
3
ArrayList<HappyConfetti> a1 = new ArrayList<HappyConfetti>();
ArrayList<FunConfetti> a2 = new ArrayList<FunConfetti>();
ArrayList<WackyConfetti> a3 = new ArrayList<WackyConfetti>();

这似乎非常不方便,因为我们真的只想要一个列表来跟踪粒子系统中的所有内容。 这可以通过多态性来实现。多态性将允许我们将不同类型的对象视为相同类型并将它们存储在单个 ArrayList 中。

继承基础

让我们举一个不同的例子,动物世界:狗、猫、猴子、熊猫、袋熊和海荨麻。 我们将从编写 Dog 类开始。 Dog 对象将有一个年龄变量(一个整数),以及 eat()、sleep() 和 bark() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Dog
{
int age;
Dog()
{
// constructor
};

void eat()
{
// eat
};

void sleep()
{
// sleep
};

void bark()
{
// bark
};

}

如果我们实现一个Cat类,该类同样具有Dog类一样的属性和函数,那么代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Cat
{
int age;
Cat()
{
// constructor
};

void eat()
{
// eat
};

void sleep()
{
// sleep
};

void meow()
{
// bark
};

}

当我们为鱼、马、考拉和狐猴重写相同的代码时,这个过程将变得相当乏味。 相反,让我们开发一个可以描述任何类型动物的通用 Animal 类。 毕竟,所有的动物都吃和睡。 那么我们可以说:

  • 狗是动物,具有动物的所有特性,可以做动物做的所有事情。 此外,狗会吠叫。
  • 猫是动物,具有动物的所有特性,可以做动物做的所有事情。 此外,猫会喵喵叫。

继承使这一切成为可能。 通过继承,类可以从其他类继承属性(变量)和功能(方法)。 Dog 类是 Animal 类的子类。 子类会自动继承父类(超类)的所有变量和函数,但也可以包含父类中没有的函数和变量。 就像“生命之树”一样,遗传遵循树形结构。 狗从犬科继承,从哺乳动物继承,从动物继承,等等。

使用继承的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// The Animal class is the parent (or super) class.
class Animal {
// Dog and Cat inherit the variable age.
int age;

Animal() {
age = 0;
}

//[full] Dog and Cat inherit the functions eat() and sleep().
void eat() {
println("Yum!");
}

void sleep() {
println("Zzzzzz");
}
//[end]
}

// The Dog class is the child (or sub) class, indicated by the code "extends Animal".
class Dog extends Animal { //[bold]
Dog() {
// super() executes code found in the parent class.
super(); //[bold]
}
// We define bark() in the child class, since it isn't part of the parent class.
void bark() {
println("WOOF!");
}
}

class Cat extends Animal {
Cat() {
super();
}
void meow() {
println("MEOW!");
}
}

继承引入的两个新术语:

  • extends 关键字用于表明正在定义的类的父级。 类只能扩展一个类。 但是,类可以扩展其他类的类,即 Dog 扩展 Animal,Terrier 扩展 Dog。
  • super() - 这会调用父类中的构造函数。 换句话说,无论你在父构造函数中做什么,在子构造函数中也一样。 除了 super() 之外,还可以将其他代码写入构造函数。 如果父构造函数定义了匹配的参数,super() 也可以接收参数。

子类中可以定义父类中不具有的属性和功能。例如,假设Dog类的对象除了age外还有haircolor属性,并且其是随机的,那么代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog extends Animal {
// A child class can introduce new variables not
// included in the parent.
color haircolor;
Dog() {
super();
haircolor = color(random(255));
}

void bark() {
println("WOOF!");
}
}

注意父类构造函数是如何通过 super() 调用的,它将年龄设置为 0,但是头发颜色是在 Dog类构造函数本身设置的。 如果 Dog 对象与一般 Animal 对象的eat方法不同,则可以通过重写(overwirte)子类中的函数来覆盖父函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog extends Animal {
color haircolor;

Dog() {
super();
haircolor = color(random(255));
}

// A child can override a parent function if necessary.
void eat() {
// A Dog's specific eating characteristics
println("Woof! Woof! Slurp.")
}

void bark() {
println("WOOF!");
}
}

但是,如果Dog类的eat方法和Animal一样,只是有一些额外的功能,子类既可以运行父类的代码,也可以合并自定义代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Dog extends Animal {
color haircolor;

Dog() {
super();
haircolor = color(random(255));
}

void eat() {
// Call eat() from Animal.
// A child can execute a function from the parent
// while adding its own code.
super.eat(); //[bold]
// Add some additional code
// for a Dog's specific eating characteristics.
println("Woof!!!");
}

void bark() {
println("WOOF!");
}
}

具有继承性的粒子

现在我们已经介绍了继承的理论及其语法,我们可以基于已有的Particle 类在 Processing 中开发一个工作示例。

一个简单的粒子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Particle {
PVector location;
PVector velocity;
PVector acceleration;

Particle(PVector l) {
acceleration = new PVector(0,0.05);
velocity = new PVector(random(-1,1),random(-2,0));
location = l.get();
}

void run() {
update();
display();
}

void update() {
velocity.add(acceleration);
location.add(velocity);
}

void display() {
fill(0);
ellipse(location.x,location.y,8,8);
}
}

接下来,我们从 Particle 创建一个子类(我们称之为 Confetti)。 它将从 Particle 继承所有实例变量和方法。 我们编写了一个名为 Confetti 的新构造函数,并通过调用 super() 从父类执行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Confetti extends Particle {

// We could add variables for only Confetti here.

Confetti(PVector l) {
super(l);
}

// There is no code here because we inherit update() from parent.


//[full] Override the display method.
void display() {
rectMode(CENTER);
fill(175);
stroke(0);
rect(location.x,location.y,8,8);
}
//[end]
}

当然可以让Confetti类的形式更加复杂,比如让其能够按照一定规律旋转,那么覆写display函数既可以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void display() {
float theta = map(location.x,0,width,0,TWO_PI*2);

rectMode(CENTER);
fill(0,lifespan);
stroke(0,lifespan);

pushMatrix();
translate(location.x,location.y);
rotate(theta);
rect(0,0,8,8);
popMatrix();
}

现在我们有了一个扩展基本粒子类的 Confetti 类,我们需要弄清楚ParticleSystem 类如何在同一系统中管理不同类型的粒子。 为了实现这个目标,让我们回到动物界继承的例子,看看这个概念是如何延伸到多态世界的。

多态基础

将 一个Dog 对象视为 Dog 类或 Animal 类(其父类)的成员的能力是多态性的一个示例。 多态性(来自希腊语 polymorphos,意为多种形式)是指以多种形式处理对象的单个实例。 狗当然是狗,但由于 Dog类继承自 Animal类,它也可以被认为是动物。 在代码中,我们可以两种方式引用它。

1
2
Dog rover = new Dog();
Animal spot = new Dog();

尽管第二行代码最初似乎违反了语法规则,但声明 Dog 对象的两种方式都是合法的。 即使我们将 spot 声明为 Animal 对象,我们实际上是在创建 Dog 对象并将其存储在 spot 变量中。 我们可以安全地调用所有 Animal 类方法,因为继承规则规定狗可以做任何动物可以做的事情。 但是,如果 Dog 类覆盖 Animal 类中的 eat() 函数怎么办? 即使将spot 声明为Animal,Java 也会确定它的真实身份是Dog 并运行适当版本的eat() 函数。 当我们有一个数组或 ArrayList 时,这特别有用。

具有多态性的粒子系统

让我们暂时假设不存在多态性并重写一个 ParticleSystem 类以包含许多 Particle 对象和许多 Confetti 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class ParticleSystem {
//[full] We’re stuck doing everything twice with two lists!
ArrayList<Particle> particles; //[bold]
ArrayList<Confetti> confetti; //[bold]
//[end]
PVector origin;

ParticleSystem(PVector location) {
origin = location.get();
//[full] We’re stuck doing everything twice with two lists!
particles = new ArrayList<Particle>(); //[bold]
confetti = new ArrayList<Confetti>(); //[bold]
//[end]
}

void addParticle() {
//[full] We’re stuck doing everything twice with two lists!
particles.add(new Particle(origin)); //[bold]
particles.add(new Confetti(origin)); //[bold]
//[end]
}

void run() {
//[full] We’re stuck doing everything twice with two lists!
Iterator<Particle> it = particles.iterator(); //[bold]
while (it.hasNext()) {
Particle p = it.next();
p.run();
if (p.isDead()) {
it.remove();
}
}
it = confetti.iterator(); //[bold]
while (it.hasNext()) {
Confetti c = it.next();
c.run();
if (c.isDead()) {
it.remove();
}
}
//[end]
}
}

注意我们有两个单独的列表,一个用于particle,一个用于confetti。 我们想要执行的每个动作都必须执行两次! 多态性允许我们通过只创建一个包含标准粒子对象和五彩纸屑对象的粒子 ArrayList 来简化上述操作。 我们不必担心哪个是哪个; 这一切都会为我们解决! (另外,请注意主程序和类的代码并没有改变,所以我们没有在这里包含它。请参阅网站以获取完整示例。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ParticleSystem {
// One list, for anything that is a Particle
// or extends Particle
ArrayList<Particle> particles; //[bold]
PVector origin;

ParticleSystem(PVector location) {
origin = location.get();
particles = new ArrayList<Particle>();
}

void addParticle() {
float r = random(1);
// We have a 50% chance of adding each kind of Particle.
if (r < 0.5) {
particles.add(new Particle(origin)); //[bold]
} else {
particles.add(new Confetti(origin)); //[bold]
}
}

void run() {
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
// Polymorphism allows us to treat everything as a
// Particle, whether it is a Particle or a Confetti.
Particle p = it.next(); //[bold]
p.run();
if (p.isDead()) {
it.remove();
}
}
}
}

具有力的粒子系统

到目前为止,在本章中,我们一直专注于以面向对象的思想来构建代码。 也许您注意到了,或者您没有注意到,但是在此过程中,我们不知不觉地从前章中的位置往后几步(这里指的是对粒子运动复杂性)。 让我们检查一下简单粒子类的构造函数。

1
2
3
4
5
6
7
8
Particle(PVector l) {
// We’re setting acceleration to a constant value!
acceleration = new PVector(0,0.05); //[bold]

velocity = new PVector(random(-1,1),random(-2,0));
location = l.get();
lifespan = 255.0;
}

update()函数

1
2
3
4
5
void applyForce(PVector force) {
PVector f = force.get();
f.div(mass);
acceleration.add(f);
}

有了上述代码,可以在update()函数中添加一行代码来清除加速度。

1
2
3
4
5
6
7
void update() {
velocity.add(acceleration);
location.add(velocity);
// There it is!
acceleration.mult(0);
lifespan -= 2.0;
}

添加加速度属性的粒子系统完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Particle {
PVector location;
PVector velocity;
PVector acceleration;
float lifespan;

// We could vary mass for more interesting results.
float mass = 1;

Particle(PVector l) {
// We now start with acceleration of 0.
acceleration = new PVector(0,0);
velocity = new PVector(random(-1,1),random(-2,0));
location = l.get();
lifespan = 255.0;
}

void run() {
update();
display();
}

// Newton’s second law & force accumulation
void applyForce(PVector force) {
PVector f = force.get();
f.div(mass);
acceleration.add(f);
}

// Standard update
void update() {
velocity.add(acceleration);
location.add(velocity);
acceleration.mult(0);
lifespan -= 2.0;
}

// Our Particle is a circle.
void display() {
stroke(255,lifespan);
fill(255,lifespan);
ellipse(location.x,location.y,8,8);
}

// Should the Particle be deleted?
boolean isDead() {
if (lifespan < 0.0) {
return true;
} else {
return false;
}
}
}

现在,粒子类已经完成,我们有一个非常重要的问题要问。 在哪里调用applyforce()函数更为好? 在粒子类update()代码中,将力施加到粒子上是否合适? 事实是没有正确或错误的答案; 这实际上取决于特定处理草图的确切功能和目标。 尽管如此,我们仍可以提出一种可能适用于大多数情况的通用解决方案,并制定一个模型,以将力施加到系统中的单个粒子。

让我们考虑以下任务:每次通过draw()函数施加一个全局的力到每个粒子上。 使用一个指定下的重力来举例。

1
PVector gravity = new PVector(0,0.1);

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ParticleSystem ps;

void setup() {
size(640,360);
ps = new ParticleSystem(new PVector(width/2,50));
}

void draw() {
background(100);

// Apply a force to all particles.
PVector gravity = new PVector(0,0.1);
ps.applyForce(gravity);

ps.addParticle();
ps.run();
}


class ParticleSystem {
ArrayList<Particle> particles;
PVector origin;

ParticleSystem(PVector location) {
origin = location.get();
particles = new ArrayList<Particle>();
}

void addParticle() {
particles.add(new Particle(origin));
}

void applyForce(PVector f) {
// Using an enhanced loop to apply the force to all particles
for (Particle p: particles) {
p.applyForce(f);
}
}

void run() {
// Can’t use the enhanced loop because we want to check for particles to delete.
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
Particle p = (Particle) it.next();
p.run();
if (p.isDead()) {
it.remove();
}
}
}
}

带排斥器的粒子系统

如果我们想让给这个例子进一步添加一个排斥器(Repeller)对象——第二章中介绍的吸引子(Attractor)的反面,该怎么办?这需要更复杂一点,因为与重力不同,吸引子或排斥器施加在粒子上的每个力都必须针对每个粒子进行计算。

让我们通过检查如何将新的 Repeller 对象合并到对简单粒子系统施力的示例中来开始解决这个问题。 需要在代码中添加两个主要内容:

  1. 一个 Repeller 对象(声明、初始化和显示)。
  2. 将 Repeller 对象传递到 ParticleSystem 的函数,以便它可以对每个粒子对象施加力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ParticleSystem ps;
// New thing: we declare a Repeller object.
Repeller repeller;

void setup() {
size(640,360);
ps = new ParticleSystem(new PVector(width/2,50));
// initialize a Repeller object.
repeller = new Repeller(width/2-20,height/2); //[bold]
}

void draw() {
background(100);
ps.addParticle();

PVector gravity = new PVector(0,0.1);
// apply gravity to every particle in system of particle
ps.applyForce(gravity);

// New thing: apply a force from a repeller.
ps.applyRepeller(repeller); //[bold]

ps.run();
// we display the Repeller object.
repeller.display();
}

Repeller类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Repeller {
PVector location;
float r = 10;

Repeller(float x, float y) {
location = new PVector(x,y);
}

void display() {
stroke(255);
fill(255);
ellipse(location.x,location.y,r*2,r*2);
}
}

更难的问题是,在粒子系统类中如何编写 applyRepeller() 函数? 与将 PVector 传递给函数的applyForce() 不同, applyRepeller()需要将 Repeller 对象传递到函数中,并在函数中完成计算 repeller 和所有粒子之间的力。首先来看一下两个函数各自需要具有的功能:

applyForce() applyRepeller
void applyForce(PVector f) {
  for (Particle p: particles) {
    p.applyForce(f);
  }
}
void applyRepeller(Repeller r) {
  for (Particle p: particles) {
    PVector force = r.repel(p);
    p.applyForce(force);
  }
}

功能几乎相同。 只有两个区别。

  1. 函数的参数不同,一个是Repeller对象,一个是PVector对象。
  2. 必须为每个粒子单独计算力(PVector)并将其作用到粒子上。

这个力是如何计算的?在repel() 的函数中,它是我们为 Attractor 类编写的吸引函数的反函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

PVector repel(Particle p) {
//1) Get force direction.
PVector dir =
PVector.sub(location,p.location);

//2) Get distance (constrain distance).
float d = dir.mag();
d = constrain(d,5,100);

dir.normalize();
// 3) Calculate magnitude.
float force = -1 * G / (d * d);
// 4) Make a vector out of direction and magnitude.
dir.mult(force);
return dir;
}

在上述添加排斥器的整个过程中,我们从未考虑过改变Particle类本身。 粒子实际上并不需要知道任何关于其类细节的信息。 它只需要管理它的位置、速度和加速度,并具有接收外力并对其施加作用的能力。 因此,我们现在可以完整地查看这个示例,再次省略没有更改的 Particle 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// One ParticleSystem
ParticleSystem ps;
// One repeller
Repeller repeller;

void setup() {
size(640,360);
ps = new ParticleSystem(new PVector(width/2,50));
repeller = new Repeller(width/2-20,height/2);
}

void draw() {
background(100);
ps.addParticle();
// We’re applying a universal gravity.
PVector gravity = new PVector(0,0.1);
ps.applyForce(gravity);
// Applying the repeller
ps.applyRepeller(repeller);

ps.run();
repeller.display();
}


// The ParticleSystem manages all the Particles.
class ParticleSystem {
ArrayList<Particle> particles;
PVector origin;

ParticleSystem(PVector location) {
origin = location.get();
particles = new ArrayList<Particle>();
}

void addParticle() {
particles.add(new Particle(origin));
}

// Applying a force as a PVector
void applyForce(PVector f) {
for (Particle p: particles) {
p.applyForce(f);
}
}

void applyRepeller(Repeller r) {
//[full] Calculating a force for each Particle based on a Repeller
for (Particle p: particles) {
PVector force = r.repel(p);
p.applyForce(force);
}
//[end]
}

void run() {
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
Particle p = (Particle) it.next();
p.run();
if (p.isDead()) {
it.remove();
}
}
}
}

class Repeller {

// How strong is the repeller?
float strength = 100;
PVector location;
float r = 10;

Repeller(float x, float y) {
location = new PVector(x,y);
}

void display() {
stroke(255);
fill(255);
ellipse(location.x,location.y,r*2,r*2);
}

PVector repel(Particle p) {
// This is the same repel algorithm we used in Chapter 2: forces based on gravitational attraction.
PVector dir = PVector.sub(location,p.location);
float d = dir.mag();
dir.normalize();
d = constrain(d,5,100);
float force = -1 * strength / (d * d);
dir.mult(force);
return dir;
//[end]
}
}

图像纹理和加性混合

尽管这本书实际上是关于行为和算法而不是计算机图形和设计,但我认为如果我们讨论粒子系统并且从未看过涉及每个纹理的示例,我们将无法忍受自己带有图像的粒子。 就设计某些类型的视觉效果而言,选择绘制粒子的方式是难题的重要组成部分。

这两个图像都是由相同的算法生成的。 唯一的区别是在图像 A 中为每个粒子绘制了一个白色圆圈,而在 B 中为每个粒子绘制了一个“模糊”斑点。

在你编写任何代码之前,需要先制作图像纹理!建议使用 PNG 格式,因为在绘制图像时将保留 alpha 通道(即透明度),这是将纹理混合为相互叠加的粒子层所必需的。


By W.h. 2022-06-15 11:28:39

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

扫一扫,分享到微信

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

请我喝杯咖啡吧~

支付宝
微信