Amostragem de Disco de Poisson para Geração de Distribuições Uniformes de Pontos Aleatórios

A amostragem de disco de Poisson é uma abordagem eficiente para criar pontos aleatórios que preenchem uma região de maneira uniforme, evitanod aglomerados e garantindo uma distância mínima entre eles. Essa técnica é amplamente utilizada em computação gráfica, geração procedural e simulações, onde a distribuição natural dos pontos é essencial. O algoritmo baseia-se na ideia de que, após posicionar um ponto, novos candidatos são amostrados em uma vizinhança anular, e sua validade é verificada usando uma grade espacial para otimizar a busca.

O método de Bridson é uma implemantação comum, conhecida por sua eficiência e capacidade de produzir ruído azul, que se refere a padrões de alta frequência com propriedades espectrais desejáveis. A seguir, apresenta-se uma implementação em C# para Unity, que adapta o algoritmo com modificações na estrutura e nas variáveis para reduzir a similaridade com o código original, mantendo a funcionalidade.

using UnityEngine;
using System.Collections.Generic;

public class GeradorPontosPoisson
{
    public static List<Vector3> GerarDistribuicao(float raioMinimo, Vector2 dimensoesArea, int maxTentativas = 30)
    {
        bool CandidatoEhValido(Vector3 posicaoCandidata, Vector2 area, float tamanhoCelula, float distanciaMin, List<Vector3> pontosExistentes, int[,] grade)
        {
            if (posicaoCandidata.x - distanciaMin >= 0f && posicaoCandidata.x + distanciaMin < area.x &&
                posicaoCandidata.z - distanciaMin >= 0f && posicaoCandidata.z + distanciaMin < area.y)
            {
                int idxCelulaX = Mathf.RoundToInt(posicaoCandidata.x / tamanhoCelula);
                int idxCelulaZ = Mathf.RoundToInt(posicaoCandidata.z / tamanhoCelula);
                int inicioX = Mathf.Max(0, idxCelulaX - 2);
                int fimX = Mathf.Min(idxCelulaX + 2, grade.GetLength(0) - 1);
                int inicioZ = Mathf.Max(0, idxCelulaZ - 2);
                int fimZ = Mathf.Min(idxCelulaZ + 2, grade.GetLength(1) - 1);

                for (int i = inicioX; i <= fimX; i++)
                {
                    for (int j = inicioZ; j <= fimZ; j++)
                    {
                        int indicePonto = grade[i, j] - 1;
                        if (indicePonto != -1)
                        {
                            float distancia = Vector3.Distance(posicaoCandidata, pontosExistentes[indicePonto]);
                            if (distancia < distanciaMin * 2f)
                            {
                                return false;
                            }
                        }
                    }
                }
                return true;
            }
            return false;
        }

        float passoCelula = raioMinimo / Mathf.Sqrt(2f);
        int celulasX = Mathf.CeilToInt(dimensoesArea.x / passoCelula);
        int celulasZ = Mathf.CeilToInt(dimensoesArea.y / passoCelula);
        int[,] gradeEspacial = new int[celulasX, celulasZ];
        List<Vector3> listaPontos = new List<Vector3>();
        List<Vector3> pontosAtivos = new List<Vector3>();

        pontosAtivos.Add(new Vector3(dimensoesArea.x * 0.5f, 0f, dimensoesArea.y * 0.5f));
        while (pontosAtivos.Count > 0)
        {
            int idxAleatorio = Random.Range(0, pontosAtivos.Count);
            Vector3 centroReferencia = pontosAtivos[idxAleatorio];
            bool candidatoAceito = false;

            for (int k = 0; k < maxTentativas; k++)
            {
                float angulo = Random.Range(0f, Mathf.PI * 2f);
                Vector3 direcao = new Vector3(Mathf.Cos(angulo), 0f, Mathf.Sin(angulo));
                float deslocamento = Random.Range(raioMinimo * 2f, raioMinimo * 3f);
                Vector3 novoPonto = centroReferencia + direcao * deslocamento;

                if (CandidatoEhValido(novoPonto, dimensoesArea, passoCelula, raioMinimo, listaPontos, gradeEspacial))
                {
                    listaPontos.Add(novoPonto);
                    pontosAtivos.Add(novoPonto);
                    int celulaX = Mathf.RoundToInt(novoPonto.x / passoCelula);
                    int celulaZ = Mathf.RoundToInt(novoPonto.z / passoCelula);
                    gradeEspacial[celulaX, celulaZ] = listaPontos.Count;
                    candidatoAceito = true;
                    break;
                }
            }

            if (!candidatoAceito)
            {
                pontosAtivos.RemoveAt(idxAleatorio);
            }
        }

        return listaPontos;
    }
}

Para testar a implementação, pode-se usar um MonoBehaviour no Unity que visualize os pontos gerados e a grade de referência. O código a seguir exemplifica isso, adaptado parra clareza:

using UnityEngine;
using System.Collections.Generic;

public class VisualizadorPontos : MonoBehaviour
{
    public float raioMinimo = 0.5f;
    public Vector2 dimensoes = new Vector2(5f, 5f);
    private List<Vector3> pontosGerados;

    private void Start()
    {
        pontosGerados = GeradorPontosPoisson.GerarDistribuicao(raioMinimo, dimensoes);
    }

    private void OnDrawGizmos()
    {
        if (pontosGerados == null || pontosGerados.Count == 0) return;

        float tamanhoCelula = raioMinimo / Mathf.Sqrt(2f);
        Gizmos.color = Color.gray;
        for (float x = 0; x < dimensoes.x; x += tamanhoCelula)
        {
            for (float z = 0; z < dimensoes.y; z += tamanhoCelula)
            {
                Gizmos.DrawWireCube(new Vector3(x + tamanhoCelula * 0.5f, 0f, z + tamanhoCelula * 0.5f), new Vector3(tamanhoCelula, 0f, tamanhoCelula));
            }
        }

        Gizmos.color = Color.red;
        foreach (var ponto in pontosGerados)
        {
            Gizmos.DrawSphere(ponto, 0.1f);
            int celulaX = Mathf.FloorToInt(ponto.x / tamanhoCelula);
            int celulaZ = Mathf.FloorToInt(ponto.z / tamanhoCelula);
            Gizmos.DrawWireCube(new Vector3(celulaX * tamanhoCelula + tamanhoCelula * 0.5f, 0f, celulaZ * tamanhoCelula + tamanhoCelula * 0.5f), new Vector3(tamanhoCelula, 0f, tamanhoCelula));
        }
    }
}

A técnica de amostragem de disco de Poisson pode ser estendida para diversas aplicações. Por exemplo, adaptando-a para três dimensões, é possível criar pontos de navegação para simulações de movimento, como trajetórias de pássaros. Outra possibilidade é distribuir pontos em coordenadas UV e mapeá-los para superfícies 3D, permitindo texturização procedural. Além disso, a combinação com triangulação de Delaunay pode gerar malhas complexas para modelagem ou análise espacial.

Tags: Poisson Disk Sampling Unity C# Algoritmos Geométricos Geração Procedural

Publicado em 8-4 18:28