HexMap学习笔记(四)——不规则化

作者:沈琰 2019-03-20

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

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

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

前言

这篇教程内容主要是对噪声纹理图的应用,在游戏中噪声是极为常用的功能,特别是与地形生成相关的。使用噪声计算出的地形比较符合自然界的地貌,专栏中还有一篇文章也运用到噪声,也可以参考阅读。

传送门:Meta42:300行代码实现Minecraft(我的世界)大地图生成

本期原文地址:Hex Map 4

这是HexMap系列的第四篇,到目前为止地图都是一个精确的蜂巢状网格,这篇教程将会给地图添加一些不规则的特性让其看起来更自然一些。

不再整齐的六边形

1.噪声

要添加一些不规则的感觉就需要一些随机性,但又不是真的全随机。我们需要的是在编辑地图时未选中的位置保持不变,不然每改动一点地图就全变了。所以需要是一种可重现的伪随机,即噪声。

柏林噪声(Perlin noise)就是一个很好的选项,它在每个点的位置都可以重现。当多种不同频率的柏林噪声叠加时,能产生大范围来看变化很大,小范围内又接近一致的噪声纹理,生成相对平滑的形变,靠近的点更倾向于黏在一起而不是向相反的方向扭曲。

柏林噪声可以程序化生成,Noise这篇教程里介绍了该如何去实现,但也能使用一张预先生成的噪声纹理图。使用噪声纹理图的好处是它比直接计算多频率叠加的噪声更容易,也更快,缺点是纹理图需要占用内存并且噪声的区域大小相对有限。所以噪声纹理图需要平铺显示并且需要覆盖比较大的区域,使得平铺看起不那么明显。

1.1噪声纹理贴图

这里准备使用噪声纹理贴图,所以不必现在去看Noise这篇教程。这表示我们需要一张纹理图,如同下面这张。

平铺的不规则柏林噪声图

这张纹理图包含多重频率叠加的平铺柏林噪声,并且是一张灰度图,其平均值接近0.5,极限值是0和1。

不过这不是最终要用的图,这张图的每个像素点上只有一个值,如果我们需要的是3D形变,我们至少得需要三个伪随机采样。所以除此之外还需要两张额外的不同噪声纹理图。

我们可以就用三张不同的纹理图,或者也可以用RGB通道来存储这些值,我们可以在一张纹理图上存储四种不同样式的噪声,如同下面这张图。

四合一噪声纹理图

这张纹理图怎么获取的?

用NumberFlow生成的,这是一个我(注:原版教程作者)为Unity写的纹理编辑插件。(注:自己在工程中使用时直接复制这张图就行)

得到这张图后导入到自己的Unity工程文件中,由于我们要用代码对纹理图进行采样,所以它必须是可读写的。勾选Read/Write Enable,这样就能把纹理贴图的数据存储到内存中并用C#代码进行访问。撤选sRGB(Color Texture)选项,确保格式设置成Automatic并且压缩方式设置成null,我们不想因为压缩纹理而破坏噪声图的样式。同样的,Generate Mip Maps也不用勾选,并不需要这个功能。


导入噪声纹理图

如果勾选sRGB有什么影响?

如果我们在着色器的某些地方使用噪声纹理图,它可能会有些不同。当使用线性渲染模式时,纹理采样数据会自动从伽马空间转换到线性空间。这会对噪声纹理的采样数据产生一个错误的结果,而这是需要避免的。(注:Unity的默认渲染模式是线性模式)

1.2噪声采样

在HexMetrics里添加噪声采样方法,这样就能在任何地方使用,这意味着HexMetrics必须拿到纹理图的引用。


由于这不是一个组件,我们不能在编辑器里赋值。我们就简单地把HexGrid当一个中转,由于HexGrid是第一个运行的,所以在它的Awake方法开始的位置传递值就行了。


但是在运行模式下,这种方式无法在重新编译时保存,静态变量不是由Unity序列化的。要解决这个问题还需要在OnEnable方法中重新赋值纹理,这个方法会在重新编译后调用。



