├── .gitignore ├── Blender_2_7 ├── cellular_automata_cave_maze.py ├── cellular_automata_cave_maze_with_walls.py ├── random_walk_cavified.py ├── random_walk_open_with_walls.py ├── random_walk_via_cube_placement.py ├── random_walk_via_extrusion.py ├── recursive_backtracking_maze.py └── recursive_division_maze.py ├── Blender_2_8 ├── castle_dungeon_generator.py ├── castle_dungeon_generator_open_with_start_and_end.py ├── castle_dungeon_generator_open_with_textures.py ├── cellular_automata_cave_maze.py ├── cellular_automata_cave_maze_with_walls.py ├── cellular_automata_cave_maze_with_walls_cavify.py ├── cellular_automata_cavified_maze.py ├── random_walk_cavified.py ├── random_walk_open_with_walls.py ├── random_walk_via_cube_placement.py ├── random_walk_via_extrusion.py ├── recursive_backtracking_maze.py ├── recursive_division_maze.py └── resources │ ├── LilySurface │ └── texturehaven │ │ ├── Castle Brick 01 │ │ └── 2k JPG 12 MB │ │ │ ├── ambientOcclusion.jpg │ │ │ ├── ambientOcclusionRough.jpg │ │ │ ├── diffuse.jpg │ │ │ ├── diffuse2.jpg │ │ │ ├── diffuse3.jpg │ │ │ ├── height.jpg │ │ │ ├── normal.jpg │ │ │ └── roughness.jpg │ │ └── Cobblestone Floor 07 │ │ └── 2k JPG 9.6 MB │ │ ├── ambientOcclusion.jpg │ │ ├── diffuse.jpg │ │ ├── diffuse2.jpg │ │ ├── diffuse3.jpg │ │ ├── height.jpg │ │ ├── normal.jpg │ │ ├── roughness.jpg │ │ └── specular.jpg │ ├── resources.blend │ └── resources.blend1 ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Pycharm config files 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /Blender_2_7/cellular_automata_cave_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | import bmesh 16 | 17 | CHANCE_TO_START_ALIVE = 0.40 18 | DEATH_LIMIT = 3 19 | BIRTH_LIMIT = 4 20 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh 21 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 22 | HEIGHT = WIDTH 23 | 24 | 25 | def initialize_map(): 26 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 27 | return initial_map 28 | 29 | 30 | def alive_calc(): 31 | if random() < CHANCE_TO_START_ALIVE: 32 | return True 33 | else: 34 | return False 35 | 36 | 37 | def perform_game_of_life_iteration(old_map): 38 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 39 | # Loop over each row and column of the level_map 40 | for x in range(len(old_map)): 41 | for y in range(len(old_map[0])): 42 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 43 | 44 | if old_map[x][y] is True: 45 | # See if it should die and become open 46 | if live_neighbor_count < DEATH_LIMIT: 47 | new_map[x][y] = False 48 | # Otherwise keep it as a wall 49 | else: 50 | new_map[x][y] = True 51 | # If the tile is currently empty 52 | else: 53 | # See if it should become a wall 54 | if live_neighbor_count > BIRTH_LIMIT: 55 | new_map[x][y] = True 56 | else: 57 | new_map[x][y] = False 58 | return new_map 59 | 60 | 61 | # Returns the number of cells in a ring around (x,y) that are alive. 62 | def count_alive_neighbors(live_map, x, y): 63 | count = 0 64 | i = -1 65 | while i < 2: 66 | j = -1 67 | while j < 2: 68 | neighbor_x = x + i 69 | neighbor_y = y + j 70 | # If we're looking at the middle point 71 | if i == 0 and j == 0: 72 | pass 73 | # Do nothing, we don't want to add ourselves in! 74 | # In case the index we're looking at it off the edge of the live_map 75 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 76 | count += 1 77 | # Otherwise, a normal check of the neighbour 78 | elif live_map[neighbor_x][neighbor_y] is True: 79 | count += 1 80 | j += 1 81 | i += 1 82 | return count 83 | 84 | 85 | # adds cubes to the map based on the level_map matrix 86 | def add_cubes(cell_map): 87 | matrix_size = WIDTH 88 | for i in range(matrix_size**2): 89 | y = math.floor(i / matrix_size) 90 | x = i - y * matrix_size 91 | if y > 1 and x == 0: 92 | cleanup_mesh() 93 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 94 | bpy.ops.mesh.primitive_cube_add( 95 | radius=1, view_align=False, enter_editmode=False, location=(x*2, y*2, 0), 96 | layers=(True, False, False, False, False, False, False, False, False, 97 | False, False, False, False, False, False, False, False, False, 98 | False, False)) 99 | 100 | 101 | # combines the cubes into one object and removes interior faces 102 | def cleanup_mesh(): 103 | # select all of the cubes 104 | bpy.ops.object.mode_set(mode='OBJECT') 105 | bpy.ops.object.select_all(action='SELECT') 106 | # join all of the separate cube objects into one 107 | bpy.ops.object.join() 108 | # jump into edit mode 109 | bpy.ops.object.editmode_toggle() 110 | # get save the mesh data into a variable 111 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 112 | bpy.ops.mesh.select_all(action='SELECT') 113 | # remove overlapping verts 114 | bpy.ops.mesh.remove_doubles() 115 | # de-select everything in edit mode 116 | bpy.ops.mesh.select_all(action='DESELECT') 117 | # select the "interior faces" 118 | bpy.ops.mesh.select_interior_faces() 119 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 120 | for f in mesh.faces: 121 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 122 | f.select = False 123 | # delete all still selected faces, leaving a hollow mesh behind 124 | bpy.ops.mesh.delete(type='FACE') 125 | 126 | 127 | # delete everything in the scene 128 | def clear_scene(): 129 | if bpy.ops.object.mode_set.poll(): 130 | bpy.ops.object.mode_set(mode='OBJECT') 131 | bpy.ops.object.select_all(action='SELECT') 132 | bpy.ops.object.delete(use_global=False) 133 | 134 | 135 | def generate_map(): 136 | # Create a new level_map 137 | # Set up the level_map with random values 138 | cellmap = initialize_map() 139 | # run the simulation for a set number of steps 140 | for i in range(NUMBER_OF_ITERATIONS): 141 | cellmap = perform_game_of_life_iteration(cellmap) 142 | return cellmap 143 | 144 | 145 | if __name__ == '__main__': 146 | clear_scene() 147 | level_map = generate_map() 148 | add_cubes(level_map) 149 | cleanup_mesh() 150 | -------------------------------------------------------------------------------- /Blender_2_7/cellular_automata_cave_maze_with_walls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | import bmesh 16 | 17 | CHANCE_TO_START_ALIVE = 0.40 # The smaller this number is, the sparser the generated maze will be 18 | DEATH_LIMIT = 3 19 | BIRTH_LIMIT = 4 20 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh 21 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 22 | HEIGHT = WIDTH 23 | 24 | 25 | visited = [] # walkable tile 26 | walls = [] 27 | 28 | 29 | def initialize_map(): 30 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 31 | return initial_map 32 | 33 | 34 | def alive_calc(): 35 | if random() < CHANCE_TO_START_ALIVE: 36 | return True 37 | else: 38 | return False 39 | 40 | 41 | def perform_game_of_life_iteration(old_map): 42 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 43 | # Loop over each row and column of the level_map 44 | for x in range(len(old_map)): 45 | for y in range(len(old_map[0])): 46 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 47 | 48 | if old_map[x][y] is True: 49 | # See if it should die and become open 50 | if live_neighbor_count < DEATH_LIMIT: 51 | new_map[x][y] = False 52 | # Otherwise keep it as a wall 53 | else: 54 | new_map[x][y] = True 55 | # If the tile is currently empty 56 | else: 57 | # See if it should become a wall 58 | if live_neighbor_count > BIRTH_LIMIT: 59 | new_map[x][y] = True 60 | else: 61 | new_map[x][y] = False 62 | return new_map 63 | 64 | 65 | # Returns the number of cells in a ring around (x,y) that are alive. 66 | def count_alive_neighbors(live_map, x, y): 67 | count = 0 68 | i = -1 69 | while i < 2: 70 | j = -1 71 | while j < 2: 72 | neighbor_x = x + i 73 | neighbor_y = y + j 74 | # If we're looking at the middle point 75 | if i == 0 and j == 0: 76 | pass 77 | # Do nothing, we don't want to add ourselves in! 78 | # In case the index we're looking at it off the edge of the live_map 79 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 80 | count += 1 81 | # Otherwise, a normal check of the neighbour 82 | elif live_map[neighbor_x][neighbor_y] is True: 83 | count += 1 84 | j += 1 85 | i += 1 86 | return count 87 | 88 | 89 | # adds cubes to the map based on the level_map matrix 90 | def add_tiles(cell_map): 91 | matrix_size = WIDTH 92 | for i in range(matrix_size**2): 93 | y = math.floor(i / matrix_size) 94 | x = i - y * matrix_size 95 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 96 | place_tile(x*2, y*2) 97 | visited.append((x*2, y*2)) 98 | 99 | 100 | def place_cube(x, y): 101 | bpy.ops.mesh.primitive_cube_add( 102 | radius=1, view_align=False, enter_editmode=False, location=(x, y, 0), 103 | layers=(True, False, False, False, False, False, False, False, False, False, 104 | False, False, False, False, False, False, False, False, False, False)) 105 | 106 | 107 | def place_tile(x, y): 108 | bpy.ops.mesh.primitive_plane_add( 109 | radius=1, view_align=False, enter_editmode=False, location=(x, y, -1), 110 | layers=(True, False, False, False, False, False, False, False, False, False, 111 | False, False, False, False, False, False, False, False, False, False)) 112 | 113 | 114 | def check_neighbors_and_place_wall(pos): 115 | offset_map = [(pos[0] - 2.0, pos[1]), 116 | (pos[0] + 2.0, pos[1]), 117 | (pos[0], pos[1] - 2.0), 118 | (pos[0], pos[1] + 2.0)] 119 | for offset_tuple in offset_map: 120 | if offset_tuple not in visited and offset_map not in walls: 121 | place_cube(offset_tuple[0], offset_tuple[1]) 122 | walls.append((offset_tuple[0], offset_tuple[1])) 123 | 124 | 125 | def build_walls(): 126 | for position in visited: 127 | check_neighbors_and_place_wall(position) 128 | 129 | 130 | # combines the cubes into one object and removes interior faces 131 | def cleanup_mesh(): 132 | # select all of the cubes 133 | bpy.ops.object.mode_set(mode='OBJECT') 134 | bpy.ops.object.select_all(action='SELECT') 135 | # join all of the separate cube objects into one 136 | bpy.ops.object.join() 137 | # jump into edit mode 138 | bpy.ops.object.editmode_toggle() 139 | # get save the mesh data into a variable 140 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 141 | bpy.ops.mesh.select_all(action='SELECT') 142 | # remove overlapping verts 143 | bpy.ops.mesh.remove_doubles() 144 | # de-select everything in edit mode 145 | bpy.ops.mesh.select_all(action='DESELECT') 146 | 147 | 148 | # delete everything in the scene 149 | def clear_scene(): 150 | if bpy.ops.object.mode_set.poll(): 151 | bpy.ops.object.mode_set(mode='OBJECT') 152 | bpy.ops.object.select_all(action='SELECT') 153 | bpy.ops.object.delete(use_global=False) 154 | 155 | 156 | def generate_map(): 157 | # Create a new level_map 158 | # Set up the level_map with random values 159 | cellmap = initialize_map() 160 | # run the simulation for a set number of steps 161 | for i in range(NUMBER_OF_ITERATIONS): 162 | cellmap = perform_game_of_life_iteration(cellmap) 163 | return cellmap 164 | 165 | 166 | if __name__ == '__main__': 167 | clear_scene() 168 | level_map = generate_map() 169 | add_tiles(level_map) 170 | build_walls() 171 | cleanup_mesh() 172 | -------------------------------------------------------------------------------- /Blender_2_7/random_walk_cavified.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | # Controls the distances that are moved 20 | # MUST BE AT LEAST 2 21 | X_MOVE_DISTANCE = 2.0 22 | Y_MOVE_DISTANCE = 2.0 23 | 24 | # Position in space 25 | y_pos = 0 26 | x_pos = 0 27 | 28 | 29 | def generate_maze(): 30 | for i in range(ITERATIONS): 31 | direction = get_random_direction() 32 | next_move(direction) 33 | cleanup_mesh() 34 | cavify() 35 | 36 | 37 | def get_random_direction(): 38 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 39 | random_num = random.randint(0, 3) 40 | return direction[random_num] 41 | 42 | 43 | def next_move(direction): 44 | global y_pos 45 | global x_pos 46 | 47 | if direction == 'up': 48 | y_pos += Y_MOVE_DISTANCE 49 | place_cube() 50 | 51 | if direction == 'right': 52 | x_pos += X_MOVE_DISTANCE 53 | place_cube() 54 | 55 | if direction == 'down': 56 | y_pos -= Y_MOVE_DISTANCE 57 | place_cube() 58 | 59 | if direction == 'left': 60 | x_pos -= X_MOVE_DISTANCE 61 | place_cube() 62 | 63 | 64 | def place_cube(): 65 | bpy.ops.mesh.primitive_cube_add( 66 | radius=1, view_align=False, enter_editmode=False, location=(x_pos, y_pos, 0), 67 | layers=(True, False, False, False, False, False, False, False, False, 68 | False, False, False, False, False, False, False, False, False, 69 | False, False)) 70 | 71 | 72 | # combines the cubes into one object and removes interior faces 73 | def cleanup_mesh(): 74 | # select all of the cubes 75 | bpy.ops.object.mode_set(mode='OBJECT') 76 | bpy.ops.object.select_all(action='SELECT') 77 | # join all of the separate cube objects into one 78 | bpy.ops.object.join() 79 | # jump into edit mode 80 | bpy.ops.object.editmode_toggle() 81 | # get save the mesh data into a variable 82 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 83 | bpy.ops.mesh.select_all(action='SELECT') 84 | # remove overlapping verts 85 | bpy.ops.mesh.remove_doubles() 86 | # de-select everything in edit mode 87 | bpy.ops.mesh.select_all(action='DESELECT') 88 | # select the "interior faces" 89 | bpy.ops.mesh.select_interior_faces() 90 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 91 | for f in mesh.faces: 92 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 93 | f.select = False 94 | # delete all still selected faces, leaving a hollow mesh behind 95 | bpy.ops.mesh.delete(type='FACE') 96 | 97 | 98 | def cavify(): 99 | win = bpy.context.window 100 | scr = win.screen 101 | areas3d = [area for area in scr.areas if area.type == 'VIEW_3D'] 102 | region = [region for region in areas3d[0].regions if region.type == 'WINDOW'] 103 | 104 | override = {'window': win, 105 | 'screen': scr, 106 | 'area': areas3d[0], 107 | 'region': region[0], 108 | 'scene': bpy.context.scene, 109 | } 110 | 111 | bpy.ops.object.mode_set(mode='EDIT') 112 | bpy.ops.mesh.loopcut_slide( 113 | override, 114 | MESH_OT_loopcut={ 115 | "number_cuts": 1, 116 | "smoothness": 0, 117 | "falloff": 'INVERSE_SQUARE', 118 | "edge_index": 1, 119 | "mesh_select_mode_init": (False, False, True) 120 | }, 121 | TRANSFORM_OT_edge_slide={ 122 | "value": 0.637373, 123 | "single_side": False, 124 | "use_even": False, 125 | "flipped": False, 126 | "use_clamp": True, 127 | "mirror": False, 128 | "snap": False, 129 | "snap_target": 'CLOSEST', 130 | "snap_point": (0, 0, 0), 131 | "snap_align": False, 132 | "snap_normal": (0, 0, 0), 133 | "correct_uv": False, 134 | "release_confirm": False, 135 | "use_accurate": False 136 | } 137 | ) 138 | 139 | # space and search for flip normals 140 | bpy.ops.mesh.select_all(action='TOGGLE') 141 | bpy.ops.mesh.flip_normals() 142 | bpy.ops.object.modifier_add(type='SUBSURF') 143 | bpy.context.object.modifiers["Subsurf"].levels = 4 144 | 145 | # add the displacement modifiers 146 | bpy.ops.object.modifier_add(type='DISPLACE') 147 | bpy.ops.texture.new() 148 | bpy.data.textures["Texture"].type = 'STUCCI' 149 | bpy.data.textures["Texture"].noise_scale = 0.75 150 | bpy.context.object.modifiers["Displace"].direction = 'X' 151 | bpy.data.textures["Texture"].turbulence = 10 152 | bpy.context.object.modifiers["Displace"].texture = bpy.data.textures["Texture"] 153 | 154 | bpy.ops.object.modifier_add(type='DISPLACE') 155 | bpy.ops.texture.new() 156 | bpy.data.textures["Texture.001"].type = 'STUCCI' 157 | bpy.data.textures["Texture.001"].noise_scale = 0.75 158 | bpy.context.object.modifiers["Displace.001"].direction = 'Y' 159 | bpy.context.object.modifiers["Displace.001"].texture = bpy.data.textures["Texture.001"] 160 | 161 | bpy.ops.object.modifier_add(type='DISPLACE') 162 | bpy.ops.texture.new() 163 | bpy.data.textures["Texture.002"].type = 'CLOUDS' 164 | bpy.data.textures["Texture.002"].noise_scale = 0.65 165 | bpy.context.object.modifiers["Displace.002"].strength = 0.20 166 | bpy.context.object.modifiers["Displace.002"].texture = bpy.data.textures["Texture.002"] 167 | 168 | bpy.context.object.modifiers["Displace"].strength = 0.7 169 | bpy.context.object.modifiers["Displace.001"].strength = 0.6 170 | 171 | bpy.ops.object.mode_set(mode='OBJECT') 172 | bpy.context.scene.objects.active = bpy.context.scene.objects.active 173 | 174 | 175 | if __name__ == "__main__": 176 | generate_maze() 177 | -------------------------------------------------------------------------------- /Blender_2_7/random_walk_open_with_walls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | # Controls the distances that are moved 20 | # MUST BE AT LEAST 2 21 | X_MOVE_DISTANCE = 2.0 22 | Y_MOVE_DISTANCE = 2.0 23 | 24 | # Position in space 25 | y_pos = 0 26 | x_pos = 0 27 | 28 | visited = [] # walkable tile 29 | walls = [] 30 | 31 | 32 | def generate_maze(): 33 | for i in range(ITERATIONS): 34 | direction = get_random_direction() 35 | next_move(direction) 36 | build_walls() 37 | cleanup_mesh() 38 | 39 | 40 | def get_random_direction(): 41 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 42 | random_num = random.randint(0, 3) 43 | return direction[random_num] 44 | 45 | 46 | def next_move(direction): 47 | global y_pos 48 | global x_pos 49 | 50 | if direction == 'up': 51 | y_pos += Y_MOVE_DISTANCE 52 | 53 | if direction == 'right': 54 | x_pos += X_MOVE_DISTANCE 55 | 56 | if direction == 'down': 57 | y_pos -= Y_MOVE_DISTANCE 58 | 59 | if direction == 'left': 60 | x_pos -= X_MOVE_DISTANCE 61 | place_tile(x_pos, y_pos) 62 | visited.append((x_pos, y_pos)) 63 | 64 | 65 | def place_cube(x, y): 66 | bpy.ops.mesh.primitive_cube_add( 67 | radius=1, view_align=False, enter_editmode=False, location=(x, y, 0), 68 | layers=(True, False, False, False, False, False, False, False, False, False, 69 | False, False, False, False, False, False, False, False, False, False)) 70 | 71 | 72 | def place_tile(x, y): 73 | bpy.ops.mesh.primitive_plane_add( 74 | radius=1, view_align=False, enter_editmode=False, location=(x, y, -1), 75 | layers=(True, False, False, False, False, False, False, False, False, False, 76 | False, False, False, False, False, False, False, False, False, False)) 77 | 78 | 79 | def check_neighbors_and_place_wall(pos): 80 | offset_map = [(pos[0] - 2.0, pos[1]), 81 | (pos[0] + 2.0, pos[1]), 82 | (pos[0], pos[1] - 2.0), 83 | (pos[0], pos[1] + 2.0)] 84 | for offset_tuple in offset_map: 85 | if offset_tuple not in visited and offset_map not in walls: 86 | place_cube(offset_tuple[0], offset_tuple[1]) 87 | walls.append((offset_tuple[0], offset_tuple[1])) 88 | 89 | 90 | def build_walls(): 91 | for position in visited: 92 | check_neighbors_and_place_wall(position) 93 | 94 | 95 | # combines the cubes into one object and removes interior faces 96 | def cleanup_mesh(): 97 | # select all of the cubes 98 | bpy.ops.object.mode_set(mode='OBJECT') 99 | bpy.ops.object.select_all(action='SELECT') 100 | # join all of the separate cube objects into one 101 | bpy.ops.object.join() 102 | # jump into edit mode 103 | bpy.ops.object.editmode_toggle() 104 | bpy.ops.mesh.select_all(action='SELECT') 105 | # remove overlapping verts 106 | bpy.ops.mesh.remove_doubles() 107 | # de-select everything in edit mode 108 | bpy.ops.mesh.select_all(action='DESELECT') 109 | 110 | 111 | if __name__ == "__main__": 112 | generate_maze() 113 | -------------------------------------------------------------------------------- /Blender_2_7/random_walk_via_cube_placement.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | # Controls the distances that are moved 20 | # MUST BE AT LEAST 2 21 | X_MOVE_DISTANCE = 2.0 22 | Y_MOVE_DISTANCE = 2.0 23 | 24 | # Position in space 25 | y_pos = 0 26 | x_pos = 0 27 | 28 | 29 | def generate_maze(): 30 | for i in range(ITERATIONS): 31 | direction = get_random_direction() 32 | next_move(direction) 33 | cleanup_mesh() 34 | 35 | 36 | def get_random_direction(): 37 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 38 | random_num = random.randint(0, 3) 39 | return direction[random_num] 40 | 41 | 42 | def next_move(direction): 43 | global y_pos 44 | global x_pos 45 | 46 | if direction == 'up': 47 | y_pos += Y_MOVE_DISTANCE 48 | place_cube() 49 | 50 | if direction == 'right': 51 | x_pos += X_MOVE_DISTANCE 52 | place_cube() 53 | 54 | if direction == 'down': 55 | y_pos -= Y_MOVE_DISTANCE 56 | place_cube() 57 | 58 | if direction == 'left': 59 | x_pos -= X_MOVE_DISTANCE 60 | place_cube() 61 | 62 | 63 | def place_cube(): 64 | bpy.ops.mesh.primitive_cube_add( 65 | radius=1, view_align=False, enter_editmode=False, location=(x_pos, y_pos, 0), 66 | layers=(True, False, False, False, False, False, False, False, False, 67 | False, False, False, False, False, False, False, False, False, 68 | False, False)) 69 | 70 | 71 | # combines the cubes into one object and removes interior faces 72 | def cleanup_mesh(): 73 | # select all of the cubes 74 | bpy.ops.object.mode_set(mode='OBJECT') 75 | bpy.ops.object.select_all(action='SELECT') 76 | # join all of the separate cube objects into one 77 | bpy.ops.object.join() 78 | # jump into edit mode 79 | bpy.ops.object.editmode_toggle() 80 | # get save the mesh data into a variable 81 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 82 | bpy.ops.mesh.select_all(action='SELECT') 83 | # remove overlapping verts 84 | bpy.ops.mesh.remove_doubles() 85 | # de-select everything in edit mode 86 | bpy.ops.mesh.select_all(action='DESELECT') 87 | # select the "interior faces" 88 | bpy.ops.mesh.select_interior_faces() 89 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 90 | for f in mesh.faces: 91 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 92 | f.select = False 93 | # delete all still selected faces, leaving a hollow mesh behind 94 | bpy.ops.mesh.delete(type='FACE') 95 | 96 | 97 | if __name__ == "__main__": 98 | generate_maze() 99 | -------------------------------------------------------------------------------- /Blender_2_7/random_walk_via_extrusion.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/a04f59e37966449c98c2839999800c8a 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | current = None 20 | next_face = None 21 | 22 | # Position in space 23 | y_pos = 1.0 24 | x_pos = 1.0 25 | 26 | # Controls the distances that are moved 27 | # MUST BE AT LEAST 2 28 | x_move_distance = 2.0 29 | y_move_distance = 2.0 30 | 31 | # For keeping track of all of the moves that have been made 32 | visited_list = [] 33 | 34 | # make sure an object called 'Cube' is present in the scene, else add one 35 | if bpy.data.objects.get('Cube'): 36 | ob = bpy.data.objects['Cube'] 37 | bpy.ops.object.mode_set(mode='EDIT') 38 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 39 | else: 40 | bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0), layers=( 41 | True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, 42 | False, False, False, False)) 43 | ob = bpy.data.objects['Cube'] 44 | bpy.ops.object.mode_set(mode='EDIT') 45 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 46 | 47 | 48 | def main(): 49 | # Ensure that no faces are currently selected 50 | for f in mesh.faces: 51 | f.select = False 52 | 53 | for i in range(ITERATIONS): 54 | direction = get_random_direction() 55 | next_mesh_move(direction) 56 | visited_list.append((x_pos, y_pos)) 57 | cleanup() 58 | 59 | 60 | def get_random_direction(): 61 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 62 | random_num = random.randint(0, 3) 63 | return direction[random_num] 64 | 65 | 66 | def next_mesh_move(direction): 67 | global next_face 68 | global y_pos 69 | global x_pos 70 | for f in mesh.faces: 71 | f.select = False 72 | 73 | if direction == 'up': 74 | if (x_pos, y_pos + y_move_distance) not in visited_list: 75 | for f in mesh.faces: 76 | # Using face.calc_center_median and a walking through an array of x,y moves 77 | # we can calculate roughly which square we should appear on. 78 | # We can then select and set the right face to start the extrusion process from 79 | if f.normal.y == 1.0 and \ 80 | round(f.calc_center_median()[1]) == y_pos and \ 81 | round(f.calc_center_median()[0]) == x_pos - 1: 82 | f.select = True 83 | next_face = f 84 | y_pos += y_move_distance 85 | extrude_up() 86 | else: 87 | y_pos += y_move_distance 88 | 89 | if direction == 'right': 90 | if (x_pos + x_move_distance, y_pos) not in visited_list: 91 | for f in mesh.faces: 92 | if f.normal.x == 1.0 and \ 93 | round(f.calc_center_median()[0]) == x_pos and \ 94 | round(f.calc_center_median()[1]) == y_pos - 1: 95 | f.select = True 96 | next_face = f 97 | x_pos += x_move_distance 98 | extrude_right() 99 | else: 100 | x_pos += x_move_distance 101 | 102 | if direction == 'down': 103 | if (x_pos, y_pos - y_move_distance) not in visited_list: 104 | for f in mesh.faces: 105 | if f.normal.y == -1.0 and \ 106 | round(f.calc_center_median()[1]) == y_pos - y_move_distance and \ 107 | round(f.calc_center_median()[0]) == x_pos - 1: 108 | f.select = True 109 | next_face = f 110 | y_pos -= y_move_distance 111 | extrude_down() 112 | else: 113 | y_pos -= y_move_distance 114 | 115 | if direction == 'left': 116 | if (x_pos - x_move_distance, y_pos) not in visited_list: 117 | for f in mesh.faces: 118 | if f.normal.x == -1.0 and \ 119 | round(f.calc_center_median()[0]) == x_pos - x_move_distance and \ 120 | round(f.calc_center_median()[1]) == y_pos - 1: 121 | f.select = True 122 | next_face = f 123 | x_pos -= x_move_distance 124 | extrude_left() 125 | else: 126 | x_pos -= x_move_distance 127 | 128 | 129 | def extrude_up(): 130 | bpy.ops.mesh.extrude_region_move() 131 | bpy.ops.transform.translate(value=(0, y_move_distance, 0), 132 | constraint_axis=(False, True, False), 133 | constraint_orientation='GLOBAL', 134 | mirror=False, proportional='DISABLED', 135 | proportional_edit_falloff='SMOOTH', 136 | proportional_size=1, 137 | snap=False, 138 | snap_target='CLOSEST', 139 | snap_point=(0, 0, 0), 140 | snap_align=False, 141 | snap_normal=(0, 0, 0), 142 | texture_space=False, 143 | release_confirm=True 144 | ) 145 | 146 | 147 | def extrude_right(): 148 | bpy.ops.mesh.extrude_region_move() 149 | bpy.ops.transform.translate(value=(x_move_distance, 0.0, 0), 150 | constraint_axis=(True, False, False), 151 | constraint_orientation='GLOBAL', 152 | mirror=False, proportional='DISABLED', 153 | proportional_edit_falloff='SMOOTH', 154 | proportional_size=1, 155 | snap=False, 156 | snap_target='CLOSEST', 157 | snap_point=(0, 0, 0), 158 | snap_align=False, 159 | snap_normal=(0, 0, 0), 160 | texture_space=False, 161 | release_confirm=True 162 | ) 163 | 164 | 165 | def extrude_down(): 166 | bpy.ops.mesh.extrude_region_move() 167 | bpy.ops.transform.translate(value=(0, -y_move_distance, 0), 168 | constraint_axis=(False, True, False), 169 | constraint_orientation='GLOBAL', 170 | mirror=False, proportional='DISABLED', 171 | proportional_edit_falloff='SMOOTH', 172 | proportional_size=1, 173 | snap=False, 174 | snap_target='CLOSEST', 175 | snap_point=(0, 0, 0), 176 | snap_align=False, 177 | snap_normal=(0, 0, 0), 178 | texture_space=False, 179 | release_confirm=True 180 | ) 181 | 182 | 183 | def extrude_left(): 184 | bpy.ops.mesh.extrude_region_move() 185 | bpy.ops.transform.translate(value=(-x_move_distance, 0.0, 0), 186 | constraint_axis=(True, False, False), 187 | constraint_orientation='GLOBAL', 188 | mirror=False, proportional='DISABLED', 189 | proportional_edit_falloff='SMOOTH', 190 | proportional_size=1, 191 | snap=False, 192 | snap_target='CLOSEST', 193 | snap_point=(0, 0, 0), 194 | snap_align=False, 195 | snap_normal=(0, 0, 0), 196 | texture_space=False, 197 | release_confirm=True 198 | ) 199 | 200 | 201 | def cleanup(): 202 | bpy.ops.object.mode_set(mode='EDIT') 203 | bpy.ops.mesh.select_all(action='SELECT') 204 | bpy.ops.mesh.remove_doubles() 205 | bpy.ops.object.mode_set(mode='OBJECT') 206 | 207 | 208 | if __name__ == "__main__": 209 | main() 210 | -------------------------------------------------------------------------------- /Blender_2_7/recursive_backtracking_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a recursive backtracking algorithm to procedurally generate 3D levels in Blender. 6 | 7 | Example 3D model output 8 | https://sketchfab.com/models/7437daa03a0543d48c5eb599681d7e07 9 | ''' 10 | import math 11 | import random 12 | import bpy 13 | import bmesh 14 | 15 | # total size of the maze to be created eg 10x10 16 | cols = 10 17 | rows = 10 18 | 19 | # global variables for keeping track of the grid and cell_stack states during execution 20 | cell_array = [] # keeps a flat list of all cells 21 | cell_stack = [] # keeps stack of the order cells have been visited 22 | direction_stack = [] # for keeping track of all of the moves that have been made 23 | 24 | current_cell = None 25 | next_face = None 26 | 27 | # Position in space 28 | y_pos = 1.0 29 | x_pos = 1.0 30 | 31 | # Controls the distances that are moved 32 | x_move_distance = 2.0 33 | y_move_distance = 2.0 34 | 35 | 36 | # make sure an object called 'Cube' is present in the scene, else add one 37 | if bpy.data.objects.get('Cube'): 38 | ob = bpy.data.objects['Cube'] 39 | bpy.ops.object.mode_set(mode='EDIT') 40 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 41 | else: 42 | bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0), layers=( 43 | True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, 44 | False, False, False, False)) 45 | ob = bpy.data.objects['Cube'] 46 | bpy.ops.object.mode_set(mode='EDIT') 47 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 48 | 49 | 50 | def setup(): 51 | global current_cell 52 | # create a 2D array of cells inside of the cell_array variable 53 | for y in range(rows): 54 | for x in range(cols): 55 | cell = Cell(x, y) 56 | cell_array.append(cell) 57 | # set the current position to the first cell in the cell_array 58 | current_cell = cell_array[0] 59 | generate_level() 60 | 61 | 62 | def generate_level(): 63 | # flag for tracking state of when the maze is finished 64 | done = False 65 | 66 | # make sure that no faces are selected 67 | for f in mesh.faces: 68 | f.select = False 69 | 70 | # enter a long running loop that can only be exited when certain criteria are met 71 | while True: 72 | global cell_array 73 | global current_cell 74 | global cell_stack 75 | global next_cell 76 | 77 | current_cell.visited = True 78 | 79 | # determine if the maze can move forward from the current cell to a yet unreached neighboring cell 80 | next_cell, direction = current_cell.check_neighbors() 81 | if next_cell: # if an unvisited neighbor cell was found to move to 82 | next_mesh_move(direction) 83 | next_cell.visited = True 84 | 85 | # keep track of the directions the maze has gone using the cell_stack 86 | cell_stack.append(current_cell) 87 | 88 | # sets the current_cell cell to the next cell 89 | current_cell = next_cell 90 | 91 | elif len(cell_stack) > 0: # if there was not an unvisited cell to move to from the current cell 92 | current_cell = cell_stack.pop() 93 | move_back() 94 | 95 | for cell in cell_array: 96 | if cell.visited is False: 97 | done = False 98 | break 99 | else: 100 | done = True 101 | if done: 102 | break 103 | 104 | 105 | class Cell: 106 | def __init__(self, x, y): 107 | self.x = x # cell's row position 108 | self.y = y # cell's column position 109 | self.visited = False 110 | 111 | def check_neighbors(self): 112 | global cell_array 113 | neighbors = [] # keep track of the neighboring cells 114 | unvisited_directions = {} # keep track of the relative direction of unvisited neighboring cells 115 | direction_key = 0 # keeps track of the number of directions available of unvisited cells 116 | # for storing each individual neighbor cell, if applicable 117 | up = None 118 | right = None 119 | down = None 120 | left = None 121 | 122 | # check each direction to determine if neighboring cells exist on the cell_array 123 | if index(self.x, self.y - 1): 124 | up = cell_array[index(self.x, self.y - 1)] 125 | if index(self.x + 1, self.y): 126 | right = cell_array[index(self.x + 1, self.y)] 127 | if index(self.x, self.y + 1): 128 | down = cell_array[index(self.x, self.y + 1)] 129 | if index(self.x - 1, self.y): 130 | left = cell_array[index(self.x - 1, self.y)] 131 | 132 | # if the cell has a neighbor in a particular direction then check if that neighbor has been visited yet 133 | # if it has not, store the 134 | if up and not up.visited: 135 | neighbors.append(up) 136 | unvisited_directions[direction_key] = 'up' 137 | direction_key += 1 138 | if right and not right.visited: 139 | neighbors.append(right) 140 | unvisited_directions[direction_key] = 'right' 141 | direction_key += 1 142 | if down and not down.visited: 143 | neighbors.append(down) 144 | unvisited_directions[direction_key] = 'down' 145 | direction_key += 1 146 | if left and not left.visited: 147 | neighbors.append(left) 148 | unvisited_directions[direction_key] = 'left' 149 | 150 | if len(neighbors) > 0: 151 | # randomly return the direction of an unvisited neighbor cell 152 | r = int(math.floor(random.uniform(0, len(neighbors) - .000001))) 153 | return neighbors[r], unvisited_directions[r] 154 | else: 155 | return None, None 156 | 157 | 158 | def index(x, y): 159 | if x < 0 or y < 0 or x > cols - 1 or y > rows - 1: 160 | return None 161 | else: 162 | return x + y * cols 163 | 164 | 165 | def next_mesh_move(direction): 166 | global next_face 167 | global y_pos 168 | global x_pos 169 | for f in mesh.faces: 170 | f.select = False 171 | 172 | if direction == 'up': 173 | for f in mesh.faces: 174 | if f.normal.y == 1.0 and \ 175 | round(f.calc_center_median()[1]) == y_pos and \ 176 | round(f.calc_center_median()[0]) == x_pos - 1: 177 | f.select = True 178 | next_face = f 179 | y_pos += y_move_distance * 2 180 | direction_stack.append(direction) 181 | extrude_up() 182 | extrude_up() 183 | 184 | if direction == 'right': 185 | for f in mesh.faces: 186 | if f.normal.x == 1.0 and \ 187 | round(f.calc_center_median()[0]) == x_pos and \ 188 | round(f.calc_center_median()[1]) == y_pos - 1: 189 | f.select = True 190 | next_face = f 191 | x_pos += x_move_distance * 2 192 | direction_stack.append(direction) 193 | extrude_right() 194 | extrude_right() 195 | 196 | if direction == 'down': 197 | for f in mesh.faces: 198 | if f.normal.y == -1.0 and \ 199 | round(f.calc_center_median()[1]) == y_pos - y_move_distance and \ 200 | round(f.calc_center_median()[0]) == x_pos - 1: 201 | f.select = True 202 | next_face = f 203 | y_pos -= y_move_distance * 2 204 | direction_stack.append(direction) 205 | extrude_down() 206 | extrude_down() 207 | 208 | if direction == 'left': 209 | for f in mesh.faces: 210 | if f.normal.x == -1.0 and \ 211 | round(f.calc_center_median()[0]) == x_pos - x_move_distance and \ 212 | round(f.calc_center_median()[1]) == y_pos - 1: 213 | f.select = True 214 | next_face = f 215 | x_pos -= x_move_distance * 2 216 | direction_stack.append(direction) 217 | extrude_left() 218 | extrude_left() 219 | 220 | 221 | # Used to keep track of the position in space being moved through 222 | def move_back(): 223 | global y_pos 224 | global x_pos 225 | global direction_stack 226 | direction = direction_stack.pop() 227 | if direction == 'up': 228 | y_pos -= y_move_distance * 2 229 | if direction == 'right': 230 | x_pos -= x_move_distance * 2 231 | if direction == 'down': 232 | y_pos += y_move_distance * 2 233 | if direction == 'left': 234 | x_pos += x_move_distance * 2 235 | 236 | 237 | def extrude_up(): 238 | bpy.ops.mesh.extrude_region_move() 239 | bpy.ops.transform.translate(value=(0, y_move_distance, 0), 240 | constraint_axis=(False, True, False), 241 | constraint_orientation='GLOBAL', 242 | mirror=False, proportional='DISABLED', 243 | proportional_edit_falloff='SMOOTH', 244 | proportional_size=1, 245 | snap=False, 246 | snap_target='CLOSEST', 247 | snap_point=(0, 0, 0), 248 | snap_align=False, 249 | snap_normal=(0, 0, 0), 250 | texture_space=False, 251 | release_confirm=True 252 | ) 253 | 254 | 255 | def extrude_right(): 256 | bpy.ops.mesh.extrude_region_move() 257 | bpy.ops.transform.translate(value=(x_move_distance, 0.0, 0), 258 | constraint_axis=(True, False, False), 259 | constraint_orientation='GLOBAL', 260 | mirror=False, proportional='DISABLED', 261 | proportional_edit_falloff='SMOOTH', 262 | proportional_size=1, 263 | snap=False, 264 | snap_target='CLOSEST', 265 | snap_point=(0, 0, 0), 266 | snap_align=False, 267 | snap_normal=(0, 0, 0), 268 | texture_space=False, 269 | release_confirm=True 270 | ) 271 | 272 | 273 | def extrude_down(): 274 | bpy.ops.mesh.extrude_region_move() 275 | bpy.ops.transform.translate(value=(0, -y_move_distance, 0), 276 | constraint_axis=(False, True, False), 277 | constraint_orientation='GLOBAL', 278 | mirror=False, proportional='DISABLED', 279 | proportional_edit_falloff='SMOOTH', 280 | proportional_size=1, 281 | snap=False, 282 | snap_target='CLOSEST', 283 | snap_point=(0, 0, 0), 284 | snap_align=False, 285 | snap_normal=(0, 0, 0), 286 | texture_space=False, 287 | release_confirm=True 288 | ) 289 | 290 | 291 | def extrude_left(): 292 | bpy.ops.mesh.extrude_region_move() 293 | bpy.ops.transform.translate(value=(-x_move_distance, 0.0, 0), 294 | constraint_axis=(True, False, False), 295 | constraint_orientation='GLOBAL', 296 | mirror=False, proportional='DISABLED', 297 | proportional_edit_falloff='SMOOTH', 298 | proportional_size=1, 299 | snap=False, 300 | snap_target='CLOSEST', 301 | snap_point=(0, 0, 0), 302 | snap_align=False, 303 | snap_normal=(0, 0, 0), 304 | texture_space=False, 305 | release_confirm=True 306 | ) 307 | 308 | 309 | if __name__ == "__main__": 310 | setup() 311 | -------------------------------------------------------------------------------- /Blender_2_7/recursive_division_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a recursive division algorithm to generate levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/models/8587b464b2034485886cd094c7adf14c 9 | ''' 10 | 11 | import math 12 | import random 13 | import bpy 14 | import bmesh 15 | 16 | SIZE = 49 17 | MIN_SIZE = SIZE / 4 18 | 19 | 20 | def generate_level(size): 21 | # deletes everything in the scene, allows for a simple and clean run 22 | clear_scene() 23 | # initialize the map matrix and store it in the level_map variable 24 | level_map = new_map(size, 0) 25 | # build out the map, these two functions do all of the procedural work 26 | add_inner_walls(level_map, 1, 1, size - 2, size - 2) 27 | add_outer_walls(level_map, SIZE) 28 | # populate the scene with cubes according to the map matrix 29 | add_cubes(level_map) 30 | # combines the cubes into one object and removes interior faces 31 | cleanup_mesh() 32 | 33 | 34 | # generate 2d array with list comprehension 35 | def new_map(size, value): 36 | return [[value for i in range(size)] for j in range(size)] 37 | 38 | 39 | # returns random number between min max inclusive 40 | def random_number(minimum, maximum): 41 | return math.floor(random.random() * (maximum - minimum + 1)) + minimum 42 | 43 | 44 | def add_outer_walls(level_map, size): 45 | for i in range(size): 46 | if i == 0 or i == size - 1: 47 | for j in range(size): 48 | level_map[i][j] = 1 49 | else: 50 | level_map[i][0] = 1 51 | level_map[i][size - 1] = 1 52 | 53 | 54 | def add_inner_walls(level_map, rmin, cmin, rmax, cmax): 55 | width = cmax - cmin 56 | height = rmax - rmin 57 | 58 | # stop recursing once room size is reached 59 | if width < MIN_SIZE or height < MIN_SIZE: 60 | return level_map 61 | 62 | # determine whether to build vertical or horizontal wall 63 | if width > height: 64 | is_vertical = True 65 | else: 66 | is_vertical = False 67 | 68 | if is_vertical: 69 | # randomize location of vertical wall 70 | col = math.floor(random_number(cmin, cmax) / 2) * 2 71 | build_wall(level_map, is_vertical, rmin, rmax, col) 72 | # recurse to the two newly divided boxes 73 | add_inner_walls(level_map, rmin, cmin, rmax, col - 1) 74 | add_inner_walls(level_map, rmin, col + 1, rmax, cmax) 75 | else: 76 | row = math.floor(random_number(rmin, rmax) / 2) * 2 77 | build_wall(level_map, is_vertical, cmin, cmax, row) 78 | add_inner_walls(level_map, rmin, cmin, row - 1, cmax) 79 | add_inner_walls(level_map, row + 1, cmin, rmax, cmax) 80 | 81 | 82 | def build_wall(level_map, is_vertical, minimum, maximum, loc): 83 | hole = math.floor(random_number(minimum, maximum) / 2) * 2 + 1 84 | for i in range(minimum, maximum + 1): 85 | if is_vertical: 86 | if i == hole: 87 | level_map[loc][i] = 0 88 | else: 89 | level_map[loc][i] = 1 90 | else: 91 | if i == hole: 92 | level_map[i][loc] = 0 93 | else: 94 | level_map[i][loc] = 1 95 | 96 | 97 | # adds cubes to the map based on the level_map matrix 98 | def add_cubes(level_map): 99 | y = 0 100 | for row in level_map: 101 | x = 0 102 | for value in row: 103 | if value == 0: # cells with value 0 get cubes placed on them 104 | bpy.ops.mesh.primitive_cube_add( 105 | radius=1, view_align=False, enter_editmode=False, location=(x, y, 0), 106 | layers=(True, False, False, False, False, False, False, False, False, 107 | False, False, False, False, False, False, False, False, False, 108 | False, False)) 109 | x += 2 110 | y += 2 111 | 112 | 113 | # combines the cubes into one object and removes interior faces 114 | def cleanup_mesh(): 115 | # select all of the cubes 116 | bpy.ops.object.select_all(action='SELECT') 117 | # join all of the separate cube objects into one 118 | bpy.ops.object.join() 119 | # jump into edit mode 120 | bpy.ops.object.editmode_toggle() 121 | # get save the mesh data into a variable 122 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 123 | # remove overlapping verts 124 | bpy.ops.mesh.remove_doubles() 125 | # de-select everything in edit mode 126 | bpy.ops.mesh.select_all(action='DESELECT') 127 | # select the "interior faces" 128 | bpy.ops.mesh.select_interior_faces() 129 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 130 | for f in mesh.faces: 131 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 132 | f.select = False 133 | # delete all still selected faces, leaving a hollow mesh behind 134 | bpy.ops.mesh.delete(type='FACE') 135 | 136 | 137 | # delete everything in the scene 138 | def clear_scene(): 139 | bpy.ops.object.select_all(action='SELECT') 140 | bpy.ops.object.delete(use_global=False) 141 | 142 | 143 | if __name__ == "__main__": 144 | generate_level(SIZE) 145 | -------------------------------------------------------------------------------- /Blender_2_8/castle_dungeon_generator.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | import bpy 3 | import bmesh 4 | 5 | ROOM_MIN = 1000 6 | 7 | 8 | class Dungeon: 9 | def __init__(self): 10 | self.x_size = 60 11 | self.y_size = 40 12 | self.min_room_size = 5 13 | self.max_room_size = 15 14 | self.min_rooms = 10 15 | self.max_rooms = 15 16 | self.num_rooms = None 17 | self.map = None 18 | self.rooms = None 19 | self.connected = None 20 | 21 | def generate(self): 22 | self.map = [] 23 | self.rooms = [] 24 | self.connected = [] 25 | 26 | for y in range(self.y_size): # build out the initial 2d grid array 27 | self.map.append([]) 28 | for x in range(self.x_size): # initialize each row 29 | self.map[y].append({}) 30 | self.map[y][x]['t'] = 0 31 | self.map[y][x]['pos'] = { 32 | 'x': x, 33 | 'y': y 34 | } 35 | self.map[y][x]['r'] = 1 36 | 37 | self.num_rooms = get_random_int(self.min_rooms, self.max_rooms) # set the total number of rooms to be generated 38 | 39 | i = 0 40 | while i < self.num_rooms: 41 | # generate rooms, check if the are overlapping, shrink the w and h by 1 42 | room = {} 43 | room['x'] = get_random_int(1, (self.x_size - self.max_room_size - 1)) 44 | room['y'] = get_random_int(1, (self.y_size - self.max_room_size - 1)) 45 | room['w'] = get_random_int(self.min_room_size, self.max_room_size) 46 | room['h'] = get_random_int(self.min_room_size, self.max_room_size) 47 | room['connected'] = False 48 | if self.does_collide(room): 49 | continue 50 | room['w'] -= 1 51 | room['h'] -= 1 52 | self.rooms.append(room) 53 | i += 1 54 | for k in range(len(self.rooms)): 55 | room = self.rooms[k] 56 | closest_room = self.find_closest(room, self.connected) 57 | if closest_room is None: 58 | break 59 | self.connect_rooms(room, closest_room, True) 60 | for k in range(len(self.rooms)): 61 | room = self.rooms[k] 62 | for y in range(room['y'], room['y'] + room['h']): 63 | for x in range(room['x'], room['x'] + room['w']): 64 | self.map[y][x]['t'] = 1 65 | self.place_cubes() 66 | 67 | def does_collide(self, room): 68 | for i in range(len(self.rooms)): 69 | comparison_room = self.rooms[i] 70 | if room == comparison_room: 71 | continue 72 | if room['x'] < comparison_room['x'] + comparison_room['w'] \ 73 | and room['x'] + room['w'] > comparison_room['x'] \ 74 | and room['y'] < comparison_room['y'] + comparison_room['h'] \ 75 | and room['y'] + room['h'] > comparison_room['y']: 76 | return True 77 | return False 78 | 79 | def find_closest(self, room, others=None): 80 | master_room = {'x': room['x'] + room['w'] / 2, 81 | 'y': room['y'] + room['h'] / 2 82 | } 83 | room_min = ROOM_MIN 84 | final_room = None 85 | for i in range(len(self.rooms)): 86 | comparison_room = self.rooms[i] 87 | if room == comparison_room: 88 | continue 89 | if others is not None: 90 | is_closest = False 91 | for j in range(len(others)): 92 | if comparison_room == others[j]: 93 | is_closest = True 94 | break 95 | if is_closest: 96 | continue 97 | room_avg = { 98 | 'x': comparison_room['x'] + comparison_room['w'] / 2, 99 | 'y': comparison_room['y'] + comparison_room['h'] / 2 100 | } 101 | room_calc = abs(room_avg['x'] - master_room['x']) + abs(room_avg['y'] - master_room['y']) 102 | if room_calc < room_min: 103 | room_min = room_calc 104 | final_room = comparison_room 105 | return final_room 106 | 107 | def connect_rooms(self, room, closest_room, should_connect): 108 | path_part_1 = { 109 | 'x': get_random_int(room['x'], room['x'] + room['w']), 110 | 'y': get_random_int(room['y'], room['y'] + room['h']) 111 | } 112 | path_part_2 = { 113 | 'x': get_random_int(closest_room['x'], closest_room['x'] + closest_room['w']), 114 | 'y': get_random_int(closest_room['y'], closest_room['y'] + closest_room['h']) 115 | } 116 | while path_part_1['x'] != path_part_2['x'] or path_part_1['y'] != path_part_2['y']: 117 | if path_part_1["x"] != path_part_2["x"]: 118 | if path_part_2["x"] < path_part_1["x"]: 119 | path_part_2["x"] += 1 120 | else: 121 | path_part_2["x"] -= 1 122 | else: 123 | if path_part_1["y"] != path_part_2["y"]: 124 | if path_part_2["y"] < path_part_1["y"]: 125 | path_part_2["y"] += 1 126 | else: 127 | path_part_2["y"] -= 1 128 | 129 | self.map[path_part_2['y']][path_part_2['x']]['t'] = 1 130 | if should_connect: 131 | room['connected'] = True 132 | closest_room['connected'] = True 133 | self.connected.append(room) 134 | 135 | def place_cubes(self): 136 | yy = 0 137 | for y in range(self.y_size): 138 | xx = 0 139 | for x in range(self.x_size): 140 | if self.map[y][x]['t'] == 0 or self.map[y][x]['t'] == 2: 141 | pass 142 | else: 143 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(xx, yy, 0)) 144 | xx += 2 145 | yy += 2 146 | 147 | 148 | def get_random_int(low, high): 149 | return randint(low, high - 1) 150 | 151 | 152 | # joins all separate cube into a single object, 153 | # deletes inner faces to make the object hollow 154 | def cleanup_mesh(): 155 | # get all the cubes selected 156 | bpy.ops.object.select_all(action='TOGGLE') 157 | bpy.ops.object.select_all(action='TOGGLE') 158 | # join all of the separate cube objects into one 159 | bpy.ops.object.join() 160 | # jump into edit mode 161 | bpy.ops.object.mode_set(mode='EDIT') 162 | # get save the mesh data into a variable 163 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 164 | # select the entire mesh 165 | bpy.ops.mesh.select_all(action='SELECT') 166 | # remove overlapping verts 167 | bpy.ops.mesh.remove_doubles() 168 | # de-select everything in edit mode 169 | bpy.ops.mesh.select_all(action='DESELECT') 170 | # select the "interior faces" 171 | bpy.ops.mesh.select_interior_faces() 172 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 173 | for f in mesh.faces: 174 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 175 | f.select = False 176 | # delete all still selected faces, leaving a hollow mesh behind 177 | bpy.ops.mesh.delete(type='FACE') 178 | # get back out of edit mode 179 | bpy.ops.object.mode_set(mode='OBJECT') 180 | 181 | 182 | # delete everything in the scene 183 | def clear_scene(): 184 | if bpy.context.active_object: 185 | if bpy.context.active_object.mode != 'OBJECT': 186 | bpy.ops.object.mode_set(mode='OBJECT') 187 | bpy.ops.object.select_all(action='SELECT') 188 | bpy.ops.object.delete(use_global=False) 189 | 190 | 191 | if __name__ == '__main__': 192 | clear_scene() 193 | dungeon = Dungeon() 194 | dungeon.generate() 195 | cleanup_mesh() 196 | -------------------------------------------------------------------------------- /Blender_2_8/castle_dungeon_generator_open_with_start_and_end.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | import bpy 4 | import bmesh 5 | 6 | 7 | class Dungeon: 8 | def __init__(self): 9 | self.x_size = 60 10 | self.y_size = 40 11 | self.min_room_size = 5 12 | self.max_room_size = 15 13 | self.num_rooms = None 14 | self.min_rooms = 10 15 | self.max_rooms = 15 16 | self.map = None 17 | self.rooms = None 18 | self.first_room = None 19 | self.last_room = None 20 | self.end = None 21 | self.start = None 22 | self.connected = None 23 | self.light = 0 24 | 25 | def generate(self): 26 | self.map = [] 27 | self.rooms = [] 28 | self.first_room = None 29 | self.last_room = None 30 | self.connected = [] 31 | 32 | # build out the initial 2d array 33 | for y in range(self.y_size): 34 | # initialize each row 35 | self.map.append([]) 36 | for x in range(self.x_size): 37 | self.map[y].append({}) 38 | self.map[y][x]['t'] = 0 39 | self.map[y][x]['path'] = False 40 | self.map[y][x]['pos'] = { 41 | 'x': x, 42 | 'y': y 43 | } 44 | if self.light == 0: 45 | self.map[y][x]['r'] = 1 46 | else: 47 | self.map[y][x] = 0 48 | self.num_rooms = get_random_int(self.min_rooms, self.max_rooms) 49 | i = 0 50 | while i < self.num_rooms: 51 | room = {} 52 | room['x'] = get_random_int(1, (self.x_size - self.max_room_size - 1)) 53 | room['y'] = get_random_int(1, (self.y_size - self.max_room_size - 1)) 54 | room['w'] = get_random_int(self.min_room_size, self.max_room_size) 55 | room['h'] = get_random_int(self.min_room_size, self.max_room_size) 56 | room['c'] = False 57 | if self.does_collide(room): 58 | continue 59 | room['w'] -= 1 60 | room['h'] -= 1 61 | self.rooms.append(room) 62 | i += 1 63 | self.shrink_map(0) 64 | for k in range(len(self.rooms)): 65 | room = self.rooms[k] 66 | closest_room = self.find_closest(room, self.connected) 67 | if closest_room is None: 68 | break 69 | self.connect_rooms(room, closest_room, True) 70 | for k in range(len(self.rooms)): 71 | room = self.rooms[k] 72 | for y in range(room['y'], room['y'] + room['h']): 73 | for x in range(room['x'], room['x'] + room['w']): 74 | self.map[y][x]['t'] = 1 75 | # this part builds the walls 76 | for y in range(self.y_size): 77 | for x in range(self.x_size): 78 | if self.map[y][x]['t'] == 1: 79 | yy = y - 1 80 | while yy <= y + 1: 81 | xx = x - 1 82 | while xx <= x+1: 83 | if self.map[yy][xx]['t'] == 0: 84 | self.map[yy][xx]['t'] = 2 85 | xx += 2 86 | yy += 2 87 | 88 | self.find_farthest() 89 | self.mark_start_and_end() 90 | self.place_geometry() 91 | 92 | def does_collide(self, room): 93 | for i in range(len(self.rooms)): 94 | comparison_room = self.rooms[i] 95 | if room == comparison_room: 96 | continue 97 | if room['x'] < comparison_room['x'] + comparison_room['w'] \ 98 | and room['x'] + room['w'] > comparison_room['x'] \ 99 | and room['y'] < comparison_room['y'] + comparison_room['h'] \ 100 | and room['y'] + room['h'] > comparison_room['y']: 101 | return True 102 | return False 103 | 104 | def point_collide(self, x, y): 105 | if self.map[x][y]['t'] == 1: 106 | return False 107 | return True 108 | 109 | def shrink_map(self, shrink_limit): 110 | for value in range(shrink_limit): 111 | for i in range(len(self.rooms)): 112 | room = self.rooms[i] 113 | if room['x'] > 1: 114 | room['x'] -= 1 115 | if room['y'] > 1: 116 | room['y'] -= 1 117 | if self.does_collide(room): 118 | if room['x'] > 1: 119 | room['x'] += 1 120 | if room['y'] > 1: 121 | room['y'] += 1 122 | continue 123 | 124 | def find_closest(self, room, others=None): 125 | master_room = {'x': room['x'] + room['w'] / 2, 126 | 'y': room['y'] + room['h'] / 2 127 | } 128 | room_min = 1000 129 | final_room = None 130 | for i in range(len(self.rooms)): 131 | comparison_room = self.rooms[i] 132 | if room == comparison_room: 133 | continue 134 | if others is not None: 135 | is_closest = False 136 | for j in range(len(others)): 137 | if comparison_room == others[j]: 138 | is_closest = True 139 | break 140 | if is_closest: 141 | continue 142 | room_avg = { 143 | 'x': comparison_room['x'] + comparison_room['w'] / 2, 144 | 'y': comparison_room['y'] + comparison_room['h'] / 2 145 | } 146 | room_calc = abs(room_avg['x'] - master_room['x']) + abs(room_avg['y'] - master_room['y']) 147 | if room_calc < room_min: 148 | room_min = room_calc 149 | final_room = comparison_room 150 | return final_room 151 | 152 | def find_farthest(self): 153 | room_pair = [] 154 | swap_room = 0 155 | for i in range(len(self.rooms)): 156 | room = self.rooms[i] 157 | midA = { 158 | 'x': room['x'] + room['w'] / 2, 159 | 'y': room['y'] + room['h'] / 2 160 | } 161 | for j in range(len(self.rooms)): 162 | if i == j: 163 | continue 164 | closest_room = self.rooms[j] 165 | midB = { 166 | 'x': closest_room['x'] + closest_room['w'] / 2, 167 | 'y': closest_room['y'] + closest_room['h'] /2 168 | } 169 | math_room = abs(midB['x'] - midA['x']) + abs(midB['y'] - midA['y']) 170 | if math_room > swap_room: 171 | swap_room = math_room 172 | room_pair = [room, closest_room] 173 | self.first_room = room_pair[0] 174 | self.last_room = room_pair[1] 175 | 176 | def connect_rooms(self, room, closest_room, good): 177 | path_part_1 = { 178 | 'x': get_random_int(room['x'], room['x'] + room['w']), 179 | 'y': get_random_int(room['y'], room['y'] + room['h']) 180 | } 181 | path_part_2 = { 182 | 'x': get_random_int(closest_room['x'], closest_room['x'] + closest_room['w']), 183 | 'y': get_random_int(closest_room['y'], closest_room['y'] + closest_room['h']) 184 | } 185 | while path_part_1['x'] != path_part_2['x'] or path_part_1['y'] != path_part_2['y']: 186 | if path_part_1["x"] != path_part_2["x"]: 187 | if path_part_2["x"] < path_part_1["x"]: 188 | path_part_2["x"] += 1 189 | else: 190 | path_part_2["x"] -= 1 191 | else: 192 | if path_part_1["y"] != path_part_2["y"]: 193 | if path_part_2["y"] < path_part_1["y"]: 194 | path_part_2["y"] += 1 195 | else: 196 | path_part_2["y"] -= 1 197 | 198 | self.map[path_part_2['y']][path_part_2['x']]['t'] = 1 199 | if good: 200 | room['c'] = True 201 | closest_room['c'] = True 202 | self.connected.append(room) 203 | 204 | def mark_start_and_end(self): 205 | self.end = { 206 | 'pos': { 207 | 'x': get_random_int(self.first_room['x'] + 1, self.first_room['x'] + self.first_room['w'] - 1), 208 | 'y': get_random_int(self.first_room['y'] + 1, self.first_room['y'] + self.first_room['h'] - 1) 209 | } 210 | } 211 | self.start = { 212 | 'pos': { 213 | 'x': get_random_int(self.last_room['x'] + 1, self.last_room['x'] + self.last_room['w'] - 1), 214 | 'y': get_random_int(self.last_room['y'] + 1, self.last_room['y'] + self.last_room['h'] - 1) 215 | } 216 | } 217 | self.map[self.end["pos"]["y"]][self.end["pos"]["x"]]["t"] = 3 218 | self.map[self.start["pos"]["y"]][self.start["pos"]["x"]]["t"] = 4 219 | 220 | def place_geometry(self): 221 | yy = 0 222 | for y in range(self.y_size): 223 | xx = 0 224 | for x in range(self.x_size): 225 | if self.map[y][x]['t'] == 0 or self.map[y][x]['r'] == 0: # these are not part of the maze body 226 | pass 227 | elif self.map[y][x]['t'] == 2: 228 | # these are the walls 229 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(xx, yy, 0)) 230 | elif self.map[y][x]['t'] == 3: 231 | # this is the beginning 232 | bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, location=(xx, yy, 0)) 233 | elif self.map[y][x]['t'] == 4: 234 | # this is the end 235 | bpy.ops.mesh.primitive_cylinder_add(radius=1, enter_editmode=False, location=(xx, yy, 0)) 236 | else: 237 | # these are the floor tiles, 238 | bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, location=(xx, yy, -1)) 239 | xx += 2 240 | yy += 2 241 | 242 | 243 | def get_random_int(low, high): 244 | return randint(low, high-1) 245 | 246 | 247 | # joins all separate cube into a single object, 248 | # deletes inner faces to make the object hollow 249 | def cleanup_mesh(): 250 | # get all the cubes selected 251 | bpy.ops.object.select_all(action='TOGGLE') 252 | bpy.ops.object.select_all(action='TOGGLE') 253 | # join all of the separate cube objects into one 254 | bpy.ops.object.join() 255 | # jump into edit mode 256 | bpy.ops.object.mode_set(mode='EDIT') 257 | # get save the mesh data into a variable 258 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 259 | # select the entire mesh 260 | bpy.ops.mesh.select_all(action='SELECT') 261 | # remove overlapping verts 262 | bpy.ops.mesh.remove_doubles() 263 | # de-select everything in edit mode 264 | bpy.ops.mesh.select_all(action='DESELECT') 265 | # get back out of edit mode 266 | bpy.ops.object.mode_set(mode='OBJECT') 267 | 268 | 269 | # delete everything in the scene 270 | def clear_scene(): 271 | if bpy.context.active_object: 272 | if bpy.context.active_object.mode != 'OBJECT': 273 | bpy.ops.object.mode_set(mode='OBJECT') 274 | bpy.ops.object.select_all(action='SELECT') 275 | bpy.ops.object.delete(use_global=False) 276 | 277 | 278 | if __name__ == '__main__': 279 | clear_scene() 280 | dungeon = Dungeon() 281 | dungeon.generate() 282 | cleanup_mesh() 283 | -------------------------------------------------------------------------------- /Blender_2_8/castle_dungeon_generator_open_with_textures.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | import bpy 3 | import bmesh 4 | 5 | 6 | class Dungeon: 7 | def __init__(self): 8 | self.x_size = 60 9 | self.y_size = 40 10 | self.min_room_size = 5 11 | self.max_room_size = 15 12 | self.num_rooms = None 13 | self.min_rooms = 10 14 | self.max_rooms = 15 15 | self.map = None 16 | self.rooms = None 17 | self.first_room = None 18 | self.last_room = None 19 | self.stairs_up = None 20 | self.stairs_down = None 21 | self.connected = None 22 | self.light = 0 23 | 24 | def generate(self): 25 | self.map = [] 26 | self.rooms = [] 27 | self.first_room = None 28 | self.last_room = None 29 | self.connected = [] 30 | 31 | # build out the initial 2d array 32 | for y in range(self.y_size): 33 | # initialize each row 34 | self.map.append([]) 35 | for x in range(self.x_size): 36 | self.map[y].append({}) 37 | self.map[y][x]['t'] = 0 38 | self.map[y][x]['path'] = False 39 | self.map[y][x]['pos'] = { 40 | 'x': x, 41 | 'y': y 42 | } 43 | if self.light == 0: 44 | self.map[y][x]['r'] = 1 45 | else: 46 | self.map[y][x] = 0 47 | self.num_rooms = get_random_int(self.min_rooms, self.max_rooms) 48 | i = 0 49 | while i < self.num_rooms: 50 | room = {} 51 | room['x'] = get_random_int(1, (self.x_size - self.max_room_size - 1)) 52 | room['y'] = get_random_int(1, (self.y_size - self.max_room_size - 1)) 53 | room['w'] = get_random_int(self.min_room_size, self.max_room_size) 54 | room['h'] = get_random_int(self.min_room_size, self.max_room_size) 55 | room['c'] = False 56 | if self.does_collide(room): 57 | continue 58 | room['w'] -= 1 59 | room['h'] -= 1 60 | self.rooms.append(room) 61 | i += 1 62 | self.shrink_map(0) 63 | for k in range(len(self.rooms)): 64 | room = self.rooms[k] 65 | closest_room = self.find_closest(room, self.connected) 66 | if closest_room is None: 67 | break 68 | self.connect_rooms(room, closest_room, True) 69 | for k in range(len(self.rooms)): 70 | room = self.rooms[k] 71 | for y in range(room['y'], room['y'] + room['h']): 72 | for x in range(room['x'], room['x'] + room['w']): 73 | self.map[y][x]['t'] = 1 74 | # this part builds the walls 75 | for y in range(self.y_size): 76 | for x in range(self.x_size): 77 | if self.map[y][x]['t'] == 1: 78 | yy = y - 1 79 | while yy <= y + 1: 80 | xx = x - 1 81 | while xx <= x+1: 82 | if self.map[yy][xx]['t'] == 0: 83 | self.map[yy][xx]['t'] = 2 84 | xx += 2 85 | yy += 2 86 | 87 | self.find_farthest() 88 | self.mark_stairs() 89 | self.place_geometry() 90 | 91 | def does_collide(self, room): 92 | for i in range(len(self.rooms)): 93 | comparison_room = self.rooms[i] 94 | if room == comparison_room: 95 | continue 96 | if room['x'] < comparison_room['x'] + comparison_room['w'] \ 97 | and room['x'] + room['w'] > comparison_room['x'] \ 98 | and room['y'] < comparison_room['y'] + comparison_room['h'] \ 99 | and room['y'] + room['h'] > comparison_room['y']: 100 | return True 101 | return False 102 | 103 | def point_collide(self, x, y): 104 | if self.map[x][y]['t'] == 1: 105 | return False 106 | return True 107 | 108 | def shrink_map(self, shrink_limit): 109 | for value in range(shrink_limit): 110 | for i in range(len(self.rooms)): 111 | room = self.rooms[i] 112 | if room['x'] > 1: 113 | room['x'] -= 1 114 | if room['y'] > 1: 115 | room['y'] -= 1 116 | if self.does_collide(room): 117 | if room['x'] > 1: 118 | room['x'] += 1 119 | if room['y'] > 1: 120 | room['y'] += 1 121 | continue 122 | 123 | def find_closest(self, room, others=None): 124 | master_room = {'x': room['x'] + room['w'] / 2, 125 | 'y': room['y'] + room['h'] / 2 126 | } 127 | room_min = 1000 128 | final_room = None 129 | for i in range(len(self.rooms)): 130 | comparison_room = self.rooms[i] 131 | if room == comparison_room: 132 | continue 133 | if others is not None: 134 | is_closest = False 135 | for j in range(len(others)): 136 | if comparison_room == others[j]: 137 | is_closest = True 138 | break 139 | if is_closest: 140 | continue 141 | room_avg = { 142 | 'x': comparison_room['x'] + comparison_room['w'] / 2, 143 | 'y': comparison_room['y'] + comparison_room['h'] / 2 144 | } 145 | room_calc = abs(room_avg['x'] - master_room['x']) + abs(room_avg['y'] - master_room['y']) 146 | if room_calc < room_min: 147 | room_min = room_calc 148 | final_room = comparison_room 149 | return final_room 150 | 151 | def find_farthest(self): 152 | room_pair = [] 153 | swap_room = 0 154 | for i in range(len(self.rooms)): 155 | room = self.rooms[i] 156 | midA = { 157 | 'x': room['x'] + room['w'] / 2, 158 | 'y': room['y'] + room['h'] / 2 159 | } 160 | for j in range(len(self.rooms)): 161 | if i == j: 162 | continue 163 | closest_room = self.rooms[j] 164 | midB = { 165 | 'x': closest_room['x'] + closest_room['w'] / 2, 166 | 'y': closest_room['y'] + closest_room['h'] /2 167 | } 168 | math_room = abs(midB['x'] - midA['x']) + abs(midB['y'] - midA['y']) 169 | if math_room > swap_room: 170 | swap_room = math_room 171 | room_pair = [room, closest_room] 172 | self.first_room = room_pair[0] 173 | self.last_room = room_pair[1] 174 | 175 | def connect_rooms(self, room, closest_room, good): 176 | path_part_1 = { 177 | 'x': get_random_int(room['x'], room['x'] + room['w']), 178 | 'y': get_random_int(room['y'], room['y'] + room['h']) 179 | } 180 | path_part_2 = { 181 | 'x': get_random_int(closest_room['x'], closest_room['x'] + closest_room['w']), 182 | 'y': get_random_int(closest_room['y'], closest_room['y'] + closest_room['h']) 183 | } 184 | while path_part_1['x'] != path_part_2['x'] or path_part_1['y'] != path_part_2['y']: 185 | if path_part_1["x"] != path_part_2["x"]: 186 | if path_part_2["x"] < path_part_1["x"]: 187 | path_part_2["x"] += 1 188 | else: 189 | path_part_2["x"] -= 1 190 | else: 191 | if path_part_1["y"] != path_part_2["y"]: 192 | if path_part_2["y"] < path_part_1["y"]: 193 | path_part_2["y"] += 1 194 | else: 195 | path_part_2["y"] -= 1 196 | 197 | self.map[path_part_2['y']][path_part_2['x']]['t'] = 1 198 | if good: 199 | room['c'] = True 200 | closest_room['c'] = True 201 | self.connected.append(room) 202 | 203 | def mark_stairs(self): 204 | self.stairs_up = { 205 | 'pos': { 206 | 'x': get_random_int(self.first_room['x'] + 1, self.first_room['x'] + self.first_room['w'] - 1), 207 | 'y': get_random_int(self.first_room['y'] + 1, self.first_room['y'] + self.first_room['h'] - 1) 208 | } 209 | } 210 | self.stairs_down = { 211 | 'pos': { 212 | 'x': get_random_int(self.last_room['x'] + 1, self.last_room['x'] + self.last_room['w'] - 1), 213 | 'y': get_random_int(self.last_room['y'] + 1, self.last_room['y'] + self.last_room['h'] - 1) 214 | } 215 | } 216 | self.map[self.stairs_up["pos"]["y"]][self.stairs_up["pos"]["x"]]["t"] = 3 217 | self.map[self.stairs_down["pos"]["y"]][self.stairs_down["pos"]["x"]]["t"] = 4 218 | 219 | def place_geometry(self): 220 | yy = 0 221 | for y in range(self.y_size): 222 | xx = 0 223 | for x in range(self.x_size): 224 | if self.map[y][x]['t'] == 0 or self.map[y][x]['r'] == 0: # these are not part of the maze body 225 | pass 226 | elif self.map[y][x]['t'] == 2: 227 | # these are the walls 228 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(xx, yy, 0)) 229 | elif self.map[y][x]['t'] == 3: 230 | # this is the beginning 231 | bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, location=(xx, yy, 0)) 232 | elif self.map[y][x]['t'] == 4: 233 | # this is the end 234 | bpy.ops.mesh.primitive_cylinder_add(radius=1, enter_editmode=False, location=(xx, yy, 0)) 235 | else: 236 | # these are the floor tiles, 237 | bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, location=(xx, yy, -1)) 238 | xx += 2 239 | yy += 2 240 | 241 | 242 | def get_random_int(low, high): 243 | return randint(low, high-1) 244 | 245 | 246 | def separate_the_floor(ob, floor_mat_index): 247 | bpy.ops.object.mode_set(mode='EDIT') 248 | # make sure there is only one object in the scene, else figure out a better way to get the object. 249 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 250 | bpy.ops.mesh.select_all(action='DESELECT') 251 | for f in mesh.faces: 252 | if f.calc_center_median()[2] == -1: 253 | f.select = True 254 | ob.active_material_index = floor_mat_index 255 | bpy.ops.object.material_slot_assign() 256 | 257 | 258 | def append_material(directory, filepath, material_name): 259 | bpy.ops.wm.append( 260 | filename=material_name, 261 | directory=directory, 262 | filepath=filepath, 263 | link=False, 264 | ) 265 | 266 | 267 | def assign_material(ob, mat_name): 268 | ob.data.materials.append(bpy.data.materials.get(mat_name)) 269 | 270 | 271 | # joins all separate cube into a single object, 272 | # deletes inner faces to make the object hollow 273 | def cleanup_mesh(): 274 | # get all the cubes selected 275 | bpy.ops.object.select_all(action='TOGGLE') 276 | bpy.ops.object.select_all(action='TOGGLE') 277 | # join all of the separate cube objects into one 278 | bpy.ops.object.join() 279 | # jump into edit mode 280 | bpy.ops.object.mode_set(mode='EDIT') 281 | # get save the mesh data into a variable 282 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 283 | # select the entire mesh 284 | bpy.ops.mesh.select_all(action='SELECT') 285 | # remove overlapping verts 286 | bpy.ops.mesh.remove_doubles() 287 | # de-select everything in edit mode 288 | bpy.ops.mesh.select_all(action='DESELECT') 289 | # get back out of edit mode 290 | bpy.ops.object.mode_set(mode='OBJECT') 291 | 292 | 293 | # delete everything in the scene 294 | def clear_scene(): 295 | if bpy.context.active_object: 296 | if bpy.context.active_object.mode != 'OBJECT': 297 | bpy.ops.object.mode_set(mode='OBJECT') 298 | bpy.ops.object.select_all(action='SELECT') 299 | bpy.ops.object.delete(use_global=False) 300 | 301 | 302 | if __name__ == '__main__': 303 | clear_scene() 304 | dungeon = Dungeon() 305 | dungeon.generate() 306 | cleanup_mesh() 307 | 308 | RESOURCE_BLEND_FILE = "resources.blend" 309 | PATH_TO_PROJECT_DIRECTORY = "C:\\Users\\aaron\\PycharmProjects" # TODO: change to your own location 310 | PATH_TO_RESOURCE_FILE = f"\\Blender-Python-Procedural-Level-Generation\\Blender_2_8\\resources\\{RESOURCE_BLEND_FILE}\\Material" 311 | append_material( 312 | directory=f"{PATH_TO_PROJECT_DIRECTORY}{PATH_TO_RESOURCE_FILE}", 313 | filepath=RESOURCE_BLEND_FILE, 314 | material_name="castlebrick", 315 | ) 316 | append_material( 317 | directory=f"{PATH_TO_PROJECT_DIRECTORY}{PATH_TO_RESOURCE_FILE}", 318 | filepath=RESOURCE_BLEND_FILE, 319 | material_name="cobblestone", 320 | ) 321 | if len(bpy.data.objects) > 0: 322 | ob = bpy.data.objects[0] 323 | assign_material(ob, 'castlebrick') 324 | assign_material(ob, 'cobblestone') 325 | separate_the_floor(ob, 1) 326 | -------------------------------------------------------------------------------- /Blender_2_8/cellular_automata_cave_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | import bmesh 16 | 17 | CHANCE_TO_START_ALIVE = 0.40 18 | DEATH_LIMIT = 3 19 | BIRTH_LIMIT = 4 20 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh 21 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 22 | HEIGHT = WIDTH 23 | 24 | 25 | def initialize_map(): 26 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 27 | return initial_map 28 | 29 | 30 | def alive_calc(): 31 | if random() < CHANCE_TO_START_ALIVE: 32 | return True 33 | else: 34 | return False 35 | 36 | 37 | def perform_game_of_life_iteration(old_map): 38 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 39 | # Loop over each row and column of the level_map 40 | for x in range(len(old_map)): 41 | for y in range(len(old_map[0])): 42 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 43 | 44 | if old_map[x][y] is True: 45 | # See if it should die and become open 46 | if live_neighbor_count < DEATH_LIMIT: 47 | new_map[x][y] = False 48 | # Otherwise keep it as a wall 49 | else: 50 | new_map[x][y] = True 51 | # If the tile is currently empty 52 | else: 53 | # See if it should become a wall 54 | if live_neighbor_count > BIRTH_LIMIT: 55 | new_map[x][y] = True 56 | else: 57 | new_map[x][y] = False 58 | return new_map 59 | 60 | 61 | # Returns the number of cells in a ring around (x,y) that are alive. 62 | def count_alive_neighbors(live_map, x, y): 63 | count = 0 64 | i = -1 65 | while i < 2: 66 | j = -1 67 | while j < 2: 68 | neighbor_x = x + i 69 | neighbor_y = y + j 70 | # If we're looking at the middle point 71 | if i == 0 and j == 0: 72 | pass 73 | # Do nothing, we don't want to add ourselves in! 74 | # In case the index we're looking at it off the edge of the live_map 75 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 76 | count += 1 77 | # Otherwise, a normal check of the neighbour 78 | elif live_map[neighbor_x][neighbor_y] is True: 79 | count += 1 80 | j += 1 81 | i += 1 82 | return count 83 | 84 | 85 | # adds cubes to the map based on the level_map matrix 86 | def add_cubes(cell_map): 87 | matrix_size = WIDTH 88 | for i in range(matrix_size**2): 89 | y = math.floor(i / matrix_size) 90 | x = i - y * matrix_size 91 | if y > 1 and x == 0: 92 | cleanup_mesh() 93 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 94 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x*2, y*2, 0)) 95 | 96 | 97 | # joins all separate objects into a single object, 98 | # deletes inner faces to make the object hollow 99 | def cleanup_mesh(): 100 | # get all the cubes selected 101 | bpy.ops.object.select_all(action='TOGGLE') 102 | bpy.ops.object.select_all(action='TOGGLE') 103 | # join all of the separate cube objects into one 104 | bpy.ops.object.join() 105 | # jump into edit mode 106 | bpy.ops.object.mode_set(mode='EDIT') 107 | # get save the mesh data into a variable 108 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 109 | # select the entire mesh 110 | bpy.ops.mesh.select_all(action='SELECT') 111 | # remove overlapping verts 112 | bpy.ops.mesh.remove_doubles() 113 | # de-select everything in edit mode 114 | bpy.ops.mesh.select_all(action='DESELECT') 115 | # select the "interior faces" 116 | bpy.ops.mesh.select_interior_faces() 117 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 118 | for f in mesh.faces: 119 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 120 | f.select = False 121 | # delete all still selected faces, leaving a hollow mesh behind 122 | bpy.ops.mesh.delete(type='FACE') 123 | # get back out of edit mode 124 | bpy.ops.object.mode_set(mode='OBJECT') 125 | 126 | 127 | # delete everything in the scene 128 | def clear_scene(): 129 | if bpy.ops.object.mode_set.poll(): 130 | bpy.ops.object.mode_set(mode='OBJECT') 131 | bpy.ops.object.select_all(action='SELECT') 132 | bpy.ops.object.delete(use_global=False) 133 | 134 | 135 | def generate_map(): 136 | # Create a new level_map 137 | # Set up the level_map with random values 138 | cellmap = initialize_map() 139 | # run the simulation for a set number of steps 140 | for i in range(NUMBER_OF_ITERATIONS): 141 | cellmap = perform_game_of_life_iteration(cellmap) 142 | return cellmap 143 | 144 | 145 | if __name__ == '__main__': 146 | clear_scene() 147 | level_map = generate_map() 148 | add_cubes(level_map) 149 | cleanup_mesh() 150 | -------------------------------------------------------------------------------- /Blender_2_8/cellular_automata_cave_maze_with_walls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender 2.8. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | 16 | CHANCE_TO_START_ALIVE = 0.40 # The smaller this number is, the sparser the generated maze will be 17 | DEATH_LIMIT = 3 18 | BIRTH_LIMIT = 4 19 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh shape 20 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 21 | HEIGHT = WIDTH 22 | 23 | 24 | visited = [] # walkable tile 25 | walls = [] 26 | 27 | 28 | def initialize_map(): 29 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 30 | return initial_map 31 | 32 | 33 | def alive_calc(): 34 | if random() < CHANCE_TO_START_ALIVE: 35 | return True 36 | else: 37 | return False 38 | 39 | 40 | def perform_game_of_life_iteration(old_map): 41 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 42 | # Loop over each row and column of the level_map 43 | for x in range(len(old_map)): 44 | for y in range(len(old_map[0])): 45 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 46 | 47 | if old_map[x][y] is True: 48 | # See if it should die and become open 49 | if live_neighbor_count < DEATH_LIMIT: 50 | new_map[x][y] = False 51 | # Otherwise keep it as a wall 52 | else: 53 | new_map[x][y] = True 54 | # If the tile is currently empty 55 | else: 56 | # See if it should become a wall 57 | if live_neighbor_count > BIRTH_LIMIT: 58 | new_map[x][y] = True 59 | else: 60 | new_map[x][y] = False 61 | return new_map 62 | 63 | 64 | # Returns the number of cells in a ring around (x,y) that are alive. 65 | def count_alive_neighbors(live_map, x, y): 66 | count = 0 67 | i = -1 68 | while i < 2: 69 | j = -1 70 | while j < 2: 71 | neighbor_x = x + i 72 | neighbor_y = y + j 73 | if i == 0 and j == 0: # If we're looking at the middle point 74 | pass # Do nothing, we don't want to add ourselves in! 75 | # In case the index we're looking at it off the edge of the live_map 76 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 77 | count += 1 78 | # Otherwise, a normal check of the neighbour 79 | elif live_map[neighbor_x][neighbor_y] is True: 80 | count += 1 81 | j += 1 82 | i += 1 83 | return count 84 | 85 | 86 | # adds cubes to the map based on the level_map matrix 87 | def add_tiles(cell_map): 88 | matrix_size = WIDTH 89 | for i in range(matrix_size**2): 90 | y = math.floor(i / matrix_size) 91 | x = i - y * matrix_size 92 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 93 | place_tile(x*2, y*2) 94 | visited.append((x*2, y*2)) 95 | 96 | 97 | def place_cube(x, y): 98 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x, y, 0)) 99 | 100 | 101 | def place_tile(x, y): 102 | bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, location=(x, y, -1)) 103 | 104 | 105 | def check_neighbors_and_place_wall(pos): 106 | offset_map = [(pos[0] - 2.0, pos[1]), 107 | (pos[0] + 2.0, pos[1]), 108 | (pos[0], pos[1] - 2.0), 109 | (pos[0], pos[1] + 2.0)] 110 | for offset_tuple in offset_map: 111 | if offset_tuple not in visited and offset_map not in walls: 112 | place_cube(offset_tuple[0], offset_tuple[1]) 113 | walls.append((offset_tuple[0], offset_tuple[1])) 114 | 115 | 116 | def build_walls(): 117 | for position in visited: 118 | check_neighbors_and_place_wall(position) 119 | 120 | 121 | # joins all separate objects into a single object and delete duplicate verts 122 | def cleanup_mesh(): 123 | # get all the cubes selected 124 | bpy.ops.object.select_all(action='SELECT') 125 | # join all of the separate cube objects into one 126 | bpy.ops.object.join() 127 | # jump into edit mode 128 | bpy.ops.object.editmode_toggle() 129 | # select all verts 130 | bpy.ops.mesh.select_all(action='SELECT') 131 | # remove overlapping verts 132 | bpy.ops.mesh.remove_doubles() 133 | # get back out of edit mode 134 | bpy.ops.object.editmode_toggle() 135 | 136 | 137 | # delete everything in the scene 138 | def clear_scene(): 139 | if bpy.ops.object.mode_set.poll(): 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | bpy.ops.object.select_all(action='SELECT') 142 | bpy.ops.object.delete(use_global=False) 143 | 144 | 145 | def generate_map(): 146 | # Create a new level_map 147 | # Set up the level_map with random values 148 | cellmap = initialize_map() 149 | # run the simulation for a set number of steps 150 | for i in range(NUMBER_OF_ITERATIONS): 151 | cellmap = perform_game_of_life_iteration(cellmap) 152 | return cellmap 153 | 154 | 155 | if __name__ == '__main__': 156 | clear_scene() 157 | level_map = generate_map() 158 | add_tiles(level_map) 159 | cleanup_mesh() # done here to give slight speedup by reducing the number of total objects in the scene 160 | build_walls() 161 | cleanup_mesh() 162 | -------------------------------------------------------------------------------- /Blender_2_8/cellular_automata_cave_maze_with_walls_cavify.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender 2.8. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | 16 | CHANCE_TO_START_ALIVE = 0.40 # The smaller this number is, the sparser the generated maze will be 17 | DEATH_LIMIT = 3 18 | BIRTH_LIMIT = 4 19 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh shape 20 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 21 | HEIGHT = WIDTH 22 | 23 | 24 | visited = [] # walkable tile 25 | walls = [] 26 | 27 | 28 | def initialize_map(): 29 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 30 | return initial_map 31 | 32 | 33 | def alive_calc(): 34 | if random() < CHANCE_TO_START_ALIVE: 35 | return True 36 | else: 37 | return False 38 | 39 | 40 | def perform_game_of_life_iteration(old_map): 41 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 42 | # Loop over each row and column of the level_map 43 | for x in range(len(old_map)): 44 | for y in range(len(old_map[0])): 45 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 46 | 47 | if old_map[x][y] is True: 48 | # See if it should die and become open 49 | if live_neighbor_count < DEATH_LIMIT: 50 | new_map[x][y] = False 51 | # Otherwise keep it as a wall 52 | else: 53 | new_map[x][y] = True 54 | # If the tile is currently empty 55 | else: 56 | # See if it should become a wall 57 | if live_neighbor_count > BIRTH_LIMIT: 58 | new_map[x][y] = True 59 | else: 60 | new_map[x][y] = False 61 | return new_map 62 | 63 | 64 | # Returns the number of cells in a ring around (x,y) that are alive. 65 | def count_alive_neighbors(live_map, x, y): 66 | count = 0 67 | i = -1 68 | while i < 2: 69 | j = -1 70 | while j < 2: 71 | neighbor_x = x + i 72 | neighbor_y = y + j 73 | if i == 0 and j == 0: # If we're looking at the middle point 74 | pass # Do nothing, we don't want to add ourselves in! 75 | # In case the index we're looking at it off the edge of the live_map 76 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 77 | count += 1 78 | # Otherwise, a normal check of the neighbour 79 | elif live_map[neighbor_x][neighbor_y] is True: 80 | count += 1 81 | j += 1 82 | i += 1 83 | return count 84 | 85 | 86 | # adds cubes to the map based on the level_map matrix 87 | def add_tiles(cell_map): 88 | matrix_size = WIDTH 89 | for i in range(matrix_size**2): 90 | y = math.floor(i / matrix_size) 91 | x = i - y * matrix_size 92 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 93 | place_tile(x*2, y*2) 94 | visited.append((x*2, y*2)) 95 | 96 | 97 | def place_cube(x, y): 98 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x, y, 0)) 99 | 100 | 101 | def place_tile(x, y): 102 | bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, location=(x, y, -1)) 103 | 104 | 105 | def check_neighbors_and_place_wall(pos): 106 | offset_map = [(pos[0] - 2.0, pos[1]), 107 | (pos[0] + 2.0, pos[1]), 108 | (pos[0], pos[1] - 2.0), 109 | (pos[0], pos[1] + 2.0)] 110 | for offset_tuple in offset_map: 111 | if offset_tuple not in visited and offset_map not in walls: 112 | place_cube(offset_tuple[0], offset_tuple[1]) 113 | walls.append((offset_tuple[0], offset_tuple[1])) 114 | 115 | 116 | def build_walls(): 117 | for position in visited: 118 | check_neighbors_and_place_wall(position) 119 | 120 | 121 | # joins all separate objects into a single object and delete duplicate verts 122 | def cleanup_mesh(): 123 | # get all the cubes selected 124 | bpy.ops.object.select_all(action='SELECT') 125 | # join all of the separate cube objects into one 126 | bpy.ops.object.join() 127 | # jump into edit mode 128 | bpy.ops.object.editmode_toggle() 129 | # select all verts 130 | bpy.ops.mesh.select_all(action='SELECT') 131 | # remove overlapping verts 132 | bpy.ops.mesh.remove_doubles() 133 | # get back out of edit mode 134 | bpy.ops.object.editmode_toggle() 135 | 136 | 137 | def cavify(): 138 | win = bpy.context.window 139 | scr = win.screen 140 | areas3d = [area for area in scr.areas if area.type == 'VIEW_3D'] 141 | region = [region for region in areas3d[0].regions if region.type == 'WINDOW'] 142 | 143 | override = {'window': win, 144 | 'screen': scr, 145 | 'area': areas3d[0], 146 | 'region': region[0], 147 | 'scene': bpy.context.scene, 148 | } 149 | 150 | bpy.ops.object.mode_set(mode='EDIT') 151 | bpy.ops.mesh.loopcut_slide( 152 | override, 153 | MESH_OT_loopcut={ 154 | "number_cuts": 1, 155 | "smoothness": 0, 156 | "falloff": 'INVERSE_SQUARE', 157 | "edge_index": 1, 158 | "mesh_select_mode_init": (False, False, True) 159 | }, 160 | TRANSFORM_OT_edge_slide={ 161 | "value": 0.637373, 162 | "single_side": False, 163 | "use_even": False, 164 | "flipped": False, 165 | "use_clamp": True, 166 | "mirror": False, 167 | "snap": False, 168 | "snap_target": 'CLOSEST', 169 | "snap_point": (0, 0, 0), 170 | "snap_align": False, 171 | "snap_normal": (0, 0, 0), 172 | "correct_uv": False, 173 | "release_confirm": False, 174 | "use_accurate": False 175 | } 176 | ) 177 | 178 | # space and search for flip normals 179 | bpy.ops.mesh.select_all(action='TOGGLE') 180 | bpy.ops.mesh.flip_normals() 181 | bpy.ops.object.modifier_add(type='SUBSURF') 182 | bpy.context.object.modifiers["Subdivision"].levels = 4 183 | 184 | # add the displacement modifiers 185 | bpy.ops.object.modifier_add(type='DISPLACE') 186 | bpy.ops.texture.new() 187 | bpy.data.textures["Texture"].type = 'STUCCI' 188 | bpy.data.textures["Texture"].noise_scale = 0.75 189 | bpy.context.object.modifiers["Displace"].direction = 'X' 190 | bpy.data.textures["Texture"].turbulence = 10 191 | bpy.context.object.modifiers["Displace"].texture = bpy.data.textures["Texture"] 192 | 193 | bpy.ops.object.modifier_add(type='DISPLACE') 194 | bpy.ops.texture.new() 195 | bpy.data.textures["Texture.001"].type = 'STUCCI' 196 | bpy.data.textures["Texture.001"].noise_scale = 0.75 197 | bpy.context.object.modifiers["Displace.001"].direction = 'Y' 198 | bpy.context.object.modifiers["Displace.001"].texture = bpy.data.textures["Texture.001"] 199 | 200 | bpy.ops.object.modifier_add(type='DISPLACE') 201 | bpy.ops.texture.new() 202 | bpy.data.textures["Texture.002"].type = 'CLOUDS' 203 | bpy.data.textures["Texture.002"].noise_scale = 0.65 204 | bpy.context.object.modifiers["Displace.002"].strength = 0.20 205 | bpy.context.object.modifiers["Displace.002"].texture = bpy.data.textures["Texture.002"] 206 | 207 | bpy.context.object.modifiers["Displace"].strength = 0.7 208 | bpy.context.object.modifiers["Displace.001"].strength = 0.6 209 | 210 | bpy.ops.object.mode_set(mode='OBJECT') 211 | 212 | 213 | # delete everything in the scene 214 | def clear_scene(): 215 | if bpy.ops.object.mode_set.poll(): 216 | bpy.ops.object.mode_set(mode='OBJECT') 217 | bpy.ops.object.select_all(action='SELECT') 218 | bpy.ops.object.delete(use_global=False) 219 | 220 | 221 | def generate_map(): 222 | # Create a new level_map 223 | # Set up the level_map with random values 224 | cellmap = initialize_map() 225 | # run the simulation for a set number of steps 226 | for i in range(NUMBER_OF_ITERATIONS): 227 | cellmap = perform_game_of_life_iteration(cellmap) 228 | return cellmap 229 | 230 | 231 | if __name__ == '__main__': 232 | clear_scene() 233 | level_map = generate_map() 234 | add_tiles(level_map) 235 | cleanup_mesh() # done here to give slight speedup by reducing the number of total objects in the scene 236 | build_walls() 237 | cleanup_mesh() 238 | cavify() 239 | -------------------------------------------------------------------------------- /Blender_2_8/cellular_automata_cavified_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a simple cellular automata / game of life algorithm to generate cave like levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 9 | ''' 10 | 11 | import math 12 | from random import random 13 | 14 | import bpy 15 | import bmesh 16 | 17 | CHANCE_TO_START_ALIVE = 0.38 # lower the number, tbe smaller the "gaps"3r 18 | DEATH_LIMIT = 3 19 | BIRTH_LIMIT = 4 20 | NUMBER_OF_ITERATIONS = 6 # number of times the game of life algorithm is run, consolidates mesh 21 | WIDTH = 40 # overall size of the maze to be generated, the higher, the bigger, but increases run time 22 | HEIGHT = WIDTH 23 | 24 | 25 | def initialize_map(): 26 | initial_map = [[alive_calc() for i in range(WIDTH)] for j in range(HEIGHT)] 27 | return initial_map 28 | 29 | 30 | def alive_calc(): 31 | if random() < CHANCE_TO_START_ALIVE: 32 | return True 33 | else: 34 | return False 35 | 36 | 37 | def perform_game_of_life_iteration(old_map): 38 | new_map = [[False for i in range(WIDTH)] for j in range(HEIGHT)] 39 | # Loop over each row and column of the level_map 40 | for x in range(len(old_map)): 41 | for y in range(len(old_map[0])): 42 | live_neighbor_count = count_alive_neighbors(old_map, x, y) 43 | 44 | if old_map[x][y] is True: 45 | # See if it should die and become open 46 | if live_neighbor_count < DEATH_LIMIT: 47 | new_map[x][y] = False 48 | # Otherwise keep it as a wall 49 | else: 50 | new_map[x][y] = True 51 | # If the tile is currently empty 52 | else: 53 | # See if it should become a wall 54 | if live_neighbor_count > BIRTH_LIMIT: 55 | new_map[x][y] = True 56 | else: 57 | new_map[x][y] = False 58 | return new_map 59 | 60 | 61 | # Returns the number of cells in a ring around (x,y) that are alive. 62 | def count_alive_neighbors(live_map, x, y): 63 | count = 0 64 | i = -1 65 | while i < 2: 66 | j = -1 67 | while j < 2: 68 | neighbor_x = x + i 69 | neighbor_y = y + j 70 | # If we're looking at the middle point 71 | if i == 0 and j == 0: 72 | pass 73 | # Do nothing, we don't want to add ourselves in! 74 | # In case the index we're looking at it off the edge of the live_map 75 | elif neighbor_x < 0 or neighbor_y < 0 or neighbor_x >= len(live_map) or neighbor_y >= len(live_map[0]): 76 | count += 1 77 | # Otherwise, a normal check of the neighbour 78 | elif live_map[neighbor_x][neighbor_y] is True: 79 | count += 1 80 | j += 1 81 | i += 1 82 | return count 83 | 84 | 85 | # adds cubes to the map based on the level_map matrix 86 | def add_cubes(cell_map): 87 | matrix_size = WIDTH 88 | for i in range(matrix_size**2): 89 | y = math.floor(i / matrix_size) 90 | x = i - y * matrix_size 91 | if y > 1 and x == 0: 92 | cleanup_mesh() 93 | if cell_map[y][x] is False: # cells with value True get cubes placed on them 94 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x*2, y*2, 0)) 95 | 96 | 97 | # # joins all separate objects into a single object, 98 | # # deletes inner faces to make the object hollow 99 | # joins all separate objects into a single object, 100 | # deletes inner faces to make the object hollow 101 | def cleanup_mesh(): 102 | # get all the cubes selected 103 | bpy.ops.object.select_all(action='TOGGLE') 104 | bpy.ops.object.select_all(action='TOGGLE') 105 | # join all of the separate cube objects into one 106 | bpy.ops.object.join() 107 | # jump into edit mode 108 | bpy.ops.object.mode_set(mode='EDIT') 109 | # get save the mesh data into a variable 110 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 111 | # select the entire mesh 112 | bpy.ops.mesh.select_all(action='SELECT') 113 | # remove overlapping verts 114 | bpy.ops.mesh.remove_doubles() 115 | # de-select everything in edit mode 116 | bpy.ops.mesh.select_all(action='DESELECT') 117 | # select the "interior faces" 118 | bpy.ops.mesh.select_interior_faces() 119 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 120 | for f in mesh.faces: 121 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 122 | f.select = False 123 | # delete all still selected faces, leaving a hollow mesh behind 124 | bpy.ops.mesh.delete(type='FACE') 125 | # get back out of edit mode 126 | bpy.ops.object.mode_set(mode='OBJECT') 127 | 128 | 129 | def cavify(): 130 | win = bpy.context.window 131 | scr = win.screen 132 | areas3d = [area for area in scr.areas if area.type == 'VIEW_3D'] 133 | region = [region for region in areas3d[0].regions if region.type == 'WINDOW'] 134 | 135 | override = {'window': win, 136 | 'screen': scr, 137 | 'area': areas3d[0], 138 | 'region': region[0], 139 | 'scene': bpy.context.scene, 140 | } 141 | 142 | bpy.ops.object.mode_set(mode='EDIT') 143 | bpy.ops.mesh.loopcut_slide( 144 | override, 145 | MESH_OT_loopcut={ 146 | "number_cuts": 1, 147 | "smoothness": 0, 148 | "falloff": 'INVERSE_SQUARE', 149 | "edge_index": 1, 150 | "mesh_select_mode_init": (False, False, True) 151 | }, 152 | TRANSFORM_OT_edge_slide={ 153 | "value": 0.637373, 154 | "single_side": False, 155 | "use_even": False, 156 | "flipped": False, 157 | "use_clamp": True, 158 | "mirror": False, 159 | "snap": False, 160 | "snap_target": 'CLOSEST', 161 | "snap_point": (0, 0, 0), 162 | "snap_align": False, 163 | "snap_normal": (0, 0, 0), 164 | "correct_uv": False, 165 | "release_confirm": False, 166 | "use_accurate": False 167 | } 168 | ) 169 | 170 | # space and search for flip normals 171 | bpy.ops.mesh.select_all(action='TOGGLE') 172 | bpy.ops.mesh.flip_normals() 173 | bpy.ops.object.modifier_add(type='SUBSURF') 174 | bpy.context.object.modifiers["Subdivision"].levels = 4 175 | 176 | # add the displacement modifiers 177 | bpy.ops.object.modifier_add(type='DISPLACE') 178 | bpy.ops.texture.new() 179 | bpy.data.textures["Texture"].type = 'STUCCI' 180 | bpy.data.textures["Texture"].noise_scale = 0.75 181 | bpy.context.object.modifiers["Displace"].direction = 'X' 182 | bpy.data.textures["Texture"].turbulence = 10 183 | bpy.context.object.modifiers["Displace"].texture = bpy.data.textures["Texture"] 184 | 185 | bpy.ops.object.modifier_add(type='DISPLACE') 186 | bpy.ops.texture.new() 187 | bpy.data.textures["Texture.001"].type = 'STUCCI' 188 | bpy.data.textures["Texture.001"].noise_scale = 0.75 189 | bpy.context.object.modifiers["Displace.001"].direction = 'Y' 190 | bpy.context.object.modifiers["Displace.001"].texture = bpy.data.textures["Texture.001"] 191 | 192 | bpy.ops.object.modifier_add(type='DISPLACE') 193 | bpy.ops.texture.new() 194 | bpy.data.textures["Texture.002"].type = 'CLOUDS' 195 | bpy.data.textures["Texture.002"].noise_scale = 0.65 196 | bpy.context.object.modifiers["Displace.002"].strength = 0.20 197 | bpy.context.object.modifiers["Displace.002"].texture = bpy.data.textures["Texture.002"] 198 | 199 | bpy.context.object.modifiers["Displace"].strength = 0.7 200 | bpy.context.object.modifiers["Displace.001"].strength = 0.6 201 | 202 | bpy.ops.object.mode_set(mode='OBJECT') 203 | 204 | 205 | # delete everything in the scene 206 | def clear_scene(): 207 | if bpy.ops.object.mode_set.poll(): 208 | bpy.ops.object.mode_set(mode='OBJECT') 209 | bpy.ops.object.select_all(action='SELECT') 210 | bpy.ops.object.delete(use_global=False) 211 | 212 | 213 | def generate_map(): 214 | # Create a new level_map 215 | # Set up the level_map with random values 216 | cellmap = initialize_map() 217 | # run the simulation for a set number of steps 218 | for i in range(NUMBER_OF_ITERATIONS): 219 | cellmap = perform_game_of_life_iteration(cellmap) 220 | return cellmap 221 | 222 | 223 | if __name__ == '__main__': 224 | clear_scene() 225 | level_map = generate_map() 226 | add_cubes(level_map) 227 | cleanup_mesh() 228 | cavify() 229 | -------------------------------------------------------------------------------- /Blender_2_8/random_walk_cavified.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | # Controls the distances that are moved 20 | # MUST BE AT LEAST 2 21 | X_MOVE_DISTANCE = 2.0 22 | Y_MOVE_DISTANCE = 2.0 23 | 24 | # Position in space 25 | y_pos = 0 26 | x_pos = 0 27 | 28 | 29 | def generate_maze(): 30 | for i in range(ITERATIONS): 31 | direction = get_random_direction() 32 | next_move(direction) 33 | cleanup_mesh() 34 | cavify() 35 | 36 | 37 | def get_random_direction(): 38 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 39 | random_num = random.randint(0, 3) 40 | return direction[random_num] 41 | 42 | 43 | def next_move(direction): 44 | global y_pos 45 | global x_pos 46 | 47 | if direction == 'up': 48 | y_pos += Y_MOVE_DISTANCE 49 | place_cube() 50 | 51 | if direction == 'right': 52 | x_pos += X_MOVE_DISTANCE 53 | place_cube() 54 | 55 | if direction == 'down': 56 | y_pos -= Y_MOVE_DISTANCE 57 | place_cube() 58 | 59 | if direction == 'left': 60 | x_pos -= X_MOVE_DISTANCE 61 | place_cube() 62 | 63 | 64 | def place_cube(): 65 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x_pos, y_pos, 0)) 66 | 67 | 68 | # combines the cubes into one object and removes interior faces 69 | def cleanup_mesh(): 70 | # select all of the cubes 71 | bpy.ops.object.mode_set(mode='OBJECT') 72 | bpy.ops.object.select_all(action='SELECT') 73 | # join all of the separate cube objects into one 74 | bpy.ops.object.join() 75 | # jump into edit mode 76 | bpy.ops.object.editmode_toggle() 77 | # get save the mesh data into a variable 78 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 79 | bpy.ops.mesh.select_all(action='SELECT') 80 | # remove overlapping verts 81 | bpy.ops.mesh.remove_doubles() 82 | # de-select everything in edit mode 83 | bpy.ops.mesh.select_all(action='DESELECT') 84 | # select the "interior faces" 85 | bpy.ops.mesh.select_interior_faces() 86 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 87 | for f in mesh.faces: 88 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 89 | f.select = False 90 | # delete all still selected faces, leaving a hollow mesh behind 91 | bpy.ops.mesh.delete(type='FACE') 92 | 93 | 94 | def cavify(): 95 | win = bpy.context.window 96 | scr = win.screen 97 | areas3d = [area for area in scr.areas if area.type == 'VIEW_3D'] 98 | region = [region for region in areas3d[0].regions if region.type == 'WINDOW'] 99 | 100 | override = {'window': win, 101 | 'screen': scr, 102 | 'area': areas3d[0], 103 | 'region': region[0], 104 | 'scene': bpy.context.scene, 105 | } 106 | 107 | bpy.ops.object.mode_set(mode='EDIT') 108 | bpy.ops.mesh.loopcut_slide( 109 | override, 110 | MESH_OT_loopcut={ 111 | "number_cuts": 1, 112 | "smoothness": 0, 113 | "falloff": 'INVERSE_SQUARE', 114 | "edge_index": 1, 115 | "mesh_select_mode_init": (False, False, True) 116 | }, 117 | TRANSFORM_OT_edge_slide={ 118 | "value": 0.637373, 119 | "single_side": False, 120 | "use_even": False, 121 | "flipped": False, 122 | "use_clamp": True, 123 | "mirror": False, 124 | "snap": False, 125 | "snap_target": 'CLOSEST', 126 | "snap_point": (0, 0, 0), 127 | "snap_align": False, 128 | "snap_normal": (0, 0, 0), 129 | "correct_uv": False, 130 | "release_confirm": False, 131 | "use_accurate": False 132 | } 133 | ) 134 | 135 | # space and search for flip normals 136 | bpy.ops.mesh.select_all(action='TOGGLE') 137 | bpy.ops.mesh.flip_normals() 138 | bpy.ops.object.modifier_add(type='SUBSURF') 139 | bpy.context.object.modifiers["Subdivision"].levels = 4 140 | 141 | # add the displacement modifiers 142 | bpy.ops.object.modifier_add(type='DISPLACE') 143 | bpy.ops.texture.new() 144 | bpy.data.textures["Texture"].type = 'STUCCI' 145 | bpy.data.textures["Texture"].noise_scale = 0.75 146 | bpy.context.object.modifiers["Displace"].direction = 'X' 147 | bpy.data.textures["Texture"].turbulence = 10 148 | bpy.context.object.modifiers["Displace"].texture = bpy.data.textures["Texture"] 149 | 150 | bpy.ops.object.modifier_add(type='DISPLACE') 151 | bpy.ops.texture.new() 152 | bpy.data.textures["Texture.001"].type = 'STUCCI' 153 | bpy.data.textures["Texture.001"].noise_scale = 0.75 154 | bpy.context.object.modifiers["Displace.001"].direction = 'Y' 155 | bpy.context.object.modifiers["Displace.001"].texture = bpy.data.textures["Texture.001"] 156 | 157 | bpy.ops.object.modifier_add(type='DISPLACE') 158 | bpy.ops.texture.new() 159 | bpy.data.textures["Texture.002"].type = 'CLOUDS' 160 | bpy.data.textures["Texture.002"].noise_scale = 0.65 161 | bpy.context.object.modifiers["Displace.002"].strength = 0.20 162 | bpy.context.object.modifiers["Displace.002"].texture = bpy.data.textures["Texture.002"] 163 | 164 | bpy.context.object.modifiers["Displace"].strength = 0.7 165 | bpy.context.object.modifiers["Displace.001"].strength = 0.6 166 | 167 | bpy.ops.object.mode_set(mode='OBJECT') 168 | 169 | 170 | if __name__ == "__main__": 171 | generate_maze() 172 | -------------------------------------------------------------------------------- /Blender_2_8/random_walk_open_with_walls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | 16 | ITERATIONS = 1000 17 | 18 | # Controls the distances that are moved 19 | # MUST BE AT LEAST 2 20 | X_MOVE_DISTANCE = 2.0 21 | Y_MOVE_DISTANCE = 2.0 22 | 23 | # Position in space 24 | y_pos = 0 25 | x_pos = 0 26 | 27 | visited = [] # walkable tile 28 | walls = [] 29 | 30 | 31 | def generate_maze(): 32 | for i in range(ITERATIONS): 33 | direction = get_random_direction() 34 | next_move(direction) 35 | build_walls() 36 | cleanup_mesh() 37 | 38 | 39 | def get_random_direction(): 40 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 41 | random_num = random.randint(0, 3) 42 | return direction[random_num] 43 | 44 | 45 | def next_move(direction): 46 | global y_pos 47 | global x_pos 48 | 49 | if direction == 'up': 50 | y_pos += Y_MOVE_DISTANCE 51 | 52 | if direction == 'right': 53 | x_pos += X_MOVE_DISTANCE 54 | 55 | if direction == 'down': 56 | y_pos -= Y_MOVE_DISTANCE 57 | 58 | if direction == 'left': 59 | x_pos -= X_MOVE_DISTANCE 60 | place_tile(x_pos, y_pos) 61 | visited.append((x_pos, y_pos)) 62 | 63 | 64 | def place_cube(x, y): 65 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x, y, 0)) 66 | 67 | 68 | def place_tile(x, y): 69 | bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, location=(x, y, -1)) 70 | 71 | 72 | def check_neighbors_and_place_wall(pos): 73 | offset_map = [(pos[0] - 2.0, pos[1]), 74 | (pos[0] + 2.0, pos[1]), 75 | (pos[0], pos[1] - 2.0), 76 | (pos[0], pos[1] + 2.0)] 77 | for offset_tuple in offset_map: 78 | if offset_tuple not in visited and offset_map not in walls: 79 | place_cube(offset_tuple[0], offset_tuple[1]) 80 | walls.append((offset_tuple[0], offset_tuple[1])) 81 | 82 | 83 | def build_walls(): 84 | for position in visited: 85 | check_neighbors_and_place_wall(position) 86 | 87 | 88 | # joins all separate objects into a single object and delete duplicate verts 89 | def cleanup_mesh(): 90 | # get all the cubes selected 91 | bpy.ops.object.select_all(action='SELECT') 92 | # join all of the separate cube objects into one 93 | bpy.ops.object.join() 94 | # jump into edit mode 95 | bpy.ops.object.editmode_toggle() 96 | # select all verts 97 | bpy.ops.mesh.select_all(action='SELECT') 98 | # remove overlapping verts 99 | bpy.ops.mesh.remove_doubles() 100 | # get back out of edit mode 101 | bpy.ops.object.editmode_toggle() 102 | 103 | 104 | if __name__ == "__main__": 105 | generate_maze() 106 | -------------------------------------------------------------------------------- /Blender_2_8/random_walk_via_cube_placement.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | # Controls the distances that are moved 20 | # MUST BE AT LEAST 2 21 | X_MOVE_DISTANCE = 2.0 22 | Y_MOVE_DISTANCE = 2.0 23 | 24 | # Position in space 25 | y_pos = 0 26 | x_pos = 0 27 | 28 | 29 | def generate_maze(): 30 | for i in range(ITERATIONS): 31 | direction = get_random_direction() 32 | next_move(direction) 33 | cleanup_mesh() 34 | 35 | 36 | def get_random_direction(): 37 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 38 | random_num = random.randint(0, 3) 39 | return direction[random_num] 40 | 41 | 42 | def next_move(direction): 43 | global y_pos 44 | global x_pos 45 | 46 | if direction == 'up': 47 | y_pos += Y_MOVE_DISTANCE 48 | place_cube() 49 | 50 | if direction == 'right': 51 | x_pos += X_MOVE_DISTANCE 52 | place_cube() 53 | 54 | if direction == 'down': 55 | y_pos -= Y_MOVE_DISTANCE 56 | place_cube() 57 | 58 | if direction == 'left': 59 | x_pos -= X_MOVE_DISTANCE 60 | place_cube() 61 | 62 | 63 | def place_cube(): 64 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x_pos, y_pos, 0)) 65 | 66 | 67 | # joins all separate objects into a single object, 68 | # deletes inner faces to make the object hollow 69 | def cleanup_mesh(): 70 | # get all the cubes selected 71 | bpy.ops.object.select_all(action='TOGGLE') 72 | bpy.ops.object.select_all(action='TOGGLE') 73 | # join all of the separate cube objects into one 74 | bpy.ops.object.join() 75 | # jump into edit mode 76 | bpy.ops.object.editmode_toggle() 77 | # get save the mesh data into a variable 78 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 79 | # remove overlapping verts 80 | bpy.ops.mesh.remove_doubles() 81 | # de-select everything in edit mode 82 | bpy.ops.mesh.select_all(action='TOGGLE') 83 | # select the "interior faces" 84 | bpy.ops.mesh.select_interior_faces() 85 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 86 | for f in mesh.faces: 87 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 88 | f.select = False 89 | # delete all still selected faces, leaving a hollow mesh behind 90 | bpy.ops.mesh.delete(type='FACE') 91 | # get back out of edit mode 92 | bpy.ops.object.editmode_toggle() 93 | 94 | 95 | if __name__ == "__main__": 96 | generate_maze() 97 | -------------------------------------------------------------------------------- /Blender_2_8/random_walk_via_extrusion.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a random walk algorithm to generate levels in Blender. 6 | The process is via extrusion to preserve the mesh’s interior faces. 7 | The interior walls property needs to be viewed with Xray view. 8 | 9 | Example of 3D model output 10 | https://sketchfab.com/models/a04f59e37966449c98c2839999800c8a 11 | ''' 12 | 13 | import random 14 | import bpy 15 | import bmesh 16 | 17 | ITERATIONS = 1000 18 | 19 | current = None 20 | next_face = None 21 | 22 | # Position in space 23 | y_pos = 1.0 24 | x_pos = 1.0 25 | 26 | # Controls the distances that are moved 27 | # MUST BE AT LEAST 2 28 | x_move_distance = 2.0 29 | y_move_distance = 2.0 30 | 31 | # For keeping track of all of the moves that have been made 32 | visited_list = [] 33 | 34 | # make sure an object called 'Cube' is present in the scene, else add one 35 | if bpy.data.objects.get('Cube'): 36 | ob = bpy.data.objects['Cube'] 37 | bpy.ops.object.mode_set(mode='EDIT') 38 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 39 | else: 40 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(0, 0, 0)) 41 | ob = bpy.data.objects['Cube'] 42 | bpy.ops.object.mode_set(mode='EDIT') 43 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 44 | 45 | 46 | def main(): 47 | # Ensure that no faces are currently selected 48 | for f in mesh.faces: 49 | f.select = False 50 | 51 | for i in range(ITERATIONS): 52 | direction = get_random_direction() 53 | next_mesh_move(direction) 54 | visited_list.append((x_pos, y_pos)) 55 | cleanup() 56 | 57 | 58 | def get_random_direction(): 59 | direction = {0: 'up', 1: 'right', 2: 'down', 3: 'left'} 60 | random_num = random.randint(0, 3) 61 | return direction[random_num] 62 | 63 | 64 | def next_mesh_move(direction): 65 | global next_face 66 | global y_pos 67 | global x_pos 68 | for f in mesh.faces: 69 | f.select = False 70 | 71 | if direction == 'up': 72 | if (x_pos, y_pos + y_move_distance) not in visited_list: 73 | for f in mesh.faces: 74 | # Using face.calc_center_median and a walking through an array of x,y moves 75 | # we can calculate roughly which square we should appear on. 76 | # We can then select and set the right face to start the extrusion process from 77 | if f.normal.y == 1.0 and \ 78 | round(f.calc_center_median()[1]) == y_pos and \ 79 | round(f.calc_center_median()[0]) == x_pos - 1: 80 | f.select = True 81 | next_face = f 82 | y_pos += y_move_distance 83 | extrude_up() 84 | else: 85 | y_pos += y_move_distance 86 | 87 | if direction == 'right': 88 | if (x_pos + x_move_distance, y_pos) not in visited_list: 89 | for f in mesh.faces: 90 | if f.normal.x == 1.0 and \ 91 | round(f.calc_center_median()[0]) == x_pos and \ 92 | round(f.calc_center_median()[1]) == y_pos - 1: 93 | f.select = True 94 | next_face = f 95 | x_pos += x_move_distance 96 | extrude_right() 97 | else: 98 | x_pos += x_move_distance 99 | 100 | if direction == 'down': 101 | if (x_pos, y_pos - y_move_distance) not in visited_list: 102 | for f in mesh.faces: 103 | if f.normal.y == -1.0 and \ 104 | round(f.calc_center_median()[1]) == y_pos - y_move_distance and \ 105 | round(f.calc_center_median()[0]) == x_pos - 1: 106 | f.select = True 107 | next_face = f 108 | y_pos -= y_move_distance 109 | extrude_down() 110 | else: 111 | y_pos -= y_move_distance 112 | 113 | if direction == 'left': 114 | if (x_pos - x_move_distance, y_pos) not in visited_list: 115 | for f in mesh.faces: 116 | if f.normal.x == -1.0 and \ 117 | round(f.calc_center_median()[0]) == x_pos - x_move_distance and \ 118 | round(f.calc_center_median()[1]) == y_pos - 1: 119 | f.select = True 120 | next_face = f 121 | x_pos -= x_move_distance 122 | extrude_left() 123 | else: 124 | x_pos -= x_move_distance 125 | 126 | 127 | def extrude_up(): 128 | extrude(0, y_move_distance, 0) 129 | 130 | 131 | def extrude_right(): 132 | extrude(x_move_distance, 0.0, 0) 133 | 134 | 135 | def extrude_down(): 136 | extrude(0, -y_move_distance, 0) 137 | 138 | 139 | def extrude_left(): 140 | extrude(-x_move_distance, 0, 0) 141 | 142 | 143 | def extrude(x, y, z): 144 | bpy.ops.mesh.extrude_region_move() 145 | bpy.ops.transform.translate(value=(x, y, z), 146 | orient_type='GLOBAL', 147 | orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 148 | orient_matrix_type='GLOBAL', 149 | constraint_axis=(False, False, True), 150 | mirror=True, 151 | use_proportional_edit=False, 152 | proportional_edit_falloff='SMOOTH', 153 | proportional_size=1, 154 | use_proportional_connected=False, 155 | use_proportional_projected=False 156 | ) 157 | 158 | 159 | 160 | def cleanup(): 161 | bpy.ops.object.mode_set(mode='EDIT') 162 | bpy.ops.mesh.select_all(action='SELECT') 163 | bpy.ops.mesh.remove_doubles() 164 | bpy.ops.object.mode_set(mode='OBJECT') 165 | 166 | 167 | if __name__ == "__main__": 168 | main() 169 | -------------------------------------------------------------------------------- /Blender_2_8/recursive_backtracking_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a recursive backtracking algorithm to procedurally generate 3D levels in Blender. 6 | 7 | Example 3D model output 8 | https://sketchfab.com/models/7437daa03a0543d48c5eb599681d7e07 9 | ''' 10 | import math 11 | import random 12 | import bpy 13 | import bmesh 14 | 15 | # total size of the maze to be created eg 10x10 16 | cols = 10 17 | rows = 10 18 | 19 | # global variables for keeping track of the grid and cell_stack states during execution 20 | cell_array = [] # keeps a flat list of all cells 21 | cell_stack = [] # keeps stack of the order cells have been visited 22 | direction_stack = [] # for keeping track of all of the moves that have been made 23 | 24 | current_cell = None 25 | next_face = None 26 | 27 | # Position in space 28 | y_pos = 1.0 29 | x_pos = 1.0 30 | 31 | # Controls the distances that are moved 32 | x_move_distance = 2.0 33 | y_move_distance = 2.0 34 | 35 | 36 | # make sure an object called 'Cube' is present in the scene, else add one 37 | if bpy.data.objects.get('Cube'): 38 | ob = bpy.data.objects['Cube'] 39 | bpy.ops.object.mode_set(mode='EDIT') 40 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 41 | else: 42 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(0, 0, 0)) 43 | ob = bpy.data.objects['Cube'] 44 | bpy.ops.object.mode_set(mode='EDIT') 45 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 46 | 47 | 48 | def setup(): 49 | global current_cell 50 | # create a 2D array of cells inside of the cell_array variable 51 | for y in range(rows): 52 | for x in range(cols): 53 | cell = Cell(x, y) 54 | cell_array.append(cell) 55 | # set the current position to the first cell in the cell_array 56 | current_cell = cell_array[0] 57 | generate_level() 58 | 59 | 60 | def generate_level(): 61 | # flag for tracking state of when the maze is finished 62 | done = False 63 | 64 | # make sure that no faces are selected 65 | for f in mesh.faces: 66 | f.select = False 67 | 68 | # enter a long running loop that can only be exited when certain criteria are met 69 | while True: 70 | global cell_array 71 | global current_cell 72 | global cell_stack 73 | global next_cell 74 | 75 | current_cell.visited = True 76 | 77 | # determine if the maze can move forward from the current cell to a yet unreached neighboring cell 78 | next_cell, direction = current_cell.check_neighbors() 79 | if next_cell: # if an unvisited neighbor cell was found to move to 80 | next_mesh_move(direction) 81 | next_cell.visited = True 82 | 83 | # keep track of the directions the maze has gone using the cell_stack 84 | cell_stack.append(current_cell) 85 | 86 | # sets the current_cell cell to the next cell 87 | current_cell = next_cell 88 | 89 | elif len(cell_stack) > 0: # if there was not an unvisited cell to move to from the current cell 90 | current_cell = cell_stack.pop() 91 | move_back() 92 | 93 | for cell in cell_array: 94 | if cell.visited is False: 95 | done = False 96 | break 97 | else: 98 | done = True 99 | if done: 100 | break 101 | 102 | 103 | class Cell: 104 | def __init__(self, x, y): 105 | self.x = x # cell's row position 106 | self.y = y # cell's column position 107 | self.visited = False 108 | 109 | def check_neighbors(self): 110 | global cell_array 111 | neighbors = [] # keep track of the neighboring cells 112 | unvisited_directions = {} # keep track of the relative direction of unvisited neighboring cells 113 | direction_key = 0 # keeps track of the number of directions available of unvisited cells 114 | # for storing each individual neighbor cell, if applicable 115 | up = None 116 | right = None 117 | down = None 118 | left = None 119 | 120 | # check each direction to determine if neighboring cells exist on the cell_array 121 | if index(self.x, self.y - 1): 122 | up = cell_array[index(self.x, self.y - 1)] 123 | if index(self.x + 1, self.y): 124 | right = cell_array[index(self.x + 1, self.y)] 125 | if index(self.x, self.y + 1): 126 | down = cell_array[index(self.x, self.y + 1)] 127 | if index(self.x - 1, self.y): 128 | left = cell_array[index(self.x - 1, self.y)] 129 | 130 | # if the cell has a neighbor in a particular direction then check if that neighbor has been visited yet 131 | # if it has not, store 132 | if up and not up.visited: 133 | neighbors.append(up) 134 | unvisited_directions[direction_key] = 'up' 135 | direction_key += 1 136 | if right and not right.visited: 137 | neighbors.append(right) 138 | unvisited_directions[direction_key] = 'right' 139 | direction_key += 1 140 | if down and not down.visited: 141 | neighbors.append(down) 142 | unvisited_directions[direction_key] = 'down' 143 | direction_key += 1 144 | if left and not left.visited: 145 | neighbors.append(left) 146 | unvisited_directions[direction_key] = 'left' 147 | 148 | if len(neighbors) > 0: 149 | # randomly return the direction of an unvisited neighbor cell 150 | r = int(math.floor(random.uniform(0, len(neighbors) - .000001))) 151 | return neighbors[r], unvisited_directions[r] 152 | else: 153 | return None, None 154 | 155 | 156 | def index(x, y): 157 | if x < 0 or y < 0 or x > cols - 1 or y > rows - 1: 158 | return None 159 | else: 160 | return x + y * cols 161 | 162 | 163 | def next_mesh_move(direction): 164 | global next_face 165 | global y_pos 166 | global x_pos 167 | for f in mesh.faces: 168 | f.select = False 169 | 170 | if direction == 'up': 171 | for f in mesh.faces: 172 | if f.normal.y == 1.0 and \ 173 | round(f.calc_center_median()[1]) == y_pos and \ 174 | round(f.calc_center_median()[0]) == x_pos - 1: 175 | f.select = True 176 | next_face = f 177 | y_pos += y_move_distance * 2 178 | direction_stack.append(direction) 179 | extrude_up() 180 | extrude_up() 181 | 182 | if direction == 'right': 183 | for f in mesh.faces: 184 | if f.normal.x == 1.0 and \ 185 | round(f.calc_center_median()[0]) == x_pos and \ 186 | round(f.calc_center_median()[1]) == y_pos - 1: 187 | f.select = True 188 | next_face = f 189 | x_pos += x_move_distance * 2 190 | direction_stack.append(direction) 191 | extrude_right() 192 | extrude_right() 193 | 194 | if direction == 'down': 195 | for f in mesh.faces: 196 | if f.normal.y == -1.0 and \ 197 | round(f.calc_center_median()[1]) == y_pos - y_move_distance and \ 198 | round(f.calc_center_median()[0]) == x_pos - 1: 199 | f.select = True 200 | next_face = f 201 | y_pos -= y_move_distance * 2 202 | direction_stack.append(direction) 203 | extrude_down() 204 | extrude_down() 205 | 206 | if direction == 'left': 207 | for f in mesh.faces: 208 | if f.normal.x == -1.0 and \ 209 | round(f.calc_center_median()[0]) == x_pos - x_move_distance and \ 210 | round(f.calc_center_median()[1]) == y_pos - 1: 211 | f.select = True 212 | next_face = f 213 | x_pos -= x_move_distance * 2 214 | direction_stack.append(direction) 215 | extrude_left() 216 | extrude_left() 217 | 218 | 219 | # Used to keep track of the position in space being moved through 220 | def move_back(): 221 | global y_pos 222 | global x_pos 223 | global direction_stack 224 | direction = direction_stack.pop() 225 | if direction == 'up': 226 | y_pos -= y_move_distance * 2 227 | if direction == 'right': 228 | x_pos -= x_move_distance * 2 229 | if direction == 'down': 230 | y_pos += y_move_distance * 2 231 | if direction == 'left': 232 | x_pos += x_move_distance * 2 233 | 234 | 235 | def extrude_up(): 236 | extrude(0, y_move_distance, 0) 237 | 238 | 239 | def extrude_right(): 240 | extrude(x_move_distance, 0.0, 0) 241 | 242 | 243 | def extrude_down(): 244 | extrude(0, -y_move_distance, 0) 245 | 246 | 247 | def extrude_left(): 248 | extrude(-x_move_distance, 0, 0) 249 | 250 | 251 | def extrude(x, y, z): 252 | bpy.ops.mesh.extrude_region_move() 253 | bpy.ops.transform.translate(value=(x, y, z), 254 | orient_type='GLOBAL', 255 | orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 256 | orient_matrix_type='GLOBAL', 257 | constraint_axis=(False, False, True), 258 | mirror=True, 259 | use_proportional_edit=False, 260 | proportional_edit_falloff='SMOOTH', 261 | proportional_size=1, 262 | use_proportional_connected=False, 263 | use_proportional_projected=False 264 | ) 265 | 266 | 267 | if __name__ == "__main__": 268 | setup() 269 | -------------------------------------------------------------------------------- /Blender_2_8/recursive_division_maze.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Aaron J. Olson 3 | https://aaronjolson.io 4 | 5 | Implementation of a recursive division algorithm to generate levels in Blender. 6 | 7 | Example of 3D model output 8 | https://sketchfab.com/models/8587b464b2034485886cd094c7adf14c 9 | ''' 10 | 11 | import math 12 | import random 13 | import bpy 14 | import bmesh 15 | 16 | SIZE = 49 17 | MIN_SIZE = SIZE / 4 18 | 19 | 20 | def generate_level(size): 21 | # deletes everything in the scene, allows for a simple and clean run 22 | clear_scene() 23 | # initialize the map matrix and store it in the level_map variable 24 | level_map = new_map(size, 0) 25 | # build out the map, these two functions do all of the procedural work 26 | add_inner_walls(level_map, 1, 1, size - 2, size - 2) 27 | add_outer_walls(level_map, SIZE) 28 | # populate the scene with cubes according to the map matrix 29 | add_cubes(level_map) 30 | # combines the cubes into one object and removes interior faces 31 | cleanup_mesh() 32 | 33 | 34 | # generate 2d array with list comprehension 35 | def new_map(size, value): 36 | return [[value for i in range(size)] for j in range(size)] 37 | 38 | 39 | # returns random number between min max inclusive 40 | def random_number(minimum, maximum): 41 | return math.floor(random.random() * (maximum - minimum + 1)) + minimum 42 | 43 | 44 | def add_outer_walls(level_map, size): 45 | for i in range(size): 46 | if i == 0 or i == size - 1: 47 | for j in range(size): 48 | level_map[i][j] = 1 49 | else: 50 | level_map[i][0] = 1 51 | level_map[i][size - 1] = 1 52 | 53 | 54 | def add_inner_walls(level_map, rmin, cmin, rmax, cmax): 55 | width = cmax - cmin 56 | height = rmax - rmin 57 | 58 | # stop recursing once room size is reached 59 | if width < MIN_SIZE or height < MIN_SIZE: 60 | return level_map 61 | 62 | # determine whether to build vertical or horizontal wall 63 | if width > height: 64 | is_vertical = True 65 | else: 66 | is_vertical = False 67 | 68 | if is_vertical: 69 | # randomize location of vertical wall 70 | col = math.floor(random_number(cmin, cmax) / 2) * 2 71 | build_wall(level_map, is_vertical, rmin, rmax, col) 72 | # recurse to the two newly divided boxes 73 | add_inner_walls(level_map, rmin, cmin, rmax, col - 1) 74 | add_inner_walls(level_map, rmin, col + 1, rmax, cmax) 75 | else: 76 | row = math.floor(random_number(rmin, rmax) / 2) * 2 77 | build_wall(level_map, is_vertical, cmin, cmax, row) 78 | add_inner_walls(level_map, rmin, cmin, row - 1, cmax) 79 | add_inner_walls(level_map, row + 1, cmin, rmax, cmax) 80 | 81 | 82 | def build_wall(level_map, is_vertical, minimum, maximum, loc): 83 | hole = math.floor(random_number(minimum, maximum) / 2) * 2 + 1 84 | for i in range(minimum, maximum + 1): 85 | if is_vertical: 86 | if i == hole: 87 | level_map[loc][i] = 0 88 | else: 89 | level_map[loc][i] = 1 90 | else: 91 | if i == hole: 92 | level_map[i][loc] = 0 93 | else: 94 | level_map[i][loc] = 1 95 | 96 | 97 | # adds cubes to the map based on the level_map matrix 98 | def add_cubes(level_map): 99 | y = 0 100 | for row in level_map: 101 | x = 0 102 | for value in row: 103 | if value == 0: # cells with value 0 get cubes placed on them 104 | bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, location=(x, y, 0)) 105 | x += 2 106 | y += 2 107 | 108 | 109 | # combines the cubes into one object and removes interior faces 110 | def cleanup_mesh(): 111 | # select all of the cubes 112 | bpy.ops.object.select_all(action='SELECT') 113 | # join all of the separate cube objects into one 114 | bpy.ops.object.join() 115 | # jump into edit mode 116 | bpy.ops.object.editmode_toggle() 117 | # get save the mesh data into a variable 118 | mesh = bmesh.from_edit_mesh(bpy.context.object.data) 119 | # remove overlapping verts 120 | bpy.ops.mesh.remove_doubles() 121 | # de-select everything in edit mode 122 | bpy.ops.mesh.select_all(action='DESELECT') 123 | # select the "interior faces" 124 | bpy.ops.mesh.select_interior_faces() 125 | # loop through and de-select all of the "floor" and "ceiling" faces by checking normals 126 | for f in mesh.faces: 127 | if f.normal[2] == 1.0 or f.normal[2] == -1.0: 128 | f.select = False 129 | # delete all still selected faces, leaving a hollow mesh behind 130 | bpy.ops.mesh.delete(type='FACE') 131 | 132 | 133 | # delete everything in the scene 134 | def clear_scene(): 135 | bpy.ops.object.select_all(action='SELECT') 136 | bpy.ops.object.delete(use_global=False) 137 | 138 | 139 | if __name__ == "__main__": 140 | generate_level(SIZE) 141 | -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/ambientOcclusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/ambientOcclusion.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/ambientOcclusionRough.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/ambientOcclusionRough.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse2.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/diffuse3.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/height.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/height.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/normal.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Castle Brick 01/2k JPG 12 MB/roughness.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/ambientOcclusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/ambientOcclusion.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse2.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/diffuse3.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/height.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/height.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/normal.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/roughness.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/specular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/LilySurface/texturehaven/Cobblestone Floor 07/2k JPG 9.6 MB/specular.jpg -------------------------------------------------------------------------------- /Blender_2_8/resources/resources.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/resources.blend -------------------------------------------------------------------------------- /Blender_2_8/resources/resources.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronjolson/Blender-Python-Procedural-Level-Generation/d155832026425efcc5491d251a2dbd7be19752a9/Blender_2_8/resources/resources.blend1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aaron J. Olson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender-Python-Procedural-Level-Generation 2 | Collection of scripts for generating 3D levels (environments/dungeons) procedurally. Leverages Blender's Python API. Currently a work in progress. 3 | 4 | ## Instructions to run 5 | 1. Open a new instance of the Blender program 6 | 2. Navigate to the **scripting** screen https://docs.blender.org/manual/en/latest/interface/window_system/screens.html 7 | 3. Import the script via the *Open* button or click the *New* button and paste the contents of a script into the script editor 8 | 4. Click the *Run Script* button 9 | 10 | For an introduction to Blender scripting with Python 11 | https://docs.blender.org/manual/en/latest/advanced/scripting/introduction.html 12 | 13 | ## Special notes about versioning 14 | The scripts in 2.7 will not work in 2.8 but the scripts in 2.8 continue to work in 2.9+ 15 | If at some point in the future breaking API changes are introduced a new folder will be added for the scripts to be ported 16 | 17 | ## 3D model output examples 18 | random walk via cube placement example 3D model output 19 | https://sketchfab.com/models/97ef663c8f6040b8aecdaca2aa87989e 20 | 21 | random walk via extrusion example 3D model output 22 | https://sketchfab.com/models/a04f59e37966449c98c2839999800c8a 23 | 24 | recursive backtracking algorithm example 3D model output 25 | https://sketchfab.com/models/7437daa03a0543d48c5eb599681d7e07 26 | 27 | recursive division algorithm example 3D model output 28 | https://sketchfab.com/models/8587b464b2034485886cd094c7adf14c 29 | 30 | cellular automata / game of life algorithm example 3D model output 31 | https://sketchfab.com/3d-models/low-poly-procedural-cave-maze-environment-2b0ad5a175b5419ea560fa595e53e9f4 32 | 33 | castle dungeon game level example output 34 | https://sketchfab.com/3d-models/procedurally-generated-level-with-wall-open-top-ca5adebd3fae4597b9d1cedfb81d742f - open 35 | https://sketchfab.com/3d-models/procedurally-generated-level-a3fc50945d13465e81d23e75417bc20f - closed --------------------------------------------------------------------------------