[Catlike Coding][Hex Map 3] Elevation

Elevation

添加单元格海拔。

三角形的斜坡。

插入阶梯。

合并阶梯和悬崖。

本教程是hexagon maps系列的第三部分。这次,我们将添加对不同标高的支持,并在它们之间创建特殊的过渡。

 海拔和阶梯。

1. Cell Elevation

我们已将地图划分为离散单元,以覆盖一个平坦区域。现在,我们将为每个单元提供其自己的标高。我们将使用离散高程,因此将其存储在HexCell中的整数字段中。

	public int elevation;

每个连续的标高台阶应该有多高?我们可以使用任何值,所以让我们将其定义为另一个HexMetrics常量。我将在每一步中使用五个单位,这将产生非常明显的过渡。对于实际的游戏,我可能会使用较小的步长。

	public const float elevationStep = 5f;

1.1 Editing Cells

到目前为止,我们只能编辑单元格的颜色,但现在我们还可以更改其高程。因此,HexGrid.ColorCell方法不再足够。此外,我们以后可能会为每个单元格添加更多可编辑选项。这需要一种新的编辑方法。

将ColorCell重命名为GetCell,并使其返回给定位置的单元格,而不是设置其颜色。由于它现在不再改变任何东西,我们也不应该立即三角化细胞。

	public HexCell GetCell (Vector3 position) {
		position = transform.InverseTransformPoint(position);
		HexCoordinates coordinates = HexCoordinates.FromPosition(position);
		int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2;
		return cells[index];
	}

现在由编辑器来调整单元格。完成后,需要再次对网格进行三角剖分。添加一个公共HexGrid.Refresh方法来解决这个问题。

	public void Refresh () {
		hexMesh.Triangulate(cells);
	}

更改HexMapEditor,使其与新方法配合使用。为它提供一个新的EditCell方法,该方法负责单元格的所有编辑,然后刷新网格。

	void HandleInput () {
		Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
		RaycastHit hit;
		if (Physics.Raycast(inputRay, out hit)) {
			EditCell(hexGrid.GetCell(hit.point));
		}
	}

	void EditCell (HexCell cell) {
		cell.color = activeColor;
		hexGrid.Refresh();
	}

只需将选定的标高指定给正在编辑的单元,即可调整标高。

	int activeElevation;

	void EditCell (HexCell cell) {
		cell.color = activeColor;
		cell.elevation = activeElevation;
		hexGrid.Refresh();
	}

就像使用颜色一样,我们需要一种方法来设置活动的标高,我们将链接到UI。我们将使用滑块从高程范围中进行选择。由于滑块使用浮动,我们的方法需要一个浮动参数。我们将把它转换成一个整数。

	public void SetElevation (float elevation) {
		activeElevation = (int)elevation;
	}

通过GameObject/Create/slider将滑块添加到画布,并将其放置在颜色面板下方。使其成为一个从下到上的垂直滑块,以便在视觉上与标高匹配。将其限制为整数,并给它一个合理的范围,如从0到6。然后将其On Value Changed事件挂钩到我们的Hex Map Editor对象的SetElevation方法。确保从动态列表中选择该方法,以便使用滑块的值调用该方法。

ui

component

 高度滑块。

 1.2 Visualizing Elevation

编辑单元时,我们现在同时设置其颜色和标高。虽然可以检查inspector以查看高程是否确实发生了变化,但三角测量过程仍然会忽略它。

我们所需要做的就是在高程发生变化时调整单元的垂直局部位置。为了方便起见,让我们将HexCell.elevation设置为私有,并添加一个公共的HexCell.elevation属性。

	public int Elevation {
		get {
			return elevation;
		}
		set {
			elevation = value;
		}
	}
	
	int elevation;

现在,无论何时编辑单元的高程,都可以调整单元的垂直位置。

		set {
			elevation = value;
			Vector3 position = transform.localPosition;
			position.y = value * HexMetrics.elevationStep;
			transform.localPosition = position;
		}

当然,这需要在HexMapEditor.EditCell中进行小的调整。

	void EditCell (HexCell cell) {
		cell.color = activeColor;
		cell.Elevation = activeElevation;
		hexGrid.Refresh();
	}

 不同高度的细胞。

