HexMap学习笔记(二)——单元格颜色混合

作者:沈琰 2019-03-05


系列文章
HexMap学习笔记(一)——创建六边形网格
HexMap学习笔记(二)——单元格颜色混合
HexMap学习笔记(三)——海拔高度与阶梯连接

HexMap学习笔记(四)——不规则化
HexMap学习笔记(五)——更大的地图
HexMap学习笔记(六)——河流
HexMap学习笔记(七)——道路

HexMap学习笔记(八)——水体
HexMap学习笔记(九)——地形特征
HexMap学习笔记(十)——城墙
HexMap学习笔记(十一)——更多种地形特征物

前言

这里是HexMap系列教程的第二篇翻译,难度与上一篇相当。

本篇主要内容是对单元格边界做一个颜色混合,用到了更为细致的三角剖分方式和一般很少用到的顶点颜色的应用。

本篇原文地址:Hex Map 2

本篇难度:★☆☆☆☆

单元格颜色混合

此教程是六边形地图的第二部分,上一篇教程中我们构建了六边形网格的基本结构并实现了编辑单元格颜色的功能。现在每个单元格都是一个单色,在单元格之间的颜色转换显得很突兀。这次我们将引入一个过渡区域,让相邻单元格之间的颜色进行混合。

类似擦除颜料留下的污渍一般的单元格颜色过渡

1.相邻单元格

在混合颜色之前,首先得知道哪些单元格是彼此相邻的。每个单元格都有六个可以通过方向识别的相邻单元格。这些方向分别是东北(NE)、东(E)、东南(SE)、西南(SW)、西(W)、西北(NW)。为此新建一个枚举类型放入脚本文件中。


什么是枚举?
使用enum关键字来定义枚举类型,是有名字的有序列表。这种类型的变量可以使用这些名称中的一个作为它的值,每个名称都对应一个数字,默认情况下从0开始。当你需要有限长度的可命名有序列表时,这些非常有用。
实际上枚举就是简单的整数。你可以对它们进行加、减操作,转换成整数再转回来。同样也可以转换成少数其他类型,但整数是其基本类型。

六个相邻单元格,六个方向

在HexCell中添加一个数组来储存相邻单元格。虽然能设置其为公共字段,但基于为后面考虑还是设置为私有,并用一个方法为其提供方向参数来获取它们。确保数组字段为序列化的以便能在代码编译后能显示出来。


需要去储存相邻单元格的引用么?
我们也能通过坐标去确认并获取相邻单元格。但直接储存引用关系更直接和方便一些,所以就这么做。

相邻单元格的数组显示在了脚本的检视面板上。因为每个单元格可以有6个相邻单元格,就把HexCell预制体上数组默认长度设置为6。

预制体为容纳邻居创建的容器

现在添加一个公共方法在每个方向上去获取相邻单元格。因为方向枚举转换成的整数肯定是在0-5之间,所以这里不用去检查数组下标是否可能越界。


同样添加一个方法去设置相邻单元格。


相邻单元格之间的相对关系是双向的,所以当在一个方向上设置了相邻单元格,在反方向上也同样成立。


相邻单元格的相对方向是相反的

当然,这是假设我们有一个方法能获取指定方向的相反方向(Oppositie())的情况下。我们可以在HexDirection里去添加一个扩展方法去实现这个功能。获取相反方向相当于在前三个方向上加上3,后三个方向上减去3。



什么是扩展方法?
扩展方法是一个静态类中的静态方法,但使用起来像是某些类型的实例方法。这个类型可以是任何东西:自定义类、接口、结构体、原生数据结构或者是枚举。扩展方法的第一个参数之前必须有this关键字,它定义方法将操作的类型和实例的值。
这个特性允许我们在任何东西上添加方法,就像是静态方法的参数可以是任何类型。在有节制适量使用的情况下这个特性非常方便。但过渡使用会造成代码结构的混乱。

1.1连接相邻单元格

我们可以在HexGrid.CreatCell里初始化单元格之间的相邻关系。当我们从左到右逐行查看单元格时,我们知道已经创建了哪些单元格。这些是我们可以连接的单元格。

最简单的是E-W向的连接,每一行的第一个没有E向相邻的单元格,而其他的都有。由于相邻单元格是在遍历到这里之前就创建好的,所以可以直接连接它们。

创建单元格时连接由东向西方向的单元格


东西方向的单元格已经相连

还有另外两个双向连接需要建立,因为这些连接是在不同行之间,我们只能通过之前创建的行连接起来。这意味着第一行被完全跳过了。


由于每行之间的连接时呈锯齿状交错,所以需要不同的处理方法。首先先处理偶数行,它们之间有SE方向的连接关系,先连接这个方向。

