HexMap學(xué)習(xí)筆記(八)——水體

作者:沈琰
這篇教程的難度還要大于第六篇,同時也是目前為止篇幅最長的。難度主要體現(xiàn)在shader上,像魔術(shù)一般沒用幾行代碼就實現(xiàn)了一個不錯的水面效果。同時對河流與水體的效果混合做了相當細致的拆分,這可能需要一定的數(shù)學(xué)功底和對shader足夠熟悉才能完全理解。
本期原文地址:https://catlikecoding.com/unity/tutorials/hex-map/part-8/
此教程為HexMap系列的第八篇,之前已經(jīng)完成了編輯河流的功能,現(xiàn)在繼續(xù)添加編輯完全被水淹沒,處于水下的單元格的功能。

1 水位的高度
最直接的在地圖中加入水體的方法是定義一個統(tǒng)一的水位高度,所有單元格自身的高度一旦低于水位高度那么它就是處于水下的單元格。但是如果能定義不同的水位高度會更靈活一些(注:比如一張地圖中既能表現(xiàn)山頂?shù)暮?,又能表現(xiàn)大陸外的海面效果),所以這里還是讓水的高度可變,這就需要每個單元格來記錄自己的水位高度。

如果要這么做,就需要強制使一些其他地形特性不能出現(xiàn)在水下。但在這個時間點先暫時不考慮這個問題,就好似水下的道路也是合理的,它代表這個單元格不久前才沉沒到水下。
1.1 水下的單元格
現(xiàn)在定義了水位高度,那么最重要的信息就是這個單元格是否在水下。當一個單元格的海拔高度低于其水位高度時,那么這個單元格就是被淹沒的。添加一個屬性存儲這個信息。

這么寫就意味著當單元格的海拔高度與水位高度相等時,單元格的實際高度是在水面之上的。所以實際的水位高度會低于單元格的高度,就像河水的水面一樣。所以我們對這兩者使用相同的高度偏移量,修改HexMetrics.riverSurfaceElevationOffset的變量名讓其更通用一些。

修改HexCell.RiverSurfaceY這個屬性,讓其使用新的名字,并為水下的單元格添加類似的屬性。


把編輯水位高度的UI添加到UI上,復(fù)制高度編輯的滑動條并修改名字,當然也別忘了修改正確的關(guān)聯(lián)方法。

2 水面的三角剖分
我們需要一個新的mesh負責(zé)水面的三角化,并使用新的材質(zhì)球。第一步,通過復(fù)制River的著色器創(chuàng)建一個新的名為Water的著色器,并修改一下讓其只應(yīng)用顏色屬性。

復(fù)制河流的材質(zhì)球來創(chuàng)建一個新材質(zhì)球,更換上面的著色器。原本的噪聲紋理可以留著,一會就會用到它。

通過復(fù)制預(yù)制體的Rivers子對象為其添加一個新的子對象Water,它應(yīng)該使用鋼線新建的Water材質(zhì)球且不需要勾選使用UV坐標。像往常一樣,新建預(yù)制體的實例并更改,應(yīng)用更改后刪除實例。(注:unity2018版本已經(jīng)可以直接編輯預(yù)制體了)


下一步,在HexGridChunk中添加對water功能的支持。

并確保關(guān)聯(lián)的是正確的子對象預(yù)制體。

2.1 水面三角形
由于水體相當于是形成了單元格的第二層mesh,就讓我們在每個方向上給其自己的三角化方法,只需要在單元格實際處于水下時調(diào)用它。

就像河流一樣,相同水位之間的單元格,其高度是保持一致的。所以我們不需要復(fù)雜的邊緣,每個方向上一個簡單的三角形就足夠了。


2.2 水面連接
我們可以用一個簡單的四邊形連接相鄰單元格的水面。


并用簡單的三角形填充角落上的連接。


