前言
在畢業之際,總算是做出了一個關於Minecraft類遊戲地形生成的DEMO作為畢業設計,雖然說不上有多高大上,但也算是給 Gameplay 技能棧多點了一個熟練度,瞭解了下一些關於地形生成的演算法。不過由於博主並未透徹研究過Minecraft的原始碼,憑著部分別參考資料去猜測地形生成的實現方式,因此這個地形生成演算法可以說算是靠自己實踐得出的經驗。這篇部落格便是記錄自己經驗的一篇部落格。
在閱讀本篇部落格之前,還需要掌握 噪聲演算法 ,若對噪聲演算法不瞭解,可參考博主以前的部落格:遊戲開發中的噪聲演算法
github原始碼地址:https://github.com/KillerAery/Minecraft-Like-TerrainGenerationDemo
程式截圖:
參考資料:
[1]. Technical jargon heavy article about how terrain is generated in DwarfCorp.
[2]. 知乎 | Minecraft的地形生成演算法是什麼?
生成地形高度
一般的地形生成中,地形高度場都是通過2D噪聲(輸入一個二維座標,輸出一個高度值)來生成的,但是一層噪聲往往具有單調的特性(單一的頻率Frequenccies 和 振幅Amplitudes),不能滿足複雜的自然地形高度:地形可能會有大段連綿、高聳山地,也會有丘陵和蝕坑,更小點的有岩石塊,甚至更小的鵝卵石塊。
為了模擬出這樣的自然噪聲特性,我們可以借鑑 分形噪聲 的思想,通過使用不同的引數進行多幾次不同引數的噪聲計算,然後將結果疊加在一起。
在DEMO程式的高度生成中,將使用三層2D噪聲進行疊加,其中:
- 第一層:振幅大,頻率小,用於模擬平坦大陸的效果
- 第二層:振幅一般,頻率一般,用於模擬山脈群的效果
- 第三層:振幅小,頻率大,用於模擬小山丘、地面小凹凸的效果
\(Height(x,y)=128∗Noise2D(4x,4y)+64∗Noise2D(8x,8y)+32∗Noise2D(16x,16y)\)
生成生物群落
生物群落(Biome),實際上相當於一個區域的基本地形面貌,例如可分為草地、高原、雪原、沙漠、熱帶雨林等。影響生物群落的因素可以有很多,包含但不限於:溫度、溼度、高度、距離大海的距離、魔力值。如何定義影響因素,完全取決於你的建模。
DEMO程式中的生物群落屬性只取決於 溫度(Temperature)、溼度(Humidity)兩個因素,而這兩個因素又是分別由不同種子設定的噪聲計算得出:
\(\begin{aligned}Temperature(x,y)&=Noise2D(8x,8y) \\ Humidity(x,y)&=Noise2D(8x,8y)\end{aligned}\)
DEMO程式將溫度(Temperature)粗略分為熱帶、溫帶、寒帶,溼度(Humidity)粗略分為乾燥、溼潤;然後也相應提供了六種不同的生物群落型別:草地、雪地、沙漠、熱帶雨林、溫帶樹林、寒帶針葉林。
模擬雨水侵蝕、生成河流(未完)
DFS思想解決,模擬大雨滴落在地面上砸出一個個小坑的效果。
-
模擬一個雨滴,先定義雨滴的質量(比如5000)
-
隨機砸下來在某個位置,並計算它周邊的梯度(下降最急的地方)
-
沿著梯度移動雨滴,同時在原位置留下一定質量的水
-
繼續追蹤雨滴進行計算,當雨滴質量衰減到0時或者流進海平面時視為終止
對一定範圍內隨機模擬多個雨滴,得到的結果將是一個有侵蝕,甚至形成河流的地形。
生成洞穴、裂谷
洞穴生成,實際上基於一層3D噪聲(輸入一個三維座標,輸出一個噪聲值)來完成:
\(Cave(x,y,z)=Noise3D(16x,16y,16z)\)
然後再給定一個閾值,做如下判斷:
- 若噪聲值高於閾值,則三維座標對應方塊挖空
- 若噪聲值低於閾值,則三維座標對應方塊保留
當閾值越小,那麼更加容易產生洞穴且洞穴規模越來越大。
然而這種洞穴往往是不規則的,顯然是不符合裂谷、峽谷這種帶有狹長特點的中空地形,對於這類地形可另外使用伸縮變換後的3D座標引數,此外還應當加入高度因素的影響(例如高度越低,意味著越接近地底,因此賦予更低的閾值),這樣也可以形成具有一定深度的裂谷。
生成植被
植被生成,則主要是在計算生成概率,它在DEMO程式中依賴四個因素(溫度、溼度、噪聲值、隨機值):
\(\begin{aligned} Possible_{tree}(x,y) &=N_{tree}+H_{tree}+T_{tree}+R_{tree} \\ N_{tree} &= C_1 \cdot Noise2D(32x,32z) \\ H_{tree} &= C_2 \cdot Humidity(x,y)\\ T_{tree} &=C_3 \cdot |Temperature(x,y)-0.4|\\ R_{tree} &=C_4 \cdot Rand(x,y) \end{aligned}\)
其中,\(C_1\)、\(C_2\)、\(C_3\)、\(C_4\) 分別代表四個因素的權重,四個權重之和為1。
植物生成概率依賴溼度、溫度因素很合理,為什麼要依賴噪聲值、隨機值呢?
- 噪聲值:讓某些區域的植物分佈足夠密集,而另一些區域的植物分佈可以稀疏甚至無分佈,這些區域之間又可以做到植物密度的平滑銜接。
- 隨機值:密集分佈區域的植物幾乎每一格都會滿足生成概率條件,為了避免過於密集,融入一些隨機值因素,讓分佈的樹木之間至少有一定的間距。
放置樹木(Bezier曲線)
一旦滿足生成概率條件,我們就可以根據當前方塊的生物群落屬性來決定放置什麼樣的植物(溫帶草、寒帶草、蘑菇、花、寒帶樹、溫帶樹、熱帶樹...)。
其中樹木的放置稍微複雜些,DEMO程式採取了程式化生成而非模板生成的方式來放置樹木:
-
用一個隨機數給出樹木的最大高度 \(h_{max}\)
-
還需要計算樹幹每層的樹葉半徑,這一步主要通過三階Bezier曲線來計算。三階Bezier曲線擁有4個控制點(2D座標),將控制點的 \(x\) 視為樹葉半徑長,而 \(y\) 視為所處在的樹幹高度。由於樹葉在最底層和最頂層都應該是沒有樹葉的,這樣就可以將第一個控制點和最後一個控制點固定在 \((0,0)\) 和 \((h_{max},0)\) ;而中間兩個控制點則可以利用兩個隨機數作為不同的隨機半徑\(r_1\)、\(r_2\),分別設定位於 \((\frac{1}{3}h_{max},r_1)\) 和 \((\frac{2}{3}h_{max},r_2)\)。
-
在每個單位高度上對貝塞爾曲線上一次取樣,從而得到每層樹葉的半徑值(取樣後四捨五入)。
如圖所示,當計算出一棵樹的隨機高度為5時,用於生成樹葉的貝塞爾曲線的第一個控制點和第四個控制點分別為\((0,0)\)和\((5,0)\)。接著,中間兩個控制點,通過隨機數4.5、2.5確定座標分別為\((1.66667,4.5)\)、\((3.33333,2.5)\)。當樹需要計算每層樹葉半徑時,就可以逐層對該貝塞爾曲線進行取樣,共取樣6次,對應6層樹葉半徑,分別為\((0,0)\)、\((1,2.2)\)、\((2,2.6)\)、\((3,2.4)\)、\((4,1.6)\)、\((5,0)\),四捨五入後即為 \((0,0)\)、\((1,2)\)、\((2,3)\)、\((3,2)\)、\((4,2)\)、\((5,0)\)。
生成建築
生成發展域(元胞自動機模型)
基於元胞自動機模型。
發展域可以理解成一個聚落的勢力範圍。而生成發展域的大概做法是:
-
在某個方塊設定聚落的源點
-
進行若干輪迭代演化,來演繹聚落的發展(擴充套件勢力範圍),其中每輪發展需要根據溫度、溼度、崎嶇度(周圍若干方塊高低差)等因素來影響發展域的擴充套件方向,而且只擴張在勢力範圍鄰接的方塊。
溫度、溼度越適中、崎嶇度越小的方塊的代價更低,從而也更容易讓聚落範圍往這種方塊的方向去擴充套件。
而在DEMO程式實現中,有以下細節:
- 需要設定一個最高發展度(迭代次數)。
- 一個發展塊設定為3*3個方塊,這是因為相同大小的勢力範圍下,一次新增3*3個方塊相比1個方塊有著更少的迭代次數。
- 每一輪迭代都從評估佇列裡將代價最低的發展塊加入聚落的勢力範圍,然後將與該發展塊相鄰的發展塊加入佇列中,並分別進行代價評估(即溫度、溼度、崎嶇度的綜合考量)。
在《DwarfCorp》中,這種元胞自動機模型又可以用於模擬各文明在地圖上的勢力範圍,讓文明源點儘可能往條件宜人、土地肥沃且少衝突的區域擴張,通過若干輪迭代後,就能得出一條合理的文明勢力邊界。
放置建築(DFS)
放置建築,主要是基於DFS演算法(在某種意義上,用高大上的名詞來講就是波函式坍縮),在前面生成好的發展域內通過DFS演算法隨機嘗試放置預製建築。
DEMO程式的大致實現:
-
在待放置位置佇列新增源點位置
-
進行若干次迴圈,每次迴圈從佇列中取出一個位置(以該位置為建築中心點)嘗試放置預製建築。
- 若建築即將放置的區域並不是發展域的子集,則嘗試放置失敗。
- 若建築即將放置的區域是發展域的子集,則嘗試放置成功,需將地形進行平整化後再放置建築。接著,將該位置上下左右四個方向一定offset(需要融入一定的隨機數,這樣得到的建築分佈就不會過於工整)的位置新增進待放置位置佇列。最後,移除發展域相應的區域方塊記錄(避免重複放置建築)。
連線道路(A*尋路)
連線道路,主要是基於A*尋路演算法,將每個建築的門口視為目標點,通過尋路演算法對所有目標點兩兩連成一條道路。然而問題在於,道路連線不是簡單的尋找最短路,還得模擬出人類聚落主幹道、分支路的特性。
DEMO程式的解決方式:
- 只需簡單地修改代價函式,使結點在道路上的開銷降低
每次生成完一條道路,需記錄道路位置資訊,以方便下次尋路查詢某個座標是否位於道路中。
這樣,第一條道路雖然總是最短路,但是往後每次連線新道路時,這些尋路演算法會相當大可能貼近或者連進原有道路,而不是直接連成最短路。若干條道路生成完畢後,就會顯而易見看到幹道、分支路的現象了。
優化
地形載入&渲染
有時候可能載入方塊太多導致記憶體不足,需要實現實時自動載入周圍區域和解除安裝過遠的區域。
其次,Minecraft類地形中往往有大量方塊被其它(上下左右前後共6個)方塊所包圍,從而不可視。而最初的渲染中,需要把所有存在的方塊都渲染出來:
如果對每個方塊做可視測試(即檢測其上下左右前後是否滿足至少有一處無方塊),通過測試的才提交渲染佇列,於是便有了下圖:
為了解決邊界問題(最外面的渲染邊界的方塊無法得知界外的方塊資訊),於是就採取了載入範圍大於渲染範圍的方案:
- 塊區(Chunk):基本的地形載入/解除安裝單位,在X軸、Y軸長度為16,在Z軸(高度軸)長度為256,可容納16*16*256共65536個方塊
- 載入塊區:計算出該塊區每個位置的方塊屬性並存於記憶體
- 渲染塊區:將該塊區裡所有應該渲染的方塊提交渲染佇列
以攝像機的位置為中心,將周圍6*6個的塊區作為需要載入的塊區,而周圍5*5個塊區作為需要渲染的塊區。這樣渲染邊界的方塊也能得知界外方塊(因為相鄰的塊區總是會被載入)的方塊資訊。
資料儲存&查詢
一般來說,儲存Minecraft類地形資料並不需要記錄太多資訊,得益於噪聲演算法的可雜湊性,幾乎僅需要一個種子和部分特殊方塊資訊,因為絕大部分方塊(正常方塊)都可以通過地形生成演算法流程便能計算得出方塊ID屬性,即 \(F(seed,position) = blockID\)。
然而對於被玩家破壞、修改、新增而導致的方塊ID屬性產生變化,這時候就需要特別額外儲存了。
此外,在查詢時可以對座標壓縮/解壓:Vector3D <=> uint64 (28 bit,28 bit,8 bit),Z軸高度由於最高為256,因此最多佔8位。