Program Listing for File PoissonDiskSampling.cpp

Return to documentation for file (SPlisHSPlasH/Utilities/PoissonDiskSampling.cpp)

#include "PoissonDiskSampling.h"

#include <algorithm>
#include <limits>

#include <iostream>
#include <fstream>
#include <string>

#define _USE_MATH_DEFINES
#include "math.h"

using namespace std;
using namespace Eigen;
using namespace SPH;

PoissonDiskSampling::PoissonDiskSampling()
{
}

void PoissonDiskSampling::sampleMesh(const unsigned int numVertices, const Vector3r *vertices, const unsigned int numFaces, const unsigned int *faces,
    const Real minRadius, const unsigned int numTrials,
    unsigned int distanceNorm, std::vector<Vector3r> &samples)
{
    m_r = minRadius;
    m_numTrials = numTrials;
    m_distanceNorm = distanceNorm;

    m_cellSize = m_r / sqrt(static_cast<Real>(3.0));

    // Init sampling
    m_maxArea = numeric_limits<Real>::min();
    determineMinX(numVertices, vertices);

    determineTriangleAreas(numVertices, vertices, numFaces, faces);

    const Real circleArea = static_cast<Real>(M_PI) * minRadius * minRadius;
    const unsigned int numInitialPoints = (unsigned int) (40.0 * (m_totalArea / circleArea));
    //cout << "# Initial points: " << numInitialPoints << endl;

    m_initialInfoVec.resize(numInitialPoints);
    m_phaseGroups.resize(27);

    computeFaceNormals(numVertices, vertices, numFaces, faces);

    // Generate initial set of candidate points
    generateInitialPointSet(numVertices, vertices, numFaces, faces);

    // Find minimal coordinates of object

    // Calculate CellIndices
    const Real factor = static_cast<Real>(1.0) / m_cellSize;

    #pragma omp parallel for schedule(static)
    for (int i = 0; i < (int)m_initialInfoVec.size(); i++)
    {
        const Vector3r& v = m_initialInfoVec[i].pos;
        const int cellPos1 = PoissonDiskSampling::floor((v.x() - m_minVec[0]) * factor) + 1;
        const int cellPos2 = PoissonDiskSampling::floor((v.y() - m_minVec[1]) * factor) + 1;
        const int cellPos3 = PoissonDiskSampling::floor((v.z() - m_minVec[2]) * factor) + 1;
        m_initialInfoVec[i].cP = CellPos(cellPos1, cellPos2, cellPos3);
    }

    // Sort Initial points for CellID
    quickSort(0, (int)m_initialInfoVec.size() - 1);

    // PoissonSampling
    parallelUniformSurfaceSampling(samples);

    // release data
    m_initialInfoVec.clear();
    for (int i = 0; i < m_phaseGroups.size(); i++)
    {
        m_phaseGroups[i].clear();
    }
    m_phaseGroups.clear();
}


void PoissonDiskSampling::determineTriangleAreas(const unsigned int numVertices, const Vector3r *vertices, const unsigned int numFaces, const unsigned int *faces)
{
    m_areas.resize(numFaces);
    Real totalArea = 0.0;
    Real tmpMaxArea = numeric_limits<Real>::min();

    #pragma omp parallel default(shared)
    {
        // Compute area of each triangle
        #pragma omp for reduction(+:totalArea) schedule(static)
        for (int i = 0; i < (int)numFaces; i++)
        {
            const Vector3r &a = vertices[faces[3 * i]];
            const Vector3r &b = vertices[faces[3 * i + 1]];
            const Vector3r &c = vertices[faces[3 * i + 2]];

            const Vector3r d1 = b - a;
            const Vector3r d2 = c - a;

            const Real area = (d1.cross(d2)).norm() / static_cast<Real>(2.0);
            m_areas[i] = area;
            totalArea += area;
            //tmpMaxArea = max(area, tmpMaxArea);

            if (area > tmpMaxArea)
            {
                #pragma omp critical
                {
                    tmpMaxArea = max(area, tmpMaxArea);
                }
            }

        }
    }
    m_maxArea = max(tmpMaxArea, m_maxArea);
    m_totalArea = totalArea;
}

