Unity 3D:如何用BoxCollider判断点是否在区域内?(泊松采样植数)
问题描述
我用泊松采样生成树木,为了方便定义种植区域,想用3D BoxCollider可视化并调整区域大小。之前用Rect.Contains(Vector2 point)判断点是否在区域内,但Rect是2D UI元素,只支持x、y坐标,没有深度维度。
我已经修改了PoissonDiscSampler类适配BoxCollider,但现在采样器只生成一个位于BoxCollider外部的随机点就停止工作了。我想找数学方法(比如box.bounds.Contains())判断采样点是否在BoxCollider范围内,避免用开销高的射线检测。
使用代码
// 遍历该岛屿上所有树木生成区域 foreach (BoxCollider spawningArea in island.GetTreeSpawningBoxes()) { // i = 0; // 尝试修复bug? i = -1; // 没用,还是不行 // 会不会是因为Samples()是IEnumerable? // 初始化新的泊松采样器 PoissonDiscSampler sampler = new PoissonDiscSampler(spawningArea, 18); // 获取采样点并遍历 foreach (Vector2 sample in sampler.Samples()) { // 忽略一半采样点 //if (i % 2 != 0) return; // 以为是这里的bug if (i++ % 2 != 0) return; // 但还是不行 // 不过这和当前问题无关,只是注意到了 // 泊松采样得到的位置 Vector3 pos = new Vector3(sample.x, 0, sample.y); // Y轴随机旋转(0-359度) Quaternion rot = Quaternion.identity; rot.eulerAngles = new Vector3(0, Random.Range(0, 360), 0); // 生成树木 var tree = Instantiate ( StaticResources.instance.SailingTrees[Random.Range (0, StaticResources.instance.SailingTrees.Length - 1)], pos, rot ); tree.transform.SetParent(island.transform); } }
修改后的PoissonDiscSampler类代码
using UnityEngine; using System.Collections; using System.Collections.Generic; /// 基于Bridson算法的泊松圆盘采样 /// 改编自Mike Bostock的JavaScript实现 /// /// 使用示例: /// PoissonDiscSampler sampler = new PoissonDiscSampler(10, 5, 0.3f); /// foreach (Vector2 sample in sampler.Samples()) { /// // ... 执行操作,比如在(sample.x, sample.y)位置实例化对象: /// Instantiate(someObject, new Vector3(sample.x, 0, sample.y), Quaternion.identity); /// } /// /// 作者:Gregory Schlomoff /// 公有领域授权 /// <summary> /// 基于Bridson算法的泊松圆盘采样 /// </summary> /// /// -------------------------------------------------------------------- /// /// 由我改进和优化 /// /// 新使用方式: /// PoissonDiscSampler sampler = new PoissonDiscSampler(BoxCollider box, float radius); /// public class PoissonDiscSampler { private const int k = 30; // 标记采样点为无效前的最大尝试次数 private BoxCollider box; private readonly float radius2; // 半径的平方 private readonly float cellSize; private Vector2[,] grid; private List<Vector2> activeSamples = new List<Vector2>(); /// 创建采样器,参数说明: /// /// box: 采样区域的BoxCollider /// radius: 每个采样点与其他采样点的最小距离为radius,最大距离为2*radius public PoissonDiscSampler(BoxCollider box, float radius) { this.box = box; radius2 = radius * radius; cellSize = radius / Mathf.Sqrt(2); grid = new Vector2[Mathf.CeilToInt(box.size.x / cellSize), Mathf.CeilToInt(box.size.z / cellSize)]; } /// 返回延迟加载的采样点序列,通常用foreach遍历: /// foreach (Vector2 sample in sampler.Samples()) { ... } public IEnumerable<Vector2> Samples() { // 第一个采样点随机生成 yield return AddSample(new Vector2(Random.value * box.size.x, Random.value * box.size.z)); while (activeSamples.Count > 0) { // 随机选择一个活跃采样点 int i = (int)Random.value * activeSamples.Count; Vector2 sample = activeSamples[i]; // 在该采样点周围[radius, 2*radius]范围内尝试k个随机候选点 bool found = false; for (int j = 0; j < k; ++j) { float angle = 2 * Mathf.PI * Random.value; float r = Mathf.Sqrt(Random.value * 3 * radius2 + radius2); // 生成圆环内的随机点 Vector2 candidate = sample + r * new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); // 如果候选点在BoxCollider范围内,且与已有采样点距离足够远,则接受该点 if (box.bounds.Contains(candidate) && IsFarEnough(candidate)) { found = true; yield return AddSample(candidate); break; } } // 如果k次尝试都没找到有效候选点,将该采样点从活跃列表中移除 if (!found) { activeSamples[i] = activeSamples[activeSamples.Count - 1]; activeSamples.RemoveAt(activeSamples.Count - 1); } } } private bool IsFarEnough(Vector2 sample) { GridPos pos = new GridPos(sample, cellSize); int xmin = Mathf.Max(pos.x - 2, 0); int ymin = Mathf.Max(pos.y - 2, 0); int xmax = Mathf.Min(pos.x + 2, grid.GetLength(0) - 1); int ymax = Mathf.Min(pos.y + 2, grid.GetLength(1) - 1); for (int y = ymin; y <= ymax; y++) { for (int x = xmin; x <= xmax; x++) { Vector2 s = grid[x, y]; if (s != Vector2.zero) { Vector2 d = s - sample; if (d.x * d.x + d.y * d.y < radius2) return false; } } } return true; // 注意:我们用零向量表示网格中未填充的单元格。这意味着如果随机选到(0,0)作为采样点, // 在距离检测时会被忽略,可能导致另一个采样点离(0,0)过近。这是个非常小的问题。 } /// 将采样点添加到活跃列表和网格中,然后返回该点 private Vector2 AddSample(Vector2 sample) { activeSamples.Add(sample); GridPos pos = new GridPos(sample, cellSize); grid[pos.x, pos.y] = sample; return sample; } /// 辅助结构体,计算采样点在网格中的x、y索引 private struct GridPos { public int x; public int y; public GridPos(Vector2 sample, float cellSize) { x = (int)(sample.x / cellSize); y = (int)(sample.y / cellSize); } } }
问题分析与修复方案
核心问题
- 坐标空间不匹配:采样器生成的是BoxCollider本地空间的坐标,但
box.bounds.Contains()判断的是世界空间的点,且未考虑BoxCollider的位置、旋转,导致生成的点永远不在有效范围内。 - 遍历逻辑错误:原代码中
if (i++ % 2 != 0) return;会直接退出foreach循环,导致仅生成一个采样点。
修复步骤
1. 统一坐标空间,基于Bounds生成采样点
修改采样器,直接使用BoxCollider的世界空间Bounds来生成和判断采样点,避免空间转换错误:
private Bounds bounds; private readonly float radius2; private readonly float cellSize; private Vector2[,] grid; private List<Vector2> activeSamples = new List<Vector2>(); public PoissonDiscSampler(Bounds bounds, float radius) { this.bounds = bounds; radius2 = radius * radius; cellSize = radius / Mathf.Sqrt(2); grid = new Vector2[Mathf.CeilToInt(bounds.size.x / cellSize), Mathf.CeilToInt(bounds.size.z / cellSize)]; } public IEnumerable<Vector2> Samples() { // 生成世界空间内的第一个采样点 Vector2 firstSample = new Vector2( Random.Range(bounds.min.x, bounds.max.x), Random.Range(bounds.min.z, bounds.max.z) ); yield return AddSample(firstSample); while (activeSamples.Count > 0) { // 安全生成随机索引,避免越界 int i = Random.Range(0, activeSamples.Count); Vector2 sample = activeSamples[i]; bool found = false; for (int j = 0; j < k; ++j) { float angle = 2 * Mathf.PI * Random.value; float r = Mathf.Sqrt(Random.value * 3 * radius2 + radius2); Vector2 candidate = sample + r * new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); // 直接用Bounds的范围判断候选点是否有效 if (candidate.x >= bounds.min.x && candidate.x <= bounds.max.x && candidate.y >= bounds.min.z && candidate.y <= bounds.max.z && IsFarEnough(candidate)) { found = true; yield return AddSample(candidate); break; } } if (!found) { activeSamples[i] = activeSamples[activeSamples.Count - 1]; activeSamples.RemoveAt(activeSamples.Count - 1); } } } // 修正AddSample和IsFarEnough方法,适配世界空间坐标转网格本地坐标 private Vector2 AddSample(Vector2 sample) { activeSamples.Add(sample); Vector2 localSample = new Vector2(sample.x - bounds.min.x, sample.y - bounds.min.z); GridPos pos = new GridPos(localSample, cellSize); grid[pos.x, pos.y] = sample; return sample; } private bool IsFarEnough(Vector2 sample) { Vector2 localSample = new Vector2(sample.x - bounds.min.x, sample.y - bounds.min.z); GridPos pos = new GridPos(localSample, cellSize); int xmin = Mathf.Max(pos.x - 2, 0); int ymin = Mathf.Max(pos.y - 2, 0); int xmax = Mathf.Min(pos.x + 2, grid.GetLength(0) - 1); int ymax = Mathf.Min(pos.y + 2, grid.GetLength(1) - 1); for (int y = ymin; y <= ymax; y++) { for (int x = xmin; x <= xmax; x++) { Vector2 s = grid[x, y]; if (s != Vector2.zero) { Vector2 d = s - sample; if (d.x * d.x + d.y * d.y < radius2) return false; } } } return true; } private struct GridPos { public int x; public int y; public GridPos(Vector2 localSample, float cellSize) { x = (int)(localSample.x / cellSize); y = (int)(localSample.y / cellSize); } }
2. 修复遍历逻辑错误
将return改为continue,避免提前退出循环:
foreach (BoxCollider spawningArea in island.GetTreeSpawningBoxes()) { int i = -1; PoissonDiscSampler sampler = new PoissonDiscSampler(spawningArea.bounds, 18); foreach (Vector2 sample in sampler.Samples()) { if (i++ % 2 != 0) continue; // 用continue跳过,而非退出循环 Vector3 pos = new Vector3(sample.x, 0, sample.y); Quaternion rot = Quaternion.Euler(0, Random.Range(0, 360), 0); var tree = Instantiate( StaticResources.instance.SailingTrees[Random.Range(0, StaticResources.instance.SailingTrees.Length)], pos, rot ); tree.transform.SetParent(island.transform); } }
内容的提问来源于stack exchange,提问作者user20494201
相关产品推荐
相关产品推荐

