HexMap學(xué)習(xí)筆記(四)——不規(guī)則化

作者:沈琰
前言
這篇教程內(nèi)容主要是對(duì)噪聲紋理圖的應(yīng)用,在游戲中噪聲是極為常用的功能,特別是與地形生成相關(guān)的。使用噪聲計(jì)算出的地形比較符合自然界的地貌,專欄中還有一篇文章也運(yùn)用到噪聲,也可以參考閱讀。
傳送門:300行代碼實(shí)現(xiàn)Minecraft(我的世界)大地圖生成
本期原文地址:https://catlikecoding.com/unity/tutorials/hex-map/part-4/
這是HexMap系列的第四篇,到目前為止地圖都是一個(gè)精確的蜂巢狀網(wǎng)格,這篇教程將會(huì)給地圖添加一些不規(guī)則的特性讓其看起來更自然一些。

1.噪聲
要添加一些不規(guī)則的感覺就需要一些隨機(jī)性,但又不是真的全隨機(jī)。我們需要的是在編輯地圖時(shí)未選中的位置保持不變,不然每改動(dòng)一點(diǎn)地圖就全變了。所以需要是一種可重現(xiàn)的偽隨機(jī),即噪聲。
柏林噪聲(Perlin noise)就是一個(gè)很好的選項(xiàng),它在每個(gè)點(diǎn)的位置都可以重現(xiàn)。當(dāng)多種不同頻率的柏林噪聲疊加時(shí),能產(chǎn)生大范圍來看變化很大,小范圍內(nèi)又接近一致的噪聲紋理,生成相對(duì)平滑的形變,靠近的點(diǎn)更傾向于黏在一起而不是向相反的方向扭曲。
柏林噪聲可以程序化生成,?Noise這篇教程里介紹了該如何去實(shí)現(xiàn),但也能使用一張預(yù)先生成的噪聲紋理圖。使用噪聲紋理圖的好處是它比直接計(jì)算多頻率疊加的噪聲更容易,也更快,缺點(diǎn)是紋理圖需要占用內(nèi)存并且噪聲的區(qū)域大小相對(duì)有限。所以噪聲紋理圖需要平鋪顯示并且需要覆蓋比較大的區(qū)域,使得平鋪看起不那么明顯。
1.1 噪聲紋理貼圖
這里準(zhǔn)備使用噪聲紋理貼圖,所以不必現(xiàn)在去看Noise這篇教程。這表示我們需要一張紋理圖,如同下面這張。

這張紋理圖包含多重頻率疊加的平鋪柏林噪聲,并且是一張灰度圖,其平均值接近0.5,極限值是0和1。
不過這不是最終要用的圖,這張圖的每個(gè)像素點(diǎn)上只有一個(gè)值,如果我們需要的是3D形變,我們至少得需要三個(gè)偽隨機(jī)采樣。所以除此之外還需要兩張額外的不同噪聲紋理圖。
我們可以就用三張不同的紋理圖,或者也可以用RGB通道來存儲(chǔ)這些值,我們可以在一張紋理圖上存儲(chǔ)四種不同樣式的噪聲,如同下面這張圖。

這張紋理圖怎么獲取的?
用NumberFlow(https://catlikecoding.com/numberflow/)生成的,這是一個(gè)我(注:原版教程作者)為Unity寫的紋理編輯插件。(注:自己在工程中使用時(shí)直接復(fù)制這張圖就行)
得到這張圖后導(dǎo)入到自己的Unity工程文件中,由于我們要用代碼對(duì)紋理圖進(jìn)行采樣,所以它必須是可讀寫的。勾選Read/Write Enable,這樣就能把紋理貼圖的數(shù)據(jù)存儲(chǔ)到內(nèi)存中并用C#代碼進(jìn)行訪問。撤選sRGB(Color Texture)選項(xiàng),確保格式設(shè)置成Automatic并且壓縮方式設(shè)置成null,我們不想因?yàn)閴嚎s紋理而破壞噪聲圖的樣式。同樣的,Generate Mip Maps也不用勾選,并不需要這個(gè)功能。


如果勾選sRGB有什么影響?
如果我們?cè)谥鞯哪承┑胤绞褂迷肼暭y理圖,它可能會(huì)有些不同。當(dāng)使用線性渲染模式時(shí),紋理采樣數(shù)據(jù)會(huì)自動(dòng)從伽馬空間轉(zhuǎn)換到線性空間。這會(huì)對(duì)噪聲紋理的采樣數(shù)據(jù)產(chǎn)生一個(gè)錯(cuò)誤的結(jié)果,而這是需要避免的。(注:Unity的默認(rèn)渲染模式是線性模式)
1.2 噪聲采樣
在HexMetrics里添加噪聲采樣方法,這樣就能在任何地方使用,這意味著HexMetrics必須拿到紋理圖的引用。

