HexMap學習筆記(六)——河流
前言
這是目前為止長度最長的一篇,難度也是直線上升。不僅此篇所用的三角剖分方法更為複雜,並且從這篇教程開始,會逐漸使用編輯著色器程式碼的方式新增一些簡單的視覺效果。
儘管經過Unity的簡化,但編輯著色器程式碼依然是Unity新手的一個難點。不僅是那與C#迥然相異的語法,如果要實現一個看的過去的效果,還需要相當紮實的數學功底。推薦基礎較為薄弱的同學先跟著做一遍,不用太過糾結原理。當然底子強的同學要深入理解也可以另行查閱作者的shader系列教程。
本篇原文地址:Hex Map 6
本篇難度:★★★☆☆
這個教程是HexMap系列的第六部分,上一篇的內容是實現一個較大的地圖,這部分現在已經完成,可以開始考慮更大範圍的地形特性了,即此篇教程中的河流。
從山上流下的河流
1單元格與河流
在六邊形地圖中新增河流有三種方法。
第一種方法是讓其從單元格中穿過,從一個單元格流向另一個單元格,這是《無盡傳說》中的做法。
第二種方法是讓其在單元格之間流過,沿著單元格個邊緣到另一個單元格的邊緣,《文明5》中是這麼做的。
第三種方法是不使用額外的河流結構特性,而是直接用特殊的單元格表示水體,《奇蹟時代3》中是這麼做的。
而在我們的工程中,由於單元格的邊緣連線已經用階梯化或陡峭的方式特殊處理過,沒有留給河流的空間,所以就採用第一種方法,讓河流從一個單元格流向另一個單元格。這意味著每個單元格要麼就是沒有河流經過,要麼河流穿過這個單元格,要麼這個單元格是河流的起點或者終點。而在有河流穿過的單元格中,要麼河流是筆直穿過,要麼是一步銳角轉彎,要麼是兩步鈍角轉彎。
五種可能的河流情況
1.1追蹤河流方向
一個單元格內的河流流向可能是流入或流出,如果這裡是河流的起點那麼只可能是流出的河流。相對的如果是終點就只會是流入的河流。我們可以在HexCell裡用兩個bool型別變數儲存這個資訊。
但僅僅知道這個還不夠,還需要知道河流的方向。在流出的情況下即河流的流向,而在流出的情況下則表明了河流是從哪個方向流入的。
我們需要在單元格三角化時獲取這些資訊,所以分別新增get屬性但不需要set,之後會新增不同的方法去設定這些值。
還有一個比較有用的資訊:單元格內是否存在河流而無論其具體情況。所以也為此新增一個屬性。
另一個特殊問題是這個單元格是否是河流的起點或者終點,如果有河流流入和流出這兩個布林型別的值不同,那就是這種情況了。也為此新建一個屬性。
1.2移除河流
在考慮如何為單元格新增河流之前,先考慮如何移除它們。第一步,建立一個方法移除流出部分的河流,如果沒有流出方向的河流那就直接跳出,否則設定其布林值為false並重新整理。
這還沒完,一條流出方向的河流肯定會流向其他單元格,所以一定會有相鄰單元有流入的河流,我們同樣也要處理這部分。
- 河流不會延伸到地圖之外麼?
- 雖然有能實現這個功能,但我們不會這麼做。所以也不用檢查相鄰單元格是否存在。
從一個單元格移除河流只會改變它自己的外觀,這一點不像編輯高度與顏色的時候還得考慮所有相鄰單元格,所以我們只需要重新整理這個單元格本身就行了。
這個RefershSelfOnly方法就是簡單的重新整理此單元格所在的地圖塊就行,當地圖網格初始化時還沒有對河流進行編輯,所以也不需要考慮此時地圖塊是否已賦值的問題。
移除流入方向的河流也是一樣的。
然後移除全部的河流即意味著同時移除流入和流出的部分。
1.3新增河流
要實現新增河流的功能,只需要一個方法設定單元格的流出方向的河流。這個方法應該覆蓋之前流出方向的河流,並設定相應的相鄰單元格的流入方向的河流。
首先,當要設定的方向已經存在河流時直接跳出。
然後得確保在這個方向上存在一個相鄰單元格。並且河流不能向著更高的位置流動,所以當檢測到相鄰單元格較高時跳出方法。
下一步,清除上一個流出方向的河流。並且當流入方向的河流與當前流出方向河流重疊時還需要清除流入方向的河流。
然後輪到設定流出方向的河流。
最後別忘了了,當相鄰單元格上已經有流入方向的河流時,移除它並設定新的流入河流。
1.4防止逆流情況
雖然我們能保證只會新增有效流向的河流,但其他的操作依然會導致無效流向的情況發生。
例如當我們編輯單元格的高度時,我們必須再一次強制改變河流流向,所有錯誤流向的河流都需要移除。
2編輯河流
要實現編輯河流的功能,我們需要新增河流的選項卡(toggle)元件到UI上。事實上我們需要新增三種編輯模式:忽略,新增和移除,就簡單的使用列舉來記錄這個值。由於這個功能只能在編輯模式中使用,所以可以在HexMapEditor這個類中去定義這個列舉和編輯模式的欄位。
並且還需要一個通過UI修改河流編輯模式的方法。
新增三個toggle元件到UI上並放到一個新的toggle group中,就像顏色編輯一樣。這裡修改了標籤名的位置在其選項框下面。這樣把三個選項框全放到一行時佔用空間會足夠薄。
河流編輯UI
- 為什麼不使用下拉選單?
- 你要喜歡你也可以用。不幸的是,Unity的下拉選單在執行模式下不能處理重編譯,選項列表會在重編譯時丟失並無法使用。
2.1檢測滑鼠拖拽事件
要建立一條河流,同時需要單元格的位置和方向這兩個資訊,目前HexMapEditor中沒有這兩資訊的獲取方式,需要新增一個新方法實現從一個單元格到另一個單元格的拖拽。
在檢測到有效拖拽事件時還需要記錄其拖拽方向和上一個單元格。
最初的時候是沒有拖拽事件的,也就沒有上一個單元格的記錄。所以當沒有輸入資訊或者沒有與地圖互動時,需要設定其為null。
當前單元格是根據射線擊中的點找到的,當在這一幀裡結束編輯時,它就會變成下一次Update裡的上一個單元格.
在確認了當前單元格之後,我們可以與前一個單元格(如果有的話)進行比較,當發現是兩個不同的單元格時,就說明可能存在有效拖動並需要去檢測,要不就是沒有拖拽事件。
如何證實確實是拖拽事件?通過檢測當前單元格是否是前一個單元格的相鄰單元格,迴圈遍歷前一個單元格所有的相鄰單元格來進行檢測,如果找到了與當前單元格相吻合的結果就能同時確認拖拽的方向。
- 這不會產生拖拽抖動麼?
- 當你移動滑鼠穿過單元格邊界時,可能會在單元格之間快速來回擺動,這確實會導致拖拽抖動,但情況沒那麼糟。
- 可以通過記錄上一次拖拽事件來減緩抖動,然後防止下一次直接向相反方向拖拽。
2.2修改單元格
現在能檢測到拖拽事件了,可以開始設定流出向的河流。同樣也能移除河流,但移除功能不需要拖拽事件的支援。
這能在兩個單元格之間建立出一條河流,但會忽略筆刷尺寸。這也許能說得通,但我們還是畫出所有被筆刷覆蓋的單元格之間的河流,這可以通過相對正在編輯的單元格來完成。這種情況下需要確保另一個單元格確實存在。
現在可以開始編輯單元格的河流了,儘管看不見,但可以通過檢視皮膚(inspector)的debug模式下的欄位來驗證是否工作正常。
檢視皮膚Debug模式下單元格的河流
什麼是debug檢視皮膚?
你可以在檢視皮膚的標籤選單裡切換為debug模式,在這個模式下檢視皮膚會顯示物件的原始資料。
3單元格之間的河道
河流的三角化可以分為兩個部分來考慮,即河道和水流。我們先建立河道,把水流放到後面。
河流的最簡單部分是流經單元格之間的連線處的位置,這裡目前用三個四邊形組成的長條形狀來三角化這個部分,可以通過降低中間四邊形的高度和新增兩道牆來建立河道。
為河流新增邊緣
但如果要這麼做就需要新增兩個額外的四邊形來生成垂直的牆,另一個方法是使用四個四邊形來組成連線單元格的部分,這樣就能通過拉低中間的頂點形成河道的傾斜牆壁。
始終只有4個四邊形
一直使用一樣四邊形數量會比較方便,所以我們選擇後一個方法。
3.1新增邊界頂點
要把邊界連線部分的三個四邊形改為四個,就需要額外的邊界頂點,因此重構EdgeVertices這個結構,首先重新命名v4為v5,v3為v4。要確保所有程式碼始終能引用正確的頂點,要使用IDE的重新命名或重構方法,這樣改動就能應用到所有地方,不然你只能手動去檢查程式碼並進行改動。
在重新命名完成後新增新的v3。
在建構函式中新增新的頂點,它應該是角頂點的一半,另外兩個頂點的插值就變成了四分之一和四分之三的位置。
同樣把v3新增到TerraceLerp方法中。
HexMesh中新增額外頂點到與邊界連線的三角扇中。
還有四邊形的條狀連線中。
四個邊界點和五個邊緣頂點的區別
3.2河床高度
我們通過拉低邊界連線部分的中間頂點建立出了河道,這定義了河床豎直方向的座標。儘管每個單元格的精確豎直座標會受不規則化的影響,但還是應該在相同高度的單元格之間保持河床的恆定,這確保河流看起來不會是逆流而上。同樣河床需要足夠低,即使單元格的豎直方向的頂點擾動達到最大值也應該與單元格的底面保持一定的距離,為水流留下足夠的空間。
讓我們在HexMetrics裡定義這個偏移量並把它作為高度等級的變數傳遞出來,一級高度等級的步長應該就足夠了。
使用這個度量標準在HexCell裡新增一個屬性,獲取當前單元格垂直座標的河床高度。
3.3建立河道
當HexMesh三角化六個方向其中之一時就可以檢測這個方向上是否有穿過的河流,如果有就修改中間頂點到河床的高度。
修改邊界連線處的中間頂點後的樣子
可以看到河流的痕跡初現並在地上留下了空隙,要填充空隙則需要在三角化連線部分時修正六個邊界上的垂直座標。
邊界連線處的河道完成
4穿過單元格的河道
現在在兩個單元格之間建立出了正確的河道,但是在河流穿過單元格時總是會在中心位置結束。要修正這個問題需要費些功夫。讓我們先從河流筆直穿過單元格,從一邊到其相反方向的另一邊這種情況開始考慮。
如果沒有河流,單元格每一個方向都是由扇形三角組成,但當河流穿過時就需要在中間插入一條河道。實際上就是需要把單元格的中心點延伸成一條線,從而把中間的兩個三角形變成了四邊形,這樣三角扇部分就變為了梯形。
強制把河道變成三角形
穿過單元格之間的河道比穿過連線處的河道要長得多,當頂點被擾動時看起來會很明顯。所以我們通過在中間和邊界之間的一半的位置插入一組新的邊界頂點來把梯形分為兩段。
河道的三角剖分結構
由於對帶有河流和沒有河流的單元格進行三角剖分會大不相同,所以為此建立一個專用方法。如果單元格內有河流就使用這個方法,不然就用之前的。
本來應該是河流的位置,現在是空洞
為了更清楚的觀察改動,暫時先禁用單元格的不規則座標擾動。
禁用頂點擾動
4.1河流筆直橫穿情況下的三角剖分
要構建筆直穿過單元格的河道,需要把單元格的中心點延伸成一條線並與河道的寬度相同。
可以通過單元格的中心點到第一個角頂點的前一個方向的角頂點移動四分之一的位置到到左邊的頂點。
同樣的方法找到右邊的頂點,這裡需要的是第二個角的下一個方向的叫頂點部分。
中心點到單元格邊界之間的一組中間線頂點可以通過建立EdgeVertices陣列獲得。
下一步,修改中間線陣列的中間頂點的座標和單元格中心點座標,使其與河道高度相同。
再用TrriangulateEdgeStrip方法填充中間線與單元格邊界之間的空間。
壓縮的河道
不幸的是河道看起來好像被壓縮了,中間邊界的的頂點靠的太近,為什麼會這樣?
考慮六邊形的外邊長是1這個情況,那麼中心點的延伸線的長度就是二分之一。因為中間邊界線兩端的頂點位於之間一半的位置,那麼中間邊界線的長度就是四分之三。
河道的寬度是不變的二分之一,由於中間邊界的長度是四分之三,剩餘的長度就是四分之一,每邊的寬度就是八分之一。
相對長度
由於現在的中間邊界線的長度是四分之三,那它長度的八分之一實際值就是六分之一,這意味著中間邊界線的第二個和第四個頂點應該使用六分之一進行插值而不是四分之一。
我們可以在EdgeVerices裡新增一個建構函式實現這個特殊版本的插值,而不是強行修改v2和v4的值,使用一個引數來控制。
現在可以在HexMesh.TriangulateWithRiver使用這個新版本的建構函式。
筆直的河道
河道恢復筆直後就可以開始第二段梯形的三角化工作了,這裡無法直接使用邊界條的生成函式,只能手動新增。第一步先建立邊上的三角形。
邊上的三角形
看起來不錯,繼續用兩個四邊形填充剩餘空間,完成河道的最後一部分。
實際上我們沒有隻用一個引數的AddQuadColor方法,在這之前我們都用不到,所以就直接建立一個。
筆直河道完成
4.2河流起點與終點的單元格的三角剖分
對起點或終點的單元格進行三角剖分與之前的方法有較大差異,這足以使我們為此建立一個專用方法.所以在Triangulate裡檢測,如果是起點或終點就呼叫這個專用f方法。
在這種情況下我們想要的是河道在單元格的中心位置終止,但這依然需要分為兩個步驟。所以還是在邊界和單元格中心之間插入一組中間線頂點。由於這一次我們確實需要河道在單元格中心終止,所以河道邊界在中心壓縮就是正確的。
為了確保河道在變淺之前有個過渡,還是將中間線的中間頂點設定為河床的高度。但是單元格中心點的高度就不必修改了。
這部分可以直接作為單個的邊界條狀帶和三角扇進行三角化。
起點和終點
4.3一折彎道
下一步,來考慮鋸齒形急彎拐入相鄰單元格的河道的三角剖分情況,這部分也歸TriangulateWithRiver方法負責,所以首先要搞清楚正在為哪種型別的河流三角化。
鋸齒彎道形河流
如果一個單元格內,流入河流的方向與流出河流的反方向相同,那毫無疑問就是筆直的河流,這種情況下就使用之前算好的中心線,否則就把中心線重新壓縮回一個點。
壓縮的鋸齒河道
我們可以通過河道是否穿過下一個方向或者上一個方向的相鄰單元格部分來檢測這個單元格內是否有鋸齒急彎。如果是這樣,我們就必須將中心線與這部分和相鄰部分之間的邊對齊。我們可以通過在中心線和共享角之間放置適當的邊來實現這一點,那麼這條線的另一端就變成了中心點。
在確認了左邊和右邊點的位置後,我們可以通過計算這兩個點的平均值來確定最後的中心點。
扭曲的中間邊緣
儘管河道在兩邊有相同的寬度,但看起來還是有些擠壓的感覺,這是因為中心線被旋轉了60度,可以將中心線的長度適當延長一點來緩解擠壓感,用三分之一而不是二分之一作為插值引數。
不再有擠壓感的鋸齒彎道
4.4兩折彎道
剩下的就是既不是急彎又不是筆直河道的情況,即分兩步旋轉產生相對平緩的曲線河流。
緩慢彎曲的河流
為了區分這兩種可能的方位,我們需要用到direction.Next().Next()這種繁瑣的寫法,為了讓其更簡化,在HexDirection裡新增Next2()和Previous2()這兩個擴充套件方法。
回到HexMesh.TrigulateWithRiver這個方法裡,現在可以用direction.Next2()來檢測是否是彎曲的河流。
在最後兩個情況中,我們把中心線推移到單元格部分的曲線上。如果我們有一個指向中間固定六邊形邊界中心的向量,我們就能用此定位結束點,先假設我們有這麼一個方法。
當然,我們要在HexMetrics裡新增這個方法,就是簡單的平均計算兩個角向量然後乘上固定內六邊形比例的一半。
有些微扭曲的曲線
中心線現在是正確的旋轉了30度,但它不夠長以至於河道有輕微擠壓的感覺。這是因為邊界線的中間點靠單元格的中心點比邊界角更近,它的距離等於中間固定部分六邊形的內徑而不是外徑,所以我們這裡應用了一個錯誤的大小。
我們已經在HexMetrics中定義了內徑到外徑的轉換,這裡要做的就是顛倒過來,所以在HexMetrics中新增兩個轉換比率變數。
現在HexMesh.TriangulatWithRiver裡轉換成了正確的比例,但由於中間線旋轉的原因,河道還是會有輕微擠壓的感覺,但這已經比鋸齒急彎要緩和多了,所以我們不必再為此額外費神了。
平滑的曲線
5單元格鄰近河流部分的三角剖分
現在河道完成了,但沒有三角化包含河流的單元格的其他部分,現在就去填充這部分空間。
河道邊上的空洞
在三角化時,當單元格內有河流但又不留經當前方向時,呼叫一個新方法。
在這個方法裡,用條狀連線帶和三角扇來填充單元格內的空隙。僅用三角扇不夠,因為還要確保能和中間邊界線吻合。
彎曲和筆直的河流上有重疊部分
5.1與河道吻合
當然,我們得確保我們使用的單元格中心點與河流部分的中心線吻合,這在鋸齒急彎部分是對的,只需要在緩彎和筆直河流上做出些額外修改。所以這裡得知道河流的型別以及相對方向。
先來看看當前方向在河流曲線內彎的情況,即前一個方向和下一個方向上都有河流穿過,在這種情況下中心點就得移動到邊緣上去。
當河流在兩邊移動時修正中心點
如果在下一個方向的邊界有河流穿過而不是前一個方向,就檢查一下是不是筆直的河流。如果是就需要把中心點向固定內六邊形的第一個角上移動。
修正了半邊的筆直河流
這能修正一半的問題,最後一種情況是當前方向的前一個方向上有河流並且是筆直河流,這就需要把中心點移向固定內六邊形的下一個角。
不再有重疊部分了
6 HexMesh廣義化
河道的的三角化已經完成,現在可以填充水了。因為水與陸地有很大不用,所以我們會使用不同的mesh,不同的頂點資料與材質。如果能用HexMesh同時處理陸地與水的mesh資訊將會比較方便,所以我們把HexMesh這個類廣義化,用其專門處理mesh資料而不用關係它到底是用來幹嘛的,HexGridChunk會去負責三角化它自己的單元格。
6.1移動頂點擾動方法
由於Petrurb方法比較通用,可能後面會用在其他地方,所以把它移動到HexMetrics中,重新命名為HexMetrics.Perturb。(注:VS的重新命名方法不能加上“.”,可以用文字替換功能,或者你也能人工一個個的修改)這是個無效的方法名,但是可以重新命名所有的程式碼讓其正確訪問。如果編輯器具有特殊的功能修改方法名,你可以用這個功能代替。
當這個方法處於HexMetrics的內部,就設定其為公共和靜態型別,然後修改名字。
6.2移動三角化方法
在HexGridChunk中,修改hexMesh變數名為公共型別的terrain。
下一步,重構所有HexMesh裡呼叫Add..開頭方法的位置為terrain.Add..,然後把所有Triangulate..開頭的的方法移動到HexGridChunk中。這一步完成後就可以修改Add..類方法並設定為公共型別.其結果就是所有複雜的三角化方法現在都在HexGridChunk裡了,並且簡單的新增資料到mesh中的方法仍然保持在HexMesh裡。
這一步還沒做完,HexGridChunk.LateUpdate裡現在呼叫它自己的Triangulate方法,再也不用傳遞單元格作為引數了,並且它應該委託清除和應用網格資料到HexMesh。
新增必須的clear()和apply()方法到HexMesh中。
SetVertices,SetColors和SetTriangles是什麼方法?
這些方法是unity最近新增到Mesh這個類中的,它能讓你直接傳遞Mesh資料到列表中.這意味著我們在更新資料時不需要再建立臨時存放資料的陣列.
SetTriangles方法有第二個整數引數,即子網格的下標.我們不用子網格,所以它一直是零.
最後,手動關聯地圖塊預製體裡子物件的Mesh,這裡不再自動賦值,因為馬上就要新增第二個網格子物件,同樣重新命名為Terrain指出其用途。
Terrain賦值
無法重新命名預製體的子物件?
工程預覽中不會更新預製體名字的改動.你可以通過建立一個預製體的例項來更新它.修改例項,然後使用Apply按鈕把這些改動儲存到預製體上.這是當前最好的修改預製體在層級視窗內資訊的方法。
6.3列表池
儘管我們已經移動了很多程式碼的位置,但我們的地圖還是與之前的工作方式一樣。給每個地圖塊新增另一個mesh會改變它的工作方式,但是如果我們使用當前的HexMesh來做就會出錯。
問題在於,我們之前一直假設在一個時間點上只會對一個mesh進行修改,這就允許我們使用靜態列表儲存臨時mesh資料。但是當我們新增水面mesh的資料時,有可能就會在同一個時間點同時對兩個mesh做出改動,所以現在不能繼續用靜態列表了。
然而我們也不需要改回到為每一個HexMesh設定一個列表笨辦法,可以換成使用一個靜態的列表池,預設資料結構是沒有池這個型別的,所以我們自己建立一個泛型列表池的類。
ListPool<T>是如何工作的?
我們已經使用了好久的泛型列表了.比如List<int>是一個儲存整數的列表.通過在ListPool中宣告型別後使用,表明它是一個泛型類。可以為泛型部分使用任何識別符號,但通常只使用T作為型別識別符號。
可以用棧來儲存列表的集合,通常不使用棧是因為Unity沒有為其序列化,不過在這個情況中沒有關係。
- Stack<List<T>>是什麼意思?
- 這是巢狀泛型型別,這意味著我們需要一個存放列表的棧,列表中的內容則取決於池的型別。
新增一個靜態公共方法去獲取池內的列表,如果棧不是空的,就彈出最上面的列表並返回這個列表,否則就建立一個新的列表。
為了能在實際情況中重複使用列表,需要在用完後再新增回池中,ListPool會負責清理列表,然後壓入棧中。
現在可以在HexMesh中使用列表池了,把靜態列表換為非靜態的私有引用,標記為NonSerialized,這樣Unity就不會在重編譯時儲存它們。寫作System.NonSerialized或者在指令碼的頭部新增using System都行。
當mesh在清除舊資料,新增新資料之前,就是從池中獲取列表的地方。
並且在資料應用之後就不再需要它們了,所以可以在這裡加回到列表池中。
這就保證了無論同時填充多少mesh資訊,列表都可以複用。
6.4碰撞可選
我們的地形mesh需要新增碰撞,但河流的mesh並不需要,射線會穿過河面擊中河道底部。所以新增一個布林型別的公共欄位useCollider,併為地形mesh開啟。
使用網格碰撞器
在指令碼里要做的就是確保只在碰撞功能開啟時建立碰撞器和賦值。
6.5頂點顏色可選
頂點顏色也同樣是可選的,我們需要不同的顏色來表示不同的地形型別,但是水的顏色是不用改變的,所以用和碰撞器一樣的方法把顏色也變為可選的。
地形當然是需要應用頂點顏色的,所以確保為開啟狀態。
使用頂點顏色
6.6 UV座標可選
到目前為止還能為UV座標新增可選功能,雖然地形不需要,但是水面會用到。
不使用UV座標
為了讓其能使用,建立為三角形和四邊形新增UV座標的方法。
建立一個額外的的AddQuadUV方法,新增一個矩形區域的UV座標,這是一個當四邊形與紋理圖是對齊時候的典型情況,一會河面也會用到這個方法。
7河流視覺效果
終於到了製作河流效果的時候了!這裡會用四邊形來代表河面,由於河水是會流動的,就用UV座標表示流動方向。為了讓其視覺化,建立一個新的標準著色器命名為River,修改它的UV座標放到紅綠反射通道里。
在HexGridChunk裡新增一個HexMesh型別的公共欄位rivers,跟地形一樣清理和應用資料。
就算沒有河流是不是也會產生額外的draw calls?
Unity很智慧,不會去繪製空的mesh,所以河流只會在能看到的時候繪製。
複製一份地形預製體的例項,重新命名為Rivers,並建立關聯。
地圖塊的預製體
建立一個河流的材質球,使用剛才新建的著色器,確保Rivers物件應用這個材質球。同時只勾選指令碼的use UV coordinates。
河流子物體
7.1水面的三角剖分
在三角化水面之前,首先要確定水面的高度。與河床的高度一樣,在HexMetrics裡去定義水面的高度偏移。因為Y方向的擾動設定的是高度等級的一半,所以我們也以此為水面高度的偏移,這確保了水面永遠不會在地形之上。
- 是不是有點低?
- 隨機的高度擾動實際上永遠也不會達到最大值,所以沒問題。當然只要你喜歡,也可以把水面設定的更低一些。
- 在HexCell裡新增一個屬性重新獲取河面的垂直座標。
現在可以在HexGridChunk裡構建河流,由於需要用到多個四邊形構建,為此建立一個專用的方法,給其傳遞四個頂點加上高度引數,這樣就能在新增四邊形之前方便的一次性為四個頂點設定垂直座標。
我們也能在這新增四邊形的UV座標,就簡單的從左到右,從上到下。
TriangulateWithRiver是第一個新增河流四邊形的方法,第一個四邊形位於單元格的中心和中間邊界線之間,第二個位於中間邊界線和單元格邊界之間。這裡就簡單的使用已經獲取的頂點作為引數,因為這些頂點較地形會低一些,頂點的位置會在傾斜的河道牆面的裡面,所以我們不用關心水面的邊界頂點是否精確吻合河道。
河流的第一個標誌
- 為什麼河面的寬度會變化?
- 這是因為單元格的高度被隨機擾動了,但是河床和河面的高度並沒有。單元格的高度越高,河道的牆面間距就越窄,這就使河面看起來變得狹窄了。
7.2順流而行
我們當前需要考慮的問題是UV座標是不是與河流的方向一致,先定義當看向下游方向時U座標值0位河流的左邊,1為右邊,並且V座標從0到1表示河流流向。
根據當面定義的規則,我們UV座標在三角化流向單元格外的河流時是正確的,對流向單元格內的則是錯誤並且剛好顛倒的。為了更方便的定義,新增一個布林型別的引數reversed到TriangulateRiverQuad裡,當需要顛倒UV座標時使用。
在TriangulateWithRiver裡,會在處理流入河流時顛倒UV的方向。
正確的流向
7.3河流的起點和終點
在TriangulateWithRiverBeginOrEnd裡,只需要檢測是否有流入的河流來確認河流的流向,這樣就能在中間邊界線和單元格邊界之間插入其他的四邊形。
單元格的中心和中間線之間的部分是一個三角形,所以我們不能使用TriangulateRiverQuad方法。唯一顯著的不同點是單元格中心點是位於河流中間的,所以它的U座標是二分之一。
起始點與終點的河流
- 結束的位置是否少了一些水面?
- 因為四邊形時由兩個三角形組成的,所以當四邊形不是平的時候,它們的形狀取決於方向。由於這個原因,河道兩邊的牆壁三角剖分是不對稱的。當水面與河道牆壁相交時,這一點尤為明顯。
- 可以通過映象四邊形來消除這種差異,但出現這種情況的原因明顯是因為暫時未應用頂點擾動,一旦這麼做對稱性就被打破了。
7.4單元格之間的河流部分
當要在單元格之間新增河流時,我們必須注意高度的差異。為了能讓水流下斜坡和懸崖,TriangulateRiverQuad需要應用兩個高度引數。
為方便使用新增一個保持相同高度的版本,就簡單的用同一個高度引數呼叫之前的方法。
現在也能在TriangulateConnection裡新增河流了,在單元格之間沒辦法知道在處理哪種型別的河流,為了確認UV座標的方向是否需要顛倒,需要檢測單元格是否有流出方向的河流和方向是否是指定方向。
河流完成
7.5拉伸V座標
目前V座標從0到1貫穿河流的每一段,單元格內是四段,再算上單元格之間的連線部分就是5段,那麼使用相同材質貼圖賦值給河流,它就會重複很多次。
我們可以拉伸V座標來減少重複性,讓從0到1表示單元格加上連線部分的所有河流。這可以通過每段河流之間的V座標遞增0.2實現。如果單元格中心時0.4,中心線位置就是0.6,到達邊界時就是0.8,連線部分就是1。
如果河流流向是相反方向,中心位置依然是0.4不變,但中間線位置就變成了0.2,邊界就是0,繼續算連線處的V座標就是-0.2。這沒有問題,因為這就等價於filter模式下的紋理設定重複時的0.8,就像0等價於1一樣。
流動的V座標
為實現這個功能,需要新增另一個引數到TriangelateRiverQuad裡。
當非顛倒UV的情況時,就用傳入的V座標設為底部,加上0.2以後的值設為頂部。
處理顛倒方向情況時則分別用0.8和0.6減去傳入值作為底部和頂部。
現在為處理流出河流提供正確的UV座標,首先在TriangulateWithRiver裡。
然後是TriangulateConnection裡。
最後在TriangulateWithRiverBeginOrEnd裡。
V座標拉伸
為了正確看到V座標的效果,確保其在著色器中保持正值。
迴圈變動的V座標
8河流動畫
處理好UV座標後,開始處理河流動畫。這部分會由著色器負責,所以不用連續更新mesh資訊表現動畫。
這篇教程不會教你如何建立一條有精細效果的河流,那些內容會放到後面。現在用一個簡單的視覺效果讓你瞭解動畫是如何工作的。
河流動畫會通過滑動基於執行時間的V座標實現,Unity中通過_Time在著色器中獲取這個變數,它的y分量中包含未修改的原始時間。我們就使用這個,其他的分量包含不同的時間縮放。
去掉V座標的修正,現在不再需要它了,相應地從V座標中減去當前時間,這將使座標向下滑動,從而產生河流向前流動的錯覺。
一秒鐘後,所有地方的V座標都會小於零,所以我們不會再看到差異。同樣,這是由於重複紋理過濾模式。但是為了看看到底發生了什麼,我們可以取V座標的小數部分。
V座標動畫
8.1使用噪聲紋理
現在河流能動起來了,但是方向和速度的過渡都很粗糙。我們的UV模式使這一點看起來非常明顯,但是當使用更像水的紋理時,就不會那麼容易發現了。因此我們去取樣一個紋理,而不是顯示原始的UV。可以就使用我們有的那張噪聲圖並對其取樣,把紋理的顏色乘上噪聲圖的第一個通道。
把噪聲圖賦值到河流的材質上,確保顏色是白色的。
使用噪聲紋理圖
因為V座標拉伸的太明顯,噪聲圖也沿著河流的方向被拉伸了,這個效果並不好看。通過縮小U座標的比例來從另一個方向拉伸,十六分之一應該是個合適的值。這意味著只在噪聲圖的一條窄帶上進行取樣。
拉伸U座標
在把河流的流速減緩到四分之一,這樣紋理完成一個迴圈就需要4秒。
流動的噪聲紋理
8.2混合噪聲
已經看起來好多了,但是河流的樣子看起來一直都是一個樣,真正的水流看起來可不像這樣。
由於只使用了紋理圖上的一條窄帶,可以滑動這個窄帶的位置來改變樣式。可以通過時間滑動U座標實現,但是要確保其變化緩慢,否則河流看起來就像是往邊上在流動。先設定這個縮放因子為0.005,這意味著200秒紋理才能完成一個迴圈。
向一側滑動的噪聲紋理
不幸的是看起來不怎麼樣,即使滑動很慢,側向移動也很明顯,並且水面的樣式看起來仍然是靜止的。可以通過組合兩個噪聲取樣來隱藏滑動,這兩個樣本都往相反的方向滑動,如果使用稍微不同的值來移動第二個樣本,它將產生一個微妙的變形動畫。
為了確保不會重疊一樣的噪聲,為第二個樣本使用噪聲圖的另一個通道。
合併兩個噪聲紋理的滑動模式
8.3半透明水面效果
水面的紋理效果已經足夠了,下一步是讓其變得半透明。
首先,確保水面不會投射陰影,設定河流mesh預製上渲染器的陰影投射為關閉狀態。
不再投射陰影
下一步把著色器改為透明模式,可以使用著色器的標籤指出,接著新增alpha關鍵字到#pragma suface這一行。然後由於不再需要陰影投射,刪除fullforwardshadows關鍵字。
現在要改變河流了顏色,與其用顏色乘上噪聲取樣的值,不如直接加行去,然後使用saturate限制取值範圍不超過1。
這就能使用材質顏色作為底色,噪聲取樣的值會增加亮度和不透明程度,試著使用藍色為底色和一個較低的透明度。其結果就是藍色的半透明水面加上白色亮點的效果。
帶有顏色並且半透明的水流
9調整優化
現在所有的東西看起來都工作正常,是時候重新啟用頂點擾動了,單元格的邊界變形也會使河流的形狀不規則。
頂點擾動與不擾動的對比
檢查一下地形,看看頂點擾動是不是會引起問題。事實證明確實會有問題,注意這些較高的瀑布。
河流被懸崖截斷
從高處落下的水會消失在懸崖的後面,當這種情況發生時是很明顯的,所以我們得做點什麼。
不太明顯的是瀑布是傾斜而不是直線下降的,雖然水不是這麼運動的,但也不會明顯感覺違和,所以忽略這個。
防止水消失的最簡單方法是使河道更深,在水面和河床之間創造更多空間,但這也使河道牆壁更偏向垂直,所以我們不修改太多。設定MexMetrics裡的streamBedElevationOffset到-1.75,這就能解決大部分問題,並且河道不會顯得太深。一些水面仍然會被截斷,但不會出現整段都隱藏在懸崖後面的情況了。
更深一些的河道
本期工程地址tank1018702/Hex-Map-Learning
有想系統學習遊戲開發的童鞋,歡迎訪問http://levelpp.com/。
下一篇教程是Roads。
作者:沈琰
專欄地址:https://zhuanlan.zhihu.com/p/57924967
相關文章
- HexMap學習筆記(一)——建立六邊形網格筆記
- HexMap學習筆記(七)——道路筆記
- HexMap學習筆記(八)——水體筆記
- HexMap學習筆記(九)——地形特徵筆記特徵
- HexMap學習筆記(四)——不規則化筆記
- HexMap學習筆記(五)——更大的地圖筆記地圖
- HexMap學習筆記(二)——單元格顏色混合筆記
- vue學習筆記(六) ----- vue元件Vue筆記元件
- python學習筆記(六)——函式Python筆記函式
- springcloud學習筆記(六)Spring Cloud ZuulSpringGCCloud筆記Zuul
- Vue學習筆記(六) 長樂未央Vue筆記
- ES6學習筆記(六)【promise,Generator】筆記Promise
- Vue學習筆記(六):監視屬性Vue筆記
- 計網學習筆記六 Network Layer Overview筆記View
- hive學習筆記之六:HiveQL基礎Hive筆記
- SAP ME學習筆記(六)搭建生產線筆記
- vue 3 學習筆記 (六)——watch 、watchEffect 新用法Vue筆記
- ES[7.6.x]學習筆記(六)分析器筆記
- orientDB學習筆記(一)六度分隔理論筆記
- async-validator 原始碼學習筆記(六):validate 方法原始碼筆記
- Redis學習筆記六:持久化實驗(AOF,RDB)Redis筆記持久化
- Java IO學習筆記六:NIO到多路複用Java筆記
- numpy的學習筆記\pandas學習筆記筆記
- Vue學習計劃基礎筆記(六) – 元件基礎Vue筆記元件
- main 函式解析(二)—— Linux-0.11 學習筆記(六)AI函式Linux筆記
- 學習筆記筆記
- 小碼哥iOS學習筆記第六天: initialize方法iOS筆記
- Webpack4 學習筆記六 多頁面配置和devtoolWeb筆記dev
- JUC併發程式設計學習筆記(六)Callable(簡單)程式設計筆記
- Kubernetes學習筆記(六):使用ConfigMap和Secret配置應用程式筆記
- Java多執行緒學習筆記(六) 長樂未央篇Java執行緒筆記
- Redis 學習筆記(六)Redis 如何實現訊息佇列Redis筆記佇列
- 爬蟲學習日記(六)爬蟲
- AngularJS學習日記(六)ChartsAngularJS
- 【學習筆記】數學筆記
- 《JAVA學習指南》學習筆記Java筆記
- 機器學習學習筆記機器學習筆記
- 學習筆記-粉筆980筆記