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方法。确保从动态列表中选择该方法,以便使用滑块的值调用该方法。
高度滑块。
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);
}
高架连接。
我们现在支持不同高度的单元,它们之间有正确倾斜的连接。但我们不能就此止步。我们要让那些斜坡更有趣。
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设置为多少。当然,我们还没有把拐角处的连接处改成阶梯。我们以后再谈。
所有边缘被台阶链接
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);
在这一点上,我们可以重新激活我们之前注释掉的代码,以处理平地和悬崖。
只有斜坡有阶梯
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;
}
}
这将使我们的阶梯不间断地围绕着单元流动,直到它们遇到悬崖或地图的尽头。
连续的阶梯。
5. 合并斜坡和悬崖
那么当斜坡遇到悬崖时呢? 如果我们知道左边是斜坡,右边是悬崖,那么顶边是什么? 它不能是平坦的,但可以是斜坡或悬崖。
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);
}
}
完成两部分的三角剖分。
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;
}
CSS和CSC三角化。
5.4 两个悬崖
剩下的唯一非平坦情况是底部单元两侧都有悬崖的情况。这将保留顶部边缘的所有选项。它可以是平坦的、斜坡的或悬崖的。我们只对悬崖斜坡案例感兴趣,因为它是唯一一个有阶梯的案例。
实际上,有两种不同的悬崖坡度,取决于哪一侧更高。他们互相映照。让我们将它们标识为CCSR和CCSL。
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;
}
然而,这将产生一个奇怪的三角化。这是因为我们现在从上到下进行三角剖分。这会导致边界插值为负,这是不正确的。解决方案是确保插值步长始终为正。
三角化 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.