不管你喜不喜歡沙盒遊戲,都無法否認我的世界(Minecraft)、泰拉瑞亞(Terraria)這些屹立了十多年仍然立於頂端的遊戲類型有多麼吸引玩家。
類似的沙盒遊戲各有千秋,但他們首要克服的問題,都是要想辦法產生一片無止境隨機卻又連續的地形。
Noise(中文翻成噪音或噪聲)是指自然環境中那些無規律變化的背景音波。在數學中模擬自然噪聲波形的方法有很多,柏林噪聲就是其中之一。
Ken Perlin於1983年發表了這個柏林噪聲演算法,當初是為了在電影中加入某些特效而想出來的,這個演算法隨後在電腦科學中被廣泛地應用於各個領域。柏林噪聲有維度之分,例如泰拉瑞亞的地形可以用一維的柏林噪聲來繪製,Minecraft的地形可以用二維的來建立。
因為二維、三維以上的數學計算比較複雜,加上Perlin當年的電腦速度不像現在這麼快,所以為了降低對電腦硬體的需求,Perlin特別想出了一些方法來優化二維以上柏林噪聲的計算。二維以及二維以上的這些變化,解釋起來有可能會搞得大家好亂,因此這裏就先跳過。以下我們專注在一維的柏林噪聲演算法就好。
想深入理解二維與三維柏林噪聲的同學,可以參考拙作
《柏林噪聲(Perlin Noise): (科普)創造亂中有序大自然的魔法》
一維柏林噪聲的核心概念很簡單,在數線上所有X為整數的位置,其對應的Y都是0,然後在這些X為整數的點上使用亂數產生器隨機決定曲線梯度(就是斜率啦),再用一個線性轉淡出的數學式,將所有的點連在一起。
由上圖可以看出柏林噪聲曲線的特性:
在寫一維噪聲的類別前,我們要先定義一下演算法要用哪個淡出公式。淡出公式也有蠻多種的,只要一個數學式可以把線性的變化轉換成淡入、淡出的變化,就可以當作淡出公式。
以下提供的是Ken Perlin在2002年發表《Improving Noise》時所用的五次方淡出公式。
/** Perlin在論文中給的五次方淡出公式
* t的範圍為0到1
* 以fade(t)取得到的值也是0到1,但是
* 在t=0與t=1的附近,fade(t)變化量趨近於0
*/
function fade(t: number): number {
return t * t * t * ((6 * t - 15) * t + 10);
}
五次方淡出公式在平面座標上的曲線
接著來寫一維柏林噪聲的類別。
// 一維柏林噪聲
class Noise {
// 我們需要一個亂數種子來計算整數點的梯度
constructor(public seed: number) {
}
/** 取得在整數X的位置上的梯度
* 回傳值介於-1到1之間
*/
getSlopeOnIntX(intX: number): number {
// 用自制亂數產生器來產生亂數
// 只要用相同的intX來取,就一定會得到同樣的梯度
let rng = new RandomGenerator(this.seed + intX);
// 回傳值在-1到1之間
return rng.next() * 2 - 1;
}
/** 取得任意x位置上的噪聲值(y) */
getValue(x: number): number {
// 左側最靠近的整數x
let x0 = Math.floor(x);
// 左側最靠近的整數x上的梯度
let slope0 = this.getSlopeOnIntX(x0);
// 右側最靠近的整數x上的梯度
let slope1 = this.getSlopeOnIntX(x0 + 1);
// 計算t = x在x0和下個整數之間的位置百分比
let t = x - x0;
// 左側梯度抬高x位置上的y
let y0 = slope0 * t;
// 右側梯度抬高x位置上的y
let y1 = slope1 * (t - 1);
// 用淡出公式改變t,使線性的t變成淡出的t
let fadeT = fade(t);
// 最後用這個fadeT把左右兩側給的y按比例合在一起
return y0 + (y1 - y0) * fadeT;
}
}
這個演算法可能看起來有點玄,不過看懂的話,其實就是用比例在整數點之間找Y而已。其中的重點是整數點上的梯度一定要保持不變,這樣每次取兩個整數點之間的噪聲值,計算的參數才會一樣,曲線才可能平滑。所以柏林噪聲在取整數點梯度的時候,必須使用種子不變的亂數產生器。
換句話說,一顆亂數種子對應著一張柏林噪聲圖。換了一顆種子,種出來的就是另一張完全不同的噪聲圖。因此,只要遊戲中載入關卡時使用同一顆亂數種子,那麼建立出來的地圖就會一模一樣。
在Perlin的論文中使用的是一張事先產生好的亂數表,然後配合整數X的一些簡單計算,去亂數表中找到對應的整數點梯度。使用亂數表的好處是計算速度快,在處理大量噪聲計算時非常有效率。
我們學會了建立柏林噪聲的曲線後,還可以複製一條相同的曲線,縮小後再疊加回去,這樣就可以產生一條走向大致相同,但是增加了細部細節的曲線。
當然,我們還可以再複製,然後縮得更小再疊加回去,以製造更加精細的自然曲線。
一般講到柏林噪聲,很容易就聯想到自然地形的製作,但其實柏林噪聲的應用遠不止於此,只要你想得到需要隨機又要連續的圖形,像是下雨下雪發生的週期、生物選擇前進的路線等等,都是柏林噪聲能夠發揮的舞台。
把柏林噪聲納入你的遊戲函式庫,絕對是個明智的決定。
CG示範專案
這個專案展示了一些柏林噪聲的應用,不過並沒有納入演算法的程式碼。想要看小哈寫的柏林噪聲類別,可以打開右邊的連結, Noise1D.ts , Noise2D.ts , Noise3D.ts,分別是一維、二維和三維的柏林噪聲演算法實作類別。