对噪声纹理采样
扰乱顶点
保持单元格为一个平面
单元格边界细分
这篇教程是六边形网格地图系列教程的第四部分。到目前为止,我们已经制作出一个蜂巢样式的地图了。本片教程中,我们将引入不规则化使我们的地图产生更强的真实感。
Figure1‑1由不规则六边形组成的地图
噪声 | Noise
为了使地图不规则化,我们需要使用随机工具。但并不是真正的随机,我们需要使地图最终呈现的样子与我们编辑它时保持一致。否则的话,地图将不能正确地随着我们的改变变化。所以我们需要一种可再现的伪随机噪声。
Perlin 噪声是一个很好的选择。Perlin 噪声在任何时候都是可再现的,并且其在大尺度范围内能够产生较大的波动,而在小尺度范围内只会在小范围内的波动。这能够产生相当平滑的扭曲。一维Perlin噪声曲线上位置相近的点趋向于组成一条直线而不是在不同的方向上无规律的排列。
我们可以使用程序来生成 Perlin 噪声,这篇噪声教程详细地解释了如何来生成它。但是我们也可以使用预制好的噪声纹理。使用噪声纹理的优点是比实时计算噪声更加方便快捷。缺点是这样需要消耗更多的内存,并且只能涵盖噪声的一小部分。所以我们需要使用瓦片纹理,并且要使用足够大的瓦片好让瓦片效果不会很明显。
噪声纹理 | Noise Texture
我们将使用纹理,所以你不必现在就去看这篇噪声教程了。这意味着我们要拥有如下的一张纹理。
Figure 1‑1 Perlin 噪声纹理瓦片
上面的这张纹理包含了多维度的 Perlin 噪声。是一张平均值是0.5、极值为0和1的灰度图。
稍等,这张图的每一个像素点只有一个值,而我们是少需要三个维度上的伪随机采样!所以我们还需要额外的两张包含不同噪声的纹理。
我们可以这样做,但是我们也可以在纹理的不同颜色通道中储存噪声值。这样的话我们可以在一张纹理中储存至少四种不同的噪声。比如说下面这张。
Figure 1‑2四通道的噪声纹理
怎么制作噪声纹理呢?
我使用 NumberFlow。这是我为Unity开发的纹理编辑程序。
将这张贴图导入到你的 Unity 工程中。因为我们将使用程序对噪声纹理进行取样,所以它必须是可读的。将 Texture Type 设置为 Advanced 然后勾选下方的 Read/Write Enabled。这将会使纹理数据被保存到内存中,好让C#对其访问。切记,要将 Format 设置为 Automatic Truecolor,否则的话将不能正常工作。我们不想使我们的噪声因为纹理压缩被破坏掉。
Figure 1‑3导入噪声纹理
为什么不能使用sRGB模式?
当我们需要在 shader 中使用噪声纹理时将会产生一些差异。当使用线性渲染模式的时候,纹理采样将会自动的将颜色数据从 Gamma 空间转换为线性颜色空间。这将会产生错误的结果。
为什么我的纹理导入设置看起来不一样?
我写完这篇教程之后设置被改变了。你应该使用默认的2D纹理设置,关闭sRGB并且不进行压缩。
噪声采样 | Sampling Noise
我们来为 HexMetrics 类加入噪声采样功能,这样的话我们便能在任何地方使用它。这就意味着 HexMetrics 类必须持有噪声纹理的引用。
public static Texture2D noiseSource;
因为 HexMetrics 并不是一个组件,所以我们不能通过编辑器为其分配纹理。我们需要使用 HexGrid 类作为中介,因为 HexGrid 类是第一个我被激活的,所以我们可以在它的 Awake 方法中传递分配纹理。
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; … }
然而,这种方法将不会在游戏模式中生效,因为 Unity 不会序列化静态变量。在 OnEnable 事件方法中重新分配纹理来解决这一问题,该方法将在重新编译后被调用。
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
Figure 1‑4分配噪声纹理
现在 HexMetrics 类可以访问纹理了,我们来为它添加一个用于噪声采样的方法。该方法以一个世界坐标作为参数并返回一个包含噪声采样的四维向量。
public static Vector4 SampleNoise (Vector3 position) { }
采样由对噪声纹理进行双线性过滤生成,使用世界空间的X、Z坐标作为UV坐标。由于我们的噪声源是2D的,我们忽略了第三个世界坐标。如果我们的噪声源是3D的,我们就需要使用Y方向坐标了。
我们只要将颜色信息转换为四维向量就可以了。这个转换是隐式的这意味着我们可以直接返回颜色信息而不用进行显示的类型转换 (Vector4)。
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
双线性过滤是如何工作的?
这篇教程渲染教程2:着色器初步解释了什么是 UV 坐标以及纹理滤波。
顶点扰动 | Perturbing Vertices
我们可以通过单独地扰动每一个顶点来扭曲我们的蜂巢状的网格。为此,我们为 HexMesh 类添加一个 Perturb 方法,该方法接受一个未被扰动的点作为输入,然后将其扰动后输出。
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
我们先将 X、Y 和 Z 方向的噪声采样直接加到相应方向的坐标上作为结果输出。
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
我们怎么才能方便的改变 HexMesh 类,以便所有的顶点都能被扰动呢?只要在每个顶点被添加到顶点列表的时候对其进行偏移就好了。
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); … } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); … }
一个四边形被扰动之后依旧是一个平面么?
就大部分来说并不是这样的。它们会变成不在一个平面上的两个三角形。然而,因为这两个三角形有两个公共顶点,所以顶点处的法线将会过渡的非常平滑,以至于你并不能够清楚地分辨出两个三角形连接处的角度差异。如果扰动的程度不是特别大的话,这个四边形依旧会看起来是一个平面。
Figure 2‑1被扰动的定点?
除了单元格的坐标标签消失了看起来并没有多大的变化。这是因为我们总是为我们的顶点加上采样的偏移量,而偏移量总是正的,所以所有的三角形都跑到了标签的上方把标签盖住了。我们必须修改便宜量的范围,好让顶点能向反方向偏移。将采样点的范围从0~1修改到-1~1。
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
Figure 2‑2在一定范围内的扰动
扰动强度 | Perturb Strength
现在我们已经能够确定我们的确对网格顶点进行了偏移,但是效果依旧不明显。因为目前我们在每个方向上至多偏移了一个单位长度,所以理论上的偏移量最多是√3 ≈ 1.73个单位长度,而且获得这个最大偏移量的概率非常低。而我们的单元格的外接圆半径是10,所以相对而言这点偏移实在是太少了。
解决方案是在 HexMetrics 类中添加一个偏移大小的权重。我们先将其设定为5,这样理论最大偏移量就变成了√75 ≈ 8.66个单位长度,这将产生非常显著的变化。
public const float cellPerturbStrength = 5f;
我们在 HexMesh.Perturb 方法中乘上这个系数。
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Figure 2‑3更大的强度
噪声缩放 | Noise Scale
在对网格进行编辑之前看起来还不错,但是一旦编辑了网格的高度就看起来糟糕透了。网格的纵坐标在不同的方向上偏移了太多,这是使用Perlin噪声本不应该发生的情况。
这是因为我们直接使用世界坐标对噪声进行采样。因为我们的网格要比纹理大得多,所以纹理被像瓦片一样排列了起来,这导致了我们实际上是对纹理进行了随机取样,这样就失去了应有的连续性。
Figure 2‑4 10*10的网格覆盖在蜂巢网格之上
我们需要调整采样点的数值以便纹理能够覆盖更大的区域。我们在 HexMetrics 类中设置这个参数为0.003,然后在采样的时候乘上它。
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
这样,我们的纹理就覆盖了333⅓个单位,偏移就显得连贯许多。
Figure 2‑5按比例缩小的噪声
单元格核心取平 | Leveling Cell Centers
扰动所有的顶点让我们的地图看起来更加自然,但是也导致了一些问题。因为现在单元格是凹凸不平的,所有他们的坐标标签有的插入到网格中显现不出来了。而且在陡坡与阶梯地貌的交汇处出现了缝隙。我们先解决单元格顶端凹凸不平的问题然后再搞定缝隙。
Figure 3‑1少一些死板 多一些随机
最简单的保持单元格核心为一个平面的方法是不对Y坐标进行偏移。
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; // position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Figure 3‑2相同海拔的单元格高度完全相同
这样做的话所有的点的纵坐标都不会进行偏移了,包括单元格核心和阶梯状斜坡。需要注意的是这将使最大偏移量减小到√50 ≈ 7.07,并且只在XZ平面上进行偏移。
这是一个很好的主意,这比单独控制每一个单元格容易多了。但是竖直方向上稍微有一些偏移相对来说还是比较好看的。
扰动单元格海拔 | Perturbing Cell Elevation
与其对每一个顶点进行竖直方向的扰动,我们不如对每一个单元格整体进行竖直方向上的扰动。这样就能够保证每个单元格的核心部分保持一个平面,而单元格之间依旧有高度的变化。我们需要在 HexMetrics 类中添加一个额外的参数表示竖直方向上的扰动,并将其默认大小设置为1.5。这将产生一个显著的变化,大概是一个台阶的高度。
public const float elevationPerturbStrength = 1.5f;
修改 HexCell.Elevation 属性来将偏移应用到单元格顶点坐标上。
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
为了确保扰动被立即实现,我么需要额外在 HexGrid.CreateCell 方法设置每一个的单元格的海拔高度。否则在开始的时候我们的网格将会是一个平面。在方法的最后——UI已经被创建之后再设置这个值。
void CreateCell (int x, int z, int i) { … cell.Elevation = 0; }
Figure 3‑3带有空隙的被扰动的海拔高度
使用相同高程 | Using the Same Heights
在网格中出现了很多空隙,这是因为我们在对网格进行三角剖分的时候没有使用相同的单元格高度。现在,我们来为 HexCell 类添加一个属性来获取位置坐标。
public Vector3 Position { get { return transform.localPosition; } }
我们将使用这个属性来获取单元格的中心。
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; … }
并且在 TriangulateConnection 方法中我们也会使用这个属性。
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; … } }
Figure 3‑4单元格内高度相同
单元格边界细分 | Subdividing Cell Edges
现在我们的单元格的扰动效果已经做得非常好了,但是他们还是很明显的六边形。这其实不是一个问题,但是我们可以做得更好。
Figure 4‑1六边形单元格
如果单元格拥有更多的顶点,就更加富于变化。所以,我们在单元格的每条边的终点处添加一个顶点来把每条边分成两部分。这就意味着 HexMesh.Triangulate 方法需要在需要为每个方向添加两个三角形而不是一个。
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
Figure 4‑2 12条边
我们来将顶点数再增加一倍来获取更多的变化。
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
Figure 4‑3 18条边
边界连接区域细分 | Subdividing Edge Connection
当然,我们还需要继续细分边界链接区域。所以需要为 TriangulateConnection 传递更多的顶点。
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
为 TriangulateConnection 方法相适应的参数以便其能使用更多的顶点正常工作。
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { … }
同样的,我么还需要计算出相邻单元格边界上的额外的顶点。我们可以在得到 v3、v4 定点之后就得到那几个顶点。
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
得到所有顶点后,我们就可以对边界桥进行细分了。现在我们先忽略阶梯状斜坡,而仅仅使用三个四边形平面代替它。
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
Figure 4‑4连接处细分
边界顶点分组 | Bundling Edge Vertices
我们现在需要四个顶点来描述六边形的一条边,将它们绑定分组将会是非常有意义的一件事。这比处理四个独立的顶点方便多了。我们创建一个 EdgeVertices 结构体,并顺时针储存这些顶点。
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
它们需要被序列化么?
我们只会在三角剖分的时候使用这个结构体。我们现在将不会储存边界顶点,所以不会对其进行序列化。
为结构体创建一个构造函数来方便的计算出中间顶点。
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
现在我们为 HexMesh 类创建一个单独的方法来对单元格中心到单元格边界的三角扇进行剖分。
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
同样也需要创建一个独立的方法来对连接桥进行三角剖分。
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
这样做将会大大的简化 Triangulate 方法。
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
回到 TriangulateConnection 方法。我们现在可以使用 TriangulateEdgeStrip 方法了,但是还需要做一些额外的修改。
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
阶梯状连接处的细分 | Subdividing Terraces
我们需要将边界结构体信息传递给 TriangulateEdgeTerraces 方法来对阶梯地貌进行顶点细分。
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
现在,我们需要修改 TriangulateEdgeTerraces 方法来适应新的参数。我们先假定 EdgeVertices 拥有一个用来插值的方便的静态方法。这样做能使我们简化 TriangulateEdgeTerraces 方法。
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
EdgeVertices.TerraceLerp 方法能够对两个边界顶点分割出的四个部分进行插值。
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
Figure 4‑5地形细分
重新连接阶梯地貌和陡坡 | Reconnecting Cliffs and Terraces
到目前为止,我们一直忽略了陡坡和阶梯地貌交界处的缝隙。现在到了解决这一问题的时候了。我们首先来考虑CSS和SCS的情况。
Figure 5‑1网格中的空隙
产生缝隙的原因是我们对陡坡边界上的顶点进行了偏移。这就意味着它们不会紧贴在陡坡边界上了。有时缝隙不会显得非常明显,有时却产生非常大的撕裂。
解决方案就是不再对陡坡边界上的顶点进行偏移,这就意味着我们需要对顶点扰动进行控制。最简单的方式就是创建一个单独的 AddTriangle 方法来单独处理那些不需要扰动的顶点。
void AddTriangleUnperturbed (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
让 TriangulateBoundaryTriangle 方法使用新的 AddTriangleUnperturbed 方法。这就意味着我们需要显示的扰动除了陡坡边界顶点之外的其他所有的顶点。
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), Perturb(v2), boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
因为我们现在已经不使用v2来推出其他的顶点了,所以可以直接对v2进行扰动。这样还能减少我们的代码量。
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Figure 5‑2未被扰动的边界
现在看起来好多了,但是还不够完美。在 TriangulateCornerTerracesCliff 方法中,陡坡边界的顶点是有左侧和右侧两个顶点进行插值得到的。而这两个顶点还未进行偏移。为了让陡坡边界顶点符合最终的陡坡,我们需要对被扰动之后的顶点进行插值。
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
在 TriangulateCornerCliffTerraces 方法中也是一样。
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
Figure 5‑3没有空隙的样子
双陡坡一缓坡的情况 | Double Cliffs and a Slope
还有有两个陡坡一个缓坡的情况需要处理。
Figure 5‑4巨大的空隙
这个问题可以通过在 TriangulateCornerTerracesCliff 方法末尾的 else 代码块中对顶点进行单独的偏移来解决。
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
TriangulateCornerCliffTerraces 方法中也是一样。
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Figure 5‑5最后一个缝隙
微调 | Tweaking
现在我们终于得到了被正确扰动的 Mesh 网格。网格的表现将取决于具体的噪声纹理,纹理的大小,和扰动的强度。教程里的扰动强度也许有些过大。即使不规则化的地图瞅着很好看,我们也并不像让单元格的顶点位置偏移过大,这样会使单元格难以识别,并且也难以向其中添加别的内容。
Figure 6‑1未扰动网格 vs. 扰动网格
强度为5 的扰动值看起来有点大。
https://gfycat.com/HarmlessEnormousDeer
固有核心系数从0到5(动图)
我们来将扰动强度减小为 4,这样看起来刚刚好。在这种情况下,X、Z方向上的最大偏移量为√32 ≈ 5.66。
public const float cellPerturbStrength = 4f;
Figure 6‑2单元格扰动强度:4
另外一个需要微调的参数是固有色核心面积。如果增大它的话,将会给未来添加的地图要素提供更多的空间。减小它的话,地图将会显得更加六边形化。
https://gfycat.com/BackFewAplomadofalcon
固有核心系数从0.75到0.95(动图)
将其设定为 0.8 将会为我们以后的工作提供很大的便利。
Figure 6‑3核心系数:0.8
最后,海拔高度的步长有点大,我们将其减小为 3。
public const float elevationStep = 3f;
Figure 6‑4海拔高度步长减小到3
我们也可以修改垂直方向上的最大偏移距离,但是我认为现在的值还不错。
更小的海拔高度步长使我们使用我们设置的全部7个海拔高度变为了可能。这样让我们的地图拥有更多的细节。
Figure 6‑5使用七个海拔高度等级
下一篇教程是大规模地图。
暂无关于此日志的评论。