-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathpoisson_disc.py
178 lines (140 loc) · 4.76 KB
/
poisson_disc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import random
from math import sqrt, pi, sin, cos
from itertools import product
class Grid:
"""
class for filling a rectangular prism of dimension >= 2
with poisson disc samples spaced at least r apart
and k attempts per active sample
override Grid.distance to change
distance metric used and get different forms
of 'discs'
"""
def __init__(self, r, *size):
self.r = r
self.size = size
self.dim = len(size)
self.cell_size = r/(sqrt(self.dim))
self.widths = [int(size[k]/self.cell_size) + 1 for k in range(self.dim)]
nums = product(*(range(self.widths[k]) for k in range(self.dim)))
self.cells = {num:-1 for num in nums}
self.samples = []
self.active = []
def clear(self):
"""
resets the grid
active points and
sample points
"""
self.samples = []
self.active = []
for item in self.cells:
self.cells[item] = -1
def generate(self, point):
"""
generates new points
in an annulus between
self.r, 2*self.r
"""
rad = random.triangular(self.r, 2*self.r, .3*(2*self.r - self.r))
# was random.uniform(self.r, 2*self.r) but I think
# this may be closer to the correct distribution
# but easier to build
angs = [random.uniform(0, 2*pi)]
if self.dim > 2:
angs.extend(random.uniform(-pi/2, pi/2) for _ in range(self.dim-2))
angs[0] = 2*angs[0]
return self.convert(point, rad, angs)
def poisson(self, seed, k=30):
"""
generates a set of poisson disc samples
"""
self.clear()
self.samples.append(seed)
self.active.append(0)
self.update(seed, 0)
while self.active:
idx = random.choice(self.active)
point = self.samples[idx]
new_point = self.make_points(k, point)
if new_point:
self.samples.append(tuple(new_point))
self.active.append(len(self.samples)-1)
self.update(new_point, len(self.samples)-1)
else:
self.active.remove(idx)
return self.samples
def make_points(self, k, point):
"""
uses generate to make up to
k new points, stopping
when it finds a good sample
using self.check
"""
n = k
while n:
new_point = self.generate(point)
if self.check(point, new_point):
return new_point
n -= 1
return False
def check(self, point, new_point):
"""
checks the neighbors of the point
and the new_point
against the new_point
returns True if none are closer than r
"""
for i in range(self.dim):
if not (0 < new_point[i] < self.size[i] or
self.cellify(new_point) == -1):
return False
for item in self.neighbors(self.cellify(point)):
if self.distance(self.samples[item], new_point) < self.r**2:
return False
for item in self.neighbors(self.cellify(new_point)):
if self.distance(self.samples[item], new_point) < self.r**2:
return False
return True
def convert(self, point, rad, angs):
"""
converts the random point
to rectangular coordinates
from radial coordinates centered
on the active point
"""
new_point = [point[0] + rad*cos(angs[0]), point[1] + rad*sin(angs[0])]
if len(angs) > 1:
new_point.extend(point[i+1] + rad*sin(angs[i]) for i in range(1,len(angs)))
return new_point
def cellify(self, point):
"""
returns the cell in which the point falls
"""
return tuple(point[i]//self.cell_size for i in range(self.dim))
def distance(self, tup1, tup2):
"""
returns squared distance between two points
"""
return sum((tup1[k] - tup2[k])**2 for k in range(self.dim))
def cell_distance(self, tup1, tup2):
"""
returns true if the L1 distance is less than 2
for the two tuples
"""
return sum(abs(tup1[k]-tup2[k]) for k in range(self.dim)) <= 2
def neighbors(self, cell):
"""
finds all occupied cells within
a distance of the given point
"""
return (self.cells[tup] for tup in self.cells
if self.cells[tup] != -1 and
self.cell_distance(cell, tup))
def update(self, point, index):
"""
updates the grid with the new point
"""
self.cells[self.cellify(point)] = index
def __str__(self):
return self.cells.__str__()