由于這不是一個(gè)組件,我們不能在編輯器里賦值。我們就簡(jiǎn)單地把HexGrid當(dāng)一個(gè)中轉(zhuǎn),由于HexGrid是第一個(gè)運(yùn)行的,所以在它的Awake方法開始的位置傳遞值就行了。

但是在運(yùn)行模式下,這種方式無法在重新編譯時(shí)保存,靜態(tài)變量不是由Unity序列化的。要解決這個(gè)問題還需要在OnEnable方法中重新賦值紋理,這個(gè)方法會(huì)在重新編譯后調(diào)用。


現(xiàn)在HexMetrics能訪問紋理貼圖了,我們添加一個(gè)方便的噪聲采樣方法到里面,這個(gè)方法生成一個(gè)包含四種噪聲模式的4D向量。

通過雙線性過濾(注:進(jìn)行縮放顯示的時(shí)候進(jìn)行紋理平滑的一種紋理過濾方法)的方式對(duì)紋理圖進(jìn)行采樣得到樣本數(shù)據(jù),是使用世界坐標(biāo)系下的X和Z軸坐標(biāo)作為UV坐標(biāo)。由于噪聲源是3D的,所以我們忽略的Y軸坐標(biāo)。
最后得到一個(gè)可以轉(zhuǎn)換成4D向量的顏色,這個(gè)轉(zhuǎn)換是隱式的,這意味著我們可以直接返回顏色,而不用顯式的轉(zhuǎn)換成Vector4。

雙線性過濾是如何工作的?
可以去看Rendering 2, Shader? Fundamentals(https://catlikecoding.com/unity/tutorials/rendering/part-2/)這篇教程,里面介紹了UV坐標(biāo)和紋理過濾。
2.頂點(diǎn)擾動(dòng)
通過分別擾動(dòng)每個(gè)頂點(diǎn)來讓整齊的網(wǎng)格發(fā)生形變,為此添加一個(gè)Peturb方法到HexMesh里負(fù)責(zé)這個(gè)工作。這個(gè)方法獲取一個(gè)點(diǎn)并返回?cái)_動(dòng)后的坐標(biāo),所以它使用擾動(dòng)之前點(diǎn)進(jìn)行采樣。

我們先簡(jiǎn)單地直接加上X、Y和Z的噪聲采樣,并將其作為結(jié)果。

要如何快速的讓HexMesh里的所有頂點(diǎn)都應(yīng)用擾動(dòng)?當(dāng)在AddTriangle和AddQuad里添加頂點(diǎn)到列表中時(shí)修改每個(gè)頂點(diǎn)就行了。

頂點(diǎn)擾動(dòng)后四邊形依然是平坦的?
很可能并不是。這些四邊形由兩個(gè)不再對(duì)其的三角形構(gòu)成,因?yàn)檫@些三角形共享兩個(gè)頂點(diǎn),這些頂點(diǎn)的法線會(huì)平滑變化。這意味著你看不到兩個(gè)三角形之間的明顯過渡,如果扭曲的不是太明顯,你仍然會(huì)感覺這個(gè)四邊形是平的。

看起來好像沒多大變化,除了單元格的坐標(biāo)標(biāo)簽不見了之外。這是因?yàn)槲覀儼秧旤c(diǎn)加上了噪聲采樣的坐標(biāo),而這些坐標(biāo)總是正值,所以所有三角形都在標(biāo)簽上并覆蓋了它們。我們需要把采樣值的原點(diǎn)放到中心,這樣就能在上下兩個(gè)方向上運(yùn)動(dòng),所以修改采樣的范圍到-1至1之間。