現(xiàn)在水體單元格就完成了,它們在相鄰的時候就會自動連接起來。不過現(xiàn)在它們在更高的高度與單元格的干燥部分留下了一個間隙,稍后來處理這個問題。
2.3 統(tǒng)一的水位高度
在我們假設(shè)相鄰的水體單元格有相同高度時,水面的三角化是正確的,一旦違反這個假設(shè)就出錯了。

我們可以試著強制讓水位高度等級相同,例如當編輯單元格的水位時,把這個變化傳播到相鄰單元格中,使水位保持同步。然而這個過程必須持續(xù)下去直到遇到高于水位的單元格,而這些被影響的單元格就決定了水的范圍。
這個方法的危險之處就在于它很快會失控,極端情況下水面會淹沒整張地圖。當所有地圖都在同時三角化時就會由于性能峰值產(chǎn)生延遲。
所以我們現(xiàn)在先不這么做,這可以作為編輯器的一個進階功能,而現(xiàn)在就需要用戶確保相鄰水位高度的統(tǒng)一。
3 水面動畫效果
讓我們做一點像是波浪的效果來代替水面的固定顏色。就像之前在其他著色器中做的一樣,并不是要做一個多么逼真的效果,僅僅簡單表示下這是波浪就夠了。


3.1 雙向效果
現(xiàn)在一點也不像波浪的效果。添加第二個噪聲紋理的采樣讓其變得更復(fù)雜一些,這一次把U坐標與時間相加。兩種噪聲采樣要使用噪聲紋理圖的不同通道,這樣就是兩個不同的噪聲模式。最后waves的值就是兩種采樣值之和。

對兩個采樣值的求和的結(jié)果取值范圍在0-2之間,所以我們把結(jié)果的取值范圍縮放到0-1之間。這里可以使用smoothstep方法得到更為有趣的結(jié)果而不是僅僅將其縮放一半。這里會把1.5-2之間的結(jié)果投影到0-1之間,所以水面的一部分沒有可見的波浪。


3.2 混合波
即使用了些額外方法,兩個噪聲的樣式?jīng)]有改變這一點依然明顯,如果噪聲的樣式能發(fā)生改變效果將會好得多。我們可以通過插值計算兩個不同通道的噪聲采樣來做到,但是這個插值不能平均計算,否則水面就會很明顯的的在同一時間變化樣式,所以用混合一條波來代替。
我們通過創(chuàng)建一條斜對角穿過水面的正弦波來生成一條混合波,把世界坐標的X和Z值相加作為sin函數(shù)的輸入并縮放這個和到合適的大小,然后加上相同的時間時間值使其動起來。



4 沿岸水域
中間的開放水域已經(jīng)完成,接下來就是填補沿岸水域的缺口。由于濱水區(qū)域需要與陸地的輪廓相吻合,所以這部分的三角剖分需要一個額外的方法。所以把TriangulateWater分成兩個方法,一個用于中間的開放水域,一個用于沿岸水域。如果當前方向的相鄰單元格存在且不處于水下,那就需要處理沿岸水域了。


由于沿岸陸地的頂點是被擾動過的,所以當前方向沿岸的水域的三角形頂點也需要微擾。所以加上一組邊緣連接頂點把原本的單個三角形分割為多個三角扇。


下一步是邊緣連接帶的三角剖分,這里就不需要對當前方向進行判斷了,因為只會對沿岸連接帶三角化時才會調(diào)用TriangulateWaterShore,也就是說方向已在調(diào)用之前就做過判斷了。


同樣的,每次還要添加角落上的三角形。


現(xiàn)在沿岸水域就完成了,它的一部分總是埋在地形mesh之下,所以不會有間隙。
4.1 沿岸水域的UV
其實可以到此為止了,但是如果沿岸的水域如果能有額外的視覺效果會顯得更有意思一些。比如離岸邊越近就越明顯的泡沫效果。要實現(xiàn)這個功能,那著色器就得知道沿海水域的位置離岸邊有多遠,可以通過UV坐標提供這些信息。
開放水域中并沒有使用UV坐標,并且它也不需要泡沫的效果,只有沿岸的水域需要,所以這兩種水體的mesh的需求是非常不同的。給予每種類型它自己的mesh是很有必要的,所以在HexGridChunk中添加另一個mesh對象。

