File Coverage

blib/lib/Random/PoissonDisc.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Random::PoissonDisc;
2 2     2   32326 use strict;
  2         6  
  2         99  
3 2     2   12 use List::Util qw(sum);
  2         4  
  2         297  
4 2     2   2555 use Math::Random::MT::Auto qw(rand gaussian);
  0            
  0            
5              
6             use vars qw($VERSION %grid_neighbours);
7             $VERSION = '0.02';
8              
9             # %grid_neighbours caches the vectors pointing to
10             # neighbours
11              
12             =head1 NAME
13              
14             Random::PoissonDisc - distribute points aesthetically in R^n
15              
16             =head1 SYNOPSIS
17              
18             my $points = Random::PoissonDisc->points(
19             dimensions => [100,100],
20             r => $r,
21             );
22             print join( ",", @$_),"\n"
23             for @$points;
24              
25             This module allows relatively fast
26             (linear in the number of points generated) generation of random points in
27             I-dimensional space with a distance of
28             at least C between each other. This distribution
29             results in aesthetic so called "blue noise".
30              
31             The algorithm was adapted from a sketch
32             by Robert Bridson
33             in L.
34              
35             =head1 DATA REPRESENTATION
36              
37             All vectors (or points) are represented
38             as anonymous arrays of numbers. All have the same
39             dimension as the cardinality of the C
40             array passed in the C<< ->points >> method.
41              
42             =head2 USER INTERFACE
43              
44             =head3 C<< Random::PoissonDisc->points( %options ) >>
45              
46             Returns a reference to an array of points.
47              
48             Acceptable options are:
49              
50             =over 4
51              
52             =item *
53              
54             C< r > - minimum distance between points.
55              
56             Default is 10 units.
57              
58             =item *
59              
60             C< dimensions > - number of dimensions and respective value range as an arrayref.
61              
62             Default is
63              
64             [ 100, 100 ]
65              
66             meaning all points will be in R^2 , with each coordinate in the
67             range [0, 100).
68              
69             =item *
70              
71             C< candidates > - Number of candidates to inspect before deciding that no
72             ew neighbours can be placed around a point.
73              
74             Default is 30.
75              
76             This number may or may not need to be tweaked if you go further up in
77             dimensionality beyond 3 dimensions. The more candidates you inspect
78             the longer the algorithm will run for generating a number of points.
79              
80             In the algorithm description, this constant is named I.
81              
82             =back
83              
84             =cut
85              
86             sub points {
87             my ($class,%options) = @_;
88            
89             $options{candidates} ||= 30;
90             $options{dimensions} ||= [100,100]; # do we only create integral points?
91             $options{r} ||= 10;
92             #$options{max} ||= 10; # we want to fill the space instead?!
93             $options{ grid } ||= {};
94            
95             my $grid_size = $options{ r } / sqrt( 0+@{$options{dimensions}});
96            
97             my @result;
98             my @work;
99            
100             # Create a first random point somewhere in our cube:
101             my $p = [map { rnd(0,$_) } @{ $options{ dimensions }}];
102             push @result, $p;
103             push @work, $p;
104             my $c = grid_coords($grid_size, $p);
105             $options{ grid }->{ $c } = $p;
106            
107             while (@work) {
108             my $origin = splice @work, int rnd(0,$#work), 1;
109             CANDIDATE: for my $candidate ( 1..$options{ candidates } ) {
110             # Create a random distance between r and 2r
111             # that is, in the annulus with radius (r,2r)
112             # surrounding our current point
113             my $dist = rnd( $options{r}, $options{r}*2 );
114            
115             # Choose a random angle in which to point
116             # this vector
117             my $angle = random_unit_vector(0+@{$options{ dimensions}});
118            
119             # Generate a new point by adding the $angle*$dist to $origin
120             my $p = [map { $origin->[$_] + $angle->[$_]* $dist } 0..$#$angle];
121            
122             # Check whether our point lies within the dimensions
123             for (0..$#$p) {
124             next CANDIDATE
125             if $p->[$_] >= $options{ dimensions }->[ $_ ]
126             or $p->[$_] < 0
127             };
128            
129             # check discs by using the grid
130             # Here we should check the "neighbours" in the grid too
131             my $c = grid_coords($grid_size, $p);
132             if (! $options{ grid }->{ $c }) {
133             my @n = neighbour_points($grid_size, $p, $options{ grid });
134             for my $neighbour (@n) {
135             if( vdist($neighbour, $p) < $options{ r }) {
136             next CANDIDATE;
137             };
138             };
139            
140             # not already in grid, no close neighbours, add it
141             push @result, $p;
142             push @work, $p;
143             $options{ grid }->{ $c } = $p;
144             #warn "$candidate Taking";
145             } else {
146             #warn "$candidate Occupied";
147             };
148             };
149             };
150            
151             \@result
152             };
153              
154             =head2 INTERNAL SUBROUTINES
155              
156             These subroutines are used for the algorithm.
157             If you want to port this module to PDL or any other
158             vector library, you will likely have to rewrite these.
159              
160             =head3 C<< rnd( $low, $high ) >>
161              
162             print rnd( 0, 1 );
163              
164             Returns a uniform distributed random number
165             in C<< [ $low, $high ) >>.
166              
167             =cut
168              
169             sub rnd {
170             my ($low,$high) = @_;
171             return $low + rand($high-$low);
172             };
173              
174             =head3 C<< grid_coords( $grid_size, $point ) >>
175              
176             Returns the string representing the coordinates
177             of the grid cell in which C<< $point >> falls.
178              
179             =cut
180              
181             sub grid_coords {
182             my ($size,$point) = @_;
183             join "\t", map { int($_/$size) } @$point;
184             };
185              
186             =head3 C<< norm( @vector ) >>
187              
188             print norm( 1,1 ); # 1.4142
189              
190             Returns the Euclidean length of the vector, passed in as array.
191              
192             =cut
193              
194             sub norm {
195             sqrt( sum @{[map {$_**2} @_]} );
196             };
197              
198             =head3 C<< vdist( $l, $r ) >>
199              
200             print vdist( [1,0], [0,1] ); # 1.4142
201              
202             Returns the Euclidean distance between two points
203             (or vectors)
204              
205             =cut
206              
207             sub vdist {
208             my ($l,$r) = @_;
209             my @connector = map { $l->[$_] - $r->[$_] } 0..$#$l;
210             norm(@connector);
211             };
212              
213             =head3 C<< neighbour_points( $size, $point, $grid ) >>
214              
215             my @neighbours = neighbour_points( $size, $p, \%grid )
216              
217             Returns the points from the grid that have a distance
218             between 0 and 2r around C<$point>. These points are
219             the candidates to check when trying to insert a new
220             random point into the space.
221              
222             =cut
223              
224             sub neighbour_points {
225             my ($size,$point,$grid) = @_;
226            
227             my $dimension = 0+@$point;
228             my $vectors;
229             if (! $grid_neighbours{ $dimension }) {
230             my @elements = (-1,0,1);
231             $grid_neighbours{ $dimension } =
232             # Count up, and use the number in ternary as our odometer
233             [map {
234             my $val = $_;
235             my $res = [ map {
236             my $res = $elements[ $val % 3 ];
237             $val = int($val/3);
238             $res
239             } 1..$dimension ];
240             } (1..3**$dimension)
241             ];
242             };
243            
244             my @coords = split /\t/, grid_coords( $size, $point );
245              
246             # Find the elements in the grid according to the offsets
247             map {
248             my $e = $_;
249             my $n = join "\t", map { $coords[$_]+$e->[$_] } 0..$#$_;
250             # Negative grid positions never get filled, conveniently!
251             $grid->{ $n } ? $grid->{ $n } : ()
252             } @{ $grid_neighbours{ $dimension }};
253             };
254              
255             =head3 C<< random_unit_vector( $dimensions ) >>
256              
257             print join ",", @{ random_unit_vector( 2 ) };
258              
259             Returns a vector of unit length
260             pointing in a random uniform distributed
261             I-dimensional direction
262             angle
263             and returns a unit vector pointing in
264             that direction
265              
266             The algorithm used is outlined in
267             Knuth, _The Art of Computer Programming_, vol. 2,
268             3rd. ed., section 3.4.1.E.6.
269             but has not been verified formally or mathematically
270             by the module author.
271              
272             =cut
273              
274             sub random_unit_vector {
275             my ($dimensions) = @_;
276             my (@vec,$len);
277            
278             # Create normal distributed coordinates
279             RETRY: {
280             @vec = map { gaussian() } 1..$dimensions;
281             $len = norm(@vec);
282             redo RETRY unless $len;
283             };
284             # Normalize our vector so we get a unit vector
285             @vec = map { $_ / $len } @vec;
286            
287             \@vec
288             };
289              
290             1;
291              
292             =head1 TODO
293              
294             The module does not use L or any other
295             vector library.
296              
297             =head1 REPOSITORY
298              
299             The public repository of this module is
300             L.
301              
302             =head1 SUPPORT
303              
304             The public support forum of this module is
305             L.
306              
307             =head1 BUG TRACKER
308              
309             Please report bugs in this module via the RT CPAN bug queue at
310             L
311             or via mail to L.
312              
313             =head1 AUTHOR
314              
315             Max Maischein C
316              
317             =head1 COPYRIGHT (c)
318              
319             Copyright 2011 by Max Maischein C.
320              
321             =head1 LICENSE
322              
323             This module is released under the same terms as Perl itself.
324              
325             =cut