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

更新於 2024/12/02閱讀時間約 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
15會員
130內容數
寫點東西自娛娛人
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。
自主代理人指的是一種實體(entity),這種實體在沒有任何人指揮以及事先規劃好的情形下,能夠自主決定在身處的環境中要怎麼行動。
前面幾章所介紹的,都是只在外部環境的作用力下,才會動一動的無生命物體和圖案,整個模擬世界讓人覺得缺乏生命力沒什麼生氣。既然如此,那能不能灌注些生命力給這些無生命的物體和圖案呢?如果讓它們可以依照自己的想法而活,那模擬世界會變成什麼樣子呢?可以讓它們擁有希望和夢想嗎?可以讓它們心存恐懼嗎?
轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。
自主代理人指的是一種實體(entity),這種實體在沒有任何人指揮以及事先規劃好的情形下,能夠自主決定在身處的環境中要怎麼行動。
前面幾章所介紹的,都是只在外部環境的作用力下,才會動一動的無生命物體和圖案,整個模擬世界讓人覺得缺乏生命力沒什麼生氣。既然如此,那能不能灌注些生命力給這些無生命的物體和圖案呢?如果讓它們可以依照自己的想法而活,那模擬世界會變成什麼樣子呢?可以讓它們擁有希望和夢想嗎?可以讓它們心存恐懼嗎?
你可能也想看
Google News 追蹤
Thumbnail
本文探討了複利效應的重要性,並藉由巴菲特的投資理念,說明如何選擇穩定產生正報酬的資產及長期持有的核心理念。透過定期定額的投資方式,不僅能減少情緒影響,還能持續參與全球股市的發展。此外,文中介紹了使用國泰 Cube App 的便利性及低手續費,幫助投資者簡化投資流程,達成長期穩定增長的財務目標。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
上兩篇有關List的文章,此篇文上兩章的延續,整理一些常用的方法和操作。 [Python]List(列表)新增、修改、刪除元素 [Python基礎]容器 list(列表),tuple(元組) 還有一些常用的 list 方法和操作,讓你能更靈活地處理列表數據
Thumbnail
浮動(float)是早期用來創建佈局的技術。元素可以向左或向右浮動,旁邊的元素會環繞浮動元素。浮動元素通常用於圖文混排或簡單的兩欄佈局。
Thumbnail
這篇教學教您如何在 Ren'py 中製作章節選擇畫面,提供素材下載以及變量設定的範例,並附加結合變量來控制章節解鎖的說明。
Thumbnail
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
Thumbnail
此章節的目的是介紹Java程式語言中的流程控制結構,包括條件語句(if, else if, else)、三元運算子、switch語句,以及各種迴圈(for, foreach, while)。同時,也解釋了如何在迴圈中使用控制語句來改變程式的執行流程。每種主題都配有示例程式碼以幫助理解。
Thumbnail
本章節提供了關於Typescript中流程控制元素的詳細介紹,包括if, else if, else語句,三元運算子,switch語句,各種for迴圈,while迴圈,循環嵌套和控制迴圈語句(break,continue和標籤)的使用。
Thumbnail
我們在「【🎓 Python的深度問答集】torchaudio 對部分段落進行音訊解碼」有分享到如何對一包包的封包進行音訊解碼, 但隨著音檔越大, 最終解碼的速度會越來越慢, 而這並非串流的本意, 串流應該就像水管一樣, 收到多少資料就運算多少量, 並不會隨著累積的容量越大而導致效能下降。 但實際
Thumbnail
這是為了搭建自己想要的工作流而開始的研究工作。
Thumbnail
題目敘述 題目會給我們一組定義好的界面和需求,要求我們設計一個資料結構,可以滿足平均O(1)的插入元素、刪除元素、隨機取得元素的操作。 RandomizedSet() 類別建構子 bool insert(int val) 插入元素的function界面 bool remove(int val
Thumbnail
本文探討了複利效應的重要性,並藉由巴菲特的投資理念,說明如何選擇穩定產生正報酬的資產及長期持有的核心理念。透過定期定額的投資方式,不僅能減少情緒影響,還能持續參與全球股市的發展。此外,文中介紹了使用國泰 Cube App 的便利性及低手續費,幫助投資者簡化投資流程,達成長期穩定增長的財務目標。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
上兩篇有關List的文章,此篇文上兩章的延續,整理一些常用的方法和操作。 [Python]List(列表)新增、修改、刪除元素 [Python基礎]容器 list(列表),tuple(元組) 還有一些常用的 list 方法和操作,讓你能更靈活地處理列表數據
Thumbnail
浮動(float)是早期用來創建佈局的技術。元素可以向左或向右浮動,旁邊的元素會環繞浮動元素。浮動元素通常用於圖文混排或簡單的兩欄佈局。
Thumbnail
這篇教學教您如何在 Ren'py 中製作章節選擇畫面,提供素材下載以及變量設定的範例,並附加結合變量來控制章節解鎖的說明。
Thumbnail
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
Thumbnail
此章節的目的是介紹Java程式語言中的流程控制結構,包括條件語句(if, else if, else)、三元運算子、switch語句,以及各種迴圈(for, foreach, while)。同時,也解釋了如何在迴圈中使用控制語句來改變程式的執行流程。每種主題都配有示例程式碼以幫助理解。
Thumbnail
本章節提供了關於Typescript中流程控制元素的詳細介紹,包括if, else if, else語句,三元運算子,switch語句,各種for迴圈,while迴圈,循環嵌套和控制迴圈語句(break,continue和標籤)的使用。
Thumbnail
我們在「【🎓 Python的深度問答集】torchaudio 對部分段落進行音訊解碼」有分享到如何對一包包的封包進行音訊解碼, 但隨著音檔越大, 最終解碼的速度會越來越慢, 而這並非串流的本意, 串流應該就像水管一樣, 收到多少資料就運算多少量, 並不會隨著累積的容量越大而導致效能下降。 但實際
Thumbnail
這是為了搭建自己想要的工作流而開始的研究工作。
Thumbnail
題目敘述 題目會給我們一組定義好的界面和需求,要求我們設計一個資料結構,可以滿足平均O(1)的插入元素、刪除元素、隨機取得元素的操作。 RandomizedSet() 類別建構子 bool insert(int val) 插入元素的function界面 bool remove(int val