The Nature of Code閱讀心得與Python實作:0.6 A Smoother Approach...

閱讀時間約 27 分鐘
這一節的標題是
A Smoother Approach with Perlin Noise
因為方格子標題字數限制,所以沒完整顯現

這一節介紹的是由Ken Perlin所開發的Perlin noise。

利用Perlin noise演算法所得到的,其實也是pseudorandom number。然而,Perlin noise與random.random()這種常見的亂數產生器所產生的亂數之間,卻有著很不一樣的性質。什麼不一樣的性質呢?先來看看下面這張圖(繪製程式見本節文末):

raw-image

從圖中可以很明顯地看出來,Perlin noise所產生的數字序列,比亂數要來得平滑。這也就是說,Perlin noise所產生的數字序列,相鄰兩個數字之間,變化幅度不會像亂數那麼大。正因為Perlin noise的這種特性,所以比起亂數來,更適合用來製作如雲、地景、物體的紋理等視覺效果。使用亂數雖然能讓用程式模擬出來的東西,行為表現更真實、更自然,但因亂數數列各個值之間的變化可能會很劇烈,導致多多少少還是會有些地方顯得不是那麼的自然。不過,要注意的是,這並不意味著所有的東西都適合用Perlin noise來處理,畢竟在真實世界中,也存在著像亂石嶙峋這種變化劇烈的景象,這時候使用亂數反而比較適合。沒有哪種工具是萬能的,不同的問題會需要不同的解決工具,Perlin noise和亂數都是很好用的工具,就看會不會用而已。現在,就來看看Perlin noise要怎麼用。

在Python的標準程式庫中,並沒有提供Perlin noise相關的package,所以必須自己寫,或者用別人寫好的。在Python Package Index (PyPI)中,有一些可以產生Perlin noie的package,其中noise這個package,有相當多的人使用,應該是個不錯的選擇。不過要注意的是,noise所提供的,是兩種改良過的Perlin noise,其中一個是improved Perlin noise,也有人稱之為Perlin's improved noise,另一個是Perlin simplex noise。

安裝好noise之後,使用下列指令,即可看到使用說明:

import noise
help(noise)

在noise這個package中,包含下列函數:

noise.pnoise1(x) # 1D improved Perlin noise
noise.pnoise2(x, y) # 2D improved Perlin noise
noise.pnoise3(x, y, z) # 3D improved Perlin noise
noise.snoise2(x, y) # 2D simplex noise
noise.snoise3(x, y, z) # 3D simplex noise
noise.snoise4(x, y, z, w) # 4D simplex noise

這些函數除了xyzw等參數外,還有一些有預設值的參數可以調整。

Perlin noise和亂數在使用上有著相當大的差異。在取亂數時,參數的作用是讓亂數落在設定的範圍內,例如random.random()所傳回的亂數,會落在[0, 1)這個區間內;而random.uniform(0.8, 3.2)所傳回的亂數,會落在[0.8, 3.2]這個區間內。然而,以1D improved Perlin noise為例,noise.pnoise1(0.3)傳回的,是在0.3這個點上的noise數值。所以,程式

a = random.random()
b = random.random()
x = noise.pnoise1(0.3)
y = noise.pnoise1(0.3)

所得到的ab,是兩個不同的數值;但是xy卻會是相同的數值。如果想取得不同數值的noise,呼叫noise.pnoise1()時,使用的參數必須不同,例如

for i in range(10):
    x = noise.pnoise1(i*0.01)
    print(x)

所印出的,就會是10個不同的數值。但是有一點要注意的,不管是幾維的pnoise還是snoise,在整數點上取到的值,往往會是0,所以應盡量避免使用整數參數。

既然要取得不同數值的noise必須使用不同的參數,那這些參數要怎麼設定呢?以上述的程式為例,是以0.01為間隔,來取得不同數值的noise。原則上,這個間隔越小,取出的noise數值會越平滑;而間隔越大,取出的noise數值,則會越不平滑,而越像亂數。

Noise Ranges

在取亂數時,我們會知道取出的亂數,是落在哪個範圍內。那從noise這個package所取得的Perlin noise,會落在哪個範圍內呢?可惜在使用說明裡頭,並沒有提到這一點。不過,從測試程式來看,不管是pnoise還是snoise所傳回的值,都會落在[-1, 1]這個區間內。

既然取得的noise值會落在區間[-1, 1]內,在實際使用時,就必須將其映射(map)到我們所需要的範圍。在Python中,並未內建這種功能的函數,所以必須自己寫,或者使用numpy中的interp()。現在先來推導公式,然後再來看看interp()要怎麼用。

