iT邦幫忙

2022 iThome 鐵人賽

DAY 18
1
Modern Web

30個遊戲程設的錦囊妙計系列 第 18

Trick 17: 綿延不絕的隨機地形是咋做出來的?

  • 分享至 

  • xImage
  •  

不管你喜不喜歡沙盒遊戲,都無法否認我的世界(Minecraft)、泰拉瑞亞(Terraria)這些屹立了十多年仍然立於頂端的遊戲類型有多麼吸引玩家。

類似的沙盒遊戲各有千秋,但他們首要克服的問題,都是要想辦法產生一片無止境隨機卻又連續的地形。
minecraft terraria

柏林噪聲

Noise(中文翻成噪音或噪聲)是指自然環境中那些無規律變化的背景音波。在數學中模擬自然噪聲波形的方法有很多,柏林噪聲就是其中之一。

Ken Perlin於1983年發表了這個柏林噪聲演算法,當初是為了在電影中加入某些特效而想出來的,這個演算法隨後在電腦科學中被廣泛地應用於各個領域。柏林噪聲有維度之分,例如泰拉瑞亞的地形可以用一維的柏林噪聲來繪製,Minecraft的地形可以用二維的來建立。

因為二維、三維以上的數學計算比較複雜,加上Perlin當年的電腦速度不像現在這麼快,所以為了降低對電腦硬體的需求,Perlin特別想出了一些方法來優化二維以上柏林噪聲的計算。二維以及二維以上的這些變化,解釋起來有可能會搞得大家好亂,因此這裏就先跳過。以下我們專注在一維的柏林噪聲演算法就好。

想深入理解二維與三維柏林噪聲的同學,可以參考拙作
柏林噪聲(Perlin Noise): (科普)創造亂中有序大自然的魔法

一維柏林噪聲的核心概念很簡單,在數線上所有X為整數的位置,其對應的Y都是0,然後在這些X為整數的點上使用亂數產生器隨機決定曲線梯度(就是斜率啦),再用一個線性轉淡出的數學式,將所有的點連在一起。
perlin noise
由上圖可以看出柏林噪聲曲線的特性:

  1. 在X為整數的位置,曲線斜率就等於事先用亂數定義的梯度。
  2. 梯度對曲線斜率的影響力,隨著離整數X位置越遠影響力越低,也越受到隔壁整數點梯度的影響。在到達下一個整數點的時候,影響力降到0,所以下個整數點的斜率就會完全由下個整數點的梯度決定。
  3. 在曲線上以很小的間距取得的一串點,會呈現自然連續類似波的曲線,這就可以拿來作為自然地形的高度參考。

在寫一維噪聲的類別前,我們要先定義一下演算法要用哪個淡出公式。淡出公式也有蠻多種的,只要一個數學式可以把線性的變化轉換成淡入、淡出的變化,就可以當作淡出公式。

以下提供的是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,分別是一維、二維和三維的柏林噪聲演算法實作類別。


上一篇
Trick 16: 用MD5亂數產生器當個造物主
下一篇
Trick 18: 收下我的承諾,遲早給你個交待-I Promise
系列文
30個遊戲程設的錦囊妙計32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言