不规则四边形填充平面 - Townscaper 网格生成算法复现

源码

github.com/FutaAlice/OrganicQuadGrid

前言

Townscaper,一款关于城镇建造的游戏。

游戏基于一张用不规则凸四边形拼接而成的平面地图,其中每个四边形虽不规则,但又“接近”正方形,适合用于城镇的搭建。

本文将用 UnityC# 实现这个算法,并逐步骤解释。

算法概述

算法分为如下三个步骤:

1. 把目标平面进行 Delaunay Triangulation

泊松分布+Delaunay 是为了获取尽量规整的三角形,

考虑到拼接无限地图的需求,此处的代码实现我们将用将 “正六边形” 三角形化的方式替代。

2. 随机剔除三角形的边

随机剔除三角形的边,使其和邻接三角形形成四边形。(本步骤不能保证所有三角形都被合并为四边形)

3. 将平面内的 “三角形” 和 “四边形” 细分为更小的四边形

我们需要仅由具有四个边的面组成的网格。但是我们当前的网格中仍然存在一些三角形。

不过,三角形可以很容易地细分为三个较小的四边形。同样,现有的四边形也可以细分为四个较小的四边形。如下图所示:

通过这样做,我们最终得到一个仅由四边形组成的网格。

4. 让这些四边形形状更加松弛

这一步被称为 SquaringRelaxation

目的是为了让网格看起来更加“美观”,评判标准比较主观,有多种实现方式。

我们将采用将不断迭代每个顶点,将其移向相邻顶点中心的方式实现。

代码实现

0. 基础代码

新建一个 c# 脚本,派生于 MonoBehaviour,此处我们叫他 Hexagird.cs

[ExecuteInEditMode]
public class Hexagrid : MonoBehaviour
{void Start() ...void Update() ...
}

点、三角形、四边形 的抽象,便于后续代码实现。


class Point // 点
{public Vector2 mPosition;public bool mSide;  // 是否在六边形最外圈
};class Triangle // 三角形
{public int mA, mB, mC;public bool mValid; // 用于剔除边的标记,剔除后赋值为 false
};class Quad // 四边形
{public int mA, mB, mC, mD;
};class Neighbours // “点” 所邻接的其他点
{public void Add(int i) ... // i 为邻接点索引public int count{get {return mNeighbour.Count;}}public List<int> mNeighbour;
};

最后给 Hexagrid 添加一些字段,用于控制生成参数,把脚本挂到场景内的一个空物体上。

[ExecuteInEditMode]
public class Hexagrid : MonoBehaviour
{[Range(2, 12)]public int mSideSize = 8;[Range(1, 20)]public int mSearchIterationCount = 12;[Range(0, 65535)]public int mSeed = 15911;private int mBaseQuadCount = 0;public bool bTriangulation = true;public bool bRemovingEdges = false;public bool bSubdivideFaces = false;public bool bRelax = false;public bool bReshape = false;public bool bDrawPositions = false;private List<Point> mPoints;private List<Triangle> mTriangles;private List<Quad> mQuads;private Neighbours[] mNeighbours;// 分别对应算法的四个步骤,先留空实现,慢慢补全void Triangulation() {}void RemovingEdges() {}void SubdivideFaces() {}void Relax() {}void Reshape() {} // 额外的轮廓相形// 绘图函数,不重要,从github把代码复制过来就行private void DrawLine(int a, int b) ...void OnDrawGizmos() ...// 当参数改变时,重新初始化private void OnValidate(){mPoints = new List<Point>();mTriangles = new List<Triangle>();mQuads = new List<Quad>();mNeighbours = new Neighbours[0];if (bTriangulation) {this.Triangulation();}if (bTriangulation && bRemovingEdges) {this.RemovingEdges();}if (bTriangulation && bRemovingEdges && bSubdivideFaces) {this.SubdivideFaces();}}// 重写 Update(),逐帧 Relax,方便看效果void Update(){if (bTriangulation && bRemovingEdges && bSubdivideFaces && bRelax) {if (bRelax) {this.Relax();}if (bRelax && bReshape) {this.Reshape();}}}
}

1. 三角形化

准备工作完成了,开始干正事:

