HexMap學習筆記(七)——道路
前言
由於不涉及對已有地形的改變,這篇教程的難度是低於上一篇的,入過完全弄懂上一篇的教程讀起來就會很容易。另外最後道路的著色器效果很贊,值得仔細研究一下思路
本期原文地址:rivers
此教程是HexMap系列的第七篇,在上篇中新增了河流的編輯功能,這一次新增道路。
文明的第一個標誌
1單元格里的道路
與河流一樣,道路是穿過單元格邊緣的中間來連線不同單元格,最大的不同點是道路沒有流動的河水規定方向,所以道路是雙向的。一個看得過去的道路網路肯定會有十字路口,所以我們還定義單元格內穿過的道路可以超過兩條。
在六個方向上都允許有道路穿過,這意味著單元格可以包含零到六條道路,這產生了十四種可能的道路結構,遠超河流的五種。為使其更容易實現,我們需要一個可以表示所有結構型別的通用方法。
十四種可能的道路結構
1.1記錄道路結構
最直接的方法是每個單元格都用一個布林型別的陣列儲存其道路結構,新增一個私有的陣列欄位並將其序列化,這樣就能在檢視皮膚上看到值,在預製體中預設這個陣列的大小為6.
每個單元格最多有六條道路
新增一個方法檢測是否在給定的方向上有道路。
要知道單元格內是否至少有一條路,新增一個屬性迴圈遍歷陣列,如果有就返回true,如果一條道路都沒有就返回false。
1.2刪除道路
與河流一樣,新增一個刪除單元格內所有道路的方法,迴圈遍歷陣列內的每個元素,全部設定為false就行。
當然我們還要確保相鄰單元格相應方向上的道路也被刪除。
完成之後還要重新整理道路刪除之後的單元格,由於道路只會影響單元格本身,所以只用重新整理單元格自身而不用重新整理所有相鄰單元格。
1.3新增道路
新增道路的邏輯與刪除道路一樣,只不過是把記錄陣列的值改成true而不是false,可以新增一個私有方法同時完成這兩種操作。
因為無法在單元格的一個方向上同時新增道路與河流,所以新增之前先檢測一下。
道路同樣也無法與較高的懸崖共存,也許能新增在緩一些的斜坡上但是陡峭的懸崖不行?為了實現這一功能,建立一個方法獲取指定方向上的相鄰單元格上的高度差異。
現在可以強制在高度差異足夠小的時候新增道路,這裡限制只能在階梯連線的斜坡上新增,所以高度差距的最大值是1。
1.4刪除無效道路
現在可以確保只在條件允許時新增道路,在來考慮稍後道路失效時候的刪除問題,例如當新增河流時。可以禁止河流在道路上,但河流不會被道路阻擋,讓其把路衝開。
只要將河流方向上的道路設定為false就行了,不用管是不是存在道路,修改道路狀態始終會重新整理受影響的單元格,所以現在不用特地在SetOutgoingRiver中去呼叫RefreshSelfOnly方法了。
另一個會使道路無效的操作是單元格高度的改變,這種情況下需要檢測所有方向上的道路,如果高度差距過大,現存的道路就要刪除。
2編輯道路
編輯道路的邏輯實在是很像編輯河流,所以HexMapEditor中需要另一個toggle組,加上隨之一起的方法來設定道路編輯的狀態。
現在EditCell方法同樣支援新增和刪除道路,這意味著檢測到滑鼠拖拽時有兩種可能的操作,稍稍修改一下程式碼,這樣在有效拖拽時兩個toggle的狀態都會被檢測到。
你可以通過賦值河流的UI皮膚並修改其呼叫的方法來快速新增一個編輯道路的皮膚,不過這樣UI看起來就太長了,所以修改了一下顏色編輯皮膚的樣子使其更緊湊一些,好與河流皮膚想吻合。
新增道路編輯之後的UI
因為現在顏色編輯皮膚使兩行三列,多出了一個顏色的位置,就新增一個橘黃色的地形顏色。
地形的五種顏色
現在就可以編輯道路了,儘管看不見,但可以在檢視皮膚上驗證其是否生效。
檢查單元格的道路狀態
3道路的三角剖分
要讓道路視覺化就需要將其三角化,這與河流mesh類似,除了地形上不會生成一條通道之外。
第一步,在新建一個標準著色器,使用UV座標為道路表面著色。
建立道路的材質球並應用這個著色器。
道路材質球
然後為地圖塊的預製體新增另一個掛載有HexMesh的子物體,只在指令碼上勾選uses UV coordinates並關閉陰影投射,比較快的辦法是複製河流的預製體修改名字和材質球。
道路子物體
在這之後新增一個HexMesh型別的公共欄位roads到HexGridChunk裡,幷包含到Triangulate方法裡,在指令碼的檢視皮膚上建立其關聯。
道路物件的關聯
3.1單元格之間的道路
第一步先考慮單元格之間的路段。就像河流一樣,道路覆蓋的範圍是連線部分中間的兩個四邊形,這裡將使用道路的四邊形將其完全覆蓋,所以可以使用相同的六個頂點。在HexGridChunk中新增一個TriangulateRoadSegment方法。
由於不用管水流的效果,所以不需要V座標,所有位置的V座標都設定成零就行。用U座標來表示是道路的中間還是邊界,這裡定義U座標1為道路的中間,0為兩邊。
路段在單元格之間的區域
TrangulateEdgeStrip即是呼叫此方法的位置,並且只有在確實存在道路時才需要這麼去做,新增一個布林型別的引數去傳遞這個資訊。
這樣當然會出現編譯錯誤,因為呼叫這個方法的位置缺少引數。現在可以在沒個呼叫方法的位置都補上一個false,或者也能宣告這個引數的預設值是false,這樣一來這個引數就變成了可選的並解決了編譯錯誤的問題。
可選引數是如何工作的?
你可以認為是函式過載的縮寫,填入了缺失的引數。例如以下方法:
- <p>int MyMethod(int x=1,int y=2){return x+y;}</p><p>
- </p><p>它等價於下面三個方法:</p><p>
- </p><p>int MyMethod(int x,int y){return x+y;}</p><p>
- </p><p>int MyMethod(int x){return MyMethod(x,2);}</p><p>
- </p><p>int MyMethod(){return MyMethod(1,2};}</p><p></p>
這裡的引數順序很重要,可選引數可以從右到左省略,最後的一個引數第一個被隱藏,而且可選引數總是放在正常引數之後。
如果這裡需要就簡單的通過六個中間頂點呼叫TriangulateRoadSegment來三角化路段。
這裡只負責平坦連線處部分,要在階梯連線處三角化道路,還需要在TriangulateEdgeTerraces裡新增傳遞資訊的引數,並傳遞到TriangulateEdgeStrip中。
TriangulateEdgeTerraces方法是在TriangulateConnection裡呼叫,這裡是能夠確認當前方向上是否有道路穿過的地方,無論是對於三角化邊界和階梯連線處都一樣。
單元格之間的路段
3.2渲染到頂部
當編輯道路時,你會看到路段在單元格之間突然出現,靠近路段中間的位置是紫色,到邊上變化為藍色。目前看起來好像符合預期,然而四處移動相機時就會發現,路段有可能出現閃爍,甚至有些時候還會完全消失。這是因為道路的三角形精確的覆蓋在地形三角上,渲染至頂部的三角形是隨機的,要修復這個問題需要分為兩步。
首先,我們想讓道路三角形總在地形三角形之後繪製,通過繪製常規集合圖形之後再渲染來實現,方法是把道路放在靠後的渲染佇列中。
第二步,我們希望即使是座標相同,道路三角形也繪製在地形的頂部。這要通過新增一個深度測試偏移量實現,使得GPU將道路三角形視為比實際距離更接近相機。
3.3穿過單元格的道路
當三角化河流時,每個單元格最多隻需要處理兩個河流的方向,於是我們能為五種可能的方案建立符合其規則的三角化方法。
然而道路有十四種可能的方案,因此我們不會用不同的方法來應對每個方案,相反將會以完全相同的方式處理每個方向,而不去考慮特定的道路結構。
當有道路穿過單元格時,直接讓其筆直達到單元格的中心並不超過這個方向的三角形區域。將從邊緣到中心畫一條路,然後用兩個三角形來覆蓋到中心的剩餘部分。
道路的三角化部分
要三角化這部分就需要獲取單元格中心頂點,左右位置的頂點和邊緣上的頂點。新增一個TriangulateRoad方法和相應的引數。
需要一個額外的頂點去構建道路,它位於左右頂點的中間。
現在就能新增剩餘的兩個三角形了。
三角形也要新增它的UV座標,它們的兩個頂點都在道路的中間,另一個在邊上。
先只考慮沒有河流在內的單元格,這種情況下Triangulate就簡單的構建三角扇。把這些程式碼移動到它自己的方法中,然後在道路確實存在時在TriangualteRoad中新增呼叫。道路左右的中間位置頂點可以通過插值計算單元格的中心點和兩個角頂點得到。
道路穿過的樣子
3.4道路邊緣
現在可以看到道路了,但是其向著單元格中心的方向是逐漸變細的。由於沒有檢測現在是十四種道路結構中的哪一種,所以沒辦法移動單元格的中心讓它更好看一些,現在能做的就是新增額外的道路邊緣部分到單元格的其他地方。
當單元格內有道路穿過,但又不是當前三角化的方向時,新增道路邊緣的三角形。這個三角形由單元格的中心點和左右邊緣的中間點構成。在這種情況中只有中心點位於道路中間,其他兩個頂點都在道路的邊緣上。
不管是要三角化整個道路還是僅僅只是道路的邊緣,都應該留給Triangulate方法負責,所以這裡得知道道路是否經過當前單元格邊界的方向,為此新增一個引數。
TriangulateWithoutRiver方法會在單元格內有道路穿過時呼叫TriangulateRoad方法,並同時傳遞道路是否經過當前方向的資訊。
道路的邊緣完成
3.5道路輪廓的平滑處理
現在道路完工,但是單元格的中心會有凸起的感覺。當有道路與左右邊的頂點相鄰時,把左右頂點放置在單元格中心和角頂點的中間時,看起來還行,但如果沒有,就會產生凸起。為解決這個問題,可以在沒有相鄰道路的情況下,把邊緣頂點放到更靠近單元格中心點的位置,具體來說就是插值的因子由二分之一改為四分之一。
建立一個額外方法去計算出需要使用哪個插值,可以把這兩個值放在一個Vector2中,它的X分量是左邊頂點的插值,Y分量是右邊頂點的插值。
如果道路在當前方向上,就把邊緣頂點都放到二分之一的位置。
否則就根據相鄰道路來決定,對於左邊的頂點,當上一個方向有道路穿過時用二分之一作為插值因子,如果沒有就用四分之一。這個邏輯對右邊的頂點也是一樣,只不過其參照的是下一個方向上有沒道路經過。
現在就能用這個新方法確定使用哪種插值,這會讓道路的輪廓顯得更平滑一些。
平滑的道路
4道路與河流的結合
在沒有河流的情況下道路的功能已經完成了,但一旦有節流穿過,道路就不會被三角化。
河流邊沒有道路
建立一個新方法TriangulateRoadAdjacentToRiver負責這種情況下道路的三角化,並新增三角化慣有的引數,在TriangulateAdjacentToRiver方法一開始呼叫。
一開始做的與沒有河流時道路的三角化一樣,檢測道路是否穿過這個方向的單元格邊緣,提供插值係數,計算中間頂點接著呼叫TriangulateRoad方法。但由於河流會擋住道路,所以要把道路移開,結果就是道路在單元格內的中心點會在不同的位置,使用roadCenter變數來記住這個新位置,它一開始的時候等於單元格的中心點。
這會在有河流穿過的單元格中生成部分道路,河流穿過的方向會在道路上形成缺口。
道路上的缺口
4.1河流起點與終點的道路處理
首先考慮包含河流起點或終點的道路處理,若要確保道路不會覆蓋在河流之上,我們需要把道路在單元格內的中線點推移到河流的範圍之外。未測需要獲得流入或流出的河流方向,在HexCell中新增一個方便獲取的屬性。
現在能在HexGridChunk中使用這個屬性,在TriangulateRoadAdjacentToRiver中把道路的中心點推移向相反的方向,沿著這個方向移動三分之一的距離就可以了。
修正中心點
下一步就是填充缺口。當與河流相鄰時我們新增額外的道路邊緣三角形來實現。如果在當前方向的上一個方向上有河流穿過,那麼就在道路中心,單元格的中心點和道路左邊中間位置的頂點之間新增一個三角形。如果下一個方向上有河流穿過,就在道路中心,道路右邊的中間頂點和單元格中心點之間新增一個三角形。
不管在處理何種河流結構都要進行這樣的處理,所以把這段程式碼放在方法的末尾。
不使用else麼?
這樣就不適用所有情況了,河流有可能同時流經這兩個方向。
道路完成
4.2筆直河流的道路處理
單元格內有筆直河流穿過情況下道路的處理帶來了額外的挑戰,因為單元格的中心點事實上被分為了兩個。我們已經新增了額外的三角形來填補沿河的空隙,但我們還得斷開河流兩邊的道路。
道路覆蓋在筆直河流之上
如果單元格內不是河流的起點或終點,那麼再檢測一下河流流入和流出方向是否相反,如果是,那毫無疑問就是筆直的河流。
為了確定河流相對於當前方向的位置,我們必須檢查相鄰的方向,河流方向不是在左邊就是在右邊。如同我們在方法末尾做的那樣,把這些檢測快取到布林值中,這也使程式碼更容易閱讀。
我們需要把道路的中心點往遠離河流方向的角上推移,如果這條河流經過前一個方向,那這個角就是固定裡六邊形的第二方向上的角,否則就是第一個角。
移動道路使其在於河流方向相鄰的位置結束,需要移動道路的中心點到朝向這個角的方向的一半的位置,然後還有把單元格的中心向這個方向移動四分之一的距離。
分開的道路
現在單元格內的道路網已經分開,當河流兩邊都有路的時候看起來沒問題,但當一邊沒有時,會在這邊留下一點孤零零的小尾巴,這部分沒什麼用也不美觀,所以把這部分去掉。
檢查一下當前方向是否有道路穿過,如果沒有,檢測一下河流同邊的其他方向,如果兩者都沒有道路穿過,就在三角化之前跳出方法。
修剪後的道路
為什麼不搭一座橋?
現在先把注意力集中在道路上,橋和其他的建築結構放在未來的教程中。
4.3鋸齒急彎河流的道路處理
下一步是包含鋸齒急彎河流單元格的道路處理,這種形狀的河流不會分割路網,所以我們只需要移動道路的中心點就行了。
最簡單的檢測是否是急彎的方法是比較單元格內河流流出和流出的方向,如果他們相鄰,那這就是急彎了。這有兩種可能的情況,取決於河流的流向。
我們可以使用河流流入方向的角作為移動道路中心點的方向,具體是哪個取決於河流的流向,把道路中心點往這個方向上移動0.2的距離。
道路從鋸齒急彎上移開
4.4彎曲河流內側的道路處理
最後的河流形狀是平滑的曲線,與筆直河流相同,這是可以分離道路的型別。但在這種情況下曲線內的一邊處理起來會有一些不同,先處理這部分。
如果當前處理方向的兩個相鄰方向都有河流穿過,那就是在彎曲河流的內側。
我們需要把道路中心點向當前方向的邊緣位置拉,使道路縮短一截,0.7的距離就差不多了。單元格的中心點位置也要移動,它的值是0.5。
被縮短的道路
與筆直河流那做的處理一樣,把單獨剩下的一點尾巴去掉。這裡只需要檢測當前的方向。
4.5彎曲河流外側的道路處理
在檢測了之前所有情況之後,唯一剩下的可能就是在彎曲河流的外測。外側的位置有單個單元格的部分,需要找到其中間的方向。一但找到就以此方向為參照把道路中心點移動0.25的距離。
在外側修正道路位置
最後一步是修剪這邊道路的多餘部分,最簡單的方法是相對中間方向檢測其包括相鄰方向在內的三個方向,如果這三個方向上均沒有道路,就跳出。
道路在修剪前後的對比
5道路的外表
到目前為止都使用UV座標為道路著色,由於只是改變了U座標,我們實際看到的是道路中間到邊緣之間的過渡。
道路顯示的UV座標
現在已經能確保道路的三角剖分的正確性,就可以開始改變道路的顏色,使其表現得更像道路,如同對河流所做的一樣。這將是一個簡單的視覺化改動,沒什麼特變的。
首先從使用純色開始,使用材質球的顏色將其塗為紅色。
紅色的道路
現在已經看起來好多了,接著使用U座標作為混合因子混合道路和地形之間的顏色。
似乎沒有效果,這是因為著色器是不透明的。現在需要把alpha值進行混合,具體來說就是需要一個混合貼花的表面著色器。可以通過在#pragma suface指令中新增decal:blend來獲得預想的著色器。
透明度混合的道路
結果產生了從中間到邊緣的線性混合,效果似乎不是太好。為了讓其看起來更像一條路,需要在路中間保留一個固定的顏色區域,然後再快速過渡到一個不透明的區域。這裡可以使用smoothstep函式,把從0到1之間的線性變化變為s形的曲線。
線性變化與平滑曲線
smoothstep函式有一個最大和最小引數,可以在任意範圍內擬合曲線,超出這個範圍的輸入被固定住,因此曲線會變得平滑。這裡使用0.4作為曲線的起點,0.7作為曲線的終點,這表示U座標從0到0.4之間是完全透明的,從0.1到1之間是完全不透明的,轉換髮生在0.4到0.7之間。
從透明到不透明之間的快速變化
5.1道路外表噪聲化
由於道路的頂點也會受到噪聲擾動,所以道路的寬度會發生變化。因此道路邊緣過渡部分的寬度也會發生變化,有時模糊有時清晰,這樣很像是土路或者沙路帶來的感覺。
讓我們更進一步,在道路的邊緣新增一些噪聲效果,這將是道路的形狀開起來更粗糙一些,不會顯得那麼稜角分明,這可以通過對噪聲紋理取樣做到。使用世界座標的XZ對噪聲紋理進行取樣,就像在擾動單元格頂點時候一樣。
要訪問表面著色器中的世界座標,需要在輸入結構中新增float3 worldPos。
現在可以在surf中使用這個座標來對主紋理圖進行取樣,一定要縮小座標的比例,不然紋理會在小範圍平鋪顯示。
通過將U座標與噪聲紋理的X值相乘來對過渡值進行擾動,但由於噪聲紋理的平均值是0.5,這將會覆蓋大部分的道路。為防止這種情況發生,在相乘之前先加上0.5。
道路邊緣的擾動
最後也對道路的顏色進行擾動,這配和凌亂的道路邊緣給其一些髒亂的效果。
將顏色乘上紋理圖的另一個通道,比如noise.y。所以顏色的平均值只有最大值的一半,這擾動的幅度有些大,所以縮小一點噪聲取樣的取值並加上一個常量,所以總值還是可以達到1。
本期工程地址Hex-Map-Learning-Roads
有想系統學習遊戲開發的童鞋,歡迎訪問http://levelpp.com/。
下一篇教程是Water。
系列文章
HexMap學習筆記(一)——建立六邊形網格
HexMap學習筆記(二)——單元格顏色混合
HexMap學習筆記(三)——海拔高度與階梯連線
HexMap學習筆記(四)——不規則化
HexMap學習筆記(五)——更大的地圖
HexMap學習筆記(六)——河流
HexMap學習筆記(七)——道路
作者:沈琰編譯
專欄地址:https://zhuanlan.zhihu.com/p/59214888
相關文章
- HexMap學習筆記(六)——河流筆記
- HexMap學習筆記(八)——水體筆記
- HexMap學習筆記(九)——地形特徵筆記特徵
- HexMap學習筆記(五)——更大的地圖筆記地圖
- HexMap學習筆記(四)——不規則化筆記
- HexMap學習筆記(一)——建立六邊形網格筆記
- HexMap學習筆記(二)——單元格顏色混合筆記
- Redis阻塞(學習筆記七)Redis筆記
- andeoid學習筆記七筆記
- Spss 學習筆記(七)SPSS筆記
- webpack學習筆記七:配置babelWeb筆記Babel
- angular學習筆記(七)-迭代1Angular筆記
- angular學習筆記(七)-迭代2Angular筆記
- angular學習筆記(七)-迭代3Angular筆記
- C++/C學習筆記(七)C++筆記
- Java學習筆記——陣列練習(七)Java筆記陣列
- Redis學習筆記(七) 資料庫Redis筆記資料庫
- ES6學習筆記(七)【class】筆記
- Laravel學習筆記七-建立部落格Laravel筆記
- Activiti 學習筆記七:連線(SequenceFlow)筆記
- OS學習筆記七:IO系統筆記
- 工作學習筆記(七)Java的介面筆記Java
- 飛機的 PHP 學習筆記七:WebPHP筆記Web
- Redis 學習筆記(篇七):Redis 持久化Redis筆記持久化
- Go語言學習筆記(七)之方法Go筆記
- Redis學習筆記七:主從叢集Redis筆記
- JavaScript學習筆記(七)—— 再說函式JavaScript筆記函式
- vue學習筆記(七)---- vue中的路由Vue筆記路由
- iOS學習筆記47 Swift(七)泛型iOS筆記Swift泛型
- Java學習筆記——第七天Java筆記
- JavaWeb學習筆記——第七天JavaWeb筆記
- hive學習筆記之七:內建函式Hive筆記函式
- 《Mastering Delphi 6》學習筆記之七 (轉)AST筆記
- 學習筆記|AS入門(七) 資料儲存篇筆記
- 【linux學習筆記七】關機重啟命令Linux筆記
- 哲學筆記——叔本華《續七》筆記
- numpy的學習筆記\pandas學習筆記筆記
- Vue.js 學習筆記之七:使用現有元件Vue.js筆記元件