The Nature of Code閱讀心得與Python實作:5.3 Flow Fields

更新於 發佈於 閱讀時間約 27 分鐘

接下來要來看看Reynolds所設計的「流場循行(flow-field following)」轉向行為。

raw-image

要設計vehicle的流場循行轉向行為,首先必須要知道什麼是「流場(flow field)」。簡單來說,如果把二維平面劃分出許多方格,而在每一個方格中,都存有一個指向特定方向的向量,那這些存有指示方向資料的方格所構成的平面,就是流場;所以在流場中的每個位置,都會有對應的方向。從這個關於流場的簡單說明,應該不難想到:流場循行,應該就是讓vehicle在流場上移動時,會依循流場位置所對應的方向,來調整他的運動方向。

知道什麼是流場之後,現在就來設計用來描述流場的類別:FlowField

要描述一個流場,FlowField類別應該要有下列屬性:

w:流場寬度;整數
h:流場高度;整數
resolution:解析度,也就是方格的邊長;整數
cols:直行數量,也就是w//resolution;整數
rows:橫列數量,也就是h//resolution;整數
field:大小為rows×cols之二維List,存放每個方格內之流場向量

有了這些屬性之後,我們就可以把流場向量給存放到field中。例如,如果流場方格內的所有方向都是向右,程式可以這樣寫:

for i in range(rows):
for j in range(cols):
field[i][j] = pygame.Vector2(1, 0)

如果方向是隨機的,程式可以寫成

for i in range(rows):
for j in range(cols):
x, y = random.uniform(-1, 1), random.uniform(-1, 1)
field[i][j] = pygame.Vector2(x, y)
if field[i][j].length() != 0:
field[i][j].normalize_ip()
else:
field[i][j] = pygame.Vector2(0, 0)

這裡我們讓所有位置的流場向量都正規化,但並非一定要這麼做;流場向量的大小,可以依照實際需要而調整。另外,在if條件判斷式中,當判斷向量為0時,我們會看起來多此一舉地將流場向量設為0向量,這是因為pygame在向量長度小於某一個值時,就會將其視為0向量;換句話說,這並不是真正的0向量,而只是誤差範圍內的0向量,所以有必要將其設為真正的0向量,以避免後續使用上可能產生的問題。

當然,也可以使用Perlin noise來模擬自然界中流場的樣式:

xoff, yoff = 1, 101
for i in range(rows):
yoff += 0.05
for j in range(cols):
xoff += 0.05
# 將介於-1~1範圍的Perlin noise映射至0~360
angle_deg = 180*(noise.pnoise2(xoff, yoff)+1)
# 將角度相符的單位向量設為流場向量
field[i][j] = pygame.Vector2.from_polar((1, angle_deg))

在這裡,我們是將介於-1~1範圍的Perlin noise映射至0~360,然後將符合這個角度的單位向量作為流場向量。這裡的寫法並非絕對的,只要能夠形成想要的樣式即可。事實上,光是使用不同的Perlin noise參數,就足以產生許多不同樣式的流場出來了。

Exercise 5.6

raw-image
for i in range(self.rows):
y = (0.5-i/self.rows)
for j in range(self.cols):
x = (0.5-j/self.cols)
self.field[i][j] = pygame.Vector2(x, y).rotate(-90)

流場向量存放在field這個二維List中,那我們怎麼知道在畫面上某個位置的流場向量方向呢?其實只要能知道那個位置是在哪個方格內,就能知道對應的流場向量了。要知道點(x, y)在哪個方格內,可以這樣計算:

