|
前言
这是目前为止长度最长的一篇,难度也是直线上升。不仅此篇所用的三角剖分方法更为复杂,并且从这篇教程开始,会逐渐使用编辑着色器代码的方式添加一些简单的视觉效果。
尽管经过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。
系列文章
HexMap学习笔记(一)——创建六边形网格
HexMap学习笔记(二)——单元格颜色混合
HexMap学习笔记(三)——海拔高度与阶梯连接
HexMap学习笔记(四)——不规则化
HexMap学习笔记(五)——更大的地图
HexMap学习笔记(六)——河流
HexMap学习笔记(七)——道路
HexMap学习笔记(八)——水体
HexMap学习笔记(九)——地形特征
HexMap学习笔记(十)——城墙
HexMap学习笔记(十一)——更多种地形特征物
作者:沈琰
专栏地址:https://zhuanlan.zhihu.com/p/57924967
|
|