假設要把區間[a, b]裡頭的點p,映射到區間[x, y]中,公式可以這麼推導:

a ≤ p ≤ b
0 ≤ p-a ≤ b-a
0 ≤ (p-a)/(b-a) ≤ 1
0 ≤ (y-x)(p-a)/(b-a) ≤ y-x
x ≤ (y-x)(p-a)/(b-a)+x ≤ y

所以p映射到[x, y]之後,就會變成

(p-a)(y-x)/(b-a) + x

當然啦,在這裡我們會假設a≠b、x≠y。

另一個推導方式,用的是幾何的觀點,步驟比較少。假設[a, b]裡頭的點p,映射到區間[x, y]中會成為q。線段ap占線段ab的比例,會等於線段xq占線段xy的比例,也就是

(p-a)/(b-a) = (q-x)/(y-x)

整理一下,可以得到

q = (p-a)(y-x)/(b-a) + x

現在,把a=-1及b=1代入推導出來的式子中,得到

q = 0.5×(p+1)(y-x) + x

接下來,來看看interp()的用法。interp()其實是用來做內插的,不過也可以拿來映射區間。要把區間[a, b]中的點p映射到區間[x, y]中,參數的設定如下:

interp(p, [a, b], [x, y])

所以,要把取得的noise的值p,映射到區間[x, y]中,要寫成

numpy.interp(p, [-1, 1], [x, y])

應用Perlin noise映射的技術,我們可以把依循亂數移動的walker,改成以比較平順的方式移動。

Example 0.6: A Perlin Noise Walker

raw-image


class Walker:
def __init__(self, x=0, y=0):
# pygame的畫面
self.screen = pygame.display.get_surface()

# walker的位置,預設值是(0, 0)
self.x, self.y = x, y

# Perlin noise參數起始值
self.tx, self.ty = 0, 10000

# 將自己顯示在螢幕上
def show(self, color=(0, 0, 0)):
self.screen.set_at((int(self.x), int(self.y)), color)

def step(self):
self.x = numpy.interp(noise.pnoise1(self.tx), [-1, 1], [0, WIDTH])
self.y = numpy.interp(noise.pnoise1(self.ty), [-1, 1], [0, HEIGHT])

self.tx += 0.01
self.ty += 0.01


# python version 3.10.9
import sys

import pygame # version 2.3.0
import noise # version 1.2.2
import numpy # version 1.23.5


pygame.init()

pygame.display.set_caption("Example 0.6: A Perlin Noise Walker")

WHITE = (255, 255, 255)

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

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