2.1 擾動(dòng)強(qiáng)度


在HexMesh.Perturb里通過相乘的方式應(yīng)用采樣數(shù)據(jù)。



.2噪聲采樣縮放
網(wǎng)格在編輯前還算正常,一旦出現(xiàn)階梯連接部分就不對(duì)了。它們的頂點(diǎn)向各個(gè)方向扭曲,看起來很混亂,使用柏林噪聲不應(yīng)該會(huì)發(fā)生這種情況。
這是因?yàn)槲覀冎苯佑檬澜缱鴺?biāo)進(jìn)行噪聲采樣,使得紋理平鋪在每個(gè)單元格上,但是我們的單元格的尺寸比紋理貼圖本身要大得多,實(shí)際上紋理圖是在任意位置被采樣,這破壞了其連貫性。

我們要對(duì)噪聲采樣進(jìn)行縮放,這樣紋理就能囊括更大的區(qū)域。我們?cè)贖exMetrics里添加這個(gè)縮放并設(shè)置其為0.003,然后把采樣坐標(biāo)與這個(gè)因素相乘。





3.對(duì)平單元格中心
對(duì)所有頂點(diǎn)進(jìn)行擾動(dòng)讓我們的地圖看起來更自然一些了,但是還有一些問題。因?yàn)楝F(xiàn)在單元格的表面不平坦,其坐標(biāo)標(biāo)間與網(wǎng)格相交了,并且在階梯連接部分與陡峭斜面間出現(xiàn)了裂縫。我們先把裂縫的問題放一放,先處理單元格表面的問題。

相交問題最簡(jiǎn)單的解決方法是保持單元格中心平坦,即在HexMesh.Perturb里不修改Y軸坐標(biāo)。



這改動(dòng)并沒有什么問題,讓識(shí)別每個(gè)單元格變得更加容易了,并且預(yù)防讓階梯化連接變得混亂的問題。但是垂直方向的擾動(dòng)依然可以用別的方式做得更好。
3.1 單元格海拔高度的噪聲擾動(dòng)
我們可以讓擾動(dòng)作用于每個(gè)單元格而不是每個(gè)頂點(diǎn),這樣就既保留了每個(gè)單元格的平坦表面又能讓不同單元格之間看起來也有差別。比較好的做法是對(duì)高度擾動(dòng)使用不同的縮放比例,所以在HexMetrics里添加一個(gè)強(qiáng)度系數(shù)。1.5的值就能帶來一些微妙的變化,這大概是階梯一級(jí)的高度。

修改HexCell.Elevation的set屬性,讓其應(yīng)用垂直坐標(biāo)的擾動(dòng)。

為了確保擾動(dòng)能立即被應(yīng)用,需要在HexGrid.CreateCell里精確設(shè)置高度。否則一開始網(wǎng)格就是平坦的。這一步放在UI創(chuàng)建之后的最后一步。


3.2 使用相同高度
大量裂縫出現(xiàn)在網(wǎng)格中,因?yàn)樵谌腔W(wǎng)格時(shí)沒有始終使用相同的高度。添加一個(gè)便利的屬性到HexCell里重新獲得自身坐標(biāo),這樣就能在任何位置使用。

現(xiàn)在能在HexMesh.Triangulate里使用這個(gè)屬性去確認(rèn)單元格的中心位置。

也可以在確認(rèn)相鄰單元格頂點(diǎn)坐標(biāo)時(shí),在TriangulateConnection里使用這個(gè)屬性。


4.細(xì)分單元格邊緣
雖然現(xiàn)在單元格有著漂亮的變化,但它們看起來仍然是六邊形。這不是什么大問題,但我們能做得更好一些。

如果有更多的頂點(diǎn)自然就能看到更多變化,所以我們把單元格的邊界分為兩個(gè)部分,在六邊形的每一片三角形底邊一半的位置添加一個(gè)頂點(diǎn)。這意味著HexMesh.Triangulate里需要添加兩個(gè)而不是一個(gè)三角形。