网格碰撞器是否调整以匹配新高程?

旧版本的Unity需要在再次指定相同网格之前将网格碰撞器设置为null。它只是假设网格不会更改,因此只有不同的网格(或null)触发碰撞器刷新。这已不再必要。因此,我们目前的方法——在三角剖分后将网格重新分配给碰撞器——已经足够了。

单元高程现在可见,但存在两个问题。首先,单元格标签在升高的单元格下方消失。其次,单元之间的连接忽略高程。让我们来解决这个问题。

1.3 重新定位单元格Lables

目前,单元格的UI标签只创建和定位一次,然后就被遗忘了。为了更新他们的垂直位置,我们必须跟踪他们。让我们为每个HexCell提供一个对其UI标签的RectTransform的引用,以便以后可以对其进行更新。

	public RectTransform uiRect;

在HexGrid.CreateCell的末尾指定它们。

	void CreateCell (int x, int z, int i) {
		…
		cell.uiRect = label.rectTransform;
	}

现在我们可以展开HexCell.Elevation属性来调整其单元格UI的位置。因为六边形网格画布被旋转了,所以标签必须沿负Z方向而不是正Y方向移动。

		set {
			elevation = value;
			Vector3 position = transform.localPosition;
			position.y = value * HexMetrics.elevationStep;
			transform.localPosition = position;

			Vector3 uiPosition = uiRect.localPosition;
			uiPosition.z = elevation * -HexMetrics.elevationStep;
			uiRect.localPosition = uiPosition;
		}

 升高的label。

1.4 创建坡度

接下来,我们必须将平面单元连接转换为坡度。这是在hextmesh.TriangulateConnection中完成的。在边缘连接的情况下,我们必须覆盖桥的另一端的高度。

		Vector3 bridge = HexMetrics.GetBridge(direction);
		Vector3 v3 = v1 + bridge;
		Vector3 v4 = v2 + bridge;
		v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep;

在角落连接的情况下,我们必须对下一个邻居的网桥进行同样的操作。

		if (direction <= HexDirection.E && nextNeighbor != null) {
			Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next());
			v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep;
			AddTriangle(v2, v4, v5);
			AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color);
		}

 高架连接。

我们现在支持不同高度的单元,它们之间有正确倾斜的连接。但我们不能就此止步。我们要让那些斜坡更有趣。

unitypackage

2. 阶梯式边缘连接

笔直的斜坡看起来没那么有趣。我们可以通过添加阶梯将其分为多步。《无尽的传奇》就是这样一款游戏。

例如,我们可以为每个坡度插入两个阶地。因此,一个大坡度变为三个小坡度,中间有两个平坦区域。为了对其进行三角剖分,我们必须将每个连接分为五个步骤。

 斜坡上的两个阶梯。

我们可以用HexMetrics定义每个坡度的阶地数量,并由此导出台阶数量。

	public const int terracesPerSlope = 2; // 每个斜坡的台阶数量

	public const int terraceSteps = terracesPerSlope * 2 + 1; // 每个斜坡的步骤数, 台阶数+小斜坡数

理想情况下,我们可以简单地沿坡度插值每一步。这并不是很简单,因为Y坐标只能在奇数步(小斜坡)上改变,而不能在偶数步(平台)上改变。否则我们就不会有平坦的露台。让我们向HexMetrics添加一个特殊的插值方法来解决这个问题。

	public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) {
		return a;
	}

如果我们知道插值步长的话,水平插值是简单的。

	public const float horizontalTerraceStepSize = 1f / terraceSteps;
	
	public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) {
		float h = step * HexMetrics.horizontalTerraceStepSize;
		a.x += (b.x - a.x) * h;
		a.z += (b.z - a.z) * h;
		return a;
	}

    // (1 - t) * a + t * b
    // a + (b - a) * t

要仅在奇数步数上调整Y,我们可以使用(step + 1) / 2.  如果我们使用整数除法,它会将序列1、2、3、4转换为1、1、2、2。

	public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1);
	
	public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) {
		float h = step * HexMetrics.horizontalTerraceStepSize;
		a.x += (b.x - a.x) * h;
		a.z += (b.z - a.z) * h;
		float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize;
		a.y += (b.y - a.y) * v;
		return a;
	}