连接每行之前由西北到东南方向的单元格


"z&1"是什么意思?
大家都知道"&&"是布尔运算符里的"与"运算,"&"就是"按位与"。它们的执行逻辑相同,但后者是对于每一位进行运算。
两个Bit都是1,与运算的结果就是1。例如:10101010&00001111结果是00001010。
从计算机原理层面上来说,所有的数字都是用二进制表示的。二进制中的1、2、3、4写作1、10、11、100。如你所见,表示是否为偶数的最低有效位是第一位。
我们把二进制的与运算作为一个遮罩,忽略除了第一位之外的其他位数,如果结果是0,那就表示这是一个偶数。

现在就可以连接SW方向的单元格了。除了每一行的第一个单元格,它们SW方向是空的。


奇数行遵循着相同的逻辑,除了镜像关系。运行完一次这个方法后,网格内所有的单元格都关联了起来。


所有的相邻单元格都连接上了

当然,不是每个单元格都精准的与六个单元格相邻。网格边缘的单元格最少有两个,最多有5个相邻单元格,这应该是在之前就意识到的。

每个单元格所拥有的邻居个数

2.颜色混合

颜色混合会使三角化每个单元格的过程更加复杂,所以我们把三角化的代码单独分开。因为我们现在能通过方向获取相邻单元格了,所以在代码里替换原来用坐标获取的部分。


获取角度也换成用方向获取,这样应该比用坐标获取好用。


但在这之前需要在HexMetrics里添加两个静态方法。这样做还有个额外的好处:我们可以设置corners数组为私有变量了。


2.1让每个三角形有多种颜色

现在HexMesh.AddTriangleColor方法只有一个颜色参数,所以每个三角形只能有一个固定的颜色。我们添加两个参数让三角形的每个顶点都有不同的颜色。


现在可以开始混合颜色了!一开始就简单的用相邻单元格的颜色作为其它顶点的颜色。


又不幸地产生了一个空引用异常,因为不是每个单元格都有六个相邻单元格。当没有邻居时就用自己的颜色代替。