復(fù)制水體的對象,在預(yù)制體中建立關(guān)聯(lián)并勾選use UV coordinate,同樣為其復(fù)制水體的材質(zhì)球和著色器。



4.2 沿岸水域的泡沫效果
現(xiàn)在沿岸水域的三角剖分沒問題,可以在此基礎(chǔ)上添加一些泡沫的效果,最簡單直接的方式是把shore的值添加到統(tǒng)一的顏色上。

要使泡沫效果在靠近海岸時顯得更大,可以在使用shore的值之前取其平方根。

當然也要讓波動與扭曲都動起來。


除了正向移動的泡沫,應(yīng)該還有逆向移動的泡沫。讓我們添加第二個正弦波來模擬逆向移動,讓其幅度稍弱一些并給一點時間上的偏移,最后的foam值應(yīng)該是兩條正弦函數(shù)的最大值。


4.3 混合波浪與泡沫的效果
現(xiàn)在中間的開放水域到沿岸水域的過渡效果很生硬,因為這兩個效果的邏輯不一樣,要修正這一點就需要在WaterShore的著色器中也能使用waves的代碼。
我們不用把waves部分的代碼全復(fù)制到WaterShore中,只需要在其中加入Water.cginc的CG文件引用,實際上就是新建一個.cginc后綴的包涵文件,然后將waves和foam的代碼都放到里面當做函數(shù)使用。
著色器的包涵文件是如何工作的?
創(chuàng)建自己的著色器包涵文件的內(nèi)容在:https://catlikecoding.com/unity/tutorials/rendering/part-5/?這篇教程中有提及。
(注:Unity中沒有直接創(chuàng)建cginc格式文件的選項,需要你在文件瀏覽器中創(chuàng)建一個txt文件再把后綴名改成cginc。有可能會提示文件可能不可用,但絕對是能用的。然后將之前shader文件中的計算代碼移到這個cginc文件中,像c++一樣在shader中添加頭文件引用就可以了)

修改Water的著色器,使其應(yīng)用新的cginc文件引用。

然后在WaterShore著色器中計算foam和waves的值,然后讓開放水域的波浪按沿岸水域的泡沫波動頻率來移動,最后的結(jié)果應(yīng)該是foam和waves的最大值。


5 更大的沿岸水域范圍
一部分的沿岸水域的mesh會隱藏在地形mesh之下,如果只有一小部分當然沒什么問題。但不幸的是較為陡峭懸崖下的水里,絕大部分的沿岸水域連帶泡沫效果都看不見了。

這個問題可以通過增加沿岸水域的尺寸,減少中間的固定六邊形的半徑來實現(xiàn)。這樣HexMetrics里就需要一個表示水體mesh內(nèi)六邊形的比例因子,并添加方法去獲取水體的角頂點。
地形的比例因子是0.8,如果想要把水體邊緣連接帶的范圍擴大一倍,水體的比例因子就要設(shè)置為0.6。


可以看到六邊形之間的距離已經(jīng)翻倍,現(xiàn)在HexMetrics里要定義新的獲取邊緣連接橋頂點的方法。


5.1 水體與固定六邊形邊緣之間的處理
雖然這給了泡沫效果更大的空間,但是也有更多的部分被隱藏到地形mesh之下。理想情況下我們可以在水體的一邊使用水體的邊緣連接方式,而在陸地的一邊使用固定六邊形邊緣的連接方式。
當我們從水體mesh的角頂點開始計算時,不能簡單的使用連接橋來獲取相反方向的固定內(nèi)六邊形的邊緣頂點(注:水體的內(nèi)六邊形比例因子是0.6,而地形是0.8,直接用水體連接角頂點計算出的邊緣頂點會比岸邊的邊緣更靠外一些)。相反可以從相鄰單元格的中心點往回算。修改TriangulateWaterShore方法使其用這種新方法。


