泊松盘采样(Poisson Disk Sampling)生成均匀随机点
当需要生成随机点且要求随机点自然均匀的分布时,使用泊松盘采样就较为适合。
但该方法与统计学上的概念关联不大,这个只相当于点在面积上服从泊松分布,
而实现这个结果有很多做法。
最终效果:
圆形为含半径的点,圆形的中心代表生成点
B站有一个不错的搬运教程(Bridson方法):
https://www.bilibili.com/video/BV1KV411x7LM
另外Bridson文章里说蓝噪声(BlueNoise)也基于此方法生成
我做了些修改,代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public static class PoissonDiscSampling { public static List<Vector3> GeneratePoints(float radius, Vector2 sampleRegionSize, int numSamplesBeforeRejection = 32) { bool IsValid(Vector3 candidate, Vector2 sampleRegionSize, float cellSize, float radius, List<Vector3> points, int[,] grid) { if (candidate.x - radius >= 0f && candidate.x + radius < sampleRegionSize.x && candidate.z - radius >= 0f && candidate.z + radius < sampleRegionSize.y) { int cellX = Mathf.RoundToInt(candidate.x / cellSize); int cellZ = Mathf.RoundToInt(candidate.z / cellSize); int searchStartX = Mathf.Max(0, cellX - 3); int searchEndX = Mathf.Min(cellX + 3, grid.GetLength(0) - 1); int searchStartZ = Mathf.Max(0, cellZ - 3); int searchEndZ = Mathf.Min(cellZ + 3, grid.GetLength(1) - 1); //如果要检测其它格子内的球,需要遍历周围6个格子 for (int x = searchStartX; x <= searchEndX; x++) { for (int z = searchStartZ; z <= searchEndZ; z++) { int pointIndex = grid[x, z] - 1;//存长度不存索引,取时减1,0就变成了-1,不需要初始化数组了 if (pointIndex != -1) { float dst = (candidate - points[pointIndex]).magnitude; if (dst < radius * 2f) { return false; } } } } return true; } return false; } float cellSize = radius / Mathf.Sqrt(2); int[,] grid = new int[Mathf.CeilToInt(sampleRegionSize.x / cellSize), Mathf.CeilToInt(sampleRegionSize.y / cellSize)]; List<Vector3> points = new List<Vector3>(); List<Vector3> spawnPoints = new List<Vector3>(); spawnPoints.Add(new Vector3(sampleRegionSize.x / 2f, 0f, sampleRegionSize.y / 2f)); while (spawnPoints.Count > 0) { int spawnIndex = Random.Range(0, spawnPoints.Count); Vector3 spawnCenter = spawnPoints[spawnIndex]; bool candidateAccepted = false; for (int i = 0; i < numSamplesBeforeRejection; i++) { float angle = Random.value * Mathf.PI * 2f; Vector3 dir = new Vector3(Mathf.Sin(angle), 0f, Mathf.Cos(angle)); Vector3 candidate = spawnCenter + dir * Random.Range(2f, 3f) * radius; if (IsValid(candidate, sampleRegionSize, cellSize, radius, points, grid)) { points.Add(candidate); spawnPoints.Add(candidate); grid[Mathf.RoundToInt(candidate.x / cellSize), Mathf.RoundToInt(candidate.z / cellSize)] = points.Count; candidateAccepted = true; break; } } if (!candidateAccepted) { spawnPoints.RemoveAt(spawnIndex); } } return points; } }
测试代码如下:
using System.Collections.Generic; using UnityEngine; public class Test : MonoBehaviour { public float radius = 0.3f; public Vector2 sampleRegionSize = new Vector2(3f, 3f); private List<Vector3> mPoints; private void OnEnable() { mPoints = PossonDiscSampling.GeneratePoints(radius, sampleRegionSize); } private void OnDrawGizmos() { if (mPoints == null) return; float cellSize = radius / Mathf.Sqrt(2); Color cacheColor = Gizmos.color; Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.5f); for (float x = 0; x < sampleRegionSize.x; x += cellSize) { for (float z = 0; z < sampleRegionSize.y; z += cellSize) Gizmos.DrawWireCube(new Vector3(x, 0f, z), new Vector3(cellSize, 0f, cellSize)); }//生成对应采样点的调试方格 Gizmos.color = cacheColor; for (int i = 0; i < mPoints.Count; i++) { Vector3 vector = mPoints[i]; Gizmos.DrawWireSphere(vector, radius); int x = Mathf.FloorToInt(vector.x / cellSize); int z = Mathf.FloorToInt(vector.z / cellSize); Gizmos.DrawWireCube(new Vector3(x, 0f, z) * cellSize, new Vector3(cellSize, 0f, cellSize)); //生成当前点所属方格 } } }
对于该做法还可以做如下应用层面的扩展:
1.扩展到3D空间,一些鸟的移动轨迹可以直接用这个做路点寻路
2.可以尝试在UV上分布,然后映射到3D空间
3.可以基于这个做三角剖分(https://www.cnblogs.com/hont/p/15310157.html)