让我们为颜色添加一个阶梯插值方法。就像连接是平的一样进行插值。

	public static Color TerraceLerp (Color a, Color b, int step) {
		float h = step * HexMetrics.horizontalTerraceStepSize;
		return Color.Lerp(a, b, h);
	}

2.1 三角剖分

由于对边连接进行三角剖分将变得更加复杂,请从HexMesh.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.Elevation * HexMetrics.elevationStep;

		TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor);
//		AddQuad(v1, v2, v3, v4);
//		AddQuadColor(cell.color, neighbor.color);
		…
	}

	void TriangulateEdgeTerraces (
		Vector3 beginLeft, Vector3 beginRight, HexCell beginCell,
		Vector3 endLeft, Vector3 endRight, HexCell endCell
	) {
		AddQuad(beginLeft, beginRight, endLeft, endRight);
		AddQuadColor(beginCell.color, endCell.color);
	}

让我们从流程的第一步开始。使用我们的特殊插值方法创建第一个四边形。这将产生比原始坡度更陡的短坡度。

void TriangulateEdgeTerraces (
		Vector3 beginLeft, Vector3 beginRight, HexCell beginCell,
		Vector3 endLeft, Vector3 endRight, HexCell endCell
	) {
		Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1);
		Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1);
		Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1);

		AddQuad(beginLeft, beginRight, v3, v4);
		AddQuadColor(beginCell.color, c2);
	}

 阶梯第一步

现在立即跳到最后一步,跳过中间的所有内容。这将完成我们的边缘连接,尽管形状还不正确。

		AddQuad(beginLeft, beginRight, v3, v4);
		AddQuadColor(beginCell.color, c2);

		AddQuad(v3, v4, endLeft, endRight);
		AddQuadColor(c2, endCell.color);

阶梯最后一步

中间步骤可以添加一个循环。每一步,前两个顶点将成为新的前两个顶点。颜色也是如此。然后计算新的向量和颜色,并添加另一个四边形。

 中间的所有步骤

现在,所有边缘连接都有两个平台,或者选择将HexMetrics.terracesPerSlope设置为多少。当然,我们还没有把拐角处的连接处改成阶梯。我们以后再谈。

所有边缘被台阶链接

 unitypackage

3. 连接类型

将所有边缘连接转换为阶梯可能不是一个好主意。当高差只有一个标高时,它看起来很好。但更大的差异会产生狭窄的阶梯,阶梯之间有很大的跳跃,这看起来并不太好。此外,平面连接根本不需要是阶梯式的。

让我们将其形式化,并定义三种边类型。平坦、斜坡和悬崖。为此创建一个新枚举。

public enum HexEdgeType {
	Flat, Slope, Cliff
}

我们如何确定我们正在处理的是哪种联系?我们可以在HexMetrics中添加一个方法,以基于两个高程级别导出该方法。

	public static HexEdgeType GetEdgeType (int elevation1, int elevation2) {
	}

如果标高相同,则我们有一个平边。

	public static HexEdgeType GetEdgeType (int elevation1, int elevation2) {
		if (elevation1 == elevation2) {
			return HexEdgeType.Flat;
		}
	}

如果高度差正好是一,那么我们有一个斜坡。坡度是上升还是下降都无所谓。在所有其他情况下,我们都有悬崖。

	public static HexEdgeType GetEdgeType (int elevation1, int elevation2) {
		if (elevation1 == elevation2) {
			return HexEdgeType.Flat;
		}
		int delta = elevation2 - elevation1;
		if (delta == 1 || delta == -1) {
			return HexEdgeType.Slope;
		}
		return HexEdgeType.Cliff;
	}

我们还将添加一个方便的HexCell.GetEdgeType方法,以获取特定方向上的单元格边缘类型。

	public HexEdgeType GetEdgeType (HexDirection direction) {
		return HexMetrics.GetEdgeType(
			elevation, neighbors[(int)direction].elevation
		);
	}

我们不应该检查一下这个方向上是否有邻居?