现在HexMetrics能访问纹理贴图了,我们添加一个方便的噪声采样方法到里面,这个方法生成一个包含四种噪声模式的4D向量。


通过双线性过滤(注:进行缩放显示的时候进行纹理平滑的一种纹理过滤方法)的方式对纹理图进行采样得到样本数据,是使用世界坐标系下的X和Z轴坐标作为UV坐标。由于噪声源是3D的,所以我们忽略的Y轴坐标。

最后得到一个可以转换成4D向量的颜色,这个转换是隐式的,这意味着我们可以直接返回颜色,而不用显式的转换成Vector4。


双线性过滤是如何工作的?

可以去看Rendering 2,Shader Fundamentals这篇教程,里面介绍了UV坐标和纹理过滤。

2.顶点扰动

通过分别扰动每个顶点来让整齐的网格发生形变,为此添加一个Peturb方法到HexMesh里负责这个工作。这个方法获取一个点并返回扰动后的坐标,所以它使用扰动之前点进行采样。


我们先简单地直接加上X、Y和Z的噪声采样,并将其作为结果。


要如何快速的让HexMesh里的所有顶点都应用扰动?当在AddTriangle和AddQuad里添加顶点到列表中时修改每个顶点就行了。

扰动后四边形依然是平坦的?

很可能并不是。这些四边形由两个不再对其的三角形构成,因为这些三角形共享两个顶点,这些顶点的法线会平滑变化。这意味着你看不到两个三角形之间的明显过渡,如果扭曲的不是太明显,你仍然会感觉这个四边形是平的。

顶点扰动但不明显

看起来好像没多大变化,除了单元格的坐标标签不见了之外。这是因为我们把顶点加上了噪声采样的坐标,而这些坐标总是正值,所以所有三角形都在标签上并覆盖了它们。我们需要把采样值的原点放到中心,这样就能在上下两个方向上运动,所以修改采样的范围到-1至1之间。


单元格中心的扰动

2.1扰动强度

显然现在网格已经被扭曲了,但效果非常不明显。每个维度最大的位移距离只有1,理论上顶点扰动的最大位移只有单位长度,并且极限值情况发生的概率极低。由于每个单元格的外径是10,这个扰动相对来说太小。

解决方案是在HexMetrics里添加一个强度设置,这样就能放大扰动。我们先试着把强度设置为5,这样一来理论上的最大位移距离就有单位长度,这样看起来就明显多了。


在HexMesh.Perturb里通过相乘的方式应用采样数据。



增强强度

2.2噪声采样缩放

网格在编辑前还算正常,一旦出现阶梯连接部分就不对了。它们的顶点向各个方向扭曲,看起来很混乱,使用柏林噪声不应该会发生这种情况。

这是因为我们直接用世界坐标进行噪声采样,使得纹理平铺在每个单元格上,但是我们的单元格的尺寸比纹理贴图本身要大得多,实际上纹理图是在任意位置被采样,这破坏了其连贯性。

10乘10的噪声纹理网格线覆盖在六边形地图的蜂巢网格上

我们要对噪声采样进行缩放,这样纹理就能囊括更大的区域。我们在HexMetrics里添加这个缩放并设置其为0.003,然后把采样坐标与这个因素相乘。


采样数据突然就覆盖了原来倍的区域,并且其连贯性也变得显而易见了。


缩放后的噪声图

新的缩放还确保在噪声在平铺之前有个过渡过程。实际上因为单元格的内径是,所以没有办法精确的以X轴的尺寸平铺。但是因为噪声纹理自身的连贯性,即使细节不吻合,你任然能在大范围内发现重复的样式,大约是每20个单元格之间。这只在地图没有其他编辑形状的功能时才比较明显。

3.对平单元格中心

对所有顶点进行扰动让我们的地图看起来更自然一些了,但是还有一些问题。因为现在单元格的表面不平坦,其坐标标间与网格相交了,并且在阶梯连接部分与陡峭斜面间出现了裂缝。我们先把裂缝的问题放一放,先处理单元格表面的问题。