這看起來好多了,只不過當大部分泡沫都可見的情況下泡沫會很明顯。為補償這些修改,可以把著色器中的shore值稍微調(diào)小一點。


6 水下的河流
現(xiàn)在水體的效果完成了,至少是沒有河流流入的情況下完成了。由于現(xiàn)在水體與河流的編輯邏輯互相不關(guān)聯(lián),所以在這種情況下河流會從水下流過。

6.1 隱藏處于水下的河流
雖然河床處于水下沒什么問題,河水確實也會流向水底,但是我們不應(yīng)該能在水底看到河流的效果,特別是它還沒有渲染在實際的水面上。關(guān)于這個問題可以通過只在非水下的單元格才繪制河段河段三角形來解決。


6.2 瀑布
現(xiàn)在水底河流的問題解決了,但是現(xiàn)在河流與水體之間產(chǎn)生了間隙。當河流所在單元格的高度與水位相同時產(chǎn)生的間隙較小或者直接覆蓋了。但從一個高度差異較大的單元格流向水面時就會產(chǎn)生較大的間隙而不是瀑布的效果,先解決這個問題。
瀑布的河段會穿過水面,它的一部分在水面上,一部分在下。我們需要保留水面上的部分而舍棄下面的,這需要費些功夫,為此創(chuàng)建一個專用的方法。
這個新方法需要四個頂點,兩個河流高度的,兩個水位高度的。把它們對齊后就能看到沿著瀑布向下的河流流向,所以前兩個左右兩邊的頂點在頂部,其余的在底部。

在當前單元格處于水下而相鄰單元格不是時,還要處理方向相反的瀑布。

這就又產(chǎn)生了水下河流的問題。接下來修改TriangulateWaterfallInWater方法,把底部的兩個頂點拉到水面上。但是僅修改Y坐標是不夠的,這相當于把河段斜著拉到水面上,在懸崖位置上會形成裂縫。必須通過插值計算頂部和底部頂點來把底部沿著連線方向拉向頂部。

得到的結(jié)果就是方向相同但更短一些的瀑布。但是由于底部頂點現(xiàn)在的位置發(fā)生了變化,它們受到的頂點擾動與之前不同,這意味的最終結(jié)果仍然會不匹配。為解決這個問題必須在插值之前手動對頂點進行擾動,然后添加一個不應(yīng)用頂點擾動的四邊形。

7 河口
當河流與水面高度相同時,河流的mesh會與沿岸水域的mesh相接。如果是河水流入海里的情況,那這里就是河流與海岸邊潮汐相遇的位置,這樣的地方稱之為河口。

河口目前有兩個問題。第一個問題,河流四邊形連接著水面邊緣的第二個和第四個頂點,跳過了第三個頂點。由于沿岸水域沒有使用這第三個頂點,最終會形成一個間隙或者直接就覆蓋了。這個問題可以通過修改河口的幾何結(jié)構(gòu)來解決。
第二個問題是沿岸的泡沫效果到河水流動效果之間的過渡十分生硬,要解決這個問題,還需要另外一個材質(zhì)球去混合這兩個效果。
這意味著河口需要特殊處理,所以為此創(chuàng)建一個專用的方法。在河流穿過當前方向時在TriangulateWaterShore里去調(diào)用。

混合效果的區(qū)域不用去填充整個邊緣連接帶,使用一個梯形就足夠 。所以我們可以在這里先添加兩個沿岸區(qū)域的三角形。

7.1 UV2坐標
河流的效果是使用的UV動畫,沿岸水域的泡沫效果也是使用的UV動畫。所以要混合彼此的效果,最終要設(shè)置兩組UV坐標。還好Unity中的Mesh最多可以支持四組UV坐標的設(shè)置,而現(xiàn)在只需要在HexMesh中添加第二個UV坐標。

通過復(fù)制并修改之前的UV設(shè)置方法添加第二組UV的設(shè)置方法。

7.2 河流著色器方法
因為兩個著色器中都要使用河流特效,所以把River著色器中的代碼整合成一個新方法放到water的包涵文件中,以便兩者都能調(diào)用到。

