六边形网格地图3 | 阶梯状地形实现

作者:arthliant
2017-10-21
12 11 1

前注

本文是 Jasper Flick 的 Unity 教程中的六边形网格地图系列教程的第三篇。
原文地址:http://catlikecoding.com/unity/tutorials/hex-map/part-3/
译者获得作者授权翻译转载于 indienova,后续教程将会陆续翻译。
本人也是初学者,如有错译,望海涵并及时纠正。

前言

为单元格添加海拔高度

斜坡的三角剖分

插入阶梯

融合阶梯与陡坡

这篇教程是六边形网格地图系列教程的第三部分。这次,我们将会加入不同的海拔高度,并且在其中加入不同的过渡效果。

1

Figure 1‑1‑1海拔高度与阶梯效果

单元格海拔 | Cell Elevation

我们已经将我们的地图分割为能够覆盖一个平面区域的众多单元格。现在我们需要让每一个单元格拥有自己的海拔高度。我们将设置离散的海拔高度,所以将其用 int 储存在 HexCell 类中。

public int  elevation;

每一个连续的海拔高度的步长应该是多大呢?我们可以将其设定为一个 HexMetrics 类的常量,默认值为5,这将产生相当明显的效果。如果在做一个真实的游戏的话,我也许会选择小一点的步长。

public const  float elevationStep = 5f;

编辑单元格 | Editing Cells

到目前为止,我们只能编辑单元格的颜色,然而现在,我们可以设置单元格的高度了。故 HexGrid.ColorCell 方法已经不够用了,而且我们以后将会为单元格添加更多的可编辑属性·。这将需要一个新的编辑函数。

将 ColorCel l 方法重命名为 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 组件绑定的方法来动态地调整海拔高度。我们将使用一个 Slider 滑块来从海拔高度范围内选择高度。因为 Slider 使用 float 类型,所以我们的方法需要一个 float 类型的参数,然后将其强转为 int。

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

向 Canvas 添加一个 Slider 对象并将其设置为颜色选择 Panel 的子对象。将其设定为由下至上的垂直方向的 Slider 以便对应海拔高度。将其限定为整数并给定一个合理的范围,例如,从0到6。然后将 HexMapEditor 类的 SetElevation 方法注册到 OnValueChanged 事件上。确保从下拉列表里选择方法,那样它才会在  Sliver 数值改变时被调用。
2

Figure 1‑1高度滑块

实现海拔高度效果 | 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.EditorCell 方法进行一些小小的改动。

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

 

3

Figure 1‑2不同海拔高度的单元格

网格碰撞器 MeshCollider 会适应新的高度么?

现在,单元格的不同海拔高度已经显现出来了,但是还有两个问题。首先,单元格的坐标标签被压在了被提升高度的单元格下面。其次,单元格的连接部分被高度差割裂了。下面,我们来解决这两个问题。

重新定位单元格标签 | Repositioning Cell Labels

到目前为止,UI 标签都是在创建时就被定位然后就被丢弃不管了。为了更新它们的竖直坐标,我们需要实时持有它们的对象。我们来给 HexCell 类添加上自己的 UI 标签的 RectTransform 的引用,以备以后更新它。

 public  RectTransform uiRect;

在HexGrid.CreateCell方法末尾就为他们赋值。

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

现在我们可以拓展 HexCell.Elevation 属性来修改单元格 UI 的坐标。因为单元格的 Canvas 已经被旋转过了,所以标签需要沿着 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;
}

 

4

Figure 1‑3被抬高的标签

创建斜坡 | Creating Slopes

下一步,我们将使单元格之间的连接转变为斜坡。这将在HexMesh.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);
}

 

5

Figure 1‑4被抬高的连接处

我们的地图现在已经能够支持带有斜坡的具有海拔高度的单元格了。但我们不会止步于此。我们还打算让这些斜坡变得更加有趣。

unitypackage

阶梯状边界连接 | Terraced Edge Connections

笔直地斜坡瞅着并不好看,我们可以将其分割成阶梯状,《无尽的传说(Endless Legend)》就是这么做的。

比方说,我们可以为每一个斜坡插入两个平台。这样的话,一个大斜坡就会被分割为带有两个平台的三个小斜坡。为了将其进行三角剖分,我们需要将一个连接分为五段。

 

6

Figure 2‑1带有连个平台的斜坡

我们可以在 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;
}

在两个值之间进行插值是如何实现的?

我们可以使用 (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);
}

三角剖分 | Triangulation

因为对边界连接处的三角剖分将会复杂得多,所以我们将 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);

 

7