最终,您可能会向恰好位于地图边界上的方向请求边类型。在这种情况下,将没有邻居,我们将得到一个NullReferenceException。我们可以在方法中检查这一点,如果是这样,我们必须抛出某种异常。但这已经发生了,所以没有必要明确地这样做。也就是说,除非您想抛出自定义异常。

请注意,我们只在已经知道不处理边界时才使用此方法。如果我们确实在某个地方出错,我们将得到NullReferenceException。

3.1 限制斜坡阶梯

现在我们可以确定正在处理的连接类型,我们可以决定是否插入阶梯。调整HexMesh.TriangulateConnection,使其仅为斜坡创建阶梯。

		if (cell.GetEdgeType(direction) == HexEdgeType.Slope) {
			TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor);
		}
//		AddQuad(v1, v2, v3, v4);
//		AddQuadColor(cell.color, neighbor.color);

在这一点上,我们可以重新激活我们之前注释掉的代码,以处理平地和悬崖。

  只有斜坡有阶梯

unitypackage

4 阶梯角连接

角连接比边连接更复杂,因为它们涉及三个单元而不是两个单元。每个角连接到三条边,这三条边可以是平面、斜坡或悬崖。因此有许多可能的配置。与边连接一样,我们最好在HexMesh中添加一种新的三角剖分方法。

我们的新方法需要角落三角形的顶点和相连接的单元。为了便于管理,让我们对连接进行排序,以便知道哪个单元的高程最低。然后我们可以从底部到左侧和右侧工作。

角落连接

	void TriangulateCorner (
		Vector3 bottom, HexCell bottomCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		AddTriangle(bottom, left, right);
		AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color);
	}

现在三角形连接必须计算出最低的单元是什么。首先,检查被三角剖分的单元是否低于其相邻单元,或者是否为最低值。如果是这种情况,我们可以将其用作底部单元格。

	void TriangulateConnection (
		HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2
	) {
		…
		
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (direction <= HexDirection.E && nextNeighbor != null) {
			Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next());
			v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep;
			
			if (cell.Elevation <= neighbor.Elevation) {
				if (cell.Elevation <= nextNeighbor.Elevation) {
					TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor);
				}
			}
		}
	}

如果最里面的检查失败,则表示下一个邻居是最低的单元。我们必须逆时针旋转三角形以保持正确的方向。

			if (cell.Elevation <= neighbor.Elevation) {
				if (cell.Elevation <= nextNeighbor.Elevation) {
					TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor);
				}
				else {
					TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor);
				}
			}

如果第一次检查已经失败,它将成为两个相邻单元之间的竞争。如果边邻居是最低的,那么我们必须顺时针旋转,否则逆时针旋转。

			if (cell.Elevation <= neighbor.Elevation) {
				if (cell.Elevation <= nextNeighbor.Elevation) {
					TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor);
				}
				else {
					TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor);
				}
			}
			else if (neighbor.Elevation <= nextNeighbor.Elevation) {
				TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell);
			}
			else {
				TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor);
			}

逆时针、不旋转和顺时针旋转。

4.1 斜坡的三角化

要知道如何对角进行三角测量,我们必须知道我们正在处理的边类型。 为方便起见,让我们向 HexCell 添加另一个方便的方法,用于确定任意两个单元格之间的斜率。

	public HexEdgeType GetEdgeType (HexCell otherCell) {
		return HexMetrics.GetEdgeType(
			elevation, otherCell.elevation
		);
	}

在 Hex Mesh.TriangulateCorner 中使用这种新方法来确定左右边缘的类型。

	void TriangulateCorner (
		Vector3 bottom, HexCell bottomCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell);
		HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell);

		AddTriangle(bottom, left, right);
		AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color);
	}

如果两个边缘都是斜坡,那么我们在左侧和右侧都有阶梯。 此外,因为底部单元格最低,我们知道这些斜率上升。 此外,这意味着左右单元格具有相同的高程,因此顶部边缘连接是平坦的。 我们可以将这种情况识别为斜坡 - 斜坡 - 平坦,或简称 SSF。

  两个斜坡, 一个平坦

