如何实现Voronoi图边缘包裹以生成程序化幻想地图?
实现Voronoi图东西向边缘包裹的方案
嘿,这个问题我做程序化幻想地图时踩过类似的坑!要实现Voronoi图的东西向边缘包裹(像地球那样左右衔接),核心是把地图当成环形空间处理,不用硬加边界点——给你一套实用的方案,适配你的Poisson采样和AS3环境:
1. 生成采样点的左右镜像副本
你现在用makePoints()生成Poisson采样点,不需要加预定义边界点,而是给靠近左右边缘的点生成镜像点,让Voronoi算法认为地图左右是连通的。
假设你的地图宽为mapWidth,高为mapHeight,Poisson采样半径为poissonRadius,适配AS3的伪代码:
// 先获取原始Poisson采样点(你的第236行函数) var originalPoints:Array = makePoints(); var wrappedPoints:Array = originalPoints.concat(); // 复制原始点到新数组 // 边缘阈值:用采样半径的2倍,避免镜像点和原始点冲突 var edgeThreshold:Number = poissonRadius * 2; for each (var p:Point in originalPoints) { // 左侧边缘的点,生成右侧镜像(x += mapWidth) if (p.x < edgeThreshold) { wrappedPoints.push(new Point(p.x + mapWidth, p.y)); } // 右侧边缘的点,生成左侧镜像(x -= mapWidth) if (p.x > mapWidth - edgeThreshold) { wrappedPoints.push(new Point(p.x - mapWidth, p.y)); } }
2. 用环形距离替代欧几里得距离
普通Voronoi算法用欧几里得距离计算点的远近,这会导致左右边缘的点互不关联。我们需要修改距离计算逻辑,让x方向的距离取地图左右两侧的最短路径:
环形距离公式(伪代码):
function getCircularDistance(p1:Point, p2:Point, mapWidth:Number):Number { var dx:Number = Math.abs(p2.x - p1.x); // x方向取最短路径:要么直接差值,要么绕地图边缘的差值 var wrappedDx:Number = Math.min(dx, mapWidth - dx); var dy:Number = p2.y - p1.y; return Math.sqrt(wrappedDx * wrappedDx + dy * dy); }
如果你用的是第三方Voronoi库,需要找到库中计算距离的函数,替换成这个环形距离;如果是自己实现的Bowyer-Watson算法,就在判断点属于哪个单元格时用这个距离。
3. 折叠跨边缘的单元格顶点
生成Voronoi单元格后,有些单元格会因为镜像点的存在,顶点超出原始地图范围(x < 0 或 x > mapWidth)。这时需要把这些顶点“折叠”回地图内:
for each (var cell:VoronoiCell in voronoiCells) { for each (var vertex:Point in cell.vertices) { // 右侧超出的顶点,减去地图宽度回到左侧 if (vertex.x > mapWidth) { vertex.x -= mapWidth; } // 左侧超出的顶点,加上地图宽度回到右侧 if (vertex.x < 0) { vertex.x += mapWidth; } } }
这样处理后,跨边缘的单元格会在左右两侧同时显示,看起来就像是从右侧延伸到左侧,完美实现东西向包裹。
额外优化建议
- 只给靠近边缘的点生成镜像:不用给所有点都做镜像,只处理x在
0~2*poissonRadius和mapWidth-2*poissonRadius~mapWidth范围内的点,减少计算量 - 测试时缩小地图宽度:方便快速观察左右边缘的衔接效果,确认逻辑正确后再放大
- 板块比例不受影响:30%陆地/70%海洋的设定,在生成单元格后用随机或海拔逻辑分配即可,包裹逻辑不会干扰这个比例
内容的提问来源于stack exchange,提问作者Olin Kirkland