把頂點(diǎn)個(gè)三角形加倍明顯有了些新變化,干脆就把頂點(diǎn)翻三倍,這樣形狀會(huì)更堅(jiān)固一些。


4.1細(xì)分邊緣連接部分
當(dāng)然,還得去細(xì)分連接部分,所以傳遞新的邊界頂點(diǎn)到TriangulateConnection 里。

在TriangulateConnection里添加匹配的參數(shù),就能使用這些新頂點(diǎn)。

還需要從相鄰單元格上計(jì)算額外的邊界連接處的頂點(diǎn),可以在連接到另一邊之后計(jì)算。

下一步需要修改邊界的三角化?,F(xiàn)在先不管階梯化部分,簡(jiǎn)單的添加三個(gè)四邊形。


4.2打包邊緣頂點(diǎn)
因?yàn)楝F(xiàn)在需要四個(gè)頂點(diǎn)表示一組邊界頂點(diǎn),那么把他們打包整合起來就有意義了,因?yàn)檫@比單獨(dú)處理四個(gè)頂點(diǎn)方便。為此創(chuàng)建一個(gè)簡(jiǎn)單的結(jié)構(gòu)體EdgeVertices,它需要包含四個(gè)順時(shí)針排列在單元格邊緣的頂點(diǎn)。

不需要序列化么?
我們只在三角化的時(shí)候使用這個(gè)結(jié)構(gòu),就此而言我們不需要存儲(chǔ)邊緣頂點(diǎn),所以不要序列化。
給它一個(gè)便利的構(gòu)造函數(shù),只負(fù)責(zé)計(jì)算確定的邊緣位置。

現(xiàn)在把三角化的方法進(jìn)行分離,在HexMesh里新建一個(gè)從單元格中心到邊緣創(chuàng)建三角形扇面的方法。

以及對(duì)兩個(gè)邊緣之間的四邊形條帶進(jìn)行三角剖分的方法。

這讓我們能夠簡(jiǎn)化Triangulate方法。

我們現(xiàn)在能在TriangulateConnection里使用TriangulateEdgestrip方法,但還有一些其他的細(xì)分工作要做。在我們第一次用v1的地方,我們應(yīng)該用e1.v1代替。以此類推,v2變成e1.v4, v3變成了e2.v1, v4變成了e2.v4。

4.3 階梯細(xì)分
還需要去細(xì)分階梯連接部分,所以把邊緣參數(shù)傳遞到TriangulateEdgeTerraces里。

現(xiàn)在要修改TriangulateEdgeTerraces方法,使之參數(shù)為兩個(gè)單元格的邊緣之間,而不是一組頂點(diǎn)。先假設(shè)EdgeVertices里有方便的靜態(tài)插值計(jì)算函數(shù),這就可以讓我們簡(jiǎn)化TriangulateEdgeTerraces方法而不是使其變得更復(fù)雜。

EdgeVertices方法就是預(yù)先在兩個(gè)邊緣之間的所有頂點(diǎn)之間計(jì)算階梯插值。


5.重新連接陡峭面與階梯
到目前為止我們都忽略了當(dāng)陡峭面與階梯相遇時(shí)產(chǎn)生的裂縫,現(xiàn)在該處理這個(gè)問題了。先處理陡峭-傾斜-傾斜(CSS)和傾斜-陡峭-傾斜(SCS)的情況。

這個(gè)問題重現(xiàn)是因?yàn)榉纸琰c(diǎn)(注:階梯連接收束到陡峭面邊緣的頂點(diǎn))的計(jì)算受到了干擾。這意味著它不是精確的處于陡峭面的邊緣線上,這就產(chǎn)生了一個(gè)裂縫,這樣的空洞有的明顯,有的不明顯。
解決方案是不要對(duì)分界點(diǎn)應(yīng)用噪聲擾動(dòng),就是說我們需要能選擇一個(gè)點(diǎn)是否應(yīng)用擾動(dòng)。最簡(jiǎn)單的辦法是創(chuàng)建一個(gè)完全不對(duì)頂點(diǎn)進(jìn)行擾動(dòng)的AddTriangle方法。

