├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── img └── img1.png ├── main.py ├── modules ├── __init__.py └── fracture.py └── res └── .empty /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 13 | trim_trailing_whitespace = true 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | res/* 2 | *.swp 3 | *.*~ 4 | profile.profile 5 | *.pyc 6 | .ropeproject 7 | build 8 | profile/* 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anders Hoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fracture 2 | ============= 3 | 4 | ![img](/img/img1.png?raw=true "image") 5 | 6 | Generative algorithm that makes fracture-like patterns from pre-distributed 7 | randomized seeds. 8 | 9 | ## Prerequisites 10 | 11 | In order for this code to run you must first download and install 12 | repositories: 13 | 14 | * `iutils`: http://github.com/inconvergent/iutils 15 | * `fn`: http://github.com/inconvergent/fn (only used to generate 16 | file names, can be removed in `main.py`.) 17 | 18 | ## Other Dependencies 19 | 20 | The code also depends on: 21 | 22 | * `numpy` 23 | * `scipy` 24 | * `python-cairo` (do not install with pip, this generally does not work) 25 | 26 | ----------- 27 | http://inconvergent.net 28 | 29 | -------------------------------------------------------------------------------- /img/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inconvergent/fracture/39b874714fa35576eb239fe21ff1a9772e817633/img/img1.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | 6 | 7 | BACK = [1,1,1,1] 8 | FRONT = [0,0,0,0.8] 9 | LIGHT = [0,0,0,0.2] 10 | CYAN = [0,0.5,0.5,0.2] 11 | BLUE = [0,0,1,0.3] 12 | 13 | 14 | NMAX = 10**6 15 | SIZE = 1200 16 | ONE = 1./SIZE 17 | LINEWIDTH = ONE*1.1 18 | 19 | INIT_NUM = 20000 20 | INIT_RAD = 0.45 21 | 22 | SOURCE_DST = 2.0*ONE 23 | 24 | FRAC_DOT = 0.85 25 | FRAC_DST = 100.*ONE 26 | FRAC_STP = ONE*2 27 | FRAC_SPD = 1.0 28 | 29 | FRAC_DIMINISH = 0.997 30 | FRAC_SPAWN_DIMINISH = 0.9 31 | 32 | 33 | SPAWN_ANGLE = 2.0 34 | SPAWN_FACTOR = 0.2 35 | 36 | 37 | 38 | def show(render,fractures): 39 | 40 | sources = fractures.sources 41 | alive_fractures = fractures.alive_fractures 42 | dead_fractures = fractures.dead_fractures 43 | 44 | def draw_sources(): 45 | for i,s in enumerate(sources): 46 | if i not in fractures.visited: 47 | render.circle(*s, r=4*ONE, fill=True) 48 | 49 | def draw_lines(fracs): 50 | for frac in fracs: 51 | start = frac.inds[0] 52 | render.ctx.move_to(*sources[start,:]) 53 | for c in frac.inds[1:]: 54 | render.ctx.line_to(*sources[c,:]) 55 | render.ctx.stroke() 56 | 57 | render.clear_canvas() 58 | 59 | # render.ctx.set_source_rgba(*LIGHT) 60 | # draw_sources() 61 | 62 | render.ctx.set_source_rgba(*LIGHT) 63 | render.set_line_width(3*LINEWIDTH) 64 | draw_lines(alive_fractures+dead_fractures) 65 | 66 | render.ctx.set_source_rgba(*FRONT) 67 | render.set_line_width(LINEWIDTH) 68 | draw_lines(alive_fractures+dead_fractures) 69 | 70 | # for f in alive_fractures: 71 | # for s in sources[f.inds,:]: 72 | # render.circle(*s, r=2*ONE, fill=False) 73 | 74 | def random_uniform_circle(rad, num): 75 | 76 | from numpy.random import random 77 | from numpy.linalg import norm 78 | from numpy import array 79 | 80 | 81 | while True: 82 | xy = 0.5-random(size=2) 83 | if norm(xy)>1.0: 84 | continue 85 | r = array([0.5]*2)+xy*rad 86 | return r 87 | 88 | 89 | 90 | def main(): 91 | 92 | from iutils.render import Animate 93 | from modules.fracture import Fractures 94 | 95 | # from dddUtils.ioOBJ import export_2d as export 96 | from fn import Fn 97 | fn = Fn(prefix='./res/',postfix='.2obj') 98 | 99 | F = Fractures( 100 | INIT_NUM, 101 | INIT_RAD, 102 | SOURCE_DST, 103 | FRAC_DOT, 104 | FRAC_DST, 105 | FRAC_STP, 106 | FRAC_SPD, 107 | FRAC_DIMINISH, 108 | FRAC_SPAWN_DIMINISH, 109 | domain = 'rect' 110 | ) 111 | 112 | print(F.sources.shape) 113 | 114 | # uniform square distribution 115 | from numpy.random import random 116 | for _ in range(5): 117 | F.blow(2, random(size=2)) 118 | 119 | # uniform circular distribution 120 | # for _ in xrange(5): 121 | # F.blow(3, random_uniform_circle(INIT_RAD, num=1)) 122 | 123 | def wrap(render): 124 | 125 | if not F.i % 20: 126 | show(render,F) 127 | # vertices, paths = F.get_vertices_and_paths() 128 | # export('fractures', fn.name(), vertices, lines=paths) 129 | render.write_to_png(fn.name()+'.png') 130 | 131 | F.print_stats() 132 | res = F.step(dbg=False) 133 | n = F.spawn_front(factor=SPAWN_FACTOR, angle=SPAWN_ANGLE) 134 | print('spawned: {:d}'.format(n)) 135 | 136 | # fn = './asdf_{:04d}.png'.format(F.i) 137 | # render.write_to_png(fn) 138 | 139 | # if not res: 140 | # vertices, paths = F.get_vertices_and_paths() 141 | # export('fractures', fn.name(), vertices, lines=paths) 142 | 143 | return res 144 | 145 | render = Animate(SIZE, BACK, FRONT, wrap) 146 | render.start() 147 | 148 | 149 | if __name__ == '__main__': 150 | 151 | main() 152 | 153 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inconvergent/fracture/39b874714fa35576eb239fe21ff1a9772e817633/modules/__init__.py -------------------------------------------------------------------------------- /modules/fracture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | 5 | from numpy import pi 6 | from numpy import array 7 | from numpy import row_stack 8 | from numpy.random import random 9 | from numpy.random import randint 10 | from numpy.linalg import norm 11 | from numpy import cos 12 | from numpy import sin 13 | from numpy import arctan2 14 | 15 | 16 | TWOPI = pi*2 17 | HPI = pi*0.5 18 | 19 | 20 | class Fracture(object): 21 | 22 | def __init__( 23 | self, 24 | fractures, 25 | fid, 26 | start, 27 | dx, 28 | frac_spd, 29 | frac_diminish 30 | ): 31 | 32 | self.i = 0 33 | self.fractures = fractures 34 | self.tree = fractures.tree 35 | self.frac_spd = frac_spd 36 | self.frac_diminish = frac_diminish 37 | 38 | self.start = start 39 | self.inds = [start] 40 | self.dxs = [dx] 41 | self.alive = True 42 | 43 | self.fid = fid 44 | 45 | def __relative_neigh_test(self, curr, new): 46 | 47 | from numpy import concatenate 48 | from numpy import unique 49 | from scipy.spatial.distance import cdist 50 | 51 | sources = self.fractures.sources 52 | visited = self.fractures.visited 53 | tri = self.fractures.tri 54 | simplices = tri.simplices 55 | simp = tri.find_simplex(new,bruteforce=True,tol=1e-10) 56 | neigh = concatenate((tri.neighbors[simp],[simp])) 57 | vv = set(list(unique(simplices[neigh,:]))) 58 | 59 | if curr in vv: 60 | vv.remove(curr) 61 | vv = array(list(vv)) 62 | 63 | dist = cdist(sources[vv, :], row_stack([new,sources[curr,:]])) 64 | mas = dist.max(axis=1) 65 | 66 | # curr_new = norm(new-sources[curr,:]) 67 | curr_new = self.fractures.frac_stp 68 | 69 | free = masdt 107 | 108 | if mask.sum()<1: 109 | self.alive = False 110 | if dbg: 111 | print(self.fid, 'no nearby sources') 112 | return False 113 | 114 | masked_diff = neardiff[mask] 115 | masked_nrm = nearnrm[mask] 116 | 117 | new_dx = (masked_diff/masked_nrm).sum(axis=0).flatten() 118 | new_dx /= norm(new_dx) 119 | new_pos = cx + new_dx*stp 120 | 121 | rel = self.__relative_neigh_test(c, new_pos) 122 | 123 | if rel>-1: 124 | dbgs += '{:d}: {:s}'.format(self.fid, 'collision (rn)') 125 | h = rel 126 | self.alive = False 127 | else: 128 | # new source 129 | dbgs += '{:d}: {:s}'.format(self.fid, 'new source') 130 | h = self.fractures._add_tmp_source(new_pos) 131 | self.alive = True 132 | visited[h] = new_dx 133 | 134 | if dbg: 135 | print(dbgs) 136 | 137 | self.dxs.append(new_dx) 138 | self.inds.append(h) 139 | 140 | return self.alive 141 | 142 | class Fractures(object): 143 | 144 | def __init__( 145 | self, 146 | init_num, 147 | init_rad, 148 | source_dst, 149 | frac_dot, 150 | frac_dst, 151 | frac_stp, 152 | frac_spd=1.0, 153 | frac_diminish=1.0, 154 | frac_spawn_diminish=1.0, 155 | domain='rect' 156 | ): 157 | 158 | self.i = 0 159 | self.init_num = init_num 160 | self.init_rad = init_rad 161 | self.source_dst = source_dst 162 | self.frac_dot = frac_dot 163 | self.frac_dst = frac_dst 164 | self.frac_stp = frac_stp 165 | self.frac_spd = frac_spd 166 | self.frac_diminish = frac_diminish 167 | self.spawn_diminish = frac_spawn_diminish 168 | 169 | self.alive_fractures = [] 170 | self.dead_fractures = [] 171 | 172 | self.visited = {} 173 | 174 | self.count = 0 175 | 176 | self.tmp_sources = [] 177 | self.__make_sources(domain=domain) 178 | 179 | def blow(self,n, x=array([0.5,0.5])): 180 | 181 | self.tmp_sources = [] 182 | 183 | for a in random(size=n)*TWOPI: 184 | dx = array([cos(a), sin(a)]) 185 | self.__make_fracture(x=x, dx=dx) 186 | 187 | self._append_tmp_sources() 188 | 189 | def __make_sources(self, xx=0.5, yy=0.5, rad=None, domain='rect'): 190 | 191 | from scipy.spatial import cKDTree as kdt 192 | from scipy.spatial import Delaunay as triag 193 | from iutils.random import darts 194 | from iutils.random import darts_rect 195 | 196 | if rad is None: 197 | rad = self.init_rad 198 | 199 | if domain=='circ': 200 | sources = darts( 201 | self.init_num, 202 | xx, 203 | yy, 204 | self.init_rad, 205 | self.source_dst 206 | ) 207 | elif domain=='rect': 208 | sources = darts_rect( 209 | self.init_num, 210 | xx, 211 | yy, 212 | 2*rad, 213 | 2*rad, 214 | self.source_dst 215 | ) 216 | else: 217 | raise ValueError('domain must be "rect" or "circ".') 218 | tree = kdt(sources) 219 | self.sources = sources 220 | self.tree = tree 221 | self.tri = triag( 222 | self.sources, 223 | incremental=False, 224 | qhull_options='QJ Qc' 225 | ) 226 | self.num_sources = len(self.sources) 227 | 228 | return len(sources) 229 | 230 | def _add_tmp_source(self, x): 231 | 232 | self.tmp_sources.append(x) 233 | return len(self.sources)+len(self.tmp_sources)-1 234 | 235 | def _append_tmp_sources(self): 236 | 237 | from scipy.spatial import cKDTree as kdt 238 | from scipy.spatial import Delaunay as triag 239 | 240 | sources = row_stack([self.sources]+self.tmp_sources) 241 | tree = kdt(sources) 242 | self.sources = sources 243 | self.tree = tree 244 | self.tmp_sources = [] 245 | self.tri = triag( 246 | self.sources, 247 | incremental=False, 248 | qhull_options='QJ Qc' 249 | ) 250 | self.num_sources = len(self.sources) 251 | 252 | return len(sources) 253 | 254 | def __make_fracture(self, x=None, p=None, dx=None, spd=None): 255 | 256 | if p is None: 257 | _,p = self.tree.query(x,1) 258 | 259 | if spd is None: 260 | spd = self.frac_spd 261 | 262 | f = Fracture( 263 | self, 264 | self.count, 265 | p, 266 | dx, 267 | spd, 268 | self.frac_diminish 269 | ) 270 | self.count += 1 271 | res = f.step() 272 | if res: 273 | self.alive_fractures.append(f) 274 | return res 275 | 276 | # def spawn_front(self, factor=1.0, angle=0.7): 277 | 278 | # if not self.alive_fractures: 279 | # return 0 280 | 281 | # self.tmp_sources = [] 282 | # count = 0 283 | 284 | # for i in (random(size=len(self.alive_fractures))f.frac_spd*factor: 306 | continue 307 | 308 | dx = f.dxs[-1] 309 | a = arctan2(dx[1], dx[0]) + (-1)**randint(2)*HPI + (0.5-random()) * angle 310 | count += int( 311 | self.__make_fracture( 312 | p=f.inds[-1], 313 | dx=array([cos(a), sin(a)]), 314 | spd=f.frac_spd*self.spawn_diminish 315 | ) 316 | ) 317 | 318 | self._append_tmp_sources() 319 | 320 | return count 321 | 322 | def step(self, dbg=False): 323 | 324 | self.i += 1 325 | 326 | self.tmp_sources = [] 327 | 328 | fracs = [] 329 | for f in self.alive_fractures: 330 | f.step(dbg) 331 | if f.alive: 332 | fracs.append(f) 333 | else: 334 | if len(f.inds)>1: 335 | self.dead_fractures.append(f) 336 | else: 337 | print('discarding path') 338 | 339 | self.alive_fractures = fracs 340 | 341 | self._append_tmp_sources() 342 | 343 | return len(fracs)>0 344 | 345 | def get_fracture_paths(self): 346 | 347 | paths = [] 348 | 349 | for f in self.alive_fractures + self.dead_fractures: 350 | if len(f.inds)<2: 351 | continue 352 | path = row_stack([self.sources[p,:] for p in f.inds]) 353 | paths.append(path) 354 | 355 | return paths 356 | 357 | def get_vertices_and_paths(self): 358 | 359 | vertices = self.sources 360 | paths = [] 361 | for f in self.alive_fractures + self.dead_fractures: 362 | if len(f.inds)<2: 363 | continue 364 | 365 | paths.append(array(f.inds, 'int')) 366 | 367 | return vertices, paths 368 | 369 | def print_stats(self): 370 | 371 | alive = len(self.alive_fractures) 372 | dead = len(self.dead_fractures) 373 | print('# {:d} a: {:d} d: {:d} s: {:d}\n' 374 | .format(self.i, alive, dead, len(self.sources)) 375 | ) 376 | 377 | -------------------------------------------------------------------------------- /res/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inconvergent/fracture/39b874714fa35576eb239fe21ff1a9772e817633/res/.empty --------------------------------------------------------------------------------