检查我们是否处于这种情况,如果是,则调用新方法 TriangulateCornerTerraces。 之后,从方法返回。 将此检查放在旧的三角测量代码之前,因此它将替换默认三角形。

	void TriangulateCorner (
		Vector3 bottom, HexCell bottomCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell);
		HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell);

		if (leftEdgeType == HexEdgeType.Slope) {
			if (rightEdgeType == HexEdgeType.Slope) {
				TriangulateCornerTerraces(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
				return;
			}
		}

		AddTriangle(bottom, left, right);
		AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color);
	}
	
	void TriangulateCornerTerraces (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		
	}

只要我们不在 TriangulateCornerTerraces 内做任何事情,一些双坡角连接就会变成洞。 是否成为一个洞取决于哪个单元格最终成为底部单元格。

 出现了一个空洞

为了填满这个洞,我们必须跨越缝隙连接左右阶梯。 该方法与边缘连接相同,但在三色三角形内而不是双色四边形内。 让我们再次从第一步开始,现在是一个三角形。

	void TriangulateCornerTerraces (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1);
		Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1);
		Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1);
		Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1);

		AddTriangle(begin, v3, v4);
		AddTriangleColor(beginCell.color, c3, c4);
	}

 第一个步骤三角形。

再次,直接跳到最后一步。 它是一个四边形,形成一个梯形。 边缘连接的唯一区别是我们在这里处理四种不同的颜色,而不是两种。

		AddTriangle(begin, v3, v4);
		AddTriangleColor(beginCell.color, c3, c4);

		AddQuad(v3, v4, left, right);
		AddQuadColor(c3, c4, leftCell.color, rightCell.color);

  最后一步四边形

中间的所有步骤也都是四边形。

		AddTriangle(begin, v3, v4);
		AddTriangleColor(beginCell.color, c3, c4);

		for (int i = 2; i < HexMetrics.terraceSteps; i++) {
			Vector3 v1 = v3;
			Vector3 v2 = v4;
			Color c1 = c3;
			Color c2 = c4;
			v3 = HexMetrics.TerraceLerp(begin, left, i);
			v4 = HexMetrics.TerraceLerp(begin, right, i);
			c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i);
			c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i);
			AddQuad(v1, v2, v3, v4);
			AddQuadColor(c1, c2, c3, c4);
		}

		AddQuad(v3, v4, left, right);
		AddQuadColor(c3, c4, leftCell.color, rightCell.color);

 所有步骤

4.2 双斜坡扩展

双斜坡情况下有两个方向不同的变体,具体取决于最终作为底部单元的单元。我们可以通过检查左右组合slope flat和flat slope来找到它们。

 SFS and FSS.

如果右边缘是平的,那么我们必须从左侧而不是底部开始分层。如果左边是平的,那么我们必须从右边开始。

		if (leftEdgeType == HexEdgeType.Slope) {
			if (rightEdgeType == HexEdgeType.Slope) {
				TriangulateCornerTerraces(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
				return;
			}
			if (rightEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					left, leftCell, right, rightCell, bottom, bottomCell
				);
				return;
			}
		}
		if (rightEdgeType == HexEdgeType.Slope) {
			if (leftEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
				return;
			}
		}

这将使我们的阶梯不间断地围绕着单元流动,直到它们遇到悬崖或地图的尽头。

  连续的阶梯。

unitypackage

5. 合并斜坡和悬崖

那么当斜坡遇到悬崖时呢? 如果我们知道左边是斜坡,右边是悬崖,那么顶边是什么? 它不能是平坦的,但可以是斜坡或悬崖。

two slopes

diagram

two cliffs

  SCS and SCC

让我们添加一种新方法来同时处理斜坡-悬崖情况。

	void TriangulateCornerTerracesCliff (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		
	}

当左边缘是斜坡时,它必须作为 TriangulateCorner 中的最后一个选项被调用。

		if (leftEdgeType == HexEdgeType.Slope) {
			if (rightEdgeType == HexEdgeType.Slope) {
				TriangulateCornerTerraces(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
				return;
			}
			if (rightEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					left, leftCell, right, rightCell, bottom, bottomCell
				);
				return;
			}
			TriangulateCornerTerracesCliff(
				bottom, bottomCell, left, leftCell, right, rightCell
			);
			return;
		}
		if (rightEdgeType == HexEdgeType.Slope) {
			if (leftEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
				return;
			}
		}