Figure 2‑2 最后一步连接

通过一个循环来加入中间的步骤。在每一步中,都将新旧两组顶点切换顺序,颜色也是一样。

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

for (int i = 2; i < HexMetrics.terraceSteps; i++) {
    Vector3 v1 = v3;
    Vector3 v2 = v4;
    Color c1 = c2;
    v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i);
    v4 = HexMetrics.TerraceLerp(beginRight, endRight, i);
    c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i);
    AddQuad(v1, v2, v3, v4);
    AddQuadColor(c1, c2);
}

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

8

Figure 2‑3连接处的所有台阶

现在所有的连接处都有两个台阶,或者是你在HexMetrics.terracesPerSlope 中对其设定的个数。我们还没有对顶点连接处设置台阶呢,一会儿我们再来做这件事。

9

Figure 2‑3所有的边界都被台阶化了

unitypackage

连接类型 | Connection Types

将所有的边界连接处都进行台阶化也许并不是一个好主意。这在高差比较小的时候看起来还不错,但是大高差的连接处产生了窄窄的平台并且平台之间高差也过大,这并不好看。所以不是所有的单元格间的连接都需要台阶化。

我们将连接的类型规范化为三种,平地、斜坡和绝壁。创建一个枚举类来保存它。

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

如果海拔高差只有1点的话就是斜坡。无所谓是向上的斜坡还是向下的斜坡。剩余的情况就是绝壁。

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

难道我们不应该检测某个方向上的邻居是否存在么?

限制阶梯的倾斜角度 | Limiting Terraces to Slopes

既然我们已经能够判断连接的种类了,我们就可以决定是否对其进行阶梯化。修改 HexMesh.TriangluateConnection 使其只为斜坡类型的连接插入台阶。

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

现在我们可以恢复以前注释掉了的代码来观察一下效果。

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

 

10

Figure 3‑1只在斜坡上产生台阶

unitypackage

阶梯化顶角连接 | Terraced Corner Connections

顶角连接处要比边界连接复杂得多,因为其涉及到三个单元格而不是两个。每个顶角还连接了三条边,它们可能是平地、斜坡亦或是绝壁。所以顶角有太多的可能性了。就像对边界连接处做的一样,我们要在 HexMesh 类里为顶角连接处创建新的三角剖分方法。

我们新的方法需要连接处三角形的顶点和与其相邻的单元格。为了便于管理,我们为相连的三个单元格排个序,首先是高程最低的然后是其左侧相连的、最后是右侧的。

11

Figure 4‑1顶角连接处
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);
}

现在 TriangulateConnection 方法需要去计算出哪一个单元格的海拔最低。首先,检查正在被绘制的单元格的海拔是否比其邻居低,或者并列最低。如果是的话我们会将其复制给 bottom。

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

如果内部的检测没有通过,这说明 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);
}

 

12

Figure 4‑2顺时针、无旋转、逆时针

三角形有两个临边都是斜坡的情况 | Slope Triangulation

我了确定如何对三角形连接处进行阶梯化,我们需要知道它的边界类型。为此我们需要在 HexCell 类中创建一个新的方法来判断单元格之间的连接类型。

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

在 HexMesh.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);
}

如果两个临边都是斜坡的话,我们就可以对其进行阶梯化了。而且,因为 bottom 永远都是最低的那个单元格,所以我们很容易判断斜坡的方向。此外,因为左右两个临边都是斜坡,所以顶端的连接处一定是平地。我们将这种情况称为 SSF (左右上)。

13

Figure 4‑3 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 方法中还什么都没做,所以 SSF 情况下的三角形连接处留下了一个空洞。

14

Figure 4‑4一个三角形空洞

为了填补这个孔洞,我们需要将左右两个阶梯状斜坡连接起来。和边界桥一样,直接使用插值的方式就可以了,唯一不同的地方在于这里我们需要处理三个顶点而不是四个。同样的,我们还是先来进行第一步,插入第一个三角形。

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

 

15

Figure 4‑5第一个三角形

然后,我们跳过中间的步骤直接进行最后一步。这将会形成一个梯形。与边界桥不同的是此处我们需要使用四种颜色而不是两种。

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

16

Figure 4‑6最后一步的四边形

中间的所有分段也都是四边形。

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

 

17

Figure 4‑7最终效果

SSF情况的变体 | Dual-slope Variants

三角形的两个斜坡边可能处于不同的位置,这取决于最低的单元格的位置。他们的区别在于当前单元格的哪一侧临边是斜坡。

18

Figure 4‑8 SFS & 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;
    }
}

