六边形镶嵌的生成
在本文中,我将解释如何生成六边形镶嵌及其在 Unity 3D 中的绘制方法
引言
在本文中,我将设计一种在平面上生成六边形镶嵌的算法。之后,我将使用
Unity3D 进行绘制。
想法
六边形的生成从一个点开始,该点将作为中心。然后,另一圈六边形将围绕中心,并成为新一圈六边形的中心。生成在达到给定圈数时结束。
在下面的图中,原始中心以红色着色,第一圈以黄色着色,第二圈以绿色着色(第二圈的中心由红色和黄色六边形组成)。
输入
Radius
- 圈数
Side
- 六边形的边长。
输出
该算法可以输出生成的每个六边形的中心;但是,在此实现中,我们将使用 Unity3D 来绘制六边形。
算法
想法是通过查看最后一个生成的中心来生成新六边形的中心。
让我们从一个二维欧几里得空间开始,固定一个点 O 作为中心,并以基 {(1,0), (0,1)} 作为我们轴的基(一个简单的具有正交 x 和 y 轴的笛卡尔平面)。注意:我们将使用二维空间进行算法设计,但将在三维空间中生成镶嵌,因此在算法实现时需要考虑这一点
我们可以从点 (0,0) 开始,这将是第一个六边形的中心。设 S
为六边形的边长。
可以以“螺旋”方式生成所有六边形的中心(见下图:最暗的六边形是第一个,最亮的六边形是最后一个;中间,六边形的阴影代表生成顺序,越暗表示“先生成”,越亮表示“后生成”)。
通过这种生成方法,要生成点 Pn
,只需知道点 Pn-1
的坐标,然后执行 Pn =<sub> </sub>Pn-1
+ (a,b)
即可,其中 (a,b)
是一个可以轻松找到的偶数,只需记住六边形的属性即可。
因此,可能的偶数 (a,b)
是
- DR -
(1.5, -sq3/2)
- DX -
(0, -sq3)
- DL -
(-1.5, -sq3/2)
- UL -
(-1.5, sq3/2)
- UX -
(0, sq3)
- UR -
(1.5, sq3/2)
其中:sq3 = sqrt(3) U/D -> 向上/向下方向,L/X/R -> 向左/无/向右方向
因此,为了从中心 (0, 0) 生成“六边形螺旋”的中心,我们需要执行以下操作。
nDR - nDX - nDL - nUL - nUX - UX - Exit? - nUR
其中 n
是当前圈数,n<action>
表示“在当前中心生成一个六边形,将与 action
对应的偶数 **乘以六边形的边长** 加到前一个中心,并重复 n
次”。Exit?
表示如果数量。因此,要生成第一个六边形
0DR - 0DX - 0DL - 0UL - 0UX - UX - Exit? - 0UR --> UX
(在 (0, 0) 生成一个六边形并向上移动,使下一个中心为 (0, sq3 * s)
,其中 s 是六边形的边长)。
第一圈六边形
1DR - 1DX - 1DL - 1UL - 1UX - UX - Exit? - 1UR
从前一个中心 (0, sq3 * s)
开始,生成 6+1 个六边形,位于
(0, sq3 * s)
(1.5*s, sq3*s/2)
(1.5*s, -sq3*s/2)
(0, -sq3*s)
(-1.5*s,-sq3*s/2)
(-1.5*s,sq3*s/2)
(-1.5*s,3*sq3*s/2)
留下中心在 (0, 2*sq3*s)
下一张图显示了算法生成的第一个 19 个中心
Unity 实现
该算法在 C# 中实现起来非常容易
using UnityEngine;
public class GenerateHexFloor : MonoBehaviour {
public GameObject Hexagon;
public uint Radius;
public float HexSideMultiplier = 1;
private const float sq3 = 1.7320508075688772935274463415059F;
// Use this for initialization
void Start () {
//Point of the next hexagon to be spawned
Vector3 currentPoint = transform.position;
if (Hexagon.transform.localScale.x != Hexagon.transform.localScale.z)
{
Debug.LogError("Hexagon has not uniform scale: cannot determine its side. Aborting");
return;
}
//Spawn scheme: nDR, nDX, nDL, nUL, nUX, End??, UX, nUR
Vector3[] mv = {
new Vector3(1.5f,0, -sq3*0.5f), //DR
new Vector3(0,0, -sq3), //DX
new Vector3(-1.5f,0, -sq3*0.5f), //DL
new Vector3(-1.5f,0, sq3*0.5f), //UL
new Vector3(0,0, sq3), //UX
new Vector3(1.5f,0, sq3*0.5f) //UR
};
int lmv = mv.Length;
float HexSide = Hexagon.transform.localScale.x * HexSideMultiplier;
for (int mult = 0; mult <= Radius; mult++)
{
int hn = 0;
for (int j = 0; j < lmv; j++)
{
for (int i = 0; i < mult; i++, hn++)
{
GameObject h = Instantiate(Hexagon, currentPoint, Hexagon.transform.rotation, transform);
h.name = string.Format("Hex Layer: {0}, n: {1}", mult, hn);
currentPoint += (mv[j] * HexSide);
}
if (j == 4)
{
GameObject h = Instantiate(Hexagon, currentPoint, Hexagon.transform.rotation, transform);
h.name = string.Format("Hex Layer: {0}, n: {1}", mult, hn);
currentPoint += (mv[j] * HexSide);
hn++;
if (mult == Radius)
break; //Finished
}
}
}
}
}
注释
有了想法,写代码就变得非常非常容易。我只想评论一些我的实现选择。
首先,对于任何不了解 Unity 3D 工作原理的人来说,基本上,任何继承自 MonoBehaviour 的类的公共字段都可以从编辑器设置并用作输入字段,因此该类的每个实例都可以轻松地从编辑器设置其自己的参数。所以我要求提供一个六边形的 3D 模型(你可以找到一个附在本文章中的模型,使用 Blender 3D 建模),提供镶嵌的半径,以及字段 HexSideMultipler
用于将六边形的边长乘以一个常数。我通过查看模型的缩放来获取六边形的边长(这就是为什么我要求 x 和 z 缩放相同)。
然后我从附加脚本的 GameObject 的原点开始:在 Unity 中,任何继承自 MonoBehaviour 的类都可以附加到任何 GameObject。在这种情况下,这个 GameObject 是 3D 空间中的一个简单点,提供 x、y 和 z 坐标。所以我是从那个点开始构建六边形镶嵌的。
其余的代码,鉴于我之前解释的想法,是自明的:我只是从前一个点(我称之为 currentPoint
)开始生成每个六边形的中心。*我(使用 Instantiate
方法)生成每个六边形,然后计算下一个点。*
关注点
尽管这个算法似乎效率不高,因为它包含三个嵌套的 for 循环,但它是最优的,因为我们对每个六边形进行一次迭代。嵌套循环的出现是因为如果镶嵌的半径是 R
,并不意味着总共有 R
个六边形!
Gist
我之前在一个 gist 中发布过这段代码,但解释没有这里深入。如果你想访问 gist,这是链接:https://gist.github.com/LuxGiammi/8c1e17feecf7d3c33a1a493657a4d153