修改一下TriangulateBoundaryTriangle讓其應(yīng)用這個(gè)方法,這意味著只對(duì)分界點(diǎn)除外的其他所有頂點(diǎn)進(jìn)行擾動(dòng)。

值得注意的是,因?yàn)槲覀儧]有用v2推導(dǎo)其他的點(diǎn),它可以直接先應(yīng)用擾動(dòng)。這是一個(gè)簡(jiǎn)單的優(yōu)化辦法并且節(jié)省代碼,所以我們改成如下這樣。


現(xiàn)在看起來好多了,但是還沒完。在TriangulateCornerTerracesCliff方法里,分界點(diǎn)是通過插值計(jì)算左右的坐標(biāo)點(diǎn)得到的,但這兩個(gè)點(diǎn)沒有應(yīng)用擾動(dòng)。要讓邊界點(diǎn)能精確吻合陡峭斜坡邊緣,需要插值去計(jì)算兩個(gè)擾動(dòng)過的坐標(biāo)點(diǎn)來求得邊界點(diǎn)。

這在TriangulateCornerCliffTerraces方法里也是一樣的。


5.1兩個(gè)陡峭斜面與一個(gè)傾斜面的情況
剩余的是有兩個(gè)陡峭斜面與一個(gè)傾斜面特征的情況。

這個(gè)問題的修正方法是在TriangulateCornerTerracessCliff方法最后一個(gè)else里,單獨(dú)擾動(dòng)除了分界點(diǎn)外的三角形頂點(diǎn)。

TriangulateCornerCliffTerraces里也一樣。


6.優(yōu)化調(diào)整
我們現(xiàn)在有了一個(gè)準(zhǔn)確的應(yīng)用擾動(dòng)的網(wǎng)格,它的具體形狀表現(xiàn)取決于一張?zhí)囟ǖ脑肼晥D,它的縮放和擾動(dòng)強(qiáng)度。在我們現(xiàn)在的例子里它的擾動(dòng)強(qiáng)度似乎過大了,雖然這對(duì)表現(xiàn)單元格的不規(guī)則化很不錯(cuò),但我們還是不想它偏離網(wǎng)格太多。畢竟最終我們還是要通過它去識(shí)別哪個(gè)單元格正在被編輯,如果變化太大就很難去填充編輯一些別的什么東西。

單元格的擾動(dòng)強(qiáng)度設(shè)置為5有點(diǎn)太大了。

讓我們降低到4,讓它變得更容易管理而又不會(huì)太有規(guī)律,保證在XZ平面的最大位移為√32 ≈ 5.66。


另一個(gè)可以調(diào)整的參數(shù)是內(nèi)部固定六邊形的范圍。如果我們?cè)黾铀?,平坦的單元格中心就?huì)變得更大一些。這給未來的內(nèi)容留下了更多空間。當(dāng)然,也也會(huì)變得更六邊形化一些。

少量增加固定六邊形范圍參數(shù)到0.8會(huì)讓之后使用起來更容易。


最后,海拔高度等級(jí)之間的差異有點(diǎn)陡峭。當(dāng)我們檢查網(wǎng)格生成是否正確時(shí)這很方便,但這一步我們現(xiàn)在已經(jīng)完成了,所以讓我們把它減少到3。


還可以調(diào)整高度擾動(dòng)強(qiáng)度。但它現(xiàn)在被設(shè)為1.5,等于高度步長(zhǎng)的一半,這已經(jīng)比較合適了。
較小的高度步長(zhǎng)也使得使用我們可以更為實(shí)用的7個(gè)高度等級(jí),這允許為地圖添加更多種類。

下一篇是:?https://catlikecoding.com/unity/tutorials/hex-map/part-5/
本期工程地址:https://github.com/tank1018702/Hex-Map-Learning/tree/Irregulatity
有想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問http://levelpp.com/?
有專業(yè)開發(fā)交(gao)流(ji)群等待大家強(qiáng)勢(shì)插入:869551769