You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}
}

问题分析与修复方案

核心问题

  1. 坐标空间不匹配:采样器生成的是BoxCollider本地空间的坐标,但box.bounds.Contains()判断的是世界空间的点,且未考虑BoxCollider的位置、旋转,导致生成的点永远不在有效范围内。
  2. 遍历逻辑错误:原代码中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 14:35:20