這一節的標題是
4.8 Particle Systems with Repellers
因為方格子標題字數限制,所以沒完整顯現
在第二章模擬萬有引力時,曾經利用它來設計會吸引物體的吸子(attractor)。現在,如果想要在模擬粒子系統時,加入會排斥物體的斥子(repeller),那要怎麼做呢?
在原來的粒子系統中,我們加入了重力的作用,這樣粒子從發射器噴射出來之後,才會向下掉。我們想要新加入的斥子,它的的作用方式和重力的作用方式,有相當大的不同。對於每個粒子而言,不管身在何處,重力都一樣大;但是斥子所施加在不同粒子的作用力大小,卻跟粒子和斥子間的距離大小有關。所以,我們必須針對每個粒子去計算,到底斥子施加了多少作用力給它。
要在模擬粒子系統時加入斥子的作用力,需要在原來的程式中加入一個Repeller
物件,以及一個將Repeller
物件傳入粒子系統發射器的方法,來讓斥子把作用力作用在每一個粒子上。這時候,主程式會長這樣:
gravity = pygame.Vector2(0, 0.1)
emitter = Emitter(320, 60, 0.5)
# 建立Repeller物件
repeller = Repeller(320, 250, 16)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
emitter.add_particle()
emitter.apply_force(gravity)
# 將Repeller物件傳入粒子系統發射器,以便讓斥子把斥力作用在粒子上
emitter.apply_repeller(repeller)
emitter.run()
repeller.show()
pygame.display.update()
frame_rate.tick(FPS)
當然啦,我們必須先設計出Repeller
類別,才能建造Repeller
物件。因為Repeller
類別和Example 2.6中的Attractor
類別其實大同小異,所以只要稍微修改一下Attractor
類別,就可以完成Repeller
類別的設計。
在設計Attractor
類別時,因為依據的是萬有引力公式,引力的強度會受到物體質量和萬有引力常數的影響。在設計Repeller
類別時,我們稍微簡化一下這部分,把物體的質量和萬有引力常數整合成一個變數power
,並用它來調控斥子斥力的強度。所以,計算斥力的式子,會變成
strength = power/distance**2
在Repeller
類別中,我們設計了repel()
方法,用來讓斥子的斥力作用在粒子上,程式如下:
def repel(self, particle):
d = self.position - particle.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
# 限制距離數值的範圍,避免極端狀況發生
distance = d.magnitude()
if distance < 5:
distance = 5
elif distance > 50:
distance = 50
strength = self.power/distance**2
force = -strength*r_hat
return force
最後在計算force
時,前面加上了個負號,那是因為r_hat
的寫法是直接從Attractor
類別的attract()
方法複製而來,加上負號,才能讓力量從吸力變成斥力。
設計好Repeller
類別之後,再來就是要設計一個Emitter
類別的方法,讓Repeller
物件能傳入發射器,以便讓斥力作用在每一個粒子上,程式如下:
def apply_repeller(self, repeller):
for particle in self.particles:
force = repeller.repel(particle)
particle.apply_force(force)
這個方法跟apply_force()
方法很像,不過有一個很大的不同點:apply_force()
方法傳入的是已經計算好,放在pygame.Vector2
物件中的作用力;但是apply_repeller()
方法傳入的,則是Repeller
物件,然後再計算作用在各個粒子上不同的斥力。
下面這個例子模擬的,就是在粒子系統中放一個斥子的情形。程式部分,因為Particle
類別沒有任何變動,所以不再列出。
Example 4.7: A Particle System with a Repeller
class Emitter:
def __init__(self, x, y, mass):
self.mass = mass
self.size = 16*self.mass
self.particles = []
# 發射器位置
self.origin = pygame.Vector2(x, y)
def add_particle(self):
self.particles.append(Particle(self.origin.x, self.origin.y, self.mass))
def apply_force(self, force):
for particle in self.particles:
particle.apply_force(force)
def apply_repeller(self, repeller):
for particle in self.particles:
force = repeller.repel(particle)
particle.apply_force(force)
def run(self):
for particle in self.particles:
particle.update()
# 移除壽命已到的粒子
self.particles = list(filter(lambda particle: not particle.is_dead(), self.particles))
for particle in self.particles:
particle.show()
class Repeller:
def __init__(self, x, y, size=1, power=150):
self.screen = pygame.display.get_surface()
self.position = pygame.Vector2(x, y)
self.size = size
self.radius = self.size/2
self.power = power
# 設定repeller所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def repel(self, particle):
d = self.position - particle.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
# 限制距離數值的範圍,避免極端狀況發生
distance = d.magnitude()
if distance < 5:
distance = 5
elif distance > 50:
distance = 50
strength = self.power/distance**2
force = -strength*r_hat
return force
def show(self):
center = (self.radius, self.radius)
color = (0, 0, 0, 150)
pygame.draw.circle(self.surface, color, center, self.radius)
# 把repeller所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 4.7: A Particle System with a Repeller")
WHITE = (255, 255, 255)
screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
gravity = pygame.Vector2(0, 0.1)
emitter = Emitter(320, 60, 0.5)
# 建立Repeller物件
repeller = Repeller(320, 250, 16)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
emitter.add_particle()
emitter.apply_force(gravity)
# 將Repeller物件傳入粒子系統發射器,以便讓斥子把斥力作用在粒子上
emitter.apply_repeller(repeller)
emitter.run()
repeller.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 4.9
Particle
類別不變。把Emitter
類別的apply_repeller()
改成apply_force_generator()
:
def apply_force_generator(self, generator):
for particle in self.particles:
force = generator.generate_force(particle)
particle.apply_force(force)
修改Attractor
類別
class Attractor:
def __init__(self, x, y, mass, G=1, distance_range=[5, 25]):
self.screen = pygame.display.get_surface()
self.position = pygame.Vector2(x, y)
self.mass = mass
self.G = G
# 用於限制距離數值的範圍,避免極端狀況發生
self.distance_range = distance_range
self.size = self.mass
self.radius = self.size/2
# 設定attractor所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def generate_force(self, mover):
d = self.position - mover.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
# 限制距離數值的範圍,避免極端狀況發生
distance = d.magnitude()
dmin, dmax = min(self.distance_range), max(self.distance_range)
if distance < dmin:
distance = dmin
elif distance > dmax:
distance = dmax
strength = self.G*self.mass*mover.mass/distance**2
return strength*r_hat
def show(self):
# 使用具透明度的白色把attractor所在的surface清空
self.surface.fill((255, 255, 255, 0))
center = pygame.Vector2(self.radius, self.radius)
color = (0, 0, 0, 150)
pygame.draw.circle(self.surface, color, center, self.radius)
# 把attractor所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)
改用繼承方式來寫Repeller
類別
class Repeller(Attractor):
def __init__(self, x, y, size=1, power=150, distance_range=[5, 50]):
super().__init__(x, y, size, power, distance_range)
def generate_force(self, particle):
return -super().generate_force(particle)/self.mass/particle.mass
主程式
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 4.9")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
gravity = pygame.Vector2(0, 0.05)
emitter = Emitter(WIDTH/2, 60, 0.5)
attractors = [Attractor(WIDTH/2+155, 150, 20, 5),
Attractor(WIDTH/2-155, 150, 20, 5)]
repellers = [Repeller(WIDTH/2+25, 255, 16, 55),
Repeller(WIDTH/2-25, 255, 16, 55)]
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
emitter.add_particle()
emitter.apply_force(gravity)
for generator in attractors+repellers:
generator.show()
emitter.apply_force_generator(generator)
emitter.run()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 4.10
讓粒子之間有吸力,每個粒子都會和其他粒子互相吸引。
在Emitter
類別新增interact()
方法
def interact(self):
n = len(self.particles)
# 作用在粒子上的合力
f_acc = [pygame.Vector2(0, 0) for i in range(n)]
for i in range(n-1):
particle_A = self.particles[i]
for j in range(i+1, n):
particle_B = self.particles[j]
# 計算作用在粒子B上,方向由粒子B指向粒子A的作用力
d = particle_A.position - particle_B.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
# 限制距離數值的範圍,避免極端狀況發生
distance = d.magnitude()
if distance < 5:
distance = 5
elif distance > 25:
distance = 25
f = r_hat*particle_A.mass*particle_B.mass/distance**2
# 作用在兩個粒子上的作用力,大小相同,方向相反
f_acc[j] += f
f_acc[i] += -f
for i in range(n):
self.particles[i].apply_force(f_acc[i])
主程式
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 4.10")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
gravity = pygame.Vector2(0, 0.05)
emitter = Emitter(WIDTH/2, 60, 0.5)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
emitter.add_particle()
emitter.apply_force(gravity)
emitter.interact()
emitter.run()
pygame.display.update()
frame_rate.tick(FPS)