HexMap学习笔记(七)——道路

作者 : 沈琰
前言
由于不涉及对已有地形的改变,这篇教程的难度是低于上一篇的,入过完全弄懂上一篇的教程读起来就会很容易。另外最后道路的着色器效果很赞,值得仔细研究一下思路
此教程是HexMap系列的第七篇,在上篇中添加了河流的编辑功能,这一次添加道路。

1 单元格里的道路
与河流一样,道路是穿过单元格边缘的中间来连接不同单元格,最大的不同点是道路没有流动的河水规定方向,所以道路是双向的。一个看得过去的道路网络肯定会有十字路口,所以我们还定义单元格内穿过的道路可以超过两条。
在六个方向上都允许有道路穿过,这意味着单元格可以包含零到六条道路,这产生了十四种可能的道路结构,远超河流的五种。为使其更容易实现,我们需要一个可以表示所有结构类型的通用方法。

1.1 记录道路结构
最直接的方法是每个单元格都用一个布尔类型的数组保存其道路结构,添加一个私有的数组字段并将其序列化,这样就能在检视面板上看到值,在预制体中预设这个数组的大小为6.




1.4 删除无效道路
现在可以确保只在条件允许时添加道路,在来考虑稍后道路失效时候的删除问题,例如当添加河流时。可以禁止河流在道路上,但河流不会被道路阻挡,让其把路冲开。
只要将河流方向上的道路设置为false就行了,不用管是不是存在道路,修改道路状态始终会刷新受影响的单元格,所以现在不用特地在SetOutgoingRiver中去调用RefreshSelfOnly方法了。

另一个会使道路无效的操作是单元格高度的改变,这种情况下需要检测所有方向上的道路,如果高度差距过大,现存的道路就要删除。

2 编辑道路
编辑道路的逻辑实在是很像编辑河流,所以HexMapEditor中需要另一个toggle组,加上随之一起的方法来设置道路编辑的状态。

现在EditCell方法同样支持添加和删除道路,这意味着检测到鼠标拖拽时有两种可能的操作,稍稍修改一下代码,这样在有效拖拽时两个toggle的状态都会被检测到。

你可以通过赋值河流的UI面板并修改其调用的方法来快速添加一个编辑道路的面板,不过这样UI看起来就太长了,所以修改了一下颜色编辑面板的样子使其更紧凑一些,好与河流面板想吻合。

因为现在颜色编辑面板使两行三列,多出了一个颜色的位置,就添加一个橘黄色的地形颜色。


现在就可以编辑道路了,尽管看不见,但可以在检视面板上验证其是否生效。

3 道路的三角剖分
要让道路可视化就需要将其三角化,这与河流mesh类似,除了地形上不会生成一条通道之外。
第一步,在新建一个标准着色器,使用UV坐标为道路表面着色。

创建道路的材质球并应用这个着色器。

然后为地图块的预制体添加另一个挂载有HexMesh的子物体,只在脚本上勾选uses UV coordinates并关闭阴影投射,比较快的办法是复制河流的预制体修改名字和材质球。

在这之后添加一个HexMesh类型的公共字段roads到HexGridChunk里,并包含到Triangulate方法里,在脚本的检视面板上创建其关联。

3.1 单元格之间的道路
第一步先考虑单元格之间的路段。就像河流一样,道路覆盖的范围是连接部分中间的两个四边形,这里将使用道路的四边形将其完全覆盖,所以可以使用相同的六个顶点。在HexGridChunk中添加一个TriangulateRoadSegment方法。

TrangulateEdgeStrip即是调用此方法的位置,并且只有在确实存在道路时才需要这么去做,添加一个布尔类型的参数去传递这个信息。

这样当然会出现编译错误,因为调用这个方法的位置缺少参数。现在可以在没个调用方法的位置都补上一个false,或者也能声明这个参数的默认值是false,这样一来这个参数就变成了可选的并解决了编译错误的问题。


如果这里需要就简单的通过六个中间顶点调用TriangulateRoadSegment来三角化路段。

这里只负责平坦连接处部分,要在阶梯连接处三角化道路,还需要在TriangulateEdgeTerraces里添加传递信息的参数,并传递到TriangulateEdgeStrip中。

TriangulateEdgeTerraces方法是在TriangulateConnection里调用,这里是能够确认当前方向上是否有道路穿过的地方,无论是对于三角化边界和阶梯连接处都一样。


3.2 渲染到顶部
当编辑道路时,你会看到路段在单元格之间突然出现,靠近路段中间的位置是紫色,到边上变化为蓝色。目前看起来好像符合预期,然而四处移动相机时就会发现,路段有可能出现闪烁,甚至有些时候还会完全消失。这是因为道路的三角形精确的覆盖在地形三角上,渲染至顶部的三角形是随机的,要修复这个问题需要分为两步。
首先,我们想让道路三角形总在地形三角形之后绘制,通过绘制常规集合图形之后再渲染来实现,方法是把道路放在靠后的渲染队列中。

第二步,我们希望即使是坐标相同,道路三角形也绘制在地形的顶部。这要通过添加一个深度测试偏移量实现,使得GPU将道路三角形视为比实际距离更接近相机。

3.3 穿过单元格的道路
当三角化河流时,每个单元格最多只需要处理两个河流的方向,于是我们能为五种可能的方案创建符合其规则的三角化方法。
然而道路有十四种可能的方案,因此我们不会用不同的方法来应对每个方案,相反将会以完全相同的方式处理每个方向,而不去考虑特定的道路结构。
当有道路穿过单元格时,直接让其笔直达到单元格的中心并不超过这个方向的三角形区域。将从边缘到中心画一条路,然后用两个三角形来覆盖到中心的剩余部分。


先只考虑没有河流在内的单元格,这种情况下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坐标,我们实际看到的是道路中间到边缘之间的过渡。

现在已经能确保道路的三角剖分的正确性,就可以开始改变道路的颜色,使其表现得更像道路,如同对河流所做的一样。这将是一个简单的可视化改动,没什么特变的。
首先从使用纯色开始,使用材质球的颜色将其涂为红色。

似乎没有效果,这是因为着色器是不透明的。现在需要把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。


本期工程地址:https://github.com/tank1018702/Hex-Map-Learning/tree/Roads
下一篇教程是:https://catlikecoding.com/unity/tutorials/hex-map/part-8/
有线下学习游戏开发打算的童鞋,欢迎访问:http://levelpp.com/
另有专业开发交(gao)流(ji)群等待大家强势插入:869551769