1\
越不整齐问题越多

相交问题最简单的解决方法是保持单元格中心平坦,即在HexMesh.Perturb里不修改Y轴坐标。


单元格对平

于是包括单元格中心与阶梯连接的每一层的所有的垂直方向的坐标都不会发生改变了。值得注意的是这让理论上的顶点扰动最大位移距离减少到了,并且只在XZ平面移动了。

这改动并没有什么问题,让识别每个单元格变得更加容易了,并且预防让阶梯化连接变得混乱的问题。但是垂直方向的扰动依然可以用别的方式做得更好。

3.1单元格海拔高度的噪声扰动

我们可以让扰动作用于每个单元格而不是每个顶点,这样就既保留了每个单元格的平坦表面又能让不同单元格之间看起来也有差别。比较好的做法是对高度扰动使用不同的缩放比例,所以在HexMetrics里添加一个强度系数。1.5的值就能带来一些微妙的变化,这大概是阶梯一级的高度。


修改HexCell.Elevation的set属性,让其应用垂直坐标的扰动。


为了确保扰动能立即被应用,需要在HexGrid.CreateCell里精确设置高度。否则一开始网格就是平坦的。这一步放在UI创建之后的最后一步。


应用单元格高度扰动产生了很多裂缝

3.2使用相同高度

大量裂缝出现在网格中,因为在三角化网格时没有始终使用相同的高度。添加一个便利的属性到HexCell里重新获得自身坐标,这样就能在任何位置使用。


现在能在HexMesh.Triangulate里使用这个属性去确认单元格的中心位置。


也可以在确认相邻单元格顶点坐标时,在TriangulateConnection里使用这个属性。


使用一致的单元格高度

4.细分单元格边缘

虽然现在单元格有着漂亮的变化,但它们看起来仍然是六边形。这不是什么大问题,但我们能做得更好一些。

单元格清晰的六边形结构

如果有更多的顶点自然就能看到更多变化,所以我们把单元格的边界分为两个部分,在六边形的每一片三角形底边一半的位置添加一个顶点。这意味着HexMesh.Triangulate里需要添加两个而不是一个三角形。


十二条边替换六条边的效果

把顶点个三角形加倍明显有了些新变化,干脆就把顶点翻三倍,这样形状会更坚固一些。


十八条边

4.1细分边缘连接部分

当然,还得去细分连接部分,所以传递新的边界顶点到TriangulateConnection里。


在TriangulateConnection里添加匹配的参数,就能使用这些新顶点。


还需要从相邻单元格上计算额外的边界连接处的顶点,可以在连接到另一边之后计算。


下一步需要修改边界的三角化。现在先不管阶梯化部分,简单的添加三个四边形。


连接处细分

4.2打包边缘顶点

因为现在需要四个顶点表示一组边界顶点,那么把他们打包整合起来就有意义了,因为这比单独处理四个顶点方便。为此创建一个简单的结构体EdgeVertices,它需要包含四个顺时针排列在单元格边缘的顶点。


不需要序列化么?

我们只在三角化的时候使用这个结构,就此而言我们不需要存储边缘顶点,所以不要序列化。

给它一个便利的构造函数,只负责计算确定的边缘位置。


现在把三角化的方法进行分离,在HexMesh里新建一个从单元格中心到边缘创建三角形扇面的方法。


以及对两个边缘之间的四边形条带进行三角剖分的方法。


这让我们能够简化Triangulate方法。


我们现在能在TriangulateConnection里使用TriangulateEdgestrip方法,但还有一些其他的细分工作要做。在我们第一次用v1的地方,我们应该用e1.v1代替。以此类推,v2变成e1.v4,v3变成了e2.v1,v4变成了e2.v4。


4.3阶梯细分

还需要去细分阶梯连接部分,所以把边缘参数传递到TriangulateEdgeTerraces里。


