前言
这篇教程的难度还要大于第六篇,同时也是目前为止篇幅最长的。难度主要体现在shader上,像魔术一般没用几行代码就实现了一个不错的水面效果。同时对河流与水体的效果混合做了相当细致的拆分,这可能需要一定的数学功底和对shader足够熟悉才能完全理解。
本期原文地址:HexMap 8
此教程为HexMap系列的第八篇,之前已经完成了编辑河流的功能,现在继续添加编辑完全被水淹没,处于水下的单元格的功能。
水位的变化
1水位的高度
最直接的在地图中加入水体的方法是定义一个统一的水位高度,所有单元格自身的高度一旦低于水位高度那么它就是处于水下的单元格。但是如果能定义不同的水位高度会更灵活一些(注:比如一张地图中既能表现山顶的湖泊,又能表现大陆外的海面效果),所以这里还是让水的高度可变,这就需要每个单元格来记录自己的水位高度。
如果要这么做,就需要强制使一些其他地形特性不能出现在水下。但在这个时间点先暂时不考虑这个问题,就好似水下的道路也是合理的,它代表这个单元格不久前才沉没到水下。
1.1水下的单元格
现在定义了水位高度,那么最重要的信息就是这个单元格是否在水下。当一个单元格的海拔高度低于其水位高度时,那么这个单元格就是被淹没的。添加一个属性存储这个信息。
这么写就意味着当单元格的海拔高度与水位高度相等时,单元格的实际高度是在水面之上的。所以实际的水位高度会低于单元格的高度,就像河水的水面一样。所以我们对这两者使用相同的高度偏移量,修改HexMetrics.riverSurfaceElevationOffset的变量名让其更通用一些。
修改HexCell.RiverSurfaceY这个属性,让其使用新的名字,并为水下的单元格添加类似的属性。
1.2水体编辑
对水位高度的编辑方法与编辑单元格的高度差不多,所以在HexMapEditor中也要记录当前的水位高度,并应用在单元格上。
新建方法并与UI相连。
并将其放入EditCell方法。
把编辑水位高度的UI添加到UI上,复制高度编辑的滑动条并修改名字,当然也别忘了修改正确的关联方法。
控制水位的滑动条
2水面的三角剖分
我们需要一个新的mesh负责水面的三角化,并使用新的材质球。第一步,通过复制River的着色器创建一个新的名为Water的着色器,并修改一下让其只应用颜色属性。
复制河流的材质球来创建一个新材质球,更换上面的着色器。原本的噪声纹理可以留着,一会就会用到它。
水体材质
通过复制预制体的Rivers子对象为其添加一个新的子对象Water,它应该使用钢线新建的Water材质球且不需要勾选使用UV坐标。像往常一样,新建预制体的实例并更改,应用更改后删除实例。(注:unity2018版本已经可以直接编辑预制体了)
下一步,在HexGridChunk中添加对water功能的支持。
并确保关联的是正确的子对象预制体。
水体对象关联
2.1水面三角形
由于水体相当于是形成了单元格的第二层mesh,就让我们在每个方向上给其自己的三角化方法,只需要在单元格实际处于水下时调用它。
就像河流一样,相同水位之间的单元格,其高度是保持一致的。所以我们不需要复杂的边缘,每个方向上一个简单的三角形就足够了。
水体多边形
2.2水面连接
我们可以用一个简单的四边形连接相邻单元格的水面。
边缘连接
并用简单的三角形填充角落上的连接。
角落连接
现在水体单元格就完成了,它们在相邻的时候就会自动连接起来。不过现在它们在更高的高度与单元格的干燥部分留下了一个间隙,稍后来处理这个问题。
2.3统一的水位高度
在我们假设相邻的水体单元格有相同高度时,水面的三角化是正确的,一旦违反这个假设就出错了。
不同的水位高度
我们可以试着强制让水位高度等级相同,例如当编辑单元格的水位时,把这个变化传播到相邻单元格中,使水位保持同步。然而这个过程必须持续下去直到遇到高于水位的单元格,而这些被影响的单元格就决定了水的范围。
这个方法的危险之处就在于它很快会失控,极端情况下水面会淹没整张地图。当所有地图都在同时三角化时就会由于性能峰值产生延迟。
所以我们现在先不这么做,这可以作为编辑器的一个进阶功能,而现在就需要用户确保相邻水位高度的统一。
3水面动画效果
让我们做一点像是波浪的效果来代替水面的固定颜色。就像之前在其他着色器中做的一样,并不是要做一个多么逼真的效果,仅仅简单表示下这是波浪就够了。
绝对水平的水面
做一些与河流动画相似的事,用世界坐标在噪声纹理上采样并与固定颜色相加,然后把时间与V坐标相加产生动画效果。
10倍速水流滚动
3.1双向效果
现在一点也不像波浪的效果。添加第二个噪声纹理的采样让其变得更复杂一些,这一次把U坐标与时间相加。两种噪声采样要使用噪声纹理图的不同通道,这样就是两个不同的噪声模式。最后waves的值就是两种采样值之和。
对两个采样值的求和的结果取值范围在0-2之间,所以我们把结果的取值范围缩放到0-1之间。这里可以使用smoothstep方法得到更为有趣的结果而不是仅仅将其缩放一半。这里会把1.5-2之间的结果投影到0-1之间,所以水面的一部分没有可见的波浪。
双向滚动10倍速
3.2混合波
即使用了些额外方法,两个噪声的样式没有改变这一点依然明显,如果噪声的样式能发生改变效果将会好得多。我们可以通过插值计算两个不同通道的噪声采样来做到,但是这个插值不能平均计算,否则水面就会很明显的的在同一时间变化样式,所以用混合一条波来代替。
我们通过创建一条斜对角穿过水面的正弦波来生成一条混合波,把世界坐标的X和Z值相加作为sin函数的输入并缩放这个和到合适的大小,然后加上相同的时间时间值使其动起来。
正弦波的浮动范围在-1到1之间,但我们需要的是一个0-1之间的值,通过把waves的值平方把值限制在0-1内。为了能单独看到结果,把输入值从颜色为这个值。
混合波浪
为了让波的效果不那么明显,为所有的值添加一些噪声扰动。
混合波浪噪声扰动
最后,对于我们的两个噪声采样,使用混合波在两个通道之间进行插值。使用噪声纹理的四个不同的通道,最大限度地增加噪声的样式。
2倍速混合波浪效果
4沿岸水域
中间的开放水域已经完成,接下来就是填补沿岸水域的缺口。由于滨水区域需要与陆地的轮廓相吻合,所以这部分的三角剖分需要一个额外的方法。所以把TriangulateWater分成两个方法,一个用于中间的开放水域,一个用于沿岸水域。如果当前方向的相邻单元格存在且不处于水下,那就需要处理沿岸水域了。
沿岸水域不再三角化了
由于沿岸陆地的顶点是被扰动过的,所以当前方向沿岸的水域的三角形顶点也需要微扰。所以加上一组边缘连接顶点把原本的单个三角形分割为多个三角扇。
由三角扇组成的沿岸水域与陆地的连接部分
下一步是边缘连接带的三角剖分,这里就不需要对当前方向进行判断了,因为只会对沿岸连接带三角化时才会调用TriangulateWaterShore,也就是说方向已在调用之前就做过判断了。
沿岸水域边缘连接
同样的,每次还要添加角落上的三角形。
沿岸水域的角落连接
现在沿岸水域就完成了,它的一部分总是埋在地形mesh之下,所以不会有间隙。
4.1沿岸水域的UV
其实可以到此为止了,但是如果沿岸的水域如果能有额外的视觉效果会显得更有意思一些。比如离岸边越近就越明显的泡沫效果。要实现这个功能,那着色器就得知道沿海水域的位置离岸边有多远,可以通过UV坐标提供这些信息。
开放水域中并没有使用UV坐标,并且它也不需要泡沫的效果,只有沿岸的水域需要,所以这两种水体的mesh的需求是非常不同的。给予每种类型它自己的mesh是很有必要的,所以在HexGridChunk中添加另一个mesh对象。
TriangulateWaterShore里会使用这个新的mesh。
复制水体的对象,在预制体中建立关联并勾选use UV coordinate,同样为其复制水体的材质球和着色器。
沿岸水域的对象和UV
修改沿岸水体的着色器,令其显示UV坐标。
由于UV坐标还没有设置,现在显示的是固定颜色,现在比较容易辨识沿岸水域确实是用了单独的mesh和材质球。
沿岸水域的mesh与水体本身分开
把离岸边的距离信息放到V坐标里,设置0为考经水域这一边,而1为靠近陆地这一边。由于不需要再表示其他的什么信息,所有的U坐标可以直接设置为0。
不正确的过渡
上边的代码在边缘连接的位置没问题,但是在角落三角形的V坐标有问题。如果下一个方向上的相邻单元格位于水下,现在的写法就是正确的,如果不在水下,那么三角形的第三个顶点就在陆地的下面。
正确的过渡
4.2沿岸水域的泡沫效果
现在沿岸水域的三角剖分没问题,可以在此基础上添加一些泡沫的效果,最简单直接的方式是把shore的值添加到统一的颜色上。
线性的foam值
为了让其看起来更有意思一些,把正弦波取值平方后乘入shore的值。
正弦平方值衰减的foam值
要使泡沫效果在靠近海岸时显得更大,可以在使用shore的值之前取其平方根。
越靠近海岸foam的值越大
添加一点扭曲使其更自然一些,让靠近岸边的扭曲变得越来越弱,这样就能很好的与沿岸的轮廓吻合。
扰动后的泡沫效果
当然也要让波动与扭曲都动起来。
泡沫的动画效果
除了正向移动的泡沫,应该还有逆向移动的泡沫。让我们添加第二个正弦波来模拟逆向移动,让其幅度稍弱一些并给一点时间上的偏移,最后的foam值应该是两条正弦函数的最大值。
前进与后退的泡沫效果混合
4.3混合波浪与泡沫的效果
现在中间的开放水域到沿岸水域的过渡效果很生硬,因为这两个效果的逻辑不一样,要修正这一点就需要在WaterShore的着色器中也能使用waves的代码。
我们不用把waves部分的代码全复制到WaterShore中,只需要在其中加入Water.cginc的CG文件引用,实际上就是新建一个.cginc后缀的包涵文件,然后将waves和foam的代码都放到里面当做函数使用。
着色器的包涵文件是如何工作的?
创建自己的着色器包涵文件的内容在Rendering 5,Multiple Lights这篇教程中有提及。
(注:Unity中没有直接创建cginc格式文件的选项,需要你在文件浏览器中创建一个txt文件再把后缀名改成cginc。有可能会提示文件可能不可用,但绝对是能用的。然后将之前shader文件中的计算代码移到这个cginc文件中,像c++一样在shader中添加头文件引用就可以了)
修改Water的着色器,使其应用新的cginc文件引用。
然后在WaterShore着色器中计算foam和waves的值,然后让开放水域的波浪按沿岸水域的泡沫波动频率来移动,最后的结果应该是foam和waves的最大值。
波浪与泡沫效果的混合
5更大的沿岸水域范围
一部分的沿岸水域的mesh会隐藏在地形mesh之下,如果只有一小部分当然没什么问题。但不幸的是较为陡峭悬崖下的水里,绝大部分的沿岸水域连带泡沫效果都看不见了。
沿岸水域几乎看不见了
这个问题可以通过增加沿岸水域的尺寸,减少中间的固定六边形的半径来实现。这样HexMetrics里就需要一个表示水体mesh内六边形的比例因子,并添加方法去获取水体的角顶点。
地形的比例因子是0.8,如果想要把水体边缘连接带的范围扩大一倍,水体的比例因子就要设置为0.6。
使用这些新方法在HexGridChunk里去获取水体mesh的角顶点。
使用水体专用的比例
可以看到六边形之间的距离已经翻倍,现在HexMetrics里要定义新的获取边缘连接桥顶点的方法。
在HexGirdChunk里修改并使用新方法。
水体六边形更长的连接桥部分
5.1水体与固定六边形边缘之间的处理
虽然这给了泡沫效果更大的空间,但是也有更多的部分被隐藏到地形mesh之下。理想情况下我们可以在水体的一边使用水体的边缘连接方式,而在陆地的一边使用固定六边形边缘的连接方式。
当我们从水体mesh的角顶点开始计算时,不能简单的使用连接桥来获取相反方向的固定内六边形的边缘顶点(注:水体的内六边形比例因子是0.6,而地形是0.8,直接用水体连接角顶点计算出的边缘顶点会比岸边的边缘更靠外一些)。相反可以从相邻单元格的中心点往回算。修改TriangulateWaterShore方法使其用这种新方法。
不正确的角落连接
可以看到这起效了,除了还要考虑下一个方向的相邻单元格是否处于水下的情况来判断角上的三角形第三个顶点的位置。
正确的角落连接
这看起来好多了,只不过当大部分泡沫都可见的情况下泡沫会很明显。为补偿这些修改,可以把着色器中的shore值稍微调小一点。
最终的泡沫效果
6水下的河流
现在水体的效果完成了,至少是没有河流流入的情况下完成了。由于现在水体与河流的编辑逻辑互相不关联,所以在这种情况下河流会从水下流过。
河流流入水体中
半透明物体的渲染顺序取决于他们与相机之间的距离,越是离相机近的物体越是最后渲染,确保它们最终显示在顶部。这意味着当你移动相机时,河流与水面到相机的距离发生变化,使得有时河流显示在水面上,有时河流显示在水面下。让我们从规定统一的渲染顺序开始,河流应该始终是绘制在水面上的,这样瀑布的效果才会正确的显示出来,我们可以通过改变River着色器的队列顺序来实现这一点。
最后绘制河流
6.1隐藏处于水下的河流
虽然河床处于水下没什么问题,河水确实也会流向水底,但是我们不应该能在水底看到河流的效果,特别是它还没有渲染在实际的水面上。关于这个问题可以通过只在非水下的单元格才绘制河段河段三角形来解决。
对于TriangulateConnection方法则是只在当前和相邻单元格都不在水下时才绘制。
不再有河流处于水下的效果了
6.2瀑布
现在水底河流的问题解决了,但是现在河流与水体之间产生了间隙。当河流所在单元格的高度与水位相同时产生的间隙较小或者直接覆盖了。但从一个高度差异较大的单元格流向水面时就会产生较大的间隙而不是瀑布的效果,先解决这个问题。
瀑布的河段会穿过水面,它的一部分在水面上,一部分在下。我们需要保留水面上的部分而舍弃下面的,这需要费些功夫,为此创建一个专用的方法。
这个新方法需要四个顶点,两个河流高度的,两个水位高度的。把它们对齐后就能看到沿着瀑布向下的河流流向,所以前两个左右两边的顶点在顶部,其余的在底部。
当相邻单元格在水下且高度能形成瀑布的情况下,在TriangulateConnection中调用这个方法。
在当前单元格处于水下而相邻单元格不是时,还要处理方向相反的瀑布。
这就又产生了水下河流的问题。接下来修改TriangulateWaterfallInWater方法,把底部的两个顶点拉到水面上。但是仅修改Y坐标是不够的,这相当于把河段斜着拉到水面上,在悬崖位置上会形成裂缝。必须通过插值计算顶部和底部顶点来把底部沿着连线方向拉向顶部。
要向上移动底部顶点,将它们在水面下的距离除以悬崖到水底的高度,就得到了插值函数的值。
得到的结果就是方向相同但更短一些的瀑布。但是由于底部顶点现在的位置发生了变化,它们受到的顶点扰动与之前不同,这意味的最终结果仍然会不匹配。为解决这个问题必须在插值之前手动对顶点进行扰动,然后添加一个不应用顶点扰动的四边形。
虽然已经有了一个添加不受顶点扰动的三角形的方法,但还没有一个同样的添加四边形方法。所以在HexMesh中添加所需的AddQuadUnperturbed方法。
瀑布在水体上结束
7河口
当河流与水面高度相同时,河流的mesh会与沿岸水域的mesh相接。如果是河水流入海里的情况,那这里就是河流与海岸边潮汐相遇的位置,这样的地方称之为河口。
河流与沿岸水域相遇,不在有顶点扰动效果
河口目前有两个问题。第一个问题,河流四边形连接着水面边缘的第二个和第四个顶点,跳过了第三个顶点。由于沿岸水域没有使用这第三个顶点,最终会形成一个间隙或者直接就覆盖了。这个问题可以通过修改河口的几何结构来解决。
第二个问题是沿岸的泡沫效果到河水流动效果之间的过渡十分生硬,要解决这个问题,还需要另外一个材质球去混合这两个效果。
这意味着河口需要特殊处理,所以为此创建一个专用的方法。在河流穿过当前方向时在TriangulateWaterShore里去调用。
混合效果的区域不用去填充整个边缘连接带,使用一个梯形就足够。所以我们可以在这里先添加两个沿岸区域的三角形。
梯形的空洞
7.1 UV2坐标
河流的效果是使用的UV动画,沿岸水域的泡沫效果也是使用的UV动画。所以要混合彼此的效果,最终要设置两组UV坐标。还好Unity中的Mesh最多可以支持四组UV坐标的设置,而现在只需要在HexMesh中添加第二个UV坐标。
通过复制并修改之前的UV设置方法添加第二组UV的设置方法。
7.2河流着色器方法
因为两个着色器中都要使用河流特效,所以把River着色器中的代码整合成一个新方法放到water的包涵文件中,以便两者都能调用到。
修改河流的着色器使其应用这个方法。
7.3单独的河口mesh物体
在HexGridChunk中添加河口mesh的字段。
通过复制并修改沿岸水域对象,创建河口的对象着色器和材质,并拖入地图块预制体中。确保其同时勾选了UV1和UV2。
河口对象
7.4河口的三角剖分
在河流结束位置到水体mesh之间添加一个三角形解决重叠或者是间隙的问题。由于河口的着色器是从沿岸水域复制过来的,设置其UV坐标与泡沫特效吻合。
中间的三角形
在中间的三角形两边添加两个四边形,这样就组成了一个梯形。
梯形完成
旋转左边的四边形,使其对角线上的连线更短一些,这样就得到一个对称的几何形状。
旋转四边形,保持对称
7.5河口的流动效果
要同时显示河流的效果就需要用到UV2坐标。中间三角形的底部顶点位于河流的正中间,所以它的U坐标就应该是0.5。由于河流是朝向水体移动的,所以河口左边的U坐标应该是1,河口右边的U坐标则是0。设置第二组UV坐标为0-1使其能吻合河流的流向。
三角形两边的四边形都应该往这个方向匹配,对于河流宽度以外的点保持相同的U坐标。
梯形的UV2
要检查一下UV2的坐标设置是否正确,用河口的着色器让其可视化。可以通过在input结构中添加float2 uv2_MainTex来获取这个结构。
UV2坐标的效果
看起来没问题,就以此为基础添加河流特效。
使用UV2去从创建河流的特效
之前定义的单元格之间河流V坐标的范围是0.8-1,所以现在河口这部分也应该沿用这个范围而不是设置成0-1。然后由于水体的内六边形的比例因子比陆地小,所以沿岸河流的连接部分比陆地单元格之间的连接部分要大50%,把这个算进去设置V坐标的范围就应该是0.8-1.1。
同步河流与河口的流动
7.6河流效果的修正
目前河口的河流效果依然是笔直的,但实际情况是当河流流入较大区域时应该向外扩展,此时流向应该是弯曲的,我们可以通过弯曲UV2的坐标来模拟这个过程。
与其让U坐标的在河面宽度外的坐标保持恒定,不如将