7.3 單獨的河口mesh物體
在HexGridChunk中添加河口mesh的字段。

通過復(fù)制并修改沿岸水域?qū)ο?,?chuàng)建河口的對象著色器和材質(zhì),并拖入地圖塊預(yù)制體中。確保其同時勾選了UV1和UV2。

7.4 河口的三角剖分
在河流結(jié)束位置到水體mesh之間添加一個三角形解決重疊或者是間隙的問題。由于河口的著色器是從沿岸水域復(fù)制過來的,設(shè)置其UV坐標與泡沫特效吻合。



7.5 河口的流動效果
要同時顯示河流的效果就需要用到UV2坐標。中間三角形的底部頂點位于河流的正中間,所以它的U坐標就應(yīng)該是0.5。由于河流是朝向水體移動的,所以河口左邊的U坐標應(yīng)該是1,河口右邊的U坐標則是0。設(shè)置第二組UV坐標為0-1使其能吻合河流的流向。

三角形兩邊的四邊形都應(yīng)該往這個方向匹配,對于河流寬度以外的點保持相同的U坐標。


看起來沒問題,就以此為基礎(chǔ)添加河流特效。


之前定義的單元格之間河流V坐標的范圍是0.8-1,所以現(xiàn)在河口這部分也應(yīng)該沿用這個范圍而不是設(shè)置成0-1。然后由于水體的內(nèi)六邊形的比例因子比陸地小,所以沿岸河流的連接部分比陸地單元格之間的連接部分要大50%,把這個算進去設(shè)置V坐標的范圍就應(yīng)該是0.8-1.1。


7.6 河流效果的修正
目前河口的河流效果依然是筆直的,但實際情況是當河流流入較大區(qū)域時應(yīng)該向外擴展,此時流向應(yīng)該是彎曲的,我們可以通過彎曲UV2的坐標來模擬這個過程。
與其讓U坐標的在河面寬度外的坐標保持恒定,不如將其移動0.5的距離。這樣一來最左邊就變成了1.5,最右邊變成了-0.5。
同時改變底部U坐標的位置放寬河道,把左邊的點由1改為0.7,右邊的點從0改為0.3。


要完成這個曲線的特效,在4個同樣的點上修改V坐標。由于河流的流向總時朝向河流的出口,把頂部的V坐標增加到1。為了讓曲線的效果更好看一些,底部的兩個V坐標增加到1.15。


7.7 混合河流與泡沫效果
最后剩下的工作是混合沿岸水域發(fā)泡沫與河流的效果,這里會使用shore的值進行線性插值來實現(xiàn)。


插值混合起效了,除了頂部左右的頂點之外。河流效果在這些位置不應(yīng)該存在,所以這里不能使用shore的值,而必須使用在這兩個頂點UV為0的值。還好還有第一組UV坐標能用,把值存儲在這里。

現(xiàn)在的河口效果包含了延展變寬的河流,岸邊的水和泡沫效果。雖然不是精確與瀑布效果匹配,但這種效果與瀑布結(jié)合在一起看也還不錯。

8 從水體向外流動的河流
盡管現(xiàn)在河流可以流入水體之中,但并不支持從水體中向外流動。但是現(xiàn)實世界中的湖泊有河流向外流出是很常見的,所以我們補充這部分。
在我們的地圖中河流從水體向外流動其實是流向了更高的高度,我們之前定義了這種情況下河流是無效的,因此要創(chuàng)建一個特殊方法,當水位高度與目標單元格高度相同時,允許河流這么流動。在HexCell中添加一個私有方法,該方法使用新的方式檢測河流是否有效。



8.1 水域之間的逆流

由于每種情況下的UV2坐標差異較大,所以為此使用單獨的代碼賦值。


OK,這篇文章就到這里。
下一篇教程為:https://catlikecoding.com/unity/tutorials/hex-map/part-9/
有想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問:http://levelpp.com/
另有專業(yè)開發(fā)交(gao)流(ji)群等待大家強勢插入:869551769