更新於 2024/10/21閱讀時間約 25 分鐘

The Nature of Code閱讀心得與Python實作:4.8 Particle Systems with...

這一節的標題是
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)


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.