void PoissonDiskSampling::generateInitialPointSet(const unsigned int numVertices, const Vector3r *vertices, const unsigned int numFaces, const unsigned int *faces)
{
    m_totalArea = 0.0;

    // Drawing random barycentric coordinates
    std::uniform_real_distribution<Real> distribution(0.0, 1.0);

    random_device r;
    std::vector<std::default_random_engine> generators;

    #ifdef _OPENMP
    const int maxThreads = omp_get_max_threads();
    #else
    const int maxThreads = 1;
    #endif

    for (int i = 0; i < maxThreads; ++i)
        generators.emplace_back(default_random_engine(r()));

    #pragma omp parallel default(shared)
    {
        // Generating the surface points
        #pragma omp for schedule(static)
        for (int i = 0; i < (int)m_initialInfoVec.size(); i++)
        {
            #ifdef _OPENMP
            int tid = omp_get_thread_num();
            #else
            int tid = 0;
            #endif
            // Get the generator based on thread id
            std::default_random_engine& generator = generators[tid];

            Real rn1 = sqrt(distribution(generator));
            Real bc1 = static_cast<Real>(1.0) - rn1;
            Real bc2 = distribution(generator)*rn1;
            Real bc3 = static_cast<Real>(1.0) - bc1 - bc2;

            // Triangle selection with probability proportional to area
            const unsigned int randIndex = getAreaIndex(m_areas, m_totalArea, generator, distribution);

            // Calculating point coordinates
            const Vector3r &v1 = vertices[faces[3 * randIndex]];
            const Vector3r &v2 = vertices[faces[3 * randIndex + 1]];
            const Vector3r &v3 = vertices[faces[3 * randIndex + 2]];

            m_initialInfoVec[i].pos = bc1*v1 + bc2*v2 + bc3*v3;
            m_initialInfoVec[i].ID = randIndex;
        }
    }
}


unsigned int PoissonDiskSampling::getAreaIndex(const vector<Real>& areas, const Real totalArea, std::default_random_engine &generator, std::uniform_real_distribution<Real> &distribution)
{
    // see https://en.wikipedia.org/wiki/Fitness_proportionate_selection
    //Real rn = m_uniform_distribution1(m_generator)*totalArea;

    //for (unsigned int i = 0; i<areas.size(); i++)
    //{
    //  rn -= areas[i];
    //  if (rn <= 0)
    //      return i;
    //}
    //return (int)areas.size() - 1;

    // Stochastic acceptance version O(1)
    bool notaccepted = true;
    unsigned int index = 0;
    while (notaccepted)
    {
        index = (int)((Real)areas.size()*distribution(generator));
        if (distribution(generator)<areas[index] / m_maxArea)
            notaccepted = false;
    }
    return index;
}