walker = Walker(WIDTH//2, HEIGHT//2)

screen.fill(WHITE)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

walker.step()
walker.show()

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

在這個例子中,我們在step()這個方法內,用兩個變數txty來取得不同的Perlin noise,以便將其映射為walker的實際位置。特別要注意的是,在__init__()中設定的txty起始值並不相同,而且差距很大,一個是0,一個是10000。這麼做的目的,是為了讓取得的兩個Perlin noise數列,彼此之間看起來沒什麼關聯性。從下面這張不同參數範圍Perlin noise比較圖(繪製程式見本節文末),就可以看出來這麼做的效果:

raw-image

除了起始值之外,txty的值也需要注意。在step()中的寫法

tx += 0.01
ty += 0.01

會導致txty每隔一定的間隔,剛好都是整數。但是如先前提到過的,在整數點上取到的noise值,往往會是0。所以這樣子寫法所取出的兩個noise,每隔一定的間隔,就會同時為0,而導致本來希望不相干的兩組noise,看起來有些關聯性。這個現象,可以從Example 0.6所畫出的walker運動軌跡中看出來。在圖中,軌跡經常會通過畫面的中心位置。這個位置就是,當分別由txty處所取得的兩個noise值,剛好都是0時,映射到xy座標後的位置。

要避免txty同時為整數的情況發生,稍微調整一下它們改變的間隔大小就可以。例如改為

tx += 0.009
ty += 0.011

walker的運動軌跡就會變成

raw-image

很明顯的,通過中心點的軌跡變少了。

當然啦!txty的起始值不要用整數,也是一個可行的辦法。總之,就是要避免在整數點上取Perlin noise就是了。

Exercise 0.7

要用Perlin noise來決定walker的步伐大小,step()可以這樣改寫:

def step(self):
self.x += numpy.interp(noise.pnoise1(self.tx), [-1, 1], [-3, 3])
self.y += numpy.interp(noise.pnoise1(self.ty), [-1, 1], [-3, 3])

self.tx += 0.009
self.ty += 0.011

這裡的[-3, 3]就是包含方向的步伐大小。改寫之後的walker移動軌跡圖如下:

raw-image

這個軌跡圖長相很奇怪,似乎右邊和下面各有一堵牆,walker怎麼樣都跨不過去。使用不同的txty間隔和起始值,也會有一樣的現象。推測這應該跟improved Perlin noise的曲線長相有關。以積分的觀點來看,似乎improved Perlin noise曲線底下所包含的面積,有upper bound存在。

無論如何,用Perlin noise來決定walker的步伐大小,看來並不是個好主意。

Two-Dimensional Noise

到目前為止,我們用的都是一維的Perlin noise。接下來,就來看看二維的Perlin noise可以怎麼用。

先來看看這個一維、二維指的是什麼東西的維度。

要計算一維、二維的Perlin noise,程式要這麼寫:

noise.pnoise1(x)    # 1D
noise.pnoise2(x, y) # 2D

所以,很明顯的,這裡的一維、二維,指的是參數的數量,說得比較有學問一點,就是參數空間的維度。從這裡也就很容易可以推廣到三維,甚至是四維或更高的維度。

從一維、二維Perlin noise的程式寫法可以看出,不管參數是幾個,取得的noise都是單一的數值。既然如此,那這一維、二維的Perlin noise之間,到底有些什麼差異?

先來看看一維的情況。Perlin noise的特點就是它的平滑性,也就是說在某個點上所取得的noise,跟在這個點前、後附近的點所取得的noise,差異不大。例如,在x-0.01、x、x+0.01這三個點所取得的noise數值就會很接近。

把一維的情況推廣到二維,就會變成:在平面上某個點所取得的noise,跟在這個點周圍附近所取得的noise,彼此間的差異不大。以九宮格的觀點來看,就是中心點所取得的noise數值,跟周圍八個點所取得的noise數值,都會很接近。下面這張圖,就是利用二維Perlin noise畫出來的。

raw-image

這張圖的製作方式,是利用Perlin noise來設定每個像素的顏色。因為Perlin noise平滑的特性,圖中的黑、白之間,並沒有很清楚的交界處,而是以漸層的方式改變顏色。這種效果,也就是當初發明Perlin noise的目的。製作圖片的完整的程式碼如下:

# python version 3.10.9
import sys

import pygame # version 2.3.0
import noise # version 1.2.2
import numpy # version 1.23.5


pygame.init()

pygame.display.set_caption("2D Perlin noise")

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

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

xoff = 0
for x in range(WIDTH):
yoff = 0
for y in range(HEIGHT):
bright = numpy.interp(noise.pnoise2(xoff, yoff), [-1, 1], [0, 255])
r = g = b = int(bright)
screen.set_at((x, y), (r, g, b))
yoff += 0.01

xoff += 0.01

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

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

在程式中,並不是直接用像素位置的座標當作取improved Perlin noise的參數,而是以間隔0.01的格點位置當成呼叫pnoise2()的參數。這麼做的原因,是因為在整數點上取得的noise都是0,畫出來的,會是一張沒有顏色變化灰色的圖。

從這個例子可以知道,計算noise時所使用的xyz等參數,並不一定會跟實際的座標位置有所關聯,它們就只是拿來產生noise時所用的數值而已。例如,我們要用pnoise2(x, y)傳回的noise值來設定(2, 3)這個位置上的像素顏色時,不見得就一定要設定成x=2y=3。在這個例子中,我們是設定成x=0.02y=0.03,但事實上是設定成任何數字都可以,關鍵在於參數點之間的間隔大小。參數點的間隔越小,算出來的noise就越平滑;參數點的間隔越大,算出來的noise就會越像亂數,至於要用多大的參數點間隔,那就完全取決於是不是能達到我們想要的目的而定。

總之,xyz就只是xyz,它們不是專指長、寬、高,也不是專指經度、緯度、海拔高度或任何其他東西。只要你喜歡,它們可以代表任何東西,只要能製造出想要達到的效果就可以。

Exercise 0.8

改變參數點間隔大小會有不同的效果

xoff += 0.03
yoff += 0.02
raw-image


xoff += 0.01
yoff += 0.1
raw-image


改變顏色

xoff += 0.01
yoff += 0.1
r = g = int(bright)
b = 255 - r
raw-image


Exercise 0.9

新增第三個參數zoff,間隔0.38,原來的兩個參數xoffyoff,間隔分別為0.210.39。圖案會隨著zoff的改變而跟著改變,取大一些的間隔,比較能看出來圖案的變化。下面是其中兩張圖案截圖:

raw-image


raw-image


完整程式如下:

# python version 3.10.9
import sys

import pygame # version 2.3.0
import noise # version 1.2.2
import numpy # version 1.23.5


pygame.init()

pygame.display.set_caption("Exercise 0.9: xoff+=0.21, yoff+=0.39, zoff+=0.38")

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

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

zoff = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

xoff = 0
for x in range(WIDTH):
yoff = 0
for y in range(HEIGHT):
#bright = numpy.interp(noise.pnoise3(xoff, yoff, zoff), [-1, 1], [0, 255])
bright = 127.5*(noise.pnoise3(xoff, yoff, zoff) + 1)
r = g = b = int(bright)
screen.set_at((x, y), (r, g, b))
yoff += 0.39

xoff += 0.21

zoff += 0.38

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


為了加快計算速度,原本用interp()來將noise映射成像素亮度,現在改成直接用先前推導出的公式來計算。將

a = -1
b = 1
x = 0
y = 255

代入後,得到

q = 127.5(p+1)

所以程式

bright = numpy.interp(noise.pnoise3(xoff, yoff, zoff), [-1, 1], [0, 255])

就改成

bright = 127.5*(noise.pnoise3(xoff, yoff, zoff) + 1)


Exercise 0.10

raw-image

因為pygame沒有支援3D繪圖功能,所以用matplotlib來畫。完整程式如下:

# python version 3.10.9
import sys

# matplotlib version 3.7.1
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits import mplot3d

import noise # version 1.2.2
import numpy as np # version 1.23.5


xoff = 0.03
yoff = 0.03
x= np.arange(0, 5, xoff)
y= np.arange(0, 5, yoff)

X, Y = np.meshgrid(x, y)

nr, nc = X.shape
Z = np.zeros((nr, nc))

elevation = 50
for i in range(nr):
for j in range(nc):
Z[i][j] = np.interp(noise.pnoise2(X[i][j], Y[i][j]), [-1, 1], [0, elevation])

fig = plt.figure(figsize =(14, 9))
ax = plt.axes(projection ='3d')
ax.set_axis_off()
ax.set_title("Exercise 0.10", y=0.85)
ax.set_box_aspect((3, 3, 0.5))
ax.plot_surface(X, Y, Z, cmap = cm.gist_earth, edgecolors='w', linewidth=0.2)

plt.show()


要畫3D的圖,重點在於參數XYZ裡頭,資料是怎麼擺放的。假設在xy平面上的格點座標如下:

(0, 5)  (1, 5)  (2, 5)
(0, 6)  (1, 6)  (2, 6)
(0, 7)  (1, 7)  (2, 7)
(0, 8)  (1, 8)  (2, 8)

X裡頭放的,應該是

0, 1, 2
0, 1, 2
0, 1, 2
0, 1, 2

Y裡頭放的,則是

5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8

至於Z,假設Z=X*Y,則Z裡頭放的資料是

0, 5, 10
0, 5, 12
0, 5, 14
0, 5, 16

也就是說,X裡頭放的,就是格點座標中的x值;而Y裡頭放的,則是格點座標中的y值;至於於Z裡頭放的,就是X和Y同樣位置的元素所對應的z值,也就是

Z[i][j] = X[i][j]*Y[i][j]

要把資料正確地放進XY,比較簡便的方式是使用numpy的meshgrid(),不過Z就沒這麼好處理了,因為不見得能像Z=X*Y這樣,直接以XY透過array運算算出來。就以Perlin noise的計算來說,並不支援array的運算,所以必須搞清楚XYZ裡頭的資料擺放方式,然後一個元素一個元素的把Z給算出來。


Perlin noise跟亂數之間的差異比較圖繪製程式
# python version 3.10.9
import random

import matplotlib.pyplot as plt # version # 3.7.1
import noise # version 1.2.2


plt.figure(figsize=(10, 3))

# Perlin noise values
x, y = [], []
for i in range(5001):
x.append(20+i*0.001)
y.append(noise.pnoise1(x[i]))

plt.subplot(1, 2, 1)
plt.xticks([])
plt.yticks([])
plt.xlim([20, 25])
plt.plot(x, y)
plt.title("Perlin noise values over time")

# random noise values
x, y = [], []
for i in range(251):
x.append(i)
y.append(random.random())

plt.subplot(1, 2, 2)
plt.xticks([])
plt.yticks([])
plt.xlim([0, 250])
plt.plot(x, y)
plt.title("Random noise values over time")

plt.show()


不同參數範圍Perlin noise比較圖繪製程式
# python version 3.10.9
import random

import matplotlib.pyplot as plt # version # 3.7.1
import noise # version 1.2.2


plt.figure(figsize=(10, 3))

plt.suptitle("Different parts of noise space")

# left part
x, y = [], []
for i in range(5001):
x.append(i*0.01)
y.append(noise.pnoise1(x[i]))

plt.subplot(1, 2, 1)
plt.xlim([0, 10])
plt.ylim([-1, 1])
plt.plot(x, y)

# right part
x, y = [], []
for i in range(5001):
x.append(10000+i*0.01)
y.append(noise.pnoise1(x[i]))

plt.subplot(1, 2, 2)
plt.xlim([10000, 10010])
plt.ylim([-1, 1])
plt.plot(x, y)

plt.show()


13會員
92內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
The Evolution of Mobile App Development Frameworks in 2024As we navigate through 2024, the mobile app development landscape continues to evolve, driven by changing consumer behaviors...
Thumbnail
avatar
Digiworld
2024-04-26
The benefits on exercise_2024.04.25複習今日所學英文~有關於運動~ 內容:成長過程中對運動的興趣、運動的正面影響。從小時候習慣了每天運動、運動選擇的改變、現在面臨的挑戰~
Thumbnail
avatar
天上的一片雲
2024-04-25
《Compassion》The sorrow of trees - a message from nature"You live for yourselves, not for humans." About trees and their feelings of sadness due to human interference.
Thumbnail
avatar
靈魂共感療域 EmpathyCave
2023-11-12
自然死亡,很難嗎? Allowing the mother nature taking over our deathNow I understand: there is no need for futile treatment or ineffective care, allowing our mother nature to take over may be the kindest approach for t
avatar
雙魚鏡方格子檔案館
2023-07-13
《The First Slam Dunk》:即使心臟噗通噗通跳,也要表現得若無其事  簡直像站在球場中看著真實的比賽讓人目不轉睛,呼吸為之屏息——傳說中灌籃高手動畫版的遺珠之憾:湘北對山王之戰終於出世。當年所有人期盼卻沒盼得的最重要的一場比賽,在生父手下脫胎而出,讓這部作品畫下奇蹟的句點。  
Thumbnail
avatar
薄荷糖
2023-01-28
The Writer And Her Story 遇見香港漫畫家智海《The Writer And Her Story》是香港漫畫家智海在1999年的作品,黑白色調的漫畫,描寫一個抑鬱的作者,被自己腦袋裡沉重的想法壓駝了背,寫下作品想為腦袋「減重」,卻被各種瑣碎問題所困,就連繪者也彷彿以上帝之手,延伸再延伸,給他沒有盡頭的樓梯和前路...
Thumbnail
avatar
林倚
2023-01-03
The Innocents Abroad👉跟馬克•吐溫遊歐亞在海上看美景,也不忘笑話別人的英語口音👉「It's gorgis, ain't it?」一起來領略一世紀前馬克•吐溫的不倒幽默!
Thumbnail
avatar
Mary Ventura
2022-09-11
the benefit of the doubt? 「懷疑的好處」是什麼語感來著?英文有一些慣用的句子,字面上怎麼讀都參不透,因為不知道背後隱含的思考邏輯。「給他懷疑的好處(give him the benefit of the doubt)」這種話在中文的邏輯中根本不成文也說不通,到底是在講什麼鬼?你只能用外國人的邏輯來想!
Thumbnail
avatar
你的英日語自學導師 譯難忘  ོꦿ༄꧁꧂
2021-06-23
《The Queen's Gambit》 你是一個奇蹟,但要學習如何生活《Queen’s Gambit》第一眼看會以為又是另一個關於天才兒童戰無不勝或者自甘墮落的故事。其實這一套劇集,講的是一個高敏感又內向的女生,如何在女權崛起之前的冷戰年代,以人生輸家之姿,晉身頂級棋手的故事,同時講述一個女孩子的前半生。
Thumbnail
avatar
Kayla 陳韻如
2021-03-19
avatar
神勇火球
2007-08-16