"??"是什么意思?
这被称为空合并运算符,简单来说"a??b"即"a!=null?a:b"的简写。
这里有个小诡计,因为Unity在把一个东西与组件比较时会自定义,(注:Unity中判断一个对象不为空时可以直接写成if(someThings),这在C#原本语法里是没有的)而这个运算符会绕过它并直接与null比较,不过这是销毁对象时才需要考虑的问题。

错误的颜色混合

坐标标签去哪了?

它还在那,不过截图时把UI层隐藏了。

2.2颜色平均化

颜色混合其效果了,不过显然现在的效果不能接受。两个单元格之间的边界的颜色应该是这两者的平均值才对。


边缘混合

现在我们混合了两个边界点上的颜色,依然得到了颜色分界线的形状。这是因为六边形的每个外部顶点总共有3个六边共享。

三个相邻单元格,四种颜色

这表示还得考虑上面和下面的相邻单元格。最终得到的应该是四种颜色,每组三种。

添加两个额外的方法到HexDirectionExtension中,让我们能方便的跳转到上一个和下一个方向。


现在我们可以获得所有的三个相邻单元格,然后执行两次三向颜色混合。


角落混合

现在除了边缘上的单元格,其他位置得到了正确的颜色变换。边界单元格因为与丢失的单元格颜色不一致,所以仍然能看到那里的边界颜色。但是总的来说,目前效果仍不理想,需要一个更好的策略。

3.混合区域

混合六边形的整个表面色导致杂乱的模糊效果,你再也不能清晰的识别出每一个单元格。我们能通过只混合靠近边界的区域来改善这一点,这可以让内部的六边形保持固定的颜色。

颜色混合区域的固定核心

混合区域相对固定区域的占比会生成不同的视觉效果,我们把这个区域定于为外径的一部分。先设定其为75%,这就新增了两个常量,两者加起来是100%。


利用新的要素,创建获取内部固定颜色的六边形的方法。


现在修改HexMesh.Triangulate方法让它变成获取内部固定色六边形的角。先暂时不改变颜色。


中间的固定区域六边形,没有边缘

3.1三角化混合区域

现在通过细分三角形来填补空缺,它在每个方向都是一个梯形。我们可以用四边形来表示,创建一个方法添加这个四边形和它的颜色。

梯形边缘


重写HexMesh.Triangulate方法,现在三角形获取固定的颜色,梯形部分混合这个固定色和两个角落上的颜色。


梯形边缘颜色混合

3.2边界连接桥

现在看起来好些了,但是还没完。两个边界的颜色混合受到了相邻单元格边界的影响。为了避免这一点,我们要把梯形的两个角切开变成矩形。现在它变成了连接相邻单元格的桥,在侧面留下了缺口。

边缘连接桥

在一开始就能通过v1和v2获取v3和v4的位置,然后沿着桥移动到边缘就能计算出坐标。v3和v4在边缘上的偏移量可以通过取相邻角的中点来获取,然后乘以混合比例的常量。这些应该是HexMetrics的范畴。


回到HexMesh里,把AddQuadColor的颜色参数改为两个比较合理。


修改Triangulate,现在它就能在相邻单元格之间创建正确的混合连接桥。


正确的连接桥颜色混合与角落的缺口

3.3填充间隙

现在当三个单元格交汇时,通过切分梯形得到一个三角形的洞,下一步是把这些间隙填充回去。首先考虑与前一个相邻单元格连接的三角形。它的第一个顶点具有单元格的颜色,第二个顶点的颜色是三色混合,最后一个顶点的颜色和连接桥颜色的一半相同。


快完成了

最后,另一个三角形也是一样的方法,除了它是第二个顶点与连接桥相接,而不是第三个顶点。


完全填充

现在我们得到了不错的混合区域效果,我们可以指定任意大小,由你自己决定模糊还是直接的边缘颜色过渡。你可能会注意到网格边界附近的混合仍然是不正确的。还是暂时不管这个问题,把注意力集中在另一件事上。

但是颜色转换依然很难看?
这是线性颜色混合的极限了。事实上纯色的效果不是很好,未来的教程中将会升级到地形材质并做一些更漂亮的混合。

4.边界合并

观察一下网格的拓扑结构,有哪些不同的形状?如果我们忽略边框,能识别出3种形状:固定色的六边形、两种颜色混合的矩形和三种颜色混合的三角形,无论是否有三个单元格交汇都能看到。

能看到的三种形状

所以每两个六边形由一个矩形桥连接起来,每三个六边形由一个三角形连接起来。而我们现在用一种更复杂的方法在进行三角剖分:用两个四边形连接一对六边形,用三个三角形连接3个六边形。这样似乎有些过于复杂了,如果我们直接用单个形状相连,甚至颜色平均都不需要了。这样就能用更少的复杂性,更少的工作量,更少的三角形实现这个功能。

比需求更为复杂

我们为什么不在一开始就这么做?
你可能会在你的一生中问许多次这个问题,但这是马后炮。这是一个代码以逻辑方式演进的例子,直到获得了新的见解,从而产生了新的方法。这样的顿悟常常发生在你认为你已经搞定的时候。

4.1直接的连接桥

边界的连接桥现在是两个四边形组合而成,要直接用一个四边形连接到相邻单元格,需要把桥的长度增加一倍。这意味着不再需要平均化HexMetrics.GetBridge里的两个角了,而变成直接相加并与混合区域占比常量相乘。


所有的连接桥都交叉重叠

现在一个桥直接连接两个六边形,但依然生成了两个连接处,在每个方向上都有一个桥被覆盖,所以现在两个相邻单元格只有一个需要创建连接桥。

先简化我们的三角化代码,删除所有边缘三角形与颜色混合的部分。然后把添加四边形连接桥的代码移动到新方法中。传递前两个顶点到这个方法中,这样就不需要再推导它们了。


现在可以很容易的限制三角化连接,处理NE方向的连接时受限只添加一个连接桥。


东北方向的连接桥

这样看起来在只在前三个方向三角化连接桥就能包含所有的连接,这三个方向分别是NE、E和SE。


里面和边界上的连接桥

相对于两个单元格的连接就完成了,但是还有一些在网格边缘无用的连接。让我们通过修改TriangulateConnection中的代码去掉它们。一旦发现没有相邻单元格方法就跳出去,所以不用再用当前单元格代替不存在的相邻单元格了。

只有里面的连接桥

4.2三角形连接处

又来开始填三角形的坑了,依然只需要在相邻单元格存在的时候才执行。


第三个顶点位置在哪?暂时用v2当占位符,但那显然不正确。因为三角形的每个顶点都连接在桥上,所以可以通过在下一个相邻六边形的连接桥上获得。


再一次,颜色过渡完成

全搞定了?还没,现在要处理三角形重叠的问题。因为是每三个单元格共享一个三角形连接处,所以只需要在两个方向添加连接三角形:NE和E方向。


下一期教程内容是HexMap3
本期工程地址:tank1018702/Hex-Map-Learning
有想系统学习游戏开发的童鞋,欢迎访问http://levelpp.com/。

专栏地址:https://zhuanlan.zhihu.com/p/55068031



最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多