void Triangulation()
{mPoints = new List<Point>();mTriangles = new List<Triangle>();mQuads = new List<Quad>();mNeighbours = new Neighbours[0];// 将六边形内散点坐标塞进 mPointsfloat sideLength = 0.5f * Mathf.Tan(Mathf.Deg2Rad * 60); // 0.5f* tanf(60deg)for (int x = 0; x < mSideSize * 2 - 1; ++x) {int height = (x < mSideSize) ? (mSideSize + x) : (mSideSize * 3 - 2 - x);float deltaHeight = mSideSize - height * 0.5f;for (int y = 0; y < height; y++) {bool isSide = x == 0 || x == (mSideSize * 2 - 2) || y == 0 || y == height - 1;mPoints.Add(new Point((x - mSideSize + 1) * sideLength, y + deltaHeight, isSide));}}// 分别给对称轴左右生成三角形(顶点顺序不同),塞进 mTrianglesint offset = 0;for (int x = 0; x < (mSideSize * 2 - 2); x++) {int height = (x < mSideSize) ? (mSideSize + x) : (mSideSize * 3 - 2 - x);if (x < mSideSize - 1) {// left sidefor (int y = 0; y < height; y++) {mTriangles.Add(new Triangle(offset + y, offset + y + height, offset + y + height + 1));if (y >= height - 1) {break;}mTriangles.Add(new Triangle(offset + y + height + 1, offset + y + 1, offset + y));}}else {// right sidefor (int y = 0; y < height - 1; y++) {mTriangles.Add(new Triangle(offset + y, offset + y + height, offset + y + 1));if (y >= height - 2) {break;}mTriangles.Add(new Triangle(offset + y + 1, offset + y + height, offset + y + height + 1));}}offset += height;}
}

现在我们得到了存储在 mPoints 里的正六边形内顶点坐标,

以及存储在 mTriangles 里的三角形 index buffer.

2. 剔除边

private int[] GetAdjacentTriangles(int triIndex);   // 获取邻接的三角形索引void RemovingEdges()
{// triangles to quadsSystem.Random rand = new System.Random(mSeed);while (true) {int triIndex;int searchCount = 0;do {triIndex = rand.Next() % mTriangles.Count;searchCount++;} while (searchCount < mSearchIterationCount && !mTriangles[triIndex].mValid);if (searchCount == mSearchIterationCount) {break;}int[] adjacents = GetAdjacentTriangles(triIndex);if (adjacents.Length > 0) {int i1 = triIndex;int i2 = adjacents[0];int[] indices = new int[6] {mTriangles[i1].mA, mTriangles[i1].mB, mTriangles[i1].mC,mTriangles[i2].mA, mTriangles[i2].mB, mTriangles[i2].mC};Array.Sort(indices);int[] unique = indices.Distinct().ToArray();Debug.Assert(unique.Length == 4);mQuads.Add(new Quad(unique[0], unique[2], unique[3], unique[1]));mTriangles[triIndex].mValid = false; ;mTriangles[adjacents[0]].mValid = false;}}this.mBaseQuadCount = mQuads.Count();
}

随机选择三角形,获取一个与其相邻的三角形,剔除一条公用边,更新 mValid 标记。

拼接成的四边形塞进 mQuads

3. 细分成四边形

// 输入三角形/四边形顶点数组,和一个记录中点位置的字典
// 细分得到的顶点和四边形 index buffer 分别塞进 mPoints 和 mQuads
void Subdivide(int[] indices, Dictionary<UInt32, int> middles);void SubdivideFaces()
{Dictionary<UInt32, int> middles = new Dictionary<UInt32, int>();// quads to 4 quadsfor (int i = 0; i < mBaseQuadCount; i++) {var quad = mQuads[i];int[] indices = new int[4] {quad.mA, quad.mB, quad.mC, quad.mD};this.Subdivide(indices, middles);}// triangles to quadsforeach (var triangle in mTriangles) {if (triangle.mValid) {int[] indices = new int[3] {triangle.mA, triangle.mB, triangle.mC};this.Subdivide(indices, middles);}}
}

4. 使网格更加松弛

