-
Notifications
You must be signed in to change notification settings - Fork 36
/
diamond.py
273 lines (194 loc) · 8.84 KB
/
diamond.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#DiamondSquare.py
#Written by Hailey K Buckingham
#On github, buckinha/DiamondSquare
#
import random
import numpy as np
def diamond_square(desired_size, min_height, max_height, roughness, random_seed=None, AS_NP_ARRAY=False):
"""Runs a diamond square algorithm and returns an array (or list) with the landscape
An important difference (possibly) between this, and other implementations of the
diamond square algorithm is how I use the roughness parameter. For each "perturbation"
I pull a random number from a uniform distribution between min_height and max_height.
I then take the weighted average between that value, and the average value of the
"neighbors", whether those be in the diamond or in the square step, as normal. The
weights used for the weighted sum are (roughness) and (1-roughness) for the random
number and the average, respectively, where roughness is a float that always falls
between 0 and 1.
The roughness value used in each iteration is based on the roughness parameter
passed in, and is computed as follows:
this_iteration_roughness = roughness**iteration_number
where the first iteration has iteration_number = 0. Thus, the first roughness value
actually used (in the very first diamond and square step) is roughness**0 = 1. Thus,
the values for those first diamond and square step entries will be entirely random.
This effectively means that I am seeding with A 3x3 grid of random values, rather
than with just the four corners.
As the process continues, the weight placed on the random number draw falls from
the original value of 1, to roughness**1, to roughness**2, and so on, ultimately
approaching 0. This means that the values of new cells will slowly shift from being
purely random, to pure averages.
OTHER NOTES:
Internally, all heights are between 0 and 1, and are rescaled at the end.
PARAMETERS
----------
size
The size of the grid to be returned. If an integer is passed, the return grid will
have sides of this length. If an array-like is passed, the first two values will be
used as the array shape.
min_height
The minimum height allowed on the landscape
max_height
The maximum height allowed on the landscape
roughness
A float with value between 0 and 1, reflecting how bumpy the landscape should be.
Values near 1 will result in landscapes that are extremly rough, and have almost no
cell-to-cell smoothness. Values near zero will result in landscapes that are almost
perfectly smooth.
random_seed
defaults to None. If a value is given, the algorithm will use it to seed the random
number generator, ensuring replicability.
AS_NP_ARRAY
A boolean reflecting whether the landscape should be returned as a numpy array. If set
to False, the method will return a 2-dimensional list.
RETURNS
-------
A 2-D numpy array or 2-D list with a diamond-square "height-map"
"""
#sanitize inputs
if roughness > 1: roughness = 1.0
if roughness < 0: roughness = 0.0
#check if size is iterable (i.e, it is likely a length-2 vector, etc...)
size = [-1,-1]
if not hasattr(desired_size, '__iter__'):
#it's not iterable, so it's probably an int
size[0] = desired_size
size[1] = desired_size
else:
size[0] = desired_size[0]
size[1] = desired_size[1]
DS_size, iterations = get_DS_size_and_iters(size)
#create the array
#DS_array = np.ndarray((DS_size,DS_size))
DS_array = np.zeros((DS_size,DS_size), dtype='float')
DS_array = DS_array - 1.0
#seed the random number generator
random.seed(random_seed)
#seed the corners
DS_array[ 0, 0] = random.uniform(0, 1)
DS_array[DS_size-1, 0] = random.uniform(0, 1)
DS_array[ 0, DS_size-1] = random.uniform(0, 1)
DS_array[DS_size-1, DS_size-1] = random.uniform(0, 1)
#do the algorithm
for i in range(iterations):
r = roughness**i
step_size = DS_size / 2**(i)
diamond_step(DS_array, step_size, r)
square_step(DS_array, step_size, r)
#rescale the array to fit the min and max heights specified
DS_array = min_height + (DS_array * (max_height - min_height))
#trim array, if needed
final_array = DS_array[:size[0],:size[1]]
if AS_NP_ARRAY:
return final_array
else:
return final_array.tolist()
def get_DS_size_and_iters(requested_size, max_power_of_two=13):
"""Returns the necessary size for a square grid which is usable in a DS algorithm.
The Diamond Square algorithm requires a grid of size n x n where n = 2**x + 1, for any
integer value of x greater than two. To accomodate a requested map size other than these
dimensions, we simply create the next largest n x n grid which can entirely contain the
requested size, and return a subsection of it.
This method computes that size.
PARAMETERS
----------
requested_size
A 2D list-like object reflecting the size of grid that is ultimately desired.
max_power_of_two
an integer greater than 2, reflecting the maximum size grid that the algorithm can EVER
attempt to make, even if the requested size is too big. This limits the algorithm to
sizes that are manageable, unless the user really REALLY wants to have a bigger one.
The maximum grid size will have an edge of size (2**max_power_of_two + 1)
RETURNS
-------
An integer of value n, as described above.
"""
if max_power_of_two < 3: max_power_of_two = 3
largest_edge = max(requested_size)
for power in range(1,max_power_of_two+1):
d = (2**power) + 1
if largest_edge <= d:
return d, power
#failsafe: no values in the dimensions array were allowed, so print a warning and return
# the maximum size.
d = 2**max_power_of_two + 1
print("DiamondSquare Warning: Requested size was too large. Grid of size {0} returned""".format(d))
return d, max_power_of_two
def diamond_step(DS_array, step_size, roughness):
"""Does the diamond step for a given iteration.
During the diamond step, the diagonally adjacent cells are filled:
Value None Value None Value ...
None FILLING None FILLING None ...
Value None Value None Value ...
... ... ... ... ... ...
So we'll step with increment step_size over BOTH axes
"""
#calculate where all the diamond corners are (the ones we'll be filling)
half_step = step_size/2
x_steps = range(half_step, DS_array.shape[0], step_size)
y_steps = x_steps[:]
for i in x_steps:
for j in y_steps:
if DS_array[i,j] == -1.0:
#print(repr((i,j)))
DS_array[i,j] = diamond_displace(DS_array, i, j, half_step, roughness)
def square_step(DS_array, step_size, roughness):
"""Does the square step for a given iteration.
During the diamond step, the diagonally adjacent cells are filled:
Value FILLING Value FILLING Value ...
FILLING DIAMOND FILLING DIAMOND FILLING ...
Value FILLING Value FILLING Value ...
... ... ... ... ... ...
So we'll step with increment step_size over BOTH axes
"""
half_step = step_size/2
steps_x = range( 0, DS_array.shape[0], half_step)
steps_y = range( 0, DS_array.shape[0], half_step)
for i in steps_x:
for j in steps_y:
if DS_array[i,j] == -1.0:
DS_array[i,j] = square_displace(DS_array, i, j, half_step, roughness)
#defines the midpoint displacement for the diamond step
def diamond_displace(DS_array, i, j, half_step, roughness):
ul = DS_array[i-half_step, j-half_step]
ur = DS_array[i-half_step, j+half_step]
ll = DS_array[i+half_step, j-half_step]
lr = DS_array[i+half_step, j+half_step]
ave = (ul + ur + ll + lr)/4.0
rand_val = random.uniform(0,1)
return (roughness*rand_val + (1.0-roughness)*ave)
#defines the midpoint displacement for the square step
def square_displace(DS_array, i, j, half_step, roughness):
_sum = 0.0
divide_by = 4
#check cell "above"
if i - half_step >= 0:
_sum += DS_array[i-half_step, j]
else:
divide_by -= 1
#check cell "below"
if i + half_step < DS_array.shape[0]:
_sum += DS_array[i+half_step, j]
else:
divide_by -= 1
#check cell "left"
if j - half_step >= 0:
_sum += DS_array[i, j-half_step]
else:
divide_by -= 1
#check cell "right"
if j + half_step < DS_array.shape[0]:
_sum += DS_array[i, j+half_step]
else:
divide_by -= 1
ave = _sum / divide_by
rand_val = random.uniform(0,1)
return (roughness*rand_val + (1.0-roughness)*ave)