HexMap學習筆記(四)——不規則化
HexMap學習筆記(一)——建立六邊形網格
HexMap學習筆記(二)——單元格顏色混合
HexMap學習筆記(三)——海拔高度與階梯連線
HexMap學習筆記(四)——不規則化
HexMap學習筆記(五)——更大的地圖
前言
這篇教程內容主要是對噪聲紋理圖的應用,在遊戲中噪聲是極為常用的功能,特別是與地形生成相關的。使用噪聲計算出的地形比較符合自然界的地貌,專欄中還有一篇文章也運用到噪聲,也可以參考閱讀。
傳送門:Meta42:300行程式碼實現Minecraft(我的世界)大地圖生成
本期原文地址:Hex Map 4
這是HexMap系列的第四篇,到目前為止地圖都是一個精確的蜂巢狀網格,這篇教程將會給地圖新增一些不規則的特性讓其看起來更自然一些。
不再整齊的六邊形
要新增一些不規則的感覺就需要一些隨機性,但又不是真的全隨機。我們需要的是在編輯地圖時未選中的位置保持不變,不然每改動一點地圖就全變了。所以需要是一種可重現的偽隨機,即噪聲。
柏林噪聲(Perlin noise)就是一個很好的選項,它在每個點的位置都可以重現。當多種不同頻率的柏林噪聲疊加時,能產生大範圍來看變化很大,小範圍內又接近一致的噪聲紋理,生成相對平滑的形變,靠近的點更傾向於黏在一起而不是向相反的方向扭曲。
柏林噪聲可以程式化生成,Noise這篇教程裡介紹了該如何去實現,但也能使用一張預先生成的噪聲紋理圖。使用噪聲紋理圖的好處是它比直接計算多頻率疊加的噪聲更容易,也更快,缺點是紋理圖需要佔用記憶體並且噪聲的區域大小相對有限。所以噪聲紋理圖需要平鋪顯示並且需要覆蓋比較大的區域,使得平鋪看起不那麼明顯。
1.1噪聲紋理貼圖
這裡準備使用噪聲紋理貼圖,所以不必現在去看Noise這篇教程。這表示我們需要一張紋理圖,如同下面這張。
平鋪的不規則柏林噪聲圖
這張紋理圖包含多重頻率疊加的平鋪柏林噪聲,並且是一張灰度圖,其平均值接近0.5,極限值是0和1。
不過這不是最終要用的圖,這張圖的每個畫素點上只有一個值,如果我們需要的是3D形變,我們至少得需要三個偽隨機取樣。所以除此之外還需要兩張額外的不同噪聲紋理圖。
我們可以就用三張不同的紋理圖,或者也可以用RGB通道來儲存這些值,我們可以在一張紋理圖上儲存四種不同樣式的噪聲,如同下面這張圖。
四合一噪聲紋理圖
這張紋理圖怎麼獲取的?
用NumberFlow生成的,這是一個我(注:原版教程作者)為Unity寫的紋理編輯外掛。(注:自己在工程中使用時直接複製這張圖就行)
得到這張圖後匯入到自己的Unity工程檔案中,由於我們要用程式碼對紋理圖進行取樣,所以它必須是可讀寫的。勾選Read/Write Enable,這樣就能把紋理貼圖的資料儲存到記憶體中並用C#程式碼進行訪問。撤選sRGB(Color Texture)選項,確保格式設定成Automatic並且壓縮方式設定成null,我們不想因為壓縮紋理而破壞噪聲圖的樣式。同樣的,Generate Mip Maps也不用勾選,並不需要這個功能。
匯入噪聲紋理圖
如果勾選sRGB有什麼影響?
如果我們在著色器的某些地方使用噪聲紋理圖,它可能會有些不同。當使用線性渲染模式時,紋理取樣資料會自動從伽馬空間轉換到線性空間。這會對噪聲紋理的取樣資料產生一個錯誤的結果,而這是需要避免的。(注:Unity的預設渲染模式是線性模式)
1.2噪聲取樣
在HexMetrics裡新增噪聲取樣方法,這樣就能在任何地方使用,這意味著HexMetrics必須拿到紋理圖的引用。
由於這不是一個元件,我們不能在編輯器裡賦值。我們就簡單地把HexGrid當一箇中轉,由於HexGrid是第一個執行的,所以在它的Awake方法開始的位置傳遞值就行了。
但是在執行模式下,這種方式無法在重新編譯時儲存,靜態變數不是由Unity序列化的。要解決這個問題還需要在OnEnable方法中重新賦值紋理,這個方法會在重新編譯後呼叫。
現在HexMetrics能訪問紋理貼圖了,我們新增一個方便的噪聲取樣方法到裡面,這個方法生成一個包含四種噪聲模式的4D向量。
通過雙線性過濾(注:進行縮放顯示的時候進行紋理平滑的一種紋理過濾方法)的方式對紋理圖進行取樣得到樣本資料,是使用世界座標系下的X和Z軸座標作為UV座標。由於噪聲源是3D的,所以我們忽略的Y軸座標。
最後得到一個可以轉換成4D向量的顏色,這個轉換是隱式的,這意味著我們可以直接返回顏色,而不用顯式的轉換成Vector4。
雙線性過濾是如何工作的?
可以去看Rendering 2,Shader Fundamentals這篇教程,裡面介紹了UV座標和紋理過濾。
2.頂點擾動
通過分別擾動每個頂點來讓整齊的網格發生形變,為此新增一個Peturb方法到HexMesh裡負責這個工作。這個方法獲取一個點並返回擾動後的座標,所以它使用擾動之前點進行取樣。
我們先簡單地直接加上X、Y和Z的噪聲取樣,並將其作為結果。
要如何快速的讓HexMesh裡的所有頂點都應用擾動?當在AddTriangle和AddQuad裡新增頂點到列表中時修改每個頂點就行了。
擾動後四邊形依然是平坦的?
很可能並不是。這些四邊形由兩個不再對其的三角形構成,因為這些三角形共享兩個頂點,這些頂點的法線會平滑變化。這意味著你看不到兩個三角形之間的明顯過渡,如果扭曲的不是太明顯,你仍然會感覺這個四邊形是平的。
頂點擾動但不明顯
看起來好像沒多大變化,除了單元格的座標標籤不見了之外。這是因為我們把頂點加上了噪聲取樣的座標,而這些座標總是正值,所以所有三角形都在標籤上並覆蓋了它們。我們需要把取樣值的原點放到中心,這樣就能在上下兩個方向上運動,所以修改取樣的範圍到-1至1之間。
單元格中心的擾動
2.1擾動強度
顯然現在網格已經被扭曲了,但效果非常不明顯。每個維度最大的位移距離只有1,理論上頂點擾動的最大位移只有單位長度,並且極限值情況發生的概率極低。由於每個單元格的外徑是10,這個擾動相對來說太小。
解決方案是在HexMetrics裡新增一個強度設定,這樣就能放大擾動。我們先試著把強度設定為5,這樣一來理論上的最大位移距離就有單位長度,這樣看起來就明顯多了。
在HexMesh.Perturb裡通過相乘的方式應用取樣資料。
增強強度
2.2噪聲取樣縮放
網格在編輯前還算正常,一旦出現階梯連線部分就不對了。它們的頂點向各個方向扭曲,看起來很混亂,使用柏林噪聲不應該會發生這種情況。
這是因為我們直接用世界座標進行噪聲取樣,使得紋理平鋪在每個單元格上,但是我們的單元格的尺寸比紋理貼圖本身要大得多,實際上紋理圖是在任意位置被取樣,這破壞了其連貫性。
10乘10的噪聲紋理網格線覆蓋在六邊形地圖的蜂巢網格上
我們要對噪聲取樣進行縮放,這樣紋理就能囊括更大的區域。我們在HexMetrics裡新增這個縮放並設定其為0.003,然後把取樣座標與這個因素相乘。
取樣資料突然就覆蓋了原來倍的區域,並且其連貫性也變得顯而易見了。
縮放後的噪聲圖
新的縮放還確保在噪聲在平鋪之前有個過渡過程。實際上因為單元格的內徑是,所以沒有辦法精確的以X軸的尺寸平鋪。但是因為噪聲紋理自身的連貫性,即使細節不吻合,你任然能在大範圍內發現重複的樣式,大約是每20個單元格之間。這隻在地圖沒有其他編輯形狀的功能時才比較明顯。
3.對平單元格中心
對所有頂點進行擾動讓我們的地圖看起來更自然一些了,但是還有一些問題。因為現在單元格的表面不平坦,其座標標間與網格相交了,並且在階梯連線部分與陡峭斜面間出現了裂縫。我們先把裂縫的問題放一放,先處理單元格表面的問題。
1\
越不整齊問題越多
相交問題最簡單的解決方法是保持單元格中心平坦,即在HexMesh.Perturb裡不修改Y軸座標。
單元格對平
於是包括單元格中心與階梯連線的每一層的所有的垂直方向的座標都不會發生改變了。值得注意的是這讓理論上的頂點擾動最大位移距離減少到了,並且只在XZ平面移動了。
這改動並沒有什麼問題,讓識別每個單元格變得更加容易了,並且預防讓階梯化連線變得混亂的問題。但是垂直方向的擾動依然可以用別的方式做得更好。
3.1單元格海拔高度的噪聲擾動
我們可以讓擾動作用於每個單元格而不是每個頂點,這樣就既保留了每個單元格的平坦表面又能讓不同單元格之間看起來也有差別。比較好的做法是對高度擾動使用不同的縮放比例,所以在HexMetrics裡新增一個強度係數。1.5的值就能帶來一些微妙的變化,這大概是階梯一級的高度。
修改HexCell.Elevation的set屬性,讓其應用垂直座標的擾動。
為了確保擾動能立即被應用,需要在HexGrid.CreateCell裡精確設定高度。否則一開始網格就是平坦的。這一步放在UI建立之後的最後一步。
應用單元格高度擾動產生了很多裂縫
3.2使用相同高度
大量裂縫出現在網格中,因為在三角化網格時沒有始終使用相同的高度。新增一個便利的屬性到HexCell裡重新獲得自身座標,這樣就能在任何位置使用。
現在能在HexMesh.Triangulate裡使用這個屬性去確認單元格的中心位置。
也可以在確認相鄰單元格頂點座標時,在TriangulateConnection裡使用這個屬性。
使用一致的單元格高度
4.細分單元格邊緣
雖然現在單元格有著漂亮的變化,但它們看起來仍然是六邊形。這不是什麼大問題,但我們能做得更好一些。
單元格清晰的六邊形結構
如果有更多的頂點自然就能看到更多變化,所以我們把單元格的邊界分為兩個部分,在六邊形的每一片三角形底邊一半的位置新增一個頂點。這意味著HexMesh.Triangulate裡需要新增兩個而不是一個三角形。
十二條邊替換六條邊的效果
把頂點個三角形加倍明顯有了些新變化,乾脆就把頂點翻三倍,這樣形狀會更堅固一些。
十八條邊
4.1細分邊緣連線部分
當然,還得去細分連線部分,所以傳遞新的邊界頂點到TriangulateConnection裡。
在TriangulateConnection裡新增匹配的引數,就能使用這些新頂點。
還需要從相鄰單元格上計算額外的邊界連線處的頂點,可以在連線到另一邊之後計算。
下一步需要修改邊界的三角化。現在先不管階梯化部分,簡單的新增三個四邊形。
連線處細分
4.2打包邊緣頂點
因為現在需要四個頂點表示一組邊界頂點,那麼把他們打包整合起來就有意義了,因為這比單獨處理四個頂點方便。為此建立一個簡單的結構體EdgeVertices,它需要包含四個順時針排列在單元格邊緣的頂點。
不需要序列化麼?
我們只在三角化的時候使用這個結構,就此而言我們不需要儲存邊緣頂點,所以不要序列化。
給它一個便利的建構函式,只負責計算確定的邊緣位置。
現在把三角化的方法進行分離,在HexMesh裡新建一個從單元格中心到邊緣建立三角形扇面的方法。
以及對兩個邊緣之間的四邊形條帶進行三角剖分的方法。
這讓我們能夠簡化Triangulate方法。
我們現在能在TriangulateConnection裡使用TriangulateEdgestrip方法,但還有一些其他的細分工作要做。在我們第一次用v1的地方,我們應該用e1.v1代替。以此類推,v2變成e1.v4,v3變成了e2.v1,v4變成了e2.v4。
4.3階梯細分
還需要去細分階梯連線部分,所以把邊緣引數傳遞到TriangulateEdgeTerraces裡。
現在要修改TriangulateEdgeTerraces方法,使之引數為兩個單元格的邊緣之間,而不是一組頂點。先假設EdgeVertices裡有方便的靜態插值計算函式,這就可以讓我們簡化TriangulateEdgeTerraces方法而不是使其變得更復雜。
EdgeVertices方法就是預先在兩個邊緣之間的所有頂點之間計算階梯插值。
階梯連線處細分
5.重新連線陡峭面與階梯
到目前為止我們都忽略了當陡峭面與階梯相遇時產生的裂縫,現在該處理這個問題了。先處理陡峭-傾斜-傾斜(CSS)和傾斜-陡峭-傾斜(SCS)的情況。
這個問題重現是因為分界點(注:階梯連線收束到陡峭面邊緣的頂點)的計算受到了干擾。這意味著它不是精確的處於陡峭面的邊緣線上,這就產生了一個裂縫,這樣的空洞有的明顯,有的不明顯。
解決方案是不要對分界點應用噪聲擾動,就是說我們需要能選擇一個點是否應用擾動。最簡單的辦法是建立一個完全不對頂點進行擾動的AddTriangle方法。
修改一下TriangulateBoundaryTriangle讓其應用這個方法,這意味著只對分界點除外的其他所有頂點進行擾動。
值得注意的是,因為我們沒有用v2推導其他的點,它可以直接先應用擾動。這是一個簡單的優化辦法並且節省程式碼,所以我們改成如下這樣。
未應用擾動的邊界點
現在看起來好多了,但是還沒完。在TriangulateCornerTerracesCliff方法裡,分界點是通過插值計算左右的座標點得到的,但這兩個點沒有應用擾動。要讓邊界點能精確吻合陡峭斜坡邊緣,需要插值去計算兩個擾動過的座標點來求得邊界點。
這在TriangulateCornerCliffTerraces方法裡也是一樣的。
不再有空洞了
5.1兩個陡峭斜面與一個傾斜面的情況
剩餘的是有兩個陡峭斜面與一個傾斜面特徵的情況。
因為一個三角形頂點位置產生的空洞
這個問題的修正方法是在TriangulateCornerTerracessCliff方法最後一個else裡,單獨擾動除了分界點外的三角形頂點。
TriangulateCornerCliffTerraces裡也一樣。
最後一個空洞也消失了
我們現在有了一個準確的應用擾動的網格,它的具體形狀表現取決於一張特定的噪聲圖,它的縮放和擾動強度。在我們現在的例子裡它的擾動強度似乎過大了,雖然這對錶現單元格的不規則化很不錯,但我們還是不想它偏離網格太多。畢竟最終我們還是要通過它去識別哪個單元格正在被編輯,如果變化太大就很難去填充編輯一些別的什麼東西。
頂點擾動與未擾動的對比
單元格的擾動強度設定為5有點太大了。
單元格頂點的擾動強度從0到5的效果
讓我們降低到4,讓它變得更容易管理而又不會太有規律,保證在XZ平面的最大位移為√32≈5.66。
強度為4的的擾動
另一個可以調整的引數是內部固定六邊形的範圍。如果我們增加它,平坦的單元格中心就會變得更大一些。這給未來的內容留下了更多空間。當然,也也會變得更六邊形化一些。
中間固定六邊形的比例從0.75到0.95
少量增加固定六邊形範圍引數到0.8會讓之後使用起來更容易。
固定六邊形的範圍為0.8
最後,海拔高度等級之間的差異有點陡峭。當我們檢查網格生成是否正確時這很方便,但這一步我們現在已經完成了,所以讓我們把它減少到3。
高度等級的步長減少為3
還可以調整高度擾動強度。但它現在被設為1.5,等於高度步長的一半,這已經比較合適了。
較小的高度步長也使得使用我們可以更為實用的7個高度等級,這允許為地圖新增更多種類。
使用了七個高度等級的地圖
下一篇是:Hex Map 5
本期工程地址tank1018702/Hex-Map-Learning
有想系統學習遊戲開發的童鞋,歡迎訪問http://levelpp.com/。
作者:沈琰
原地址:https://zhuanlan.zhihu.com/p/55518523
相關文章
- HexMap學習筆記(六)——河流筆記
- HexMap學習筆記(七)——道路筆記
- HexMap學習筆記(八)——水體筆記
- HexMap學習筆記(九)——地形特徵筆記特徵
- HexMap學習筆記(五)——更大的地圖筆記地圖
- HexMap學習筆記(一)——建立六邊形網格筆記
- HexMap學習筆記(二)——單元格顏色混合筆記
- ES6學習筆記(四)【正則,集合】筆記
- 學習筆記:深度學習中的正則化筆記深度學習
- Vue學習筆記(四) 久處不厭Vue筆記
- 正規表示式學習筆記(1)-認識正則筆記
- JavaScript正則學習筆記JavaScript筆記
- springboot 學習筆記(四)Spring Boot筆記
- goLang學習筆記(四)Golang筆記
- TS學習筆記(四)筆記
- ONNXRuntime學習筆記(四)筆記
- activiti學習筆記(四)managementService筆記
- 四元數 學習筆記筆記
- DP學習筆記(四)(2024.10.2)筆記
- c++學習筆記(四)C++筆記
- 吳恩達機器學習筆記 —— 8 正則化吳恩達機器學習筆記
- 機器學習筆記——模型選擇與正則化機器學習筆記模型
- Java學習筆記:資料庫中的正規化和反正規化Java筆記資料庫
- Gradle外掛學習筆記(四)Gradle筆記
- ES6 學習筆記四筆記
- Kubernetes學習筆記(四):服務筆記
- TS學習筆記(四):函式筆記函式
- Redis 學習筆記(四)RDB 和 AOF 持久化機制Redis筆記持久化
- 強化學習筆記強化學習筆記
- 機器學習基礎——規則化(Regularization)機器學習
- 正規表示式學習筆記筆記
- 動態規劃學習筆記動態規劃筆記
- 分數規劃學習筆記筆記
- 學習筆記——正則匹配方法整理筆記
- 依賴倒轉原則--學習筆記筆記
- Java學習筆記 第四天Java筆記
- springcloud學習筆記(四)Spring Cloud HystrixSpringGCCloud筆記
- 李巨集毅深度學習 筆記(四)深度學習筆記