column = int(x//resolution)
row = int(y//resolution)

所以,點(x, y)所在位置的流場向量,就是存放在field[row][column]中之向量。這裡因為xy有可能是浮點數,而List的索引值只能是整數,所以在計算時,必須將算出來的結果轉為整數。

在實際應用時,因為vehicle有可能會跑出畫面而離開流場,導致在存取field時,因為超出索引值的範圍而出現錯誤。所以,我們就加上判斷式,讓columnrow不會超出範圍:

if column < 0:
column = 0
elif column > cols-1:
column = cols-1

if row < 0:
row = 0
elif row > rows-1:
row = rows-1

完整的FlowField類別程式碼如下:

class FlowField:
def __init__(self, field_width, field_height, resolution):
self.screen = pygame.display.get_surface()

self.w = field_width
self.h = field_height
self.resolution = resolution

self.cols = self.w//self.resolution
self.rows = self.h//self.resolution

self.field = [[0]*self.cols for _ in range(self.rows)]

def init(self):
xoff, yoff = random.uniform(1, 100), random.uniform(101, 200)
for i in range(self.rows):
yoff += 0.05
for j in range(self.cols):
xoff += 0.05
# 將介於-1~1範圍的Perlin noise映射至0~360
angle_deg = 180*(noise.pnoise2(xoff, yoff)+1)
# 將角度相符的單位向量設為流場向量
self.field[i][j] = pygame.Vector2.from_polar((1, angle_deg))

def lookup(self, x, y):
column = int(x//self.resolution)
if column < 0:
column = 0
elif column > self.cols-1:
column = self.cols-1

row = int(y//self.resolution)
if row < 0:
row = 0
elif row > self.rows-1:
row = self.rows-1

return self.field[row][column]

def show(self):
for i in range(self.rows):
for j in range(self.cols):
v = self.field[i][j]
if v.length() > 0:
# 流場方格內之向量線段長度為v的長度之兩倍
v.scale_to_length(self.resolution/3)

# 流場方格中心點
pt = pygame.Vector2((j+0.5)*self.resolution, (i+0.5)*self.resolution)

# 畫流場方格內之向量線段
pygame.draw.line(self.screen, (0, 0, 0), pt-v, pt+v)

def show_vector(self):
for i in range(self.rows):
for j in range(self.cols):
v = self.field[i][j]
if v.length() > 0:
# 流場方格內之向量線段長度為v的長度之兩倍
v.scale_to_length(self.resolution/3)

# 流場方格中心點
pt = pygame.Vector2((j+0.5)*self.resolution, (i+0.5)*self.resolution)

# 畫流場方格內之向量線段
pygame.draw.line(self.screen, (0, 0, 0), pt-v, pt+v)

# 畫向量箭頭
s1 = -v.rotate(30)
if s1.length() > 0:
s1.scale_to_length(s1.length()/2)

s2 = -v.rotate(-30)
if s2.length() > 0:
s2.scale_to_length(s2.length()/2)

pygame.draw.line(self.screen, (0, 0, 0), pt+v, pt+v+s1)
pygame.draw.line(self.screen, (0, 0, 0), pt+v, pt+v+s2)

def show_grid(self):
# 畫橫線
for i in range(self.rows+1):
pt1 = (0, i*self.resolution)
pt2 = (self.cols*self.resolution, i*self.resolution)
pygame.draw.line(self.screen, (200, 200, 200), pt1, pt2, 3)

# 畫直線
for i in range(self.cols+1):
pt1 = (i*self.resolution, 0)
pt2 = (i*self.resolution, self.rows*self.resolution)
pygame.draw.line(self.screen, (200, 200, 200), pt1, pt2, 3)

設計好流場類別之後,接下來就來讓vehicle具有流場循行轉向行為的能力。

要讓vehicle具有流場循行的能力,只要把vdesired速度的方向設定成流場的方向即可。在Reynolds的原始設計中,vehicle會去預測他在未來某個時間點上會位於流場的哪個位置,然後把vdesired速度的方向,設定成和那個位置對應的流場方向一致。不過,原書在這裡略微簡化了一下,直接就讓vdesired速度的方向,和vehicle現在所在位置的流場方向一致。根據這樣的做法,可以在Vehicle類別中,加入下面的方法,來讓vehicle具備流場循行轉向行為的能力:

def follow(self, flow_field):
x, y = self.position
desired = flow_field.lookup(x, y)
if desired.length() != 0:
desired.scale_to_length(self.max_speed)

steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)

self.apply_force(steer)

在這個方法中,vehicle會利用FlowField類別的lookup()方法來取得所在位置的流場向量。原書特別提到,lookup()方法也可以寫在Vehicle類別中;不過,從OOP封裝的原則來看,把lookup()方法寫在FlowField類別中會是比較合理的做法,畢竟lookup()方法所要擷取的資料,是屬於FlowField類別的。

在下面這個例子中,vehicle會展現出流場循行的轉向行為。執行時,按滑鼠任一鍵,可以重置流場;按鍵盤空白鍵,可以切換是否畫出流場。

Example 5.4: Flow-Field Following

raw-image

主程式如下:

# python version 3.10.9
import math
import random
import sys

import noise # version 1.2.2
import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 5.4: Flow-Field Following")

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

flow_field = FlowField(WIDTH, HEIGHT, 20)
flow_field.init()

vehicles = []
for _ in range(120):
x = random.randint(0, WIDTH)
y = random.randint(0, HEIGHT)
vehicle = Vehicle(x, y)
vehicle.max_speed = random.uniform(2, 5)
vehicle.max_force = random.uniform(0.1, 0.5)
vehicles.append(vehicle)

show_field = True

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
flow_field.init()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
show_field = not show_field

screen.fill(WHITE)

if show_field:
#flow_field.show()
flow_field.show_vector()
# flow_field.show_grid()

for vehicle in vehicles:
vehicle.follow(flow_field)
vehicle.update()
vehicle.check_edges()
vehicle.show()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 5.7

修改FlowField類別的init()方法:

def init(self):
xoff, yoff, zoff = 1, 101, time.localtime().tm_sec/10
for i in range(self.rows):
yoff += 0.05
for j in range(self.cols):
xoff += 0.05
# 將介於-1~1範圍的Perlin noise映射至0~360
angle_deg = 180*(noise.pnoise3(xoff, yoff, zoff)+1)
# 將角度相符的單位向量設為流場向量
self.field[i][j] = pygame.Vector2.from_polar((1, angle_deg))

主程式中,加入讓流場每隔固定時間就重置的功能:

# python version 3.10.9
import math
import random
import sys
import time

import noise # version 1.2.2
import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 5.7")

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

flow_field = FlowField(WIDTH, HEIGHT, 20)
flow_field.init()

vehicles = []
for _ in range(120):
x = random.randint(0, WIDTH)
y = random.randint(0, HEIGHT)
vehicle = Vehicle(x, y)
vehicle.max_speed = random.uniform(2, 5)
vehicle.max_force = random.uniform(0.1, 0.5)
vehicles.append(vehicle)

show_field = True

t_start = time.localtime().tm_sec
# 重置流場之時間間隔,單位為「秒」
time_window = 2

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
flow_field.init()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
show_field = not show_field

screen.fill(WHITE)

# 若經過的時間大於設定的時間間隔,則重置流場
t_end = time.localtime().tm_sec
if abs(t_end - t_start) >= time_window:
flow_field.init()
t_start = t_end

if show_field:
flow_field.show()
# flow_field.show_vector()
# flow_field.show_grid()

for vehicle in vehicles:
vehicle.follow(flow_field)
vehicle.update()
vehicle.check_edges()
vehicle.show()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 5.8

定義像素亮度的值為r+g+b,並將流場向量方向設定為由暗到亮的方向。

def init(self, img):
for i in range(self.rows):
y0 = i*self.resolution
for j in range(self.cols):
x0 = j*self.resolution
# 計算方格內像素之亮度值、座標值
pixel_data = []
for y in range(y0, y0+self.resolution):
for x in range(x0, x0+self.resolution):
r, g, b = img.get_at((x, y))[:3]
pixel_data.append((r+g+b, pygame.Vector2(x, y)))

# 向量方向為從最暗點到最亮點的方向
pt_max = max(pixel_data, key=lambda x: x[0])[1]
pt_min = min(pixel_data, key=lambda x: x[0])[1]
self.field[i][j] = pt_max - pt_min
留言
avatar-img
留言分享你的想法!
avatar-img
ysf的沙龍
15會員
142內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2025/03/10
在模擬群聚行為時,隨著boid的數量越來越多,需要的計算量也會越來越多,導致程式的執行速度也跟著越來越慢,最後甚至於動彈不得。要克服這個問題,在寫程式時使用效率比較好的演算法,就是個不錯的主意。
Thumbnail
2025/03/10
在模擬群聚行為時,隨著boid的數量越來越多,需要的計算量也會越來越多,導致程式的執行速度也跟著越來越慢,最後甚至於動彈不得。要克服這個問題,在寫程式時使用效率比較好的演算法,就是個不錯的主意。
Thumbnail
2025/02/03
到目前為止,我們所設計出來的自主代理人都是孤鳥,既不知道有其他自主代理人的存在,也不會跟其他自主代理人有任何互動。在這一節,我們將讓自主代理人能感知到其他自主代理人的存在,並且與其他自主代理人互動,最後形成由自主代理人所組成的複雜系統(complex system)。
2025/02/03
到目前為止,我們所設計出來的自主代理人都是孤鳥,既不知道有其他自主代理人的存在,也不會跟其他自主代理人有任何互動。在這一節,我們將讓自主代理人能感知到其他自主代理人的存在,並且與其他自主代理人互動,最後形成由自主代理人所組成的複雜系統(complex system)。
2024/12/30
不同於用來找出兩點間最短距離演算法的路徑搜尋(path finding),路徑循行(path following),指的是依循已經設定好的路徑來移動的轉向行為。這一節就要來研究Reynolds所設計的路徑循行轉向行為。
2024/12/30
不同於用來找出兩點間最短距離演算法的路徑搜尋(path finding),路徑循行(path following),指的是依循已經設定好的路徑來移動的轉向行為。這一節就要來研究Reynolds所設計的路徑循行轉向行為。
看更多
你可能也想看
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
本章節詳細介紹循序邏輯概念,同時為正反器的章節做鋪墊,意在幫助有意入門數位邏輯設計的大學生。
Thumbnail
本章節詳細介紹循序邏輯概念,同時為正反器的章節做鋪墊,意在幫助有意入門數位邏輯設計的大學生。
Thumbnail
在x軸上依序取一些點,然後把這些點以及其對應的sin函數的值所構成的二維座標點畫出來時,就可以看到由這個sin函數所產生的像波一樣的圖案,也就是波型(wave pattern)。不同樣式的波型,可以用來設計生物的軀幹或肢體,也可以用來模擬像水這類柔軟的表面。
Thumbnail
在x軸上依序取一些點,然後把這些點以及其對應的sin函數的值所構成的二維座標點畫出來時,就可以看到由這個sin函數所產生的像波一樣的圖案,也就是波型(wave pattern)。不同樣式的波型,可以用來設計生物的軀幹或肢體,也可以用來模擬像水這類柔軟的表面。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
這一節的標題是A Smoother Approach with Perlin Noise,介紹由Ken Perlin所開發的Perlin noise,及其應用方式。
Thumbnail
這一節的標題是A Smoother Approach with Perlin Noise,介紹由Ken Perlin所開發的Perlin noise,及其應用方式。
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
直觀理解 導數:考慮的是單一變數的函數,描述的是函數在某點的斜率或變化率。 偏導數:考慮的是多變數函數,描述的是函數在某個變數變化時的變化率,其他變數保持不變。  (針對各維度的調整 或者稱變化 你要調多少) 應用 導數:在物理學中應用廣泛,例如描述速度和加速度。 偏導數:在多變量分析、優
Thumbnail
這篇文章,會帶著大家複習以前學過的BFS框架, 並且以圖論的應用題與概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 BFS 框架 + 演算法 虛擬碼 # Queue 通常初始化成根結點,作為起點 BFS_queue = deque([root])​ # 先
Thumbnail
這篇文章,會帶著大家複習以前學過的BFS框架, 並且以圖論的應用題與概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 BFS 框架 + 演算法 虛擬碼 # Queue 通常初始化成根結點,作為起點 BFS_queue = deque([root])​ # 先
Thumbnail
根據泰勒定理,f(x)可以寫成右邊一長串的導數的組合 為了更好理解這個東西我們可以用python實作 首先定義f(x)和定義factorial怎麼算 然後寫泰勒定理 f(x) = f(a) + f'(a)(x-a) ....後面還有一串 注意公式往後面看其實是有規律的 例如從
Thumbnail
根據泰勒定理,f(x)可以寫成右邊一長串的導數的組合 為了更好理解這個東西我們可以用python實作 首先定義f(x)和定義factorial怎麼算 然後寫泰勒定理 f(x) = f(a) + f'(a)(x-a) ....後面還有一串 注意公式往後面看其實是有規律的 例如從
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News