那么我们如何进行三角化呢? 这个问题可以分为两部分,底部和顶部。

5.1 底部三角化

底部左侧有梯田,右侧有悬崖。 我们必须以某种方式合并它们。 一个简单的方法是折叠梯田,让它们在右角相遇。 这将使梯田向上逐渐变细。

 折叠阶梯

 但我们实际上不想让它们在右角相遇,因为这会干扰顶部可能存在的梯田。 此外,我们可能正在处理一个非常高的悬崖,这将导致非常陡峭和细长的三角形。 相反,我们将它们折叠到位于悬崖边的边界点。

 在边界折叠

让我们将此边界点放置在底部单元上方一个高程处。 我们可以通过基于高程差的插值来找到它。

	void TriangulateCornerTerracesCliff (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		float b = 1f / (rightCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, right, b);
		Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);
	}

看看我们是否做对了,用一个三角形覆盖整个底部。

		float b = 1f / (rightCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, right, b);
		Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);

		AddTriangle(begin, left, boundary);
		AddTriangleColor(beginCell.color, leftCell.color, boundaryColor);

 下面的三角形

边界在正确的位置后,我们可以继续对阶梯进行三角化。 再一次,让我们从第一步开始。

		float b = 1f / (rightCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, right, b);
		Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);

		Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1);
		Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1);

		AddTriangle(begin, v2, boundary);
		AddTriangleColor(beginCell.color, c2, boundaryColor);

折叠的第一步

这一次,最后一步也是一个三角形。

		AddTriangle(begin, v2, boundary);
		AddTriangleColor(beginCell.color, c2, boundaryColor);

		AddTriangle(v2, left, boundary);
		AddTriangleColor(c2, leftCell.color, boundaryColor);

 折叠的最后一步

中间的所有步骤也是三角形。

		AddTriangle(begin, 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);
			AddTriangle(v1, v2, boundary);
			AddTriangleColor(c1, c2, boundaryColor);
		}

		AddTriangle(v2, left, boundary);
		AddTriangleColor(c2, leftCell.color, boundaryColor);

 折叠阶梯

我们不能保持梯田水平吗?
我们确实可以通过在开始点和边界点之间进行插值来保持梯田平坦,而不是总是使用边界点。 这将需要使用四边形作为梯田之间的斜坡。 然而,这些四边形不会位于平面上,因为它们的左右边不会有相同的斜率。 结果看起来很乱

5.2 完成角落

底部部分完成后,我们可以查看顶部部分。如果顶部边缘是斜坡,我们需要再次连接阶梯和悬崖。让我们把代码移到它自己的方法。

	void TriangulateCornerTerracesCliff (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		float b = 1f / (rightCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, right, b);
		Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);

		TriangulateBoundaryTriangle(
			begin, beginCell, left, leftCell, boundary, boundaryColor
		);
	}

	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);

		AddTriangle(begin, 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);
			AddTriangle(v1, v2, boundary);
			AddTriangleColor(c1, c2, boundaryColor);
		}

		AddTriangle(v2, left, boundary);
		AddTriangleColor(c2, leftCell.color, boundaryColor);
	}

现在完成上面的部分很简单。如果有坡度,请添加旋转的边界三角形。否则,一个简单的三角形就足够了。

	void TriangulateCornerTerracesCliff (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		float b = 1f / (rightCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, right, b);
		Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);
		
		TriangulateBoundaryTriangle(
			begin, beginCell, left, leftCell, boundary, boundaryColor
		);

		if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) {
			TriangulateBoundaryTriangle(
				left, leftCell, right, rightCell, boundary, boundaryColor
			);
		}
		else {
			AddTriangle(left, right, boundary);
			AddTriangleColor(leftCell.color, rightCell.color, boundaryColor);
		}
	}

double slope

single slope

 完成两部分的三角剖分。

 

 5.3 镜框

我们已经覆盖了斜坡悬崖案例。还有两个案例,左边是悬崖。

 CSS and CSC.

