http://devmag.org.za/2009/05/03/poisson-disk-sampling/
This article originally appeared in Dev.Mag Issue 21, released in March 2008.
One way to populate large worlds with objects is to simply place objects on a grid, or randomly. While fast and easy to implement, both these methods result in unsatisfying worlds: either too regular or too messy. In this article we look at an alternative algorithm that returns a random set of points with nice properties:
Figure 1 shows an example of such a set, which is called Poisson-disk sample set. For comparison, two other sample sets are also shown: a uniform random sample, and a jittered grid sample.
Poisson disk sampling has many applications in games:
In this article we will look mostly at object placement and briefly at texture generation.
Figure 1
There are several algorithms for producing a Poisson disk sample set. The one presented here is easy to implement, and runs reasonably fast. It is also easy adapted for specific applications (described in the next section).
The basic idea is to generate points around existing points, and to check whether they can be added so that they don’t disturb the minimum distance requirement. A grid is used to perform fast lookups of points. Two lists keep track of points that are being generated, and those that needs processing.
Here are the details:
1. Choose a random point from the processing list.
2. For this point, generate up to k points, randomly selected from the annulus surrounding the point. You can choose k – a value of 30 gives good results. In general, larger values give tighter packings, but make the algorithm run slower. For every generated point:
Here is how all this look in pseudo code:
generate_poisson(width, height, min_dist, new_points_count)
{
//Create the grid
cellSize = min_dist/sqrt(2);
grid = Grid2D(Point(
(ceil(width/cell_size), //grid width
ceil(height/cell_size)))); //grid height
//RandomQueue works like a queue, except that it
//pops a random element from the queue instead of
//the element at the head of the queue
processList = RandomQueue();
samplePoints = List();
//generate the first point randomly
//and updates
firstPoint = Point(rand(width), rand(height));
//update containers
processList.push(firstPoint);
samplePoints.push(firstPoint);
grid[imageToGrid(firstPoint, cellSize)] = firstPoint;
//generate other points from points in queue.
while (not processList.empty())
{
point = processList.pop();
for (i = 0; i < new_points_count; i++)
{
newPoint = generateRandomPointAround(point, min_dist);
//check that the point is in the image region
//and no points exists in the point's neighbourhood
if (inRectangle(newPoint) and
not inNeighbourhood(grid, newPoint, min_dist,
cellSize))
{
//update containers
processList.push(newPoint);
samplePoints.push(newPoint);
grid[imageToGrid(newPoint, cellSize)] = newPoint;
}
}
}
return samplePoints;
}
The grid coordinates of a point can be easily calculated:
imageToGrid(point, cellSize)
{
gridX = (int)(point.x / cellSize);
gridY = (int)(point.y / cellSize);
return Point(gridX, gridY);
}
Figure 2 shows how a random point (red) is selected in the annulus around an existing point (blue). Two parameters determine the new point’s position: the angle (randomly chosen between 0 and 360 degrees), and the distance from the original point (randomly chosen between the minimum distance and twice the minimum distance). In pseudo code:
generateRandomPointAround(point, mindist)
{ //non-uniform, favours points closer to the inner ring, leads to denser packings
r1 = Random.nextDouble(); //random point between 0 and 1
r2 = Random.nextDouble();
//random radius between mindist and 2 * mindist
radius = mindist * (r1 + 1);
//random angle
angle = 2 * PI * r2;
//the new point is generated around the point (x, y)
newX = point.x + radius * cos(angle);
newY = point.y + radius * sin(angle);
return Point(newX, newY);
}
Figure 2 - Generating a new sample point.
Before a newly generated point is admitted as a sample point, we have to check that no previously generated points are too close. Figure 3 shows a piece of the grid. The red dot is a potential new sample point. We have to check for existing points in the region contained by the red circles (they are the circles at the corners of the cell of the new point). The blue squares are cells that are partially or completely covered by a circle. We need only check these cells. However, to simplify the algorithm, we check all 25 cells.
Here is the pseudo code:
inNeighbourhood(grid, point, mindist, cellSize)
{
gridPoint = imageToGrid(point, cellSize)
//get the neighbourhood if the point in the grid
cellsAroundPoint = squareAroundPoint(grid, gridPoint, 5)
for every cell in cellsAroundPoint
if (cell != null)
if distance(cell, point) < mindist
return true
return false
}
Figure 3 - Checking the neighbourhood of a potential sample point.
Figure 4 - Spheres placed at points in a Poisson disk sample of 3D space.
The algorithm can easily be modified for 3D:
generateRandomPointAround(point, minDist)
{ //non-uniform, leads to denser packing.
r1 = Random.nextDouble(); //random point between 0 and 1
r2 = Random.nextDouble();
r3 = Random.nextDouble();
//random radius between mindist and 2* mindist
radius = mindist * (r1 + 1);
//random angle
angle1 = 2 * PI * r2;
angle2 = 2 * PI * r3;
//the new point is generated around the point (x, y, z)
newX = point.x + radius * cos(angle1) * sin(angle2);
newY = point.y + radius * sin(angle1) * sin(angle2);
newZ = point.z + radius * cos(angle2);
return Point(newX, newY, newZ);
}
Placing objects at the positions of a Poisson disk sample set is the simplest way to use this algorithm in games (Figure 5). Ideally, the algorithm can be built into your level editing tool with features that allows the artist to select the region to fill, and the models to fill them with.
Figure 5 - An example with shrubs placed at Poisson disk sample points.
Figure 5 – An example with shrubs placed at Poisson disk sample points.
One important variation of a Poisson sample set is one where the minimum distance between points is not constant, but varies across the image. In this variation, we feed the algorithm a greyscale image, which is used to modulate the minimum distance between points.
To make this work, you need to modify the algorithm as follows:
min_dist = min_radius + grey * (max_radius - min_radius)
As an example, you can use Perlin noise to drive the minimum distance, giving you interesting clusters of objects (Figure 6). This method is especially useful for generating a field of plants.
Figure 6 - Poisson disk sample, where the minimum distance is driven by Perlin noise.
When using different radii as explained above, you might run into some problems. Take the following precautions:
min_dist[i, j] = dist((i, j), (x0, y0))
But because new points are generated at exactly this distance, many more points are excluded than expected, leading to a rather poor result. A better sample can be obtained by using the square root of the distance. (See Figure 7.)
Figure 7
In another situation, sections of large radii might be too close to other sections of large radii, so that no points are produced in sections of small radii (see Figure 8).
Figure 8 - The high radius is too great; some low radius regions are skipped.
Figure 9
Poisson samples can also be used to generate certain textures. In Figure 10, the droplets on the bottle and glass have been created by combining three layers of Poisson disk sample points.
The modified algorithm creates three layers of points, each layer with a smaller minimum distance between points than the next. In every layer, the algorithm checks that there are no circles in the previous layers. To do this, the look-up grids for all generated layers are used in a final step to eliminate unwanted points.
The local minimum distance of a every sample point is stored, so that it can be used as a radius to draw a circle once all points have been found.
The raw texture is then further processed by clever artists through filters and shaders to produce the final result.
Figure 10.
Poisson disk samples also form the basis of the procedural textures shown in Figure 11.
The first texture was produced by painting filled translucent circles for every Poisson sample point, for three separate samples. A different colour was used for every sample set, with small random variations.
The second texture was produced by painting circles for two sample sets; one with filled circles, the other with outlines only.
The third texture was created by painting daisies (randomly scaled and rotated) onto an existing grass texture.
Figure 11 - Procedural textures generated from Poisson disk samples.
(Some of these links were added after the article was first published).