void PoissonDiskSampling::parallelUniformSurfaceSampling(std::vector<Vector3r> &samples)
{

    // Sort initial points into HashMap storing only the index of the first point of cell
    // and build phase groups
    unordered_map<CellPos, HashEntry, CellPosHasher> hMap(2 * m_initialInfoVec.size());
    samples.clear();
    samples.reserve(m_initialInfoVec.size());

    // Already insert first Initial point as start of first cell in hashmap
    {
        const CellPos& cell = m_initialInfoVec[0].cP;
        HashEntry &entry = hMap[cell];
        entry.startIndex = 0;
        entry.samples.reserve(5);
        int index = cell[0] % 3 + 3 * (cell[1] % 3) + 9 * (cell[2] % 3);
        m_phaseGroups[index].push_back(cell);
    }

    for (int i = 1; i < (int)m_initialInfoVec.size(); i++)
    {
        const CellPos& cell = m_initialInfoVec[i].cP;
        if (cell != m_initialInfoVec[i - 1].cP)
        {
            HashEntry &entry = hMap[cell];
            entry.startIndex = i;
            entry.samples.reserve(5);
            int index = cell[0] % 3 + 3 * (cell[1] % 3) + 9 * (cell[2] % 3);
            m_phaseGroups[index].push_back(cell);
        }
    }
    // Loop over number of tries to find a sample in a cell
    for (int k = 0; k < (int)m_numTrials; k++)
    {
        // Loop over the 27 cell groups
        for (int pg = 0; pg < m_phaseGroups.size(); pg++)
        {
            const vector<CellPos>& cells = m_phaseGroups[pg];
            // Loop over the cells in each cell group
            #pragma omp parallel for schedule(static)
            for (int i = 0; i < (int)cells.size(); i++)
            {
                const auto entryIt = hMap.find(cells[i]);
                // Check if cell exists
                if (entryIt != hMap.end())
                {
                    // Check if max Index is not exceeded
                    HashEntry& entry = entryIt->second;
                    if (entry.startIndex + k < m_initialInfoVec.size())
                    {
                        if (m_initialInfoVec[entry.startIndex].cP == m_initialInfoVec[entry.startIndex + k].cP)
                        {
                            // choose kth point from cell
                            const InitialPointInfo& test = m_initialInfoVec[entry.startIndex + k];
                            // Assign sample
                            if (!nbhConflict(hMap, test))
                            {
                                const int index = entry.startIndex + k;
                                #pragma omp critical
                                {
                                    entry.samples.push_back(index);
                                    samples.push_back(m_initialInfoVec[index].pos);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

bool PoissonDiskSampling::nbhConflict(const unordered_map<CellPos, HashEntry, CellPosHasher>& hMap, const InitialPointInfo& iPI)
{
    CellPos nbPos = iPI.cP;

    // check neighboring cells inside to outside
    if (checkCell(hMap, nbPos, iPI))
        return true;
    for (int level = 1; level < 3; level++)
    {
        for (int ud = -level; ud < level + 1; ud += 2 * level)
        {
            for (int i = -level+1; i < level ; i++)
            {
                for (int j = -level + 1; j < level ; j++)
                {
                    nbPos = CellPos(i, ud, j) + iPI.cP;
                    if (checkCell(hMap, nbPos, iPI))
                        return true;
                }
            }

            for (int i = -level; i < level + 1; i++)
            {
                for (int j = -level + 1; j < level ; j++)
                {
                    nbPos = CellPos(j, i, ud) + iPI.cP;
                    if (checkCell(hMap, nbPos, iPI))
                        return true;
                }

                for (int j = -level; j < level + 1; j++)
                {
                    nbPos = CellPos(ud, i, j) + iPI.cP;
                    if (checkCell(hMap, nbPos, iPI))
                        return true;
                }
            }
        }
    }
    return false;
}

bool PoissonDiskSampling::checkCell(const unordered_map<CellPos, HashEntry, CellPosHasher>& hMap, const CellPos& cell, const InitialPointInfo& iPI)
{
    const auto nbEntryIt = hMap.find(cell);
    if (nbEntryIt != hMap.end())
    {
        const HashEntry& nbEntry = nbEntryIt->second;
        for (unsigned int i = 0; i < nbEntry.samples.size(); i++)
        {
            const InitialPointInfo &info = m_initialInfoVec[nbEntry.samples[i]];
            Real dist;
            if (m_distanceNorm == 0 || iPI.ID == info.ID)
            {
                dist = (iPI.pos - info.pos).norm();
            }
            else if (m_distanceNorm == 1)
            {
                Vector3r v = (info.pos - iPI.pos).normalized();
                Real c1 = m_faceNormals[iPI.ID].dot(v);
                Real c2 = m_faceNormals[info.ID].dot(v);

                dist = (iPI.pos - info.pos).norm();
                if (fabs(c1 - c2) > 0.00001f)
                    dist *= (asin(c1) - asin(c2)) / (c1 - c2);
                else
                    dist /= (sqrt(static_cast<Real>(1.0) - c1*c1));
            }
            else
            {
                return true;
            }

            if (dist < m_r)
                return true;
        }
    }
    return false;
}

void PoissonDiskSampling::determineMinX(const unsigned int numVertices, const Vector3r *vertices)
{
    m_minVec = Vector3r(numeric_limits<Real>::max(), numeric_limits<Real>::max(), numeric_limits<Real>::max());

    for (int i = 0; i < (int)numVertices; i++)
    {
        const Vector3r& v = vertices[i];
        m_minVec[0] = std::min(m_minVec[0], v[0]);
        m_minVec[1] = std::min(m_minVec[1], v[1]);
        m_minVec[2] = std::min(m_minVec[2], v[2]);
    }
}

void PoissonDiskSampling::quickSort(int left, int right)
{
    if (left < right)
    {
        int index = partition(left, right);
        quickSort(left, index - 1);
        quickSort(index, right);
    }
}

int PoissonDiskSampling::partition(int left, int right)
{
    int i = left;
    int j = right;
    Vector3r tmpPos;
    CellPos tmpCell;
    InitialPointInfo tmpInfo;
    CellPos pivot = m_initialInfoVec[left + (right - left) / 2].cP;

    while (i <= j)
    {
        while (compareCellID(m_initialInfoVec[i].cP, pivot))
            i++;

        while (compareCellID(pivot, m_initialInfoVec[j].cP))
            j--;

        if (i <= j)
        {
            tmpInfo = m_initialInfoVec[i];
            m_initialInfoVec[i] = m_initialInfoVec[j];
            m_initialInfoVec[j] = tmpInfo;
            i++;
            j--;
        }
    }
    return i;
}

bool PoissonDiskSampling::compareCellID(CellPos& a, CellPos& b)
{
    for (unsigned int i = 0; i < 3; i++)
    {
        if (a[i] < b[i]) return true;
        if (a[i] > b[i]) return false;
    }

    return false;
}

void PoissonDiskSampling::computeFaceNormals(const unsigned int numVertices, const Vector3r *vertices, const unsigned int numFaces, const unsigned int *faces)
{
    m_faceNormals.resize(numFaces);

    #pragma omp parallel default(shared)
    {
        #pragma omp for schedule(static)
        for (int i = 0; i < (int) numFaces; i++)
        {
            // Get first three points of face
            const Vector3r &a = vertices[faces[3 * i]];
            const Vector3r &b = vertices[faces[3 * i + 1]];
            const Vector3r &c = vertices[faces[3 * i + 2]];

            // Create normal
            Vector3r v1 = b - a;
            Vector3r v2 = c - a;

            m_faceNormals[i] = v1.cross(v2);
            m_faceNormals[i].normalize();
        }
    }
}