void Relax()
{mNeighbours = new Neighbours[mPoints.Count];for (int i = 0; i < mPoints.Count; ++i) {mNeighbours[i] = new Neighbours();}for (int i = mBaseQuadCount; i < mQuads.Count(); ++i) {var quad = mQuads[i];int[] indices = new int[4] {quad.mA, quad.mB, quad.mC, quad.mD};for (int j = 0; j < 4; j++) {int index1 = indices[j];int index2 = indices[(j + 1) & 3];{var neighbour = mNeighbours[index1];// checkbool good = true;for (int k = 0; k < neighbour.count; k++) {if (neighbour.mNeighbour[k] == index2) {good = false;break;}}if (good) {Debug.Assert(neighbour.count < 6);neighbour.Add(index2);}}{var neighbour = mNeighbours[index2];// checkbool good = true;for (int k = 0; k < neighbour.count; k++) {if (neighbour.mNeighbour[k] == index1) {good = false;break;}}if (good) {Debug.Assert(neighbour.count < 6);neighbour.Add(index1);}}}}for (int i = 0; i < mPoints.Count; i++) {if (mPoints[i].mSide) {continue;}var neighbour = mNeighbours[i];Vector2 sum = Vector2.zero;for (int j = 0; j < neighbour.count; j++) {sum += mPoints[neighbour.mNeighbour[j]].mPosition;}sum /= (float)neighbour.count;mPoints[i].mPosition = sum;}
}

5. 还可以一定程度扭曲外轮廓

void Reshape()
{float radius = mSideSize - 1.0f;Vector2 center = new Vector2(0, (mSideSize * 2 - 1) * 0.5f);foreach (var point in mPoints) {if (!point.mSide) {continue;}Vector2 D = point.mPosition - center;float distance = radius - Mathf.Sqrt(D.x * D.x + D.y * D.y);point.mPosition += (D * distance) * 0.1f;}
}

不规则四边形填充平面 - Townscaper 网格生成算法复现相关推荐

  1. 点在不规则图形内算法python_目标检测算法中规则矩形和不规则四边形IOU的Python实现...

    交并比(Intersection-over-Union,IoU),目标检测中使用的一个概念,我们在进行目标检测算法测试时,重要的指标,是产生的预测框(candidate bound)与标记框(grou ...

  2. 两个不规则四边形IOU计算方法

    1 引言 我们常用的目标检测为矩形框检测,但是我们有时检测出来的框为不规则的四边形,如下图为常见的遥感图像,此时我们在使用矩形框计算IOU的方式就不再适用.我们可以将问题进行抽象,有木有计算两个不规则 ...

  3. 基于马尔可夫过程的一种新型混合PSO粒子群算法(SCI二区高被引文献)介绍及算法复现(使用chatgpt)

    以下是一篇算法领域的SCI二区文献(原文见附件),介绍了一种使用Markov概率转移矩阵对种群拓扑结构进行加权的粒子群算法,相比于标准PSO算法该算法提高了全局覆盖率,更容易跳出局部最优,但是在局部最 ...

  4. 计算几何@_@不规则四边形求法

    一.托勒密定理(感觉有联系) 凸四边形 托勒密定理指出凸四边形两组对边乘积之和不小于两条对角线的乘积,等号当且仅当四边形为圆内接四边形,或退 化为直线取得(这时也称为欧拉定理).狭义的托勒密定理也可以 ...

  5. 基于八叉树的网格生成算法剖析

    基于八叉树的网格生成算法剖析 前言 对于网格生成这个主题,之前的网格生成系列的三篇博客文章分别介绍了MC算法,SMC算法以及Cuberille算法三种方法.同时还有一篇介绍网格生成与种子点生长算法高效 ...

  6. 不规则四边形回归框计算IOU与NMS

    前言 在复现DPR论文过程中,遇到了计算IOU和NMS的问题.传统目标检测大多基于anchor,给出的预测也是矩形框,所以需要进行相应的修改,以套用到不规则四边形回归框上. 注1:编者水平有限,如有谬 ...

  7. 29 基于PCL的点云平面分割拟合算法技术路线(针对有噪声的点云数据)

    0 引言 最近项目中用到了基于PCL开发的基于平面的点云和CAD模型的配准算法,点云平面提取采用的算法如下. 1 基于PCL的点云平面分割拟合算法 2 参数及其意义介绍 (1)点云下采样 1. 参数: ...

  8. python代码颜色不同_Python填充任意颜色,不同算法时间差异分析说明

    我就废话不多说了,大家还是直接看代码吧! import time import numpy as np import cv2 #方法一 start = time.time() for i in ran ...

  9. python3实现cv2+numpy裁剪不规则四边形图片

    python3实现cv2+numpy裁剪不规则四边形图片 功能 效果图![在这里插入图片描述](https://img-blog.csdnimg.cn/a3dd447909db43489050b3b8 ...

最新文章

  1. Uva 3767 Dynamic len(set(a[L:R])) 树套树
  2. 【统计学习方法】线性可分支持向量机对鸢尾花(iris)数据集进行二分类
  3. socket编程listen函数限制连接数的解决方案_网络编程——服务器篇
  4. python列表的切片操作
  5. 一份技术简历是如何搞定BAT的面试官?
  6. AAAI2020录用论文汇总(一)
  7. 新员工入职自动加入所在部门的邮件组。
  8. linux消息队列总结
  9. 利用JQuery在动态页面的倒计时器
  10. 微星电脑win、ctrl等组合键时而失灵
  11. 线性代数及其应用(第三版)1.3节习题解答
  12. 入门vue+springboot项目
  13. linux 查看ln链接地址,linux ln链接详解
  14. KSQL:Apache Kafka的流式SQL
  15. 聪明人的游戏提高篇:贝贝的数学课 (change)
  16. 算法时间复杂度计算方法
  17. 招聘 | 胡传鹏博士课题组招硕士、博士
  18. 【bypass】403绕过
  19. 2013华为校园招聘机试题
  20. python温度转换_一步一步教会你,详解用Python实现一个简易的温度换算GUI小工具...

热门文章

  1. vlookup多项匹配_VLOOKUP函数一对多查找,就是这么简单!
  2. 蒋晓海:Testin云测,如何让应用更有价值
  3. 一本通1360:奇怪的电梯(lift)
  4. vscode背景图片设置分享
  5. php 汉语转换拼音
  6. Vivado使用技巧(17):时序异常 Timing Exception
  7. linux内核编译时bad register name `%dil'错误
  8. 《Unity2018AR与VR开发快速上手》随书内容资源相关说明
  9. springboot企业人力资源管理系统毕业设计源码291816
  10. 【STM32】标准库 USART通信