以上已经覆盖所有不含绝壁的情况了。

 

19

Figure 4‑9连续的阶梯

unitypackage

融合缓坡与绝壁 | Merging Slopes and Cliffs

当缓坡遇到绝壁的情况下应该怎么办呢?如果已经确定左侧是缓坡而右侧是绝壁的话,那么三角形的上边将会是什么情况呢?它绝不可能是平地,要么是缓坡,要么就是陡坡。

20

21

22

Figure 5‑1 SCS & 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;
    }
}

那么,我们如何才能对这种情况进行剖分呢?我们需要将这个图形分为上下两部分。

底部 | The Bottom Part

底部是有左侧的缓坡和右侧的陡坡构成的。我们需要将他们以一种方式融合在一起。最简单的方式就是直接将左侧台阶处的顶点连接到三角形的右顶点处。这将会是台阶的宽度向上逐渐变细。

23

Figure 5‑2 直接相连

但是我们并不想这么做,因为这种情况会将台阶插入上方的三角形,而且在绝壁特别高的情况下,顶部形成的尖角将会特别尖,不好看。所以我们将其连接到右侧陡坡边界上的一点,而不是右侧顶点。

24

Figure 5‑3连接到边界上

我们将边界上这点的高度设置为一个单位海拔高度。我们可以通过插值的方式找到这点的坐标。

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

 

25

Figure 5‑4底部的三角形

我们以此点为基准,向右逐步阶梯化底部的三角形。和往常一样,我们先来做第一步。

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

 

26

Figure 5‑5第一次效果

然后,我们试一下最后一步。

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

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

 

27

Figure 5‑6最后一步效果

然后插入中间的所有三角形。

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

 

28

Figure 5‑7最终效果

完成顶点连接处 | Completing the Corner

底部完成之后,我们来看看顶部。如果点改变是一个斜坡的话,我们只需要重复一遍上述过程。所以我们来讲复用的代码放到一个函数里。

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

 

29

30

Figure 5‑8最终效果

镜像情况 | The Mirror Cases

斜坡和绝壁相交还有两个情况和之前我们做的是镜像的。

31

Figure 5‑9 CSS & CSC

我们还将使用之前的方法,只不过将参数调换一下。

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

 

32

33

Figure 5‑10最终效果

双绝壁的情况 | Double Cliffs

三角形连接处唯一一种没有平地边的情况就是那些最低单元格的两条临边都是绝壁的时候。这时候顶边会出现三种情况,平地、缓坡和绝壁。我们只对 CCS 这种情况感兴趣,因为只有在这种情况下三角形才需要被阶梯化。

事实上,存在两种不同的 CCS 版本,一种右侧单元格更高、一种左侧更高。我们分别将其称为 CCSR 和 CCSL。

34

35

36

Figure 5‑11 CCSR & CCSL

Figure 5‑11 CCSR & CCSL

我们可以通过在 TriangulateCorner 以不同的参数顺序调用 TriangulateCornerTerracesCliff 方法来实现这两种情况。

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

然而,这产生了一些错误。这是因为陡坡边界点的位置改变了,不再是据底端一个海拔高度,而是顶端。

 void  TriangulateCornerTerracesCliff (
    Vector3  begin, HexCell beginCell,
    Vector3  left, HexCell leftCell,
    Vector3  right, HexCell rightCell
    )  {
        float  b = 1f / (rightCell.Elevation - beginCell.Elevation);
        if  (b < 0) {
            b  = -b;
        }
         …
} 

void  TriangulateCornerCliffTerraces (
    Vector3  begin, HexCell beginCell,
    Vector3  left, HexCell leftCell,
    Vector3  right, HexCell rightCell
    )  {
        float  b = 1f / (leftCell.Elevation - beginCell.Elevation);
        if  (b < 0) {
            b  = -b;
        }
        …
}

整理代码 | Cleanup

我们现在已经处理好了所有需要被特殊处理的情况。

37

Figure 5‑12被正确绘制的阶梯连接处

我们来整理一下 TriangulateCorner 方法,用 else if 取代 return。

 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。这些情况下三角形都不需要被阶梯化。

38

Figure 5‑13所有情况

下一篇教程是不规则化

近期点赞的会员

 分享这篇文章

arthliant 

SE本科在读 独立开发者  

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. arthliant 2017-10-21

    有的地方有被漏下的(不是我的锅,逃~)
    日志里的是全的 https://indienova.com/u/arthliant/blogread/4088
    日志里的代码块中的新代码还有原帖的高亮背景

您需要登录或者注册后才能发表评论

登录/注册