现在要修改TriangulateEdgeTerraces方法,使之参数为两个单元格的边缘之间,而不是一组顶点。先假设EdgeVertices里有方便的静态插值计算函数,这就可以让我们简化TriangulateEdgeTerraces方法而不是使其变得更复杂。


EdgeVertices方法就是预先在两个边缘之间的所有顶点之间计算阶梯插值。


阶梯连接处细分

5.重新连接陡峭面与阶梯

到目前为止我们都忽略了当陡峭面与阶梯相遇时产生的裂缝,现在该处理这个问题了。先处理陡峭-倾斜-倾斜(CSS)和倾斜-陡峭-倾斜(SCS)的情况。


这个问题重现是因为分界点(注:阶梯连接收束到陡峭面边缘的顶点)的计算受到了干扰。这意味着它不是精确的处于陡峭面的边缘线上,这就产生了一个裂缝,这样的空洞有的明显,有的不明显。

解决方案是不要对分界点应用噪声扰动,就是说我们需要能选择一个点是否应用扰动。最简单的办法是创建一个完全不对顶点进行扰动的AddTriangle方法。


修改一下TriangulateBoundaryTriangle让其应用这个方法,这意味着只对分界点除外的其他所有顶点进行扰动。


值得注意的是,因为我们没有用v2推导其他的点,它可以直接先应用扰动。这是一个简单的优化办法并且节省代码,所以我们改成如下这样。


未应用扰动的边界点

现在看起来好多了,但是还没完。在TriangulateCornerTerracesCliff方法里,分界点是通过插值计算左右的坐标点得到的,但这两个点没有应用扰动。要让边界点能精确吻合陡峭斜坡边缘,需要插值去计算两个扰动过的坐标点来求得边界点。


这在TriangulateCornerCliffTerraces方法里也是一样的。


不再有空洞了

5.1两个陡峭斜面与一个倾斜面的情况

剩余的是有两个陡峭斜面与一个倾斜面特征的情况。

因为一个三角形顶点位置产生的空洞

这个问题的修正方法是在TriangulateCornerTerracessCliff方法最后一个else里,单独扰动除了分界点外的三角形顶点。


TriangulateCornerCliffTerraces里也一样。


最后一个空洞也消失了

6.优化调整

我们现在有了一个准确的应用扰动的网格,它的具体形状表现取决于一张特定的噪声图,它的缩放和扰动强度。在我们现在的例子里它的扰动强度似乎过大了,虽然这对表现单元格的不规则化很不错,但我们还是不想它偏离网格太多。毕竟最终我们还是要通过它去识别哪个单元格正在被编辑,如果变化太大就很难去填充编辑一些别的什么东西。

顶点扰动与未扰动的对比

单元格的扰动强度设置为5有点太大了。

单元格顶点的扰动强度从0到5的效果

让我们降低到4,让它变得更容易管理而又不会太有规律,保证在XZ平面的最大位移为√32≈5.66。


强度为4的的扰动

另一个可以调整的参数是内部固定六边形的范围。如果我们增加它,平坦的单元格中心就会变得更大一些。这给未来的内容留下了更多空间。当然,也也会变得更六边形化一些。

中间固定六边形的比例从0.75到0.95

少量增加固定六边形范围参数到0.8会让之后使用起来更容易。


固定六边形的范围为0.8

最后,海拔高度等级之间的差异有点陡峭。当我们检查网格生成是否正确时这很方便,但这一步我们现在已经完成了,所以让我们把它减少到3。


高度等级的步长减少为3

还可以调整高度扰动强度。但它现在被设为1.5,等于高度步长的一半,这已经比较合适了。

较小的高度步长也使得使用我们可以更为实用的7个高度等级,这允许为地图添加更多种类。

使用了七个高度等级的地图

下一篇是:Hex Map 5

本期工程地址tank1018702/Hex-Map-Learning

有想系统学习游戏开发的童鞋,欢迎访问http://levelpp.com/。

作者:沈琰
原地址:https://zhuanlan.zhihu.com/p/55518523





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

商务合作 查看更多

编辑推荐 查看更多