Skip to content

Commit

Permalink
Refine Poisson disk sampler options and update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
nzfeng committed Aug 24, 2024
1 parent 1fa5215 commit 129494d
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 49 deletions.
33 changes: 22 additions & 11 deletions docs/docs/surface/algorithms/surface_sampling.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ Currently the algorithm only works on manifold meshes.

The algorithm has a few parameters that roughly correspond to the algorithm of Bridson's 2007 [Fast Poisson Disk Sampling in Arbitrary Dimensions](https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf).

Additionally, you can specify points around samples should be avoided (shown in red below.) By default, samples will avoid these points with the same radius `r` used in the rest of the algorithm. You can optionally specify a "radius of avoidance" for these points, where the radius of avoidance is given in multiples of `r`.

The radius of avoidance can be further be specified to be a radius in 3D space, or a radius in terms of distance along the surface. The former will produce a radius of avoidance that will appear perfectly round and is likely more visually pleasing, but for very large radii may occlude samples from opposite sides of the mesh. The latter will restrict the radius of avoidance to only be along the surface, but such a metric ball will not appear perfectly round, especially in areas with very large and sudden changes in curvature.
Additionally, you can specify points around samples should be avoided (shown in red below.)

![poisson disk sample with point of avoidance](/media/poisson_disk_sample.png)

Expand All @@ -23,15 +21,9 @@ The radius of avoidance can be further be specified to be a radius in 3D space,

The mesh and geometry cannot be changed after construction.

??? func "`#!cpp std::vector<SurfacePoint> PoissonDiskSampler::sample(double rCoef = 1.0, int kCandidates = 30, std::vector<SurfacePoint> pointsToAvoid = std::vector<SurfacePoint>(), int rAvoidance = 1, bool use3DAvoidanceRadius = true);`"
??? func "`#!cpp std::vector<SurfacePoint> sample(const PoissonDiskOptions& options = PoissonDiskOptions())`"

Poisson disk-sample the surface mesh.

- `rCoef`: corresponds to the minimum distance between samples, expressed as a multiple of the mean edge length. The actual minimum distance is computed as `r = rCoef * meanEdgeLength`
- `kCandidates`: the number of candidate points chosen from the (r,2r)-annulus around each sample.
- `pointsToAvoid`: SurfacePoints which samples should avoid.
- `rAvoidance`: the radius of avoidance around each point to avoid, expressed as a multiple of `r`.
- `use3DAvoidanceRadius`: If true, the radius of avoidance will specify a solid ball in 3D space around which samples are avoided. Otherwise, samples are avoided within a ball _on the surface_.

### Example