方法与之前相同,但由于方向不同,存在一些小差异。复制 TriangulateCornerTerracesCliff 并进行相应调整。我只标出了不同之处。

	void TriangulateCornerCliffTerraces (
		Vector3 begin, HexCell beginCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		float b = 1f / (leftCell.Elevation - beginCell.Elevation);
		Vector3 boundary = Vector3.Lerp(begin, left, b);
		Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b);

		TriangulateBoundaryTriangle(
			right, rightCell, begin, beginCell, boundary, boundaryColor
		);

		if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) {
			TriangulateBoundaryTriangle(
				left, leftCell, right, rightCell, boundary, boundaryColor
			);
		}
		else {
			AddTriangle(left, right, boundary);
			AddTriangleColor(leftCell.color, rightCell.color, boundaryColor);
		}
	}

将这些案例包括在TriangulateCorner 中。

		if (leftEdgeType == HexEdgeType.Slope) {
			…
		}
		if (rightEdgeType == HexEdgeType.Slope) {
			if (leftEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
				return;
			}
			TriangulateCornerCliffTerraces(
				bottom, bottomCell, left, leftCell, right, rightCell
			);
			return;
		}

 double slope

 single slope

CSS和CSC三角化。

 5.4 两个悬崖

剩下的唯一非平坦情况是底部单元两侧都有悬崖的情况。这将保留顶部边缘的所有选项。它可以是平坦的、斜坡的或悬崖的。我们只对悬崖斜坡案例感兴趣,因为它是唯一一个有阶梯的案例。

实际上,有两种不同的悬崖坡度,取决于哪一侧更高。他们互相映照。让我们将它们标识为CCSR和CCSL。

 

right higher

left higher

  

diagram

 CCSR and CCSL.

通过使用不同的单元格旋转调用我们的TriangulateCornerCliffTerries和TriangulateCornerTerriesCliffIf方法,我们可以在TriangulateCorner中涵盖这两种情况。

		if (leftEdgeType == HexEdgeType.Slope) {
			…
		}
		if (rightEdgeType == HexEdgeType.Slope) {
			…
		}
		if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) {
			if (leftCell.Elevation < rightCell.Elevation) {
				TriangulateCornerCliffTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
			}
			else {
				TriangulateCornerTerracesCliff(
					left, leftCell, right, rightCell, bottom, bottomCell
				);
			}
			return;
		}

然而,这将产生一个奇怪的三角化。这是因为我们现在从上到下进行三角剖分。这会导致边界插值为负,这是不正确的。解决方案是确保插值步长始终为正。

right higher

left higher

三角化 CCSR and CCSL

 

5.5 清理

我们现在已经涵盖了所有需要特殊处理的情况,以确保阶梯正确地进行三角形划分。

  完成阶梯的三角测量

通过去掉return语句,使用else块,我们可以稍微清理一下TriangulateCorner。

	void TriangulateCorner (
		Vector3 bottom, HexCell bottomCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell);
		HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell);

		if (leftEdgeType == HexEdgeType.Slope) {
			if (rightEdgeType == HexEdgeType.Slope) {
				TriangulateCornerTerraces(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
			}
			else if (rightEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					left, leftCell, right, rightCell, bottom, bottomCell
				);
			}
			else {
				TriangulateCornerTerracesCliff(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
			}
		}
		else if (rightEdgeType == HexEdgeType.Slope) {
			if (leftEdgeType == HexEdgeType.Flat) {
				TriangulateCornerTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
			}
			else {
				TriangulateCornerCliffTerraces(
					bottom, bottomCell, left, leftCell, right, rightCell
				);
			}
		}
		else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) {
			if (leftCell.Elevation < rightCell.Elevation) {
				TriangulateCornerCliffTerraces(
					right, rightCell, bottom, bottomCell, left, leftCell
				);
			}
			else {
				TriangulateCornerTerracesCliff(
					left, leftCell, right, rightCell, bottom, bottomCell
				);
			}
		}
		else {
			AddTriangle(bottom, left, right);
			AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color);
		}
	}

最后一个else块涵盖了我们尚未涵盖的所有剩余案例。这些情况为FFF、CCF、CCCR和CCCL。它们都由一个三角形覆盖。

 所有情况

下一个教程是Irregularity.

unitypackage

PDF

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值