Expand All @@ -51,4 +43,23 @@ std::tie(mesh, geometry) = readManifoldSurfaceMesh(filename);
// construct a solver
PoissonDiskSampler poissonSampler(*mesh, *geometry);
std::vector<SurfacePoint> samples = poissonSampler.sample(); // sample using default parameters
```

// Sample with some different parameters.
PoissonDiskOptions sampleOptions;
sampleOptions.r = 2.;
std::vector<SurfacePoint> newSamples = poissonSampler.sample(sampleOptions);
```
## Helper Types
### Options
Options are passed in to `options` via a `PoissonDiskOptions` object.
| Field | Default value |Meaning|
|---|---|---|
| `#!cpp double r`| `1` | The minimum distance between samples, expressed in world-space units. |
| `#!cpp int kCandidates`| `30` | The number of candidate points chosen from the (`r`,2`r`)-annulus around each sample. |
| `#!cpp std::vector<SurfacePoint> pointsToAvoid`| `std::vector<SurfacePoint>()` | Points which samples should avoid. |
| `#!cpp double rAvoidance`| `1` | The radius of avoidance around each point to avoid, expressed in world-space units. |
| `#!cpp bool use3DAvoidance`| `true` | If true, the radius of avoidance will specify a solid ball in 3D space around which samples are avoided. Otherwise, samples are avoided within a geodesic ball on the surface. |
Using the `use3DAvoidance` option, the radius of avoidance `rAvoidance` can specify either the radius in 3D space, or in terms of distance along the surface. The former will produce a radius of avoidance that will appear perfectly round, and is likely more visually pleasing, but for very large radii may occlude samples from opposite sides of the mesh. The latter will restrict the radius of avoidance to only be along the surface, but such a metric ball may not appear perfectly round, especially in areas with very large changes in curvature.
18 changes: 7 additions & 11 deletions include/geometrycentral/surface/poisson_disk_sampler.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ namespace geometrycentral {
namespace surface {

struct PoissonDiskOptions {
double rCoef = 1.0; // minimum distance between samples, expressed as a multiple of the mean edge length
double absoluteRadius = -1.0; // actual minimum distance between samples
int kCandidates = 30; // number of candidate points chosen from the (r,2r)-annulus around each sample
double r = 1.0; // minimum distance between samples
int kCandidates = 30; // number of candidate points chosen from the (r,2r)-annulus around each sample
std::vector<SurfacePoint> pointsToAvoid;
int rAvoidance = 1; // radius of avoidance, expressed as a multiple of the mean edge length
bool use3DAvoidanceRadius = true;
double rAvoidance = 1.0; // radius of avoidance
bool use3DAvoidance = true;
};

class PoissonDiskSampler {
Expand All @@ -29,8 +28,6 @@ class PoissonDiskSampler {
// ===== The main function: Sample the surface using Poisson Disk Sampling.
std::vector<SurfacePoint> sample(const PoissonDiskOptions& options = PoissonDiskOptions());

double getMeanEdgeLength() const { return meanEdgeLength; }

private:
// ===== Members
ManifoldSurfaceMesh& mesh;
Expand All @@ -39,9 +36,8 @@ class PoissonDiskSampler {
int kCandidates; // number of candidate points chosen from the (r,2r)-annulus around each sample
std::vector<SurfacePoint> pointsToAvoid;

double rMinDist; // the actual minimum distance between samples
double meanEdgeLength; // the mean edge length
double sideLength; // side length of each bucket
double rMinDist; // the minimum distance between samples
double sideLength; // side length of each bucket

std::vector<Face> faceFromEachComponent; // holds one face for each connected component in the mesh
std::vector<SurfacePoint> activeList; // holds candidate points
Expand All @@ -59,7 +55,7 @@ class PoissonDiskSampler {
void addNewSample(const SurfacePoint& sample);
SpatialKey positionKey(const Vector3& position) const;
void addPointToSpatialLookup(const Vector3& newPos);
void addPointToSpatialLookupWithRadius(const SurfacePoint& newPoint, int R = 0, bool use3DAvoidanceRadius = true);
void addPointToSpatialLookupWithRadius(const SurfacePoint& newPoint, double radius = 0, bool use3DAvoidance = true);
bool isCandidateValid(const SurfacePoint& candidate) const;
void sampleOnConnectedComponent(const Face& f);
void clearData();
Expand Down
41 changes: 14 additions & 27 deletions src/surface/poisson_disk_sampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ namespace surface {
PoissonDiskSampler::PoissonDiskSampler(ManifoldSurfaceMesh& mesh_, VertexPositionGeometry& geometry_)
: mesh(mesh_), geometry(geometry_) {

// Compute mean edge length.
geometry.requireEdgeLengths();
meanEdgeLength = 0.;
for (Edge e : mesh.edges()) {
meanEdgeLength += geometry.edgeLengths[e];
}
meanEdgeLength /= mesh.nEdges();
geometry.unrequireEdgeLengths();

// Prepare spatial lookup
Vector3 bboxMin, bboxMax;
std::tie(bboxMin, bboxMax) = boundingBox();
Expand Down Expand Up @@ -124,17 +115,15 @@ void PoissonDiskSampler::addPointToSpatialLookup(const Vector3& newPos) {
}

/*
* Mark all buckets with a radius of <R> buckets as occupied, as well.
* Mark all buckets within a radius of <radius> as occupied.
*/
void PoissonDiskSampler::addPointToSpatialLookupWithRadius(const SurfacePoint& newPoint, int R,
bool use3DAvoidanceRadius) {
void PoissonDiskSampler::addPointToSpatialLookupWithRadius(const SurfacePoint& newPoint, double radius,
bool use3DAvoidance) {

Vector3 newPos = newPoint.interpolate(geometry.vertexPositions);
addPointToSpatialLookup(newPos);

if (R <= 0) return;

if (!use3DAvoidanceRadius) {
if (!use3DAvoidance) {
// This places fictitious points in a metric ball approximately of radius R*r centered at <newPoint>.
// The solid ball is built by constructing R layers, radially outward; each layer is spaced r apart, and
// points in each layer are spaced approx. r apart around the circular layer. The idea is that no point can be added
Expand All @@ -144,21 +133,22 @@ void PoissonDiskSampler::addPointToSpatialLookupWithRadius(const SurfacePoint& n
// "spiky" meshes.
SurfacePoint pathEndpoint;
TraceGeodesicResult trace;
for (int rIter = 0; rIter <= R; rIter++) {
double dist = rIter * rMinDist;
for (double theta = 0.; theta <= 2. * PI; theta += rMinDist / dist / 2.) {
trace = traceGeodesic(geometry, newPoint, dist * Vector2::fromAngle(theta));
double r = rMinDist;
while (r < radius) {
for (double theta = 0.; theta <= 2. * PI; theta += rMinDist / r / 2.) {
trace = traceGeodesic(geometry, newPoint, r * Vector2::fromAngle(theta));
pathEndpoint = trace.endPoint;
addPointToSpatialLookup(pathEndpoint.interpolate(geometry.vertexPositions));
}
r += rMinDist;
}
} else {
// This places fictitious points in a *solid 3D ball* approximately of radius R*r centered at <newPoint>.
// The solid ball is built by constructing R layers, radially outward; each layer is spaced r apart, and
// points in each layer are spaced approx. r apart in a "grid" (a mapping from the cylinder to the sphere that has
// been scaled s.t. projected points end up being approx. r apart.)
for (int rIter = 0; rIter <= R; rIter++) {
double r = rIter * rMinDist;
double r = rMinDist;
while (r < radius) {
for (double z = -0.99; z <= 0.99; z += rMinDist) {
double coeff = std::sqrt(1. - z * z);
for (double theta = 0.0; theta <= 2.0 * PI; theta += rMinDist / coeff) {
Expand All @@ -167,6 +157,7 @@ void PoissonDiskSampler::addPointToSpatialLookupWithRadius(const SurfacePoint& n
addPointToSpatialLookup(pos);
}
}
r += rMinDist;
}
}
}
Expand Down Expand Up @@ -261,18 +252,14 @@ std::vector<SurfacePoint> PoissonDiskSampler::sample(const PoissonDiskOptions& o
clearData();

// Set parameters.
if (options.rCoef > 0.0) {
rMinDist = options.rCoef * meanEdgeLength;
} else if (options.absoluteRadius > 0.0) {
rMinDist = options.absoluteRadius;
}
rMinDist = options.r;
sideLength = rMinDist / std::sqrt(3.0);
kCandidates = options.kCandidates;
pointsToAvoid = options.pointsToAvoid;

// Add points to avoid.
for (const SurfacePoint& pt : pointsToAvoid) {
addPointToSpatialLookupWithRadius(pt, options.rAvoidance - 1, options.use3DAvoidanceRadius);
addPointToSpatialLookupWithRadius(pt, options.rAvoidance, options.use3DAvoidance);
}

// Carry out sampling process for each connected component.
Expand Down

0 comments on commit 129494d

Please sign in to comment.