├── README.md ├── dataTypesClasses ├── RoomTypes.py ├── __pycache__ │ ├── RoomTypes.cpython-38.pyc │ └── buildingType.cpython-38.pyc └── buildingType.py ├── exampleImages ├── img1.png └── img2.png ├── functions ├── __pycache__ │ ├── inputReader.cpython-38.pyc │ ├── miscFunctions.cpython-38.pyc │ ├── modellingFunctions.cpython-38.pyc │ └── statisticFunctions.cpython-38.pyc ├── inputReader.py ├── miscFunctions.py ├── modellingFunctions.py └── statisticFunctions.py ├── generateDataForStatistics.py ├── main_generateBuilding.py ├── methodsOfGeneration ├── __pycache__ │ ├── methodGrid.cpython-38.pyc │ └── methodTreemaps.cpython-38.pyc ├── gridRelatedClasses │ ├── Grid.py │ ├── Room.py │ ├── TileWalker.py │ └── __pycache__ │ │ ├── Grid.cpython-38.pyc │ │ ├── Room.cpython-38.pyc │ │ └── TileWalker.cpython-38.pyc ├── methodGrid.py ├── methodTreemaps.py └── treemapsRelated │ ├── __pycache__ │ └── squarifiedTreemaps.cpython-38.pyc │ └── squarifiedTreemaps.py ├── panelGUI.py └── project.blend /README.md: -------------------------------------------------------------------------------- 1 | # Procedural generation of building models 2 | Implementation of two methods of procedural building models generation: grid placement and squarified treemaps. The main aim of the project is to create logical connections between rooms of specified sizes and applications inside a given floor plan. Blender add-on written in python allows to parametrize, generate and display those plane in visually appealing 3D models. 3 | 4 | ## Example images: 5 | Examples of building models generated with the application: 6 | - ![example image 1](https://github.com/wojtryb/proceduralBuildingGenerator/tree/master/exampleImages/img1.png?raw=true) 7 | - ![example image 2](https://github.com/wojtryb/proceduralBuildingGenerator/tree/master/exampleImages/img2.png?raw=true) 8 | 9 | ## Requirements: 10 | Add-on was developed with **blender 2.82** and **python 3.8.5**. Using newer versions may require tweaking the differences in blender API. 11 | 12 | ## Installation: 13 | - download the .zip file and extract it 14 | - open the **project.blend** file with blender. 15 | - in scripting tab, run the three files: main_generateBuilding, generateDataForStatistics and panelGUI. These files are saved internally in blend file, but their original forms are available in the project directory. 16 | -use GUI on the **"World Properties"** dock on the right (red "Earth" icon) to parametrize the generator and push the **"Generate new house"** button to start the generator. 17 | 18 | ## Methods: 19 | - **grid placement** uses a 2D grid that describes a floor plan. Every room of the building gets its initial position according to its desired size and relations with other rooms already placed on the grid. After initialization, the rooms start to grow, competing for the available space. 20 | - **squarified treemaps** is a classical building generation technique allowing to recursively divide a rectangular building plan into specified list of rooms. 21 | 22 | ## 3D model generation: 23 | regardless of the generation method used, the plan can be modelled into a full 3D model. Blender API is used to perform the whole process from extruding walls, through cutting doors and windows, to generating and setting materials. 24 | 25 | ## Licence: 26 | The project is part of a master thesis in computer science. Please don't sell or redistribute. Use for educational purposes only. 27 | -------------------------------------------------------------------------------- /dataTypesClasses/RoomTypes.py: -------------------------------------------------------------------------------- 1 | from methodsOfGeneration.gridRelatedClasses.Room import room 2 | 3 | class diningroom(room): 4 | def __init__(self, size = 7): 5 | wantedNeighbours = [bedroom, toilet, kitchen, bathroom] 6 | super().__init__(name = "diningroom", size = size, wantedNeighbours = wantedNeighbours, outsideConnection = True) 7 | 8 | class bedroom(room): 9 | def __init__(self, size = 3): 10 | wantedNeighbours = [diningroom] 11 | super().__init__(name = "bedroom", size = size, wantedNeighbours = wantedNeighbours) 12 | 13 | class bathroom(room): 14 | def __init__(self, size = 3): 15 | wantedNeighbours = [diningroom, kitchen] 16 | super().__init__(name = "bathroom", size = size, wantedNeighbours = wantedNeighbours) 17 | 18 | class toilet(room): 19 | def __init__(self, size = 2): 20 | wantedNeighbours = [bedroom, diningroom] 21 | super().__init__(name = "toilet", size = size, wantedNeighbours = wantedNeighbours) 22 | 23 | class kitchen(room): 24 | def __init__(self, size = 3): 25 | wantedNeighbours = [diningroom, pantry, bathroom] 26 | super().__init__(name = "kitchen", size = size, wantedNeighbours = wantedNeighbours, outsideConnection = True) 27 | 28 | class pantry(room): 29 | def __init__(self, size = 1): 30 | wantedNeighbours = [kitchen] 31 | super().__init__(name = "pantry", size = size, wantedNeighbours = wantedNeighbours) -------------------------------------------------------------------------------- /dataTypesClasses/__pycache__/RoomTypes.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/dataTypesClasses/__pycache__/RoomTypes.cpython-38.pyc -------------------------------------------------------------------------------- /dataTypesClasses/__pycache__/buildingType.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/dataTypesClasses/__pycache__/buildingType.cpython-38.pyc -------------------------------------------------------------------------------- /dataTypesClasses/buildingType.py: -------------------------------------------------------------------------------- 1 | from functions.miscFunctions import addZero 2 | 3 | class buildingType: 4 | def __init__(self, width, length, rooms): 5 | self.width = width 6 | self.length = length 7 | self.rooms = rooms 8 | 9 | def getName(self): 10 | return (addZero(str(self.width)) + 'x' + addZero(str(self.length)) + '_' + str(self.rooms)) 11 | 12 | buildings = [ 13 | buildingType(4, 6, 3), 14 | buildingType(6, 8, 3), 15 | buildingType(6, 8, 4), 16 | buildingType(10, 12, 4), 17 | buildingType(10, 12, 5), 18 | buildingType(12, 14, 6), 19 | buildingType(12, 14, 7), 20 | buildingType(12, 14, 8), 21 | buildingType(14, 18, 8), 22 | buildingType(16, 20, 8)] -------------------------------------------------------------------------------- /exampleImages/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/exampleImages/img1.png -------------------------------------------------------------------------------- /exampleImages/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/exampleImages/img2.png -------------------------------------------------------------------------------- /functions/__pycache__/inputReader.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/functions/__pycache__/inputReader.cpython-38.pyc -------------------------------------------------------------------------------- /functions/__pycache__/miscFunctions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/functions/__pycache__/miscFunctions.cpython-38.pyc -------------------------------------------------------------------------------- /functions/__pycache__/modellingFunctions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/functions/__pycache__/modellingFunctions.cpython-38.pyc -------------------------------------------------------------------------------- /functions/__pycache__/statisticFunctions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/functions/__pycache__/statisticFunctions.cpython-38.pyc -------------------------------------------------------------------------------- /functions/inputReader.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Operator 3 | from bpy.props import FloatVectorProperty 4 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 5 | 6 | from math import sqrt 7 | import random 8 | 9 | from functions.miscFunctions import readInput 10 | 11 | class inputReader: 12 | def __init__(self, method = None, roomsAmount = None, fieldSize = None, sizeX = None, sizeY = None, indent = None, indentValueX = None, indentValueY = None, records = None, dynamicFieldSize = False ,getGlobalValues = False): 13 | if not getGlobalValues: 14 | self.method = method 15 | self.roomsAmount = roomsAmount 16 | self.dynamicFieldSize = dynamicFieldSize 17 | self.fieldSize = fieldSize 18 | self.sizeX = sizeX 19 | self.sizeY = sizeY 20 | self.indent = None 21 | if indent: 22 | self.indent = True 23 | 24 | self.indentValueX = indentValueX 25 | self.indentValueY = indentValueY 26 | self.records = records 27 | else: 28 | self.method = bpy.context.scene.my_tool.my_enum 29 | self.roomsAmount = readInput(bpy.context.scene.roomsAmount, onlyInt = True) 30 | self.dynamicFieldSize = bpy.context.scene.dynamicFieldSize 31 | self.fieldSize = readInput(bpy.context.scene.fieldSize, onlyInt = False) 32 | self.sizeX = readInput(bpy.context.scene.sizeX, onlyInt = True) 33 | self.sizeY = readInput(bpy.context.scene.sizeY, onlyInt = True) 34 | self.indent = None 35 | if bpy.context.scene.allowIndent: 36 | self.indent = True 37 | self.indentValueX = readInput(bpy.context.scene.indentValueX, onlyInt = False) 38 | self.indentValueY = readInput(bpy.context.scene.indentValueY, onlyInt = False) 39 | self.records = readInput(bpy.context.scene.records, onlyInt = True) 40 | 41 | #further calculation 42 | if self.method == 'M1': self.method = True 43 | else: self.method = False 44 | 45 | self.indentFieldsX = int(self.indentValueX/self.fieldSize) 46 | self.indentValueX = int(self.indentValueX/self.fieldSize)*self.fieldSize 47 | 48 | self.indentFieldsY = int(self.indentValueY/self.fieldSize) 49 | self.indentValueY = int(self.indentValueY/self.fieldSize)*self.fieldSize 50 | 51 | #not needed in data export 52 | self.indentOffsetX = readInput(bpy.context.scene.indentOffsetX, onlyInt = False) 53 | self.indentOffsetX = int(self.indentOffsetX/self.fieldSize) 54 | self.indentOffsetY = readInput(bpy.context.scene.indentOffsetY, onlyInt = False) 55 | self.indentOffsetY = int(self.indentOffsetY/self.fieldSize) 56 | self.indentProbability = readInput(bpy.context.scene.indentProbability, onlyInt = False) 57 | 58 | self.height = readInput(bpy.context.scene.height, onlyInt = False) 59 | self.wallsThickness = readInput(bpy.context.scene.wallsThickness, onlyInt = False) 60 | self.doorPosition = readInput(bpy.context.scene.doorPosition, onlyInt = False) 61 | 62 | if self.indent: 63 | self.indent = (self.indentFieldsX, self.indentFieldsY, self.indentOffsetX, self.indentOffsetY) 64 | if random.random() > self.indentProbability: 65 | self.indent = None 66 | 67 | if self.dynamicFieldSize: 68 | #overwrite the given field size 69 | self.countDynamicGridSize(time = 0.5) 70 | self.gridSizeX = int(self.sizeX/self.fieldSize) 71 | self.gridSizeY = int(self.sizeY/self.fieldSize) 72 | 73 | def countDynamicGridSize(self, time): 74 | def findFieldsAmount(rooms, time): 75 | a = 1.8050335467626258e-07 76 | b = -0.0006210710068291847 77 | c = -0.04235601986738292 78 | d = 0.00019351610903631125 79 | e = 0.20384065326193784 80 | 81 | A = a 82 | B = b + d*rooms 83 | C = -time + c*rooms + e 84 | delta = B**2 - 4*A*C 85 | print(delta) 86 | x1 = (-B + sqrt(delta))/(2*a) 87 | x2 = (-B - sqrt(delta))/(2*a) 88 | 89 | print(x1,x2) 90 | 91 | return int(max(x1, x2)) 92 | 93 | amountOfFields = findFieldsAmount(self.roomsAmount, time) 94 | buildingArea = self.sizeX * self.sizeY 95 | fieldPerSquareMeter = amountOfFields / buildingArea 96 | self.fieldSize = 1/sqrt(fieldPerSquareMeter) -------------------------------------------------------------------------------- /functions/miscFunctions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | from random import randint, uniform, seed 4 | import os 5 | 6 | def LT(pos): 7 | return (pos[0]-1, pos[1]-1) 8 | def LM(pos): # 9 | return (pos[0], pos[1]-1) 10 | def LB(pos): 11 | return (pos[0]+1, pos[1]-1) 12 | def MT(pos): # 13 | return (pos[0]-1, pos[1]) 14 | def MM(pos): # 15 | return (pos[0], pos[1]) 16 | def MB(pos): # 17 | return (pos[0]+1, pos[1]) 18 | def RT(pos): 19 | return (pos[0]-1, pos[1]+1) 20 | def RM(pos): # 21 | return (pos[0], pos[1]+1) 22 | def RB(pos): 23 | return (pos[0]+1, pos[1]+1) 24 | 25 | directions4List = [MT, LM, RM, MB] 26 | directions8List = [LT, MT, RT, LM, RM, LB, MB, RB] 27 | directions9List = [LT, MT, RT, LM, MM, RM, LB, MB, RB] 28 | 29 | #displays 2D plan representation using OpenCV 30 | def printResult(Grid, Walker = None, currentIsLShaped = None, corner = None): 31 | 32 | scale = 50 33 | image = np.zeros((Grid.gridSize[1] * scale ,Grid.gridSize[0] * scale ,3), np.uint8) 34 | colors = [(200,150,150), (150,200,100), (150,100,200), (100,60,60), (60,100,60), (60,60,100), (30,30,100), (30,100,30), (100,30,30), (150,100,50)] 35 | 36 | for x in range(Grid.gridSize[0]): 37 | for y in range(Grid.gridSize[1]): 38 | if Grid.grid[x,y] == None: 39 | cv2.rectangle(image, (x*scale, y*scale), ((x+1)*scale, (y+1)*scale),(256,256,256) ,-1) 40 | elif Grid.grid[x,y] == -1: 41 | cv2.rectangle(image, (x*scale, y*scale), ((x+1)*scale, (y+1)*scale),(220,220,220) ,-1) 42 | else: cv2.rectangle(image, (x*scale, y*scale), ((x+1)*scale, (y+1)*scale),colors[Grid.grid[x,y]] ,-1) 43 | 44 | for x in range(1, Grid.gridSize[0]): 45 | for y in range(1, Grid.gridSize[1]): 46 | p = (x * scale,y * scale) 47 | cv2.line(image, p, p, (0,0,0), 3) 48 | 49 | if Walker != None: 50 | cv2.rectangle(image, (Walker.position[0]*scale, Walker.position[1]*scale), ((Walker.position[0]+1)*scale, (Walker.position[1]+1)*scale),(0,0,0) ,-1) 51 | if currentIsLShaped != None: 52 | cv2.rectangle(image, (Walker.lookBottomRightCorner()[0]*scale, Walker.lookBottomRightCorner()[1]*scale), ((Walker.lookBottomRightCorner()[0]+1)*scale, (Walker.lookBottomRightCorner()[1]+1)*scale), (0,255,0) ,-1) 53 | 54 | if corner != None: 55 | point = (int(corner[0] * scale), int(corner[1] * scale)) 56 | cv2.line(image, point, point, (255,0,0),6) 57 | 58 | font = cv2.FONT_HERSHEY_SIMPLEX 59 | fontScale = 0.6 60 | fontColor = (0,0,0) 61 | lineType = 2 62 | for Room in Grid.roomsInGrid: 63 | pos = Room.findStartPos(Grid) 64 | pos = (pos[0] * scale, int((pos[1]+0.5) * scale)) 65 | cv2.putText(image, 66 | Room.name, 67 | pos, 68 | font, 69 | fontScale, 70 | fontColor, 71 | lineType) 72 | # cv2.imwrite('plan.png', image) 73 | cv2.imshow('Output', image) 74 | cv2.waitKey(0) 75 | cv2.destroyAllWindows() 76 | 77 | #reading value from blender textbox. Allows specifying int and float values 78 | # and picking random number from specified range. 79 | def readInput(inputString, onlyInt = True): 80 | def convert(stri, onlyInt): 81 | if onlyInt: 82 | return int(stri) 83 | else: 84 | return float(stri) 85 | 86 | def checkIfConvertable(stri, onlyInt): 87 | 88 | if len(stri) == 0: 89 | return False 90 | if stri[0] == "." or stri[-1] == ".": 91 | return False 92 | 93 | if onlyInt: 94 | dot = True 95 | else: 96 | dot = False 97 | 98 | for character in stri: 99 | if character == ".": 100 | if dot == False: 101 | dot = True 102 | else: 103 | return False 104 | elif not ("0" <= character <= "9"): 105 | return False 106 | return convert(stri, onlyInt) 107 | 108 | split = inputString.split("-") 109 | if len(split) == 1: 110 | number = split[0] 111 | ret = checkIfConvertable(number, onlyInt) 112 | return ret 113 | elif len(split) == 2: 114 | number = split[0] 115 | low = checkIfConvertable(number, onlyInt) 116 | number = split[1] 117 | high = checkIfConvertable(number, onlyInt) 118 | if low != False and high != False: 119 | low = convert(low, onlyInt) 120 | high = convert(high, onlyInt) 121 | if low < high: 122 | if onlyInt: 123 | return randint(low, high) 124 | else: 125 | return round(uniform(low, high), 2) 126 | return False 127 | 128 | #creates a directory in a system 129 | def makeDirectory(path, name): 130 | directory = os.path.join(path, name) 131 | if not os.path.exists(directory): 132 | os.makedirs(directory) 133 | return directory 134 | 135 | #used to add zeros to a number string 136 | def addZero(number): 137 | number = str(number) 138 | if len(number) == 1: number = '0' + number 139 | return number -------------------------------------------------------------------------------- /functions/modellingFunctions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | #all the functions that are used in modelling: creating, selecting, deleting objects 5 | #getting edges info, creating edges lists and discarding elements from them 6 | #handling modifiers and materials 7 | 8 | #--------------basic operations-------------# 9 | #assigns blender polygons to Room objects 10 | def updateRoomList(roomList, object): 11 | for i, Room in enumerate(roomList): 12 | Room.blenderObject = object.data.polygons[i] 13 | 14 | #returns reference of blender object by given name 15 | def getObject(name = "Plan"): 16 | obj = [obj for obj in bpy.context.scene.objects if obj.name.startswith(name)] 17 | if len(obj) > 0: return obj[0] 18 | else: return None 19 | 20 | #deletes the object 21 | def deleteObject(name): 22 | bpy.ops.object.mode_set(mode='OBJECT') 23 | toDelete = getObject(name) 24 | if toDelete != None: 25 | bpy.ops.object.select_all(action='DESELECT') 26 | toDelete.select_set(True) 27 | bpy.ops.object.delete() 28 | 29 | #creates new object 30 | def newObject(name): 31 | deleteObject(name) 32 | mesh = bpy.data.meshes.new(name) 33 | obj = bpy.data.objects.new(name, mesh) 34 | bpy.context.scene.collection.objects.link(obj) 35 | bpy.context.view_layer.objects.active = obj 36 | return obj 37 | 38 | #ensures the object will be the only selected one in edit mode 39 | def changeActiveObjectEdit(objectToActivate): 40 | bpy.context.view_layer.objects.active = objectToActivate 41 | bpy.ops.object.mode_set(mode='EDIT') 42 | bpy.ops.object.mode_set(mode='OBJECT') 43 | bpy.ops.object.mode_set(mode='EDIT') 44 | bpy.ops.mesh.select_all(action='DESELECT') 45 | 46 | #splits one object 47 | def separateSelectedOrByMaterial(object, newObjectName, materialName = None): 48 | bpy.ops.object.mode_set(mode='EDIT') 49 | if materialName != None: 50 | bpy.ops.mesh.select_all(action='DESELECT') 51 | object.active_material_index = getMaterialIndex(materialName, object) 52 | bpy.ops.object.material_slot_select() 53 | bpy.ops.mesh.separate(type='SELECTED') 54 | for object in bpy.context.selected_objects: 55 | object.name = newObjectName 56 | return getObject(newObjectName) 57 | 58 | #joins two objects 59 | def joinTwoObjects(name1, name2, resultName): 60 | bpy.data.objects[name1].select_set(True) 61 | bpy.data.objects[name2].select_set(True) 62 | bpy.ops.object.join() 63 | for object in bpy.context.selected_objects: #rename 64 | object.name = resultName 65 | return getObject(resultName) 66 | 67 | #-----------edge manipulation-----------# 68 | 69 | #true if the edge points have constant height 70 | def isEdgeHorizontal(edge, object): 71 | vert1 = object.data.vertices[edge[0]].co 72 | vert2 = object.data.vertices[edge[1]].co 73 | if abs(vert1[0] - vert2[0]) > abs(vert1[1] - vert2[1]): return True 74 | else: return False 75 | 76 | #returns center of the edge 77 | def edgeCenter(edge, object, inp, position = 0.5, checkEnds = True): 78 | positionInv = 1 - position 79 | vert1 = object.data.vertices[edge[0]].co.copy() 80 | vert2 = object.data.vertices[edge[1]].co.copy() 81 | 82 | if checkEnds: 83 | if isEdgeHorizontal(edge, object): 84 | if vert1[0] < vert2[0]: 85 | vert1[0] += inp.wallsThickness/2 + 0.5 86 | vert2[0] -= inp.wallsThickness/2 + 0.5 87 | else: 88 | vert1[0] -= inp.wallsThickness/2 + 0.5 89 | vert2[0] += inp.wallsThickness/2 + 0.5 90 | else: 91 | if vert1[1] < vert2[1]: 92 | vert1[1] += inp.wallsThickness/2 + 0.5 93 | vert2[1] -= inp.wallsThickness/2 + 0.5 94 | else: 95 | vert1[1] -= inp.wallsThickness/2 + 0.5 96 | vert2[1] += inp.wallsThickness/2 + 0.5 97 | 98 | centerX = (vert1[0] * position + vert2[0] * positionInv) 99 | centerY = (vert1[1] * position + vert2[1] * positionInv) 100 | centerZ = (vert1[2] * position + vert2[2] * positionInv) 101 | 102 | center = (centerX, centerY, centerZ) 103 | return center 104 | 105 | #returns edge lenght 106 | def countEdgeLength(object, edge): 107 | vert1 = object.data.vertices[edge[0]].co 108 | vert2 = object.data.vertices[edge[1]].co 109 | 110 | length = 0 111 | for i in range(3): 112 | length += (vert1[i] - vert2[i])**2 113 | return math.sqrt(length) 114 | 115 | #returns a list with all the edges in building plan 116 | def createEdgesDataList(obj, roomList, inp): 117 | edgesData = [] 118 | for edge in obj.data.edge_keys: 119 | rooms = [] 120 | for Room in roomList: 121 | if edge in Room.blenderObject.edge_keys: 122 | if countEdgeLength(obj, edge)-inp.wallsThickness-1 >= 0: 123 | rooms.append(Room) 124 | edgesData.append([edge, rooms]) 125 | return edgesData 126 | 127 | #----------edges: doors related-----------# 128 | #removes the edges from a list that don't need a door 129 | def removeNoDoorsEdges(edgesData): 130 | doorEdgesData = [] 131 | for edgeData in edgesData: 132 | if len(edgeData[1]) == 2: 133 | room = edgeData[1][0] 134 | neighbour = edgeData[1][1] 135 | if type(neighbour) in room.wantedNeighbours: 136 | doorEdgesData.append(edgeData) 137 | return doorEdgesData 138 | 139 | #returns a list of only the longest edges from each group 140 | def pickLongestEdgeFromEachGroup(obj, groups, edgesData): 141 | doorEdgesData = [] 142 | for group in groups: 143 | longestEdge = group[0] 144 | maxLength = countEdgeLength(obj, longestEdge[0]) 145 | for edge in group: 146 | length = countEdgeLength(obj, edge[0]) 147 | if length > maxLength: 148 | longestEdge = edge 149 | maxLength = length 150 | doorEdgesData.append(longestEdge) 151 | return doorEdgesData 152 | 153 | #groups the rooms 154 | def groupBy(lis, keyIndex): 155 | listCopy = lis.copy() 156 | groups = [] 157 | while(len(listCopy) > 0): 158 | group = [] 159 | element = listCopy[0] 160 | key = element[keyIndex] 161 | 162 | group.append(element) 163 | listCopy.pop(0) 164 | 165 | for i, element in enumerate(listCopy): 166 | if element[1] == key: 167 | group.append(element) 168 | listCopy.pop(i) 169 | groups.append(group) 170 | return groups 171 | 172 | #finds all the islands (groups of rooms connected with each other) 173 | def findAllIslands(roomList, doorEdgesData): 174 | def findOneIsland(roomList, doorEdgesData): 175 | roomsVisited = [] 176 | roomsToCheck = [roomList[0]] 177 | 178 | while len(roomsToCheck) > 0: 179 | currentRoom = roomsToCheck[0] 180 | roomsToCheck.pop(0) 181 | 182 | for edgeData in doorEdgesData: 183 | rooms = edgeData[1] 184 | if currentRoom in rooms: 185 | if currentRoom == rooms[0]: neighbour = rooms[1] 186 | else: neighbour = rooms[0] 187 | if not neighbour in roomsVisited: 188 | roomsToCheck.append(neighbour) 189 | roomsVisited.append(currentRoom) 190 | 191 | roomsLeft = [] 192 | for Room in roomList: 193 | if not Room in roomsVisited: 194 | roomsLeft.append(Room) 195 | return roomsVisited, roomsLeft 196 | 197 | roomsLeft = roomList 198 | islands = [] 199 | while(len(roomsLeft) > 0): 200 | island, roomsLeft = findOneIsland(roomsLeft, doorEdgesData) 201 | islands.append(island) 202 | return islands 203 | 204 | #adds the doors to merge unconnected islands 205 | def mergeAllIslands(obj, islands, doorEdgesData, edgesData): 206 | def mergeIslands(obj, island1, island2, edgesData): 207 | candidates = [] 208 | for edgeData in edgesData: 209 | if len(edgeData[1]) == 2: 210 | room1 = edgeData[1][0] 211 | room2 = edgeData[1][1] 212 | if room1 in island1 and room2 in island2 \ 213 | or room1 in island2 and room2 in island1: 214 | candidates.append(edgeData) 215 | if len(candidates) > 0: 216 | bestEdgeData = pickLongestEdgeFromEachGroup(obj, [candidates], edgesData) 217 | bestEdgeData = bestEdgeData[0] 218 | return bestEdgeData 219 | else: 220 | print("cannot merge islands! Critical situation") 221 | return None 222 | 223 | while len(islands) > 1: 224 | edgeData = mergeIslands(obj, islands[0], islands[1], edgesData) 225 | if edgeData != None: 226 | doorEdgesData.append(edgeData) 227 | islandsTemp = [islands[0] + islands[1]] 228 | islandsTemp.extend(islands[2:]) 229 | islands = islandsTemp 230 | 231 | #---------edges: windows related--------# 232 | #leaves only the edges of the buildings, removing internal edges between rooms 233 | def removeInnerEdges(edgesData): 234 | outerEdgesData = [] 235 | for edgeData in edgesData: 236 | if len(edgeData[1]) == 1: 237 | outerEdgesData.append(edgeData) 238 | return outerEdgesData 239 | 240 | #----------------modelling--------------# 241 | #adds a plane that can be extruded to form a door 242 | def addDoorPlaneToActiveObject(obj, inp, edgeData, doorEdgesData, position): 243 | bpy.ops.object.mode_set(mode='EDIT') 244 | bpy.ops.mesh.primitive_plane_add(enter_editmode=False, size = 1, location=(edgeCenter(edgeData[0], obj, inp, position))) 245 | if isEdgeHorizontal(edgeData[0], obj): 246 | bpy.ops.transform.resize(value=(1, inp.wallsThickness+0.1, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 247 | else: 248 | bpy.ops.transform.resize(value=(inp.wallsThickness+0.1, 1, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 249 | 250 | #adds a plane that can be extruded to form a window 251 | def addWindowPlaneToActiveObject(obj, inp, edgeData, position, windows): 252 | bpy.ops.mesh.primitive_plane_add(enter_editmode=False, size = 1, location=(edgeCenter(edgeData[0], obj, inp, position, False))) 253 | if isEdgeHorizontal(edgeData[0], obj): 254 | bpy.ops.transform.resize(value=(1.5, inp.wallsThickness+0.1, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 255 | bpy.ops.mesh.duplicate_move(MESH_OT_duplicate={"mode":1}, TRANSFORM_OT_translate={"value":(0, 0, 0), "orient_type":'GLOBAL', "orient_matrix":((0, 0, 0), (0, 0, 0), (0, 0, 0)), "orient_matrix_type":'GLOBAL', "constraint_axis":(False, False, False), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 256 | bpy.ops.transform.resize(value=(1, 0.1, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 257 | else: 258 | bpy.ops.transform.resize(value=(inp.wallsThickness+0.1, 1.5, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 259 | bpy.ops.mesh.duplicate_move(MESH_OT_duplicate={"mode":1}, TRANSFORM_OT_translate={"value":(0, 0, 0), "orient_type":'GLOBAL', "orient_matrix":((0, 0, 0), (0, 0, 0), (0, 0, 0)), "orient_matrix_type":'GLOBAL', "constraint_axis":(False, False, False), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 260 | bpy.ops.transform.resize(value=(0.1, 1, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 261 | setMaterialToActive("glass", windows) 262 | 263 | #----------------modifiers-----------------# 264 | #creates and applies a boolean modifier that cuts a mesh in a building 265 | def cutObjectInPlan(name, objToCut, obj): 266 | bpy.ops.object.mode_set(mode='OBJECT') 267 | bpy.context.view_layer.objects.active = bpy.data.objects['Plan'] 268 | bpy.context.space_data.context = 'MODIFIER' 269 | bpy.ops.object.modifier_add(type='BOOLEAN') 270 | objToCut = getObject(name) 271 | obj.modifiers["Boolean"].object = objToCut 272 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean") 273 | bpy.ops.object.select_all(action='DESELECT') 274 | 275 | #adds volume to the wooden details on a floor 276 | def addWireframeModifier(strips, thickness, offset): 277 | bpy.ops.object.modifier_add(type='WIREFRAME') 278 | strips.modifiers["Wireframe"].thickness = thickness 279 | strips.modifiers["Wireframe"].offset = offset 280 | bpy.ops.object.mode_set(mode='OBJECT') 281 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Wireframe") 282 | 283 | #--------------materials--------------# 284 | #gets material slot index from passing material name 285 | def getMaterialIndex(materialName, obj): 286 | material = bpy.data.materials.get(materialName) 287 | for i, objectMaterial in enumerate(obj.data.materials): 288 | if material == objectMaterial: return i 289 | return None 290 | 291 | #sets a material of a given name to the active polygons 292 | def setMaterialToActive(materialName, obj): 293 | material = bpy.data.materials.get(materialName) 294 | bpy.ops.object.mode_set(mode='EDIT') 295 | bpy.context.tool_settings.mesh_select_mode = [False, False, True] 296 | 297 | i = getMaterialIndex(materialName, obj) 298 | if i == None: 299 | obj.data.materials.append(material) 300 | i = len(obj.data.materials) - 1 301 | obj.active_material_index = i 302 | bpy.ops.object.material_slot_assign() 303 | 304 | #sets a meterial specified in the Room object 305 | def setCorrespondingMaterial(Room, obj): 306 | bpy.ops.object.mode_set(mode='EDIT') 307 | bpy.ops.mesh.select_all(action='DESELECT') 308 | bpy.ops.object.mode_set(mode='OBJECT') 309 | Room.blenderObject.select = True 310 | bpy.ops.object.mode_set(mode='EDIT') 311 | setMaterialToActive(Room.name, obj) 312 | -------------------------------------------------------------------------------- /functions/statisticFunctions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mathutils import Vector 3 | import math 4 | from copy import copy 5 | 6 | from methodsOfGeneration.gridRelatedClasses.Room import room 7 | 8 | def countRoomsWithoutWindows(roomList): 9 | roomsWithoutWindow = 0 10 | for Room in roomList: 11 | if Room.amountOfWindows == 0: roomsWithoutWindow += 1 12 | return roomsWithoutWindow 13 | 14 | def countRoomRatioStatistics(obj, roomList): 15 | areaRatioList = [] 16 | dimRatioList = [] 17 | for Room in roomList: 18 | minX = np.inf 19 | minY = np.inf 20 | maxX = - np.inf 21 | maxY = - np.inf 22 | for vertId in Room.blenderObject.vertices: 23 | x = obj.data.vertices[vertId].co[0] 24 | y = obj.data.vertices[vertId].co[1] 25 | if x < minX: minX = x 26 | if x > maxX: maxX = x 27 | if y < minY: minY = y 28 | if y > maxY: maxY = y 29 | width = maxX - minX 30 | height = maxY - minY 31 | boundingArea = width * height 32 | dimRatio = min(width, height) / max(width, height) 33 | areaRatio = round(Room.blenderObject.area / boundingArea, 6) 34 | 35 | dimRatioList.append(dimRatio) 36 | areaRatioList.append(areaRatio) 37 | return areaRatioList, dimRatioList 38 | 39 | def countDistancesBetweenRooms(inp, roomList, doorEdgesData, outerEdgesData): 40 | def floyd(distance): 41 | #floyd alghoritm counts distance between each 2 rooms 42 | for middle in roomList: 43 | for begin in roomList: 44 | for end in roomList: 45 | if distance[begin.ID][end.ID] > distance[begin.ID][middle.ID] + distance[middle.ID][end.ID]: 46 | distance[begin.ID][end.ID] = distance[begin.ID][middle.ID] + distance[middle.ID][end.ID] 47 | return distance 48 | 49 | #floyd initialization with distances between those directly connected with doors 50 | distance = np.full((inp.roomsAmount+1, inp.roomsAmount+1), np.inf) 51 | for i in range(inp.roomsAmount+1): distance[i][i] = 0 52 | for edgeData in doorEdgesData: #between rooms 53 | rooms = edgeData[1] 54 | directDist1 = rooms[0].blenderObject.center - Vector(edgeData[2]) 55 | directDist1 = math.sqrt(directDist1.x ** 2 + directDist1.y ** 2) 56 | directDist2 = rooms[1].blenderObject.center - Vector(edgeData[2]) 57 | directDist2 = math.sqrt(directDist2.x ** 2 + directDist2.y ** 2) 58 | distance[rooms[0].ID][rooms[1].ID] = distance[rooms[1].ID][rooms[0].ID] = directDist1 + directDist2 59 | 60 | for edgeData in outerEdgesData: #connected with outside 61 | if len(edgeData) == 3: #door, not window 62 | Room = edgeData[1][0] 63 | directDist = Room.blenderObject.center - Vector(edgeData[2]) 64 | directDist = math.sqrt(directDist.x ** 2 + directDist.y ** 2) 65 | distance[Room.ID][inp.roomsAmount] = distance[inp.roomsAmount][Room.ID] = directDist # no checks as there can be only one outside door per room 66 | 67 | #using floyd alghoritm on a plan 68 | distanceInside = floyd(copy(distance[:-1, :-1])) 69 | roomList.append(room(forceRoomID = inp.roomsAmount)) 70 | distanceOutside = list(floyd(copy(distance))[-1, :-1]) 71 | roomList.pop(-1) 72 | 73 | rearrangedDistanceInside = [] 74 | for x in range(0, len(distanceInside)-1): 75 | for y in range(x+1, len(distanceInside)): 76 | rearrangedDistanceInside.append(distanceInside[x,y]) 77 | distanceInside = rearrangedDistanceInside 78 | 79 | return distanceInside, distanceOutside 80 | 81 | def countAreaErrors(inp, roomList): 82 | areaErrorList = [] 83 | for Room in roomList: 84 | Room.countPerfectArea(inp.fieldSize) 85 | areaError = min(Room.perfectArea, Room.blenderObject.area) / max(Room.perfectArea, Room.blenderObject.area) 86 | areaErrorList.append(areaError) 87 | return areaErrorList -------------------------------------------------------------------------------- /generateDataForStatistics.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Operator 3 | from bpy.props import FloatVectorProperty 4 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 5 | 6 | from main_generateBuilding import generateBuildingAction 7 | from functions.miscFunctions import readInput, makeDirectory, addZero 8 | from functions.inputReader import inputReader 9 | from dataTypesClasses.buildingType import buildings as BUILDINGS 10 | 11 | import csv 12 | import random 13 | import numpy as np 14 | import os 15 | import statistics 16 | 17 | def generateAndWriteToFile(blender, context, inp, directory): 18 | 19 | xString = addZero(inp.gridSizeX) 20 | yString = addZero(inp.gridSizeY) 21 | 22 | xStringM = addZero(inp.sizeX) 23 | yStringM = addZero(inp.sizeY) 24 | 25 | filename = xString + 'x' + yString + '_' + xStringM + 'mx' + yStringM + 'm_' + str(inp.roomsAmount) 26 | if inp.method: 27 | directory = makeDirectory(directory, 'grid') 28 | else: directory = makeDirectory(directory, 'treemaps') 29 | 30 | directory = os.path.join(directory, filename + '.csv') 31 | with open(directory, mode='w') as buildingData: 32 | allData = [] 33 | csvWriter = csv.writer(buildingData, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) 34 | 35 | if inp.method: 36 | csvWriter.writerow(['method', "grid placement"]) 37 | csvWriter.writerow(['building size [m x m, f x f]', inp.sizeX, inp.sizeY, inp.gridSizeX, inp.gridSizeY]) 38 | csvWriter.writerow(['amount of rooms []', inp.roomsAmount]) 39 | csvWriter.writerow(['field size [m]', inp.fieldSize]) 40 | if not inp.indent: 41 | csvWriter.writerow(['indent', 'None']) 42 | else: 43 | csvWriter.writerow(['indent[m x m, f x f]', inp.indentValueX, inp.indentValueY, inp.indentFieldsX, inp.indentFieldsY]) 44 | else: #inp.method == M2 45 | csvWriter.writerow(['method', "squarified treemaps"]) 46 | csvWriter.writerow(['building size [m x m]', inp.sizeX, inp.sizeY]) 47 | csvWriter.writerow(['amount of rooms []', inp.roomsAmount]) 48 | 49 | csvWriter.writerow([]) 50 | csvWriter.writerow([]) 51 | csvWriter.writerow(['liczba wysp', 'znormalizowana liczba wysp', 'wyspy bez połączenia', 'znormalizowana liczba pokoi bez okien', 'najdłuższy dystans wewnątrz[d]', 'średni dystans wewnątrz[d]', 'najdłuższy dystans na zewnątrz[d]', 'średni dystans na zewnątrz[d]', 'największa różnica pola', 'średnia różnica pola' , 'najgorsze proporcje', 'średnie proporcje', 'najgorszy współczynnik kształtu', 'średni współczynnik kształtu', 'czas[s]', "jakość działania"]) 52 | 53 | def addQualityToStats(stats): 54 | qualityWages = [0, 0.255, 0.42, 0.035, 0.04, 0.085, 0.025, 0.05, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01, 0.01] 55 | finalQuality = 0 56 | stats = list(stats) 57 | for i, stat in enumerate(stats): 58 | finalQuality += stat * qualityWages[i] 59 | stats.append(finalQuality) 60 | return stats 61 | 62 | if inp.method: #actual data generation 63 | for i in range(inp.records): 64 | data = generateBuildingAction(blender, context, inp, False) 65 | data = addQualityToStats(data) 66 | csvWriter.writerow(data) 67 | allData.append(data) 68 | 69 | #getting avarage and standard devitation row 70 | stats = [[] for i in range(len(allData[0]))] 71 | for record in allData: 72 | for i, stat in enumerate(record[:]): 73 | if stat != np.inf: 74 | stats[i].append(stat) 75 | 76 | meanStats = []; stdevStats = [] 77 | for i in range(len(stats)): 78 | meanStats.append(statistics.mean(stats[i])) 79 | if len(stats[i]) > 1: #normal situation 80 | stdevStats.append(statistics.stdev(stats[i], meanStats[i])) 81 | else: #all buildings were unmergable 82 | stdevStats.append(0) 83 | 84 | meanStats.pop() #remove quality and add calculate it for the average building 85 | meanStats = addQualityToStats(meanStats) 86 | 87 | csvWriter.writerow([]) 88 | csvWriter.writerow(stdevStats) 89 | csvWriter.writerow(meanStats) 90 | 91 | else: #inp.method == M2 92 | allData = generateBuildingAction(blender, context, inp, False) 93 | allData = addQualityToStats(allData) 94 | csvWriter.writerow([]) 95 | csvWriter.writerow(allData) 96 | 97 | return allData 98 | 99 | def readPath(): 100 | path = bpy.context.scene.writePath 101 | path = bpy.path.abspath(path) 102 | return path 103 | 104 | def createDataFromGUI(self, context): 105 | 106 | path = readPath() 107 | directory = makeDirectory(path, 'dataGUI') 108 | 109 | inp = inputReader(getGlobalValues = True) 110 | generateAndWriteToFile(self, context, inp, directory) 111 | 112 | def createPlannedData(self, context): 113 | path = self.readPath() 114 | RECORDS = readInput(bpy.context.scene.records, onlyInt = True) 115 | EXPERIMENT = readInput(bpy.context.scene.experiment, onlyInt = True) 116 | 117 | #seed for planned data 118 | SEED = readInput(bpy.context.scene.seed, onlyInt = True) 119 | if SEED != False: 120 | random.seed(SEED) 121 | 122 | if EXPERIMENT == 1: 123 | path = makeDirectory(path, 'gridSizeExperiment') 124 | fieldRange = list(np.arange(0.2, 0.8, 0.05)) + list(np.arange(0.8, 2.0, 0.1)) 125 | for rooms in range(3, 9): 126 | directory = makeDirectory(path, str(rooms)) 127 | for fieldSize in fieldRange: 128 | 129 | inp = inputReader('M1', rooms, fieldSize, 12,14, False, 0, 0, RECORDS) 130 | generateAndWriteToFile(self, context, inp, directory) 131 | 132 | elif EXPERIMENT == 2: 133 | path = makeDirectory(path, 'methodComparisonExperiment') 134 | for building in BUILDINGS: 135 | directory = makeDirectory(path, building.getName()) 136 | 137 | inp = inputReader('M1', building.rooms, 1, building.width ,building.length, False, 0, 0, RECORDS, True) 138 | generateAndWriteToFile(self, context, inp, directory) 139 | 140 | inp = inputReader('M2', building.rooms, 1, building.width ,building.length, False, 0, 0, RECORDS) 141 | generateAndWriteToFile(self, context, inp, directory) 142 | 143 | 144 | class OBJECT_OT_data(Operator, AddObjectHelper): 145 | """generate data""" 146 | bl_idname = "mesh.data" 147 | bl_label = "Generate data" 148 | bl_options = {'REGISTER', 'UNDO'} 149 | 150 | scale: FloatVectorProperty( 151 | name="scale", 152 | default=(1.0, 1.0, 1.0), 153 | subtype='TRANSLATION', 154 | description="scaling", 155 | ) 156 | 157 | def execute(self, context): 158 | createDataFromGUI(self, context) 159 | return {'FINISHED'} 160 | 161 | class OBJECT_OT_allData(Operator, AddObjectHelper): 162 | """generate all data""" 163 | bl_idname = "mesh.alldata" 164 | bl_label = "Generate all data" 165 | bl_options = {'REGISTER', 'UNDO'} 166 | 167 | scale: FloatVectorProperty( 168 | name="scale", 169 | default=(1.0, 1.0, 1.0), 170 | subtype='TRANSLATION', 171 | description="scaling", 172 | ) 173 | 174 | def execute(self, context): 175 | createPlannedData(self, context) 176 | return {'FINISHED'} 177 | 178 | def register(): 179 | bpy.utils.register_class(OBJECT_OT_data) 180 | bpy.utils.register_class(OBJECT_OT_allData) 181 | 182 | def unregister(): 183 | bpy.utils.unregister_class(OBJECT_OT_data) 184 | bpy.utils.unregister_class(OBJECT_OT_allData) 185 | 186 | if __name__ == "__main__": 187 | register() -------------------------------------------------------------------------------- /main_generateBuilding.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Procudural building models generator", 3 | "author": "Wojciech Trybus", 4 | "version": (1, 0), 5 | "blender": (2, 80, 0), 6 | "location": "View3D > Add > Mesh > Add Building", 7 | "description": "Creates a new building", 8 | "warning": "", 9 | "wiki_url": "", 10 | "category": "Add Mesh", 11 | } 12 | 13 | import bpy 14 | from bpy.types import Operator 15 | from bpy.props import FloatVectorProperty 16 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 17 | 18 | #python libraries 19 | import numpy as np 20 | import math 21 | import random 22 | import sys 23 | import os 24 | import csv 25 | from copy import copy 26 | from statistics import mean 27 | from time import time 28 | 29 | #allows to import code to blender 30 | dir = os.path.dirname(bpy.data.filepath) 31 | if not dir in sys.path: 32 | sys.path.append(dir) 33 | 34 | #self-written components 35 | from functions.miscFunctions import * #various helper functions 36 | from functions.modellingFunctions import * #functions used specifically in modelling 37 | from functions.statisticFunctions import * #functions that get statistic data about the output 38 | from functions.inputReader import inputReader #reads all the data from GUI 39 | 40 | from methodsOfGeneration.methodGrid import methodGrid #two plan generation methods 41 | from methodsOfGeneration.methodTreemaps import methodTreemaps 42 | 43 | from methodsOfGeneration.gridRelatedClasses.Grid import grid #2D grid containing a building plan 44 | from methodsOfGeneration.gridRelatedClasses.TileWalker import tileWalker #object used for moving across the grid 45 | 46 | from dataTypesClasses.RoomTypes import * #specific room types containing their name and default size 47 | 48 | #======================MAIN GENERATION FUNCTION=====================# 49 | def generateBuildingAction(self, context, inp = None, model3d = True): 50 | 51 | #picking the seed 52 | SEED = readInput(bpy.context.scene.seed, onlyInt = True) 53 | if SEED != False and model3d: #randomize seed only if called for full generation, not as part of experiment 54 | random.seed(SEED) 55 | 56 | #read input values from GUI if not specified from code (automatic generation) 57 | if not inp: 58 | inp = inputReader(getGlobalValues = True) 59 | 60 | #defining list of rooms in order of priority 61 | roomList = [diningroom(), bathroom(), bedroom(4), kitchen(), bedroom(), bedroom(), toilet(), pantry()] 62 | roomList = roomList[:inp.roomsAmount] 63 | 64 | #measuring time of building plan generation(statistics) 65 | startTime = time() 66 | if inp.method: 67 | verts, faces, _ = methodGrid(roomList, inp.roomsAmount, (inp.gridSizeX, inp.gridSizeY), inp.fieldSize, inp.indent) 68 | else: 69 | verts, faces = methodTreemaps(roomList, (inp.sizeX, inp.sizeY)) 70 | endTime = time() 71 | 72 | #replace the previously generated model - would need to be removed if addon 73 | #is used for generating multiple building models. 74 | deleteObject("Plan") 75 | 76 | #creating an object from generated vertices and faces data 77 | meshName = "Plan" 78 | mesh = bpy.data.meshes.new(meshName) 79 | mesh.from_pydata(verts, [], faces) 80 | if inp.method: mesh.flip_normals() 81 | obj = bpy.data.objects.new(meshName, mesh) 82 | bpy.context.scene.collection.objects.link(obj) 83 | bpy.context.view_layer.objects.active = obj 84 | 85 | #prepare for modelling - merge duplicated edges, translate model to world center 86 | bpy.ops.object.mode_set(mode='EDIT') 87 | bpy.ops.mesh.select_all(action='SELECT') 88 | bpy.ops.mesh.remove_doubles() 89 | bpy.ops.transform.translate(value=(-inp.sizeX/2, -inp.sizeY/2, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, True, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 90 | bpy.ops.mesh.select_all(action='DESELECT') 91 | bpy.ops.object.mode_set(mode='OBJECT') 92 | 93 | #load polygons from blender to room objects 94 | updateRoomList(roomList, obj) 95 | 96 | #set default and appropriate materials to each room polygon 97 | setMaterialToActive("default", obj) 98 | for Room in roomList: 99 | setCorrespondingMaterial(Room, obj) 100 | 101 | #--------inner doors position-----------# 102 | #get all mesh edges in a structure 103 | #edgesData structure: [edge, [Room1, Room2]] 104 | edgesData = createEdgesDataList(obj, roomList, inp) 105 | 106 | #remove those edges, that don't originally want to have doors 107 | doorEdgesData = removeNoDoorsEdges(edgesData) 108 | 109 | #removing duplicate doors (many walls between two rooms) 110 | groups = groupBy(doorEdgesData, 1) 111 | doorEdgesData = pickLongestEdgeFromEachGroup(obj, groups, edgesData) 112 | 113 | #find separate islands (island - all rooms connected with doors) and merge them on longest edge 114 | islands = findAllIslands(roomList, doorEdgesData) 115 | mergeAllIslands(obj, islands, doorEdgesData, edgesData) 116 | 117 | #--------inner doors 2D objects---------# 118 | doors = newObject("Doors") 119 | for edgeData in doorEdgesData: 120 | addDoorPlaneToActiveObject(obj, inp, edgeData, doorEdgesData, position = inp.doorPosition) 121 | edgeData.append(edgeCenter(edgeData[0], obj, inp, inp.doorPosition)) #for INFO - door position 122 | 123 | #-----outer walls: windows and exit doors---# 124 | outerEdgesData = removeInnerEdges(edgesData) 125 | 126 | windows = newObject("Windows") 127 | setMaterialToActive("default", windows) #default material, to difeerentiate from glass in windows object 128 | bpy.ops.object.mode_set(mode='EDIT') 129 | 130 | for edgeData in outerEdgesData: #getting windows position 131 | length = countEdgeLength(obj, edgeData[0]) 132 | windowsAmount = math.floor(length/4) 133 | if windowsAmount == 0 and length > 1.5: windowsAmount = 1 134 | edgeData[1][0].amountOfWindows += windowsAmount 135 | cellsAmount = windowsAmount*2 + 1 136 | cellWidth = length/cellsAmount 137 | 138 | for i in range(windowsAmount): 139 | pos = (i*2 + 1.5)*cellWidth/length 140 | if edgeData[1][0].outsideConnection: #wants outside doors and it's the first window/door 141 | changeActiveObjectEdit(doors) 142 | addDoorPlaneToActiveObject(obj, inp, edgeData, edgeData, pos) 143 | edgeData.append(edgeCenter(edgeData[0], obj, inp, pos)) # for INFO - exit door position 144 | changeActiveObjectEdit(windows) 145 | edgeData[1][0].outsideConnection = False #no more doors in this room 146 | edgeData[1][0].amountOfWindows -= 1 147 | else: #usual windows 148 | addWindowPlaneToActiveObject(obj, inp, edgeData, pos, windows) 149 | 150 | #at this point all the data (room shapes, window and door position) 151 | #is specified and ready for modelling. 152 | 153 | #============STATISTICS============# 154 | 155 | islandsAfter = findAllIslands(roomList, doorEdgesData) 156 | 157 | #count amount of rooms without windows 158 | roomsWithoutWindow = countRoomsWithoutWindows(roomList) 159 | 160 | #counting bounding box area of each room 161 | areaRatioList, dimRatioList = countRoomRatioStatistics(obj, roomList) 162 | 163 | #room distance between each other (last element is building's exit) 164 | distanceInside, distanceOutside = countDistancesBetweenRooms(inp, roomList, doorEdgesData, outerEdgesData) 165 | 166 | #how much the room areas differ from the specified values 167 | areaErrorList = countAreaErrors(inp, roomList) 168 | 169 | #find if some islands could not be merged (unseccesful generation) 170 | if len(islandsAfter) == 1: unmergeable = 0 171 | else: unmergeable = 1 172 | 173 | #diagonal of a building plan used in statistics to make indicators independent 174 | #from building size 175 | diagonal = math.sqrt(inp.sizeX ** 2 + inp.sizeY ** 2) 176 | 177 | #create a table with all statistics data 178 | retData = [len(islands), #amount of islands 179 | (len(islands)-1)/(inp.roomsAmount-1), #islands indicator (0-only one, 1-max) 180 | unmergeable, #were the islands mergeable 181 | 182 | roomsWithoutWindow / inp.roomsAmount, #what part of rooms don't have windows 183 | 184 | max(distanceInside)/diagonal, #longest distance between pair of rooms 185 | mean(distanceInside)/diagonal, #avarage distance between pair of rooms 186 | 187 | max(distanceOutside)/diagonal, #longest distance from room to exit 188 | mean(distanceOutside)/diagonal, #avarage distance from room to exit 189 | 190 | 1 - min(areaErrorList), #biggest difference between specified and actual room area 191 | 1 - mean(areaErrorList), #avarage distance between specified and actual room area 192 | 193 | 1 - min(dimRatioList), #room proportions the most different from 1:1 194 | 1 - mean(dimRatioList), #avarage room proportions 195 | 196 | 1 - min(areaRatioList), #the least part of bounding box filled 197 | 1 - mean(areaRatioList), #avarage part of bounding box filled 198 | 199 | endTime - startTime] #time of generation 200 | 201 | #don't model the house if generation is only for collection statistics 202 | if not model3d: 203 | return retData 204 | 205 | #================MODELLING==============# 206 | #modelling windows 207 | changeActiveObjectEdit(windows) 208 | bpy.ops.mesh.select_all(action='SELECT') 209 | 210 | #moving windows up, and extruding them 211 | bpy.ops.transform.translate(value=(0, 0, 0.9), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 212 | wallsUp = inp.height-0.8 213 | if wallsUp > 1.7: wallsUp = 1.7 214 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, wallsUp), "orient_type":'NORMAL', "orient_matrix":((0.5547, -0.83205, 0), (0.83205, 0.5547, -0), (0, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 215 | bpy.ops.mesh.select_all(action='DESELECT') 216 | 217 | #separating glass from windows 218 | deleteObject("Glass") #in case something was left at previous generation 219 | separateSelectedOrByMaterial(windows, "Glass", materialName = "glass") 220 | 221 | #selecting doors 222 | bpy.data.objects["Glass"].select_set(False) 223 | changeActiveObjectEdit(doors) 224 | bpy.ops.mesh.select_all(action='SELECT') 225 | 226 | #modelling doors 227 | bpy.ops.transform.translate(value=(0, 0, 0.4), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) 228 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, 2.), "orient_type":'NORMAL', "orient_matrix":((0.5547, -0.83205, 0), (0.83205, 0.5547, -0), (0, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 229 | bpy.ops.mesh.select_all(action='DESELECT') 230 | 231 | #modelling building plan 232 | bpy.ops.object.mode_set(mode='OBJECT') 233 | bpy.context.view_layer.objects.active = obj 234 | bpy.ops.object.mode_set(mode='EDIT') 235 | bpy.ops.mesh.select_all(action='SELECT') 236 | 237 | #two extrusions, as lower one will be made thicker later 238 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, 0.5), "orient_type":'NORMAL', "orient_matrix":((0, -1, 0), (1, 0, -0), (0, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 239 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, inp.height-0.5), "orient_type":'NORMAL', "orient_matrix":((0, -1, 0), (1, 0, -0), (0, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 240 | 241 | #color outer walls (those higher ones) 242 | bpy.ops.mesh.select_all(action='INVERT') 243 | setMaterialToActive("basic", obj) 244 | bpy.ops.mesh.select_all(action='INVERT') 245 | bpy.ops.mesh.select_more() 246 | 247 | #deselect all rooms, color higher part of outer walls 248 | for material in obj.data.materials: 249 | if material.name != "basic": 250 | obj.active_material_index = getMaterialIndex(material.name, obj) 251 | bpy.ops.object.material_slot_deselect() 252 | setMaterialToActive("outer walls", obj) 253 | 254 | #select all the rooms again (using materials) 255 | bpy.ops.mesh.select_all(action='SELECT') 256 | for material in obj.data.materials: 257 | if material.name == "basic" or material.name == "outer walls": 258 | obj.active_material_index = getMaterialIndex(material.name, obj) 259 | bpy.ops.object.material_slot_deselect() 260 | 261 | # inset walls between rooms, extrude them down, apply inner walls material 262 | bpy.ops.mesh.inset(thickness=inp.wallsThickness/2, depth=0, use_individual=True) 263 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, inp.height-0.2), "orient_type":'NORMAL', "orient_matrix":((1, -1.19209e-06, 3.99145e-10), (-1.19209e-06, -1, 2.13154e-09), (3.99142e-10, -2.13154e-09, -1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 264 | bpy.ops.mesh.select_all(action='INVERT') 265 | 266 | #apply default walls material. Rooms are not selected, have to deselect outer walls 267 | i = getMaterialIndex("outer walls", obj) 268 | obj.active_material_index = i 269 | bpy.ops.object.material_slot_deselect() 270 | setMaterialToActive("basic", obj) 271 | 272 | #floor strips 273 | bpy.ops.mesh.select_all(action='INVERT') #selection 274 | obj.active_material_index = getMaterialIndex("outer walls", obj) 275 | bpy.ops.object.material_slot_deselect() 276 | 277 | bpy.ops.mesh.duplicate_move(MESH_OT_duplicate={"mode":1}, TRANSFORM_OT_translate={"value":(0, -0, 0), "orient_type":'GLOBAL', "orient_matrix":((1, 0, 0), (0, 1, 0), (0, 0, 1)), "orient_matrix_type":'GLOBAL', "constraint_axis":(False, False, False), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 278 | strips = separateSelectedOrByMaterial(obj, "Strips") 279 | changeActiveObjectEdit(strips) 280 | bpy.ops.mesh.select_all(action='SELECT') 281 | setMaterialToActive("strips", strips) 282 | 283 | addWireframeModifier(strips, thickness = 0.13, offset = 0.43) 284 | obj = joinTwoObjects("Plan", "Strips", "Plan") 285 | 286 | #extrude outer walls 287 | bpy.ops.object.mode_set(mode='EDIT') 288 | bpy.ops.mesh.select_all(action='DESELECT') 289 | obj.active_material_index = i 290 | bpy.ops.object.material_slot_select() 291 | bpy.ops.mesh.extrude_region_shrink_fatten(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_shrink_fatten={"value":-inp.wallsThickness/2, "use_even_offset":False, "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "release_confirm":False, "use_accurate":False}) 292 | 293 | #add fancy ending on top - selecting topmost part of building 294 | bpy.ops.mesh.select_all(action='DESELECT') 295 | obj.active_material_index = getMaterialIndex("diningroom", obj) #select any room 296 | bpy.ops.object.material_slot_select() 297 | bpy.ops.mesh.select_similar(type='NORMAL', threshold=0.01) # select all faces with the same normal 298 | for material in obj.data.materials: # deselect all faces that are rooms 299 | if material.name != "basic": 300 | obj.active_material_index = getMaterialIndex(material.name, obj) 301 | bpy.ops.object.material_slot_deselect() 302 | 303 | #insetting those walls, applying material 304 | bpy.ops.mesh.inset(thickness=inp.wallsThickness/6, depth=0) 305 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, -0.0401709), "orient_type":'NORMAL', "orient_matrix":((-1, 7.06783e-08, 1.89e-10), (-7.06783e-08, -1, 1.33582e-17), (1.89e-10, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False}) 306 | bpy.ops.mesh.select_more() 307 | setMaterialToActive("outer walls", obj) 308 | 309 | #cutting doors and windows in building 310 | bpy.ops.object.mode_set(mode='OBJECT') 311 | bpy.ops.object.select_all(action='DESELECT') 312 | 313 | cutObjectInPlan("Doors", doors, obj) 314 | cutObjectInPlan("Windows", windows, obj) 315 | 316 | bpy.data.objects["Glass"].select_set(True) 317 | bpy.data.objects["Plan"].select_set(True) 318 | bpy.ops.object.join() #rename 319 | for object in bpy.context.selected_objects: 320 | object.name = "Plan" 321 | 322 | deleteObject("Doors") 323 | deleteObject("Windows") 324 | deleteObject("Glass") 325 | 326 | bpy.context.space_data.context = 'WORLD' 327 | 328 | #end of 3D model generation - returns data for statistics 329 | return retData 330 | 331 | #blender addon body 332 | class addObjectClass(Operator, AddObjectHelper): 333 | """Create a new Mesh Object""" 334 | bl_idname = "mesh.house" 335 | bl_label = "Generate new house" 336 | bl_options = {'REGISTER', 'UNDO'} 337 | 338 | scale: FloatVectorProperty( 339 | name="scale", 340 | default=(1.0, 1.0, 1.0), 341 | subtype='TRANSLATION', 342 | description="scaling", 343 | ) 344 | 345 | def execute(self, context): 346 | generateBuildingAction(self, context) 347 | return {'FINISHED'} 348 | 349 | # Registration 350 | def add_object_button(self, context): 351 | self.layout.operator( 352 | addObjectClass.bl_idname, 353 | text="house", 354 | icon='PLUGIN') 355 | 356 | # This allows you to right click on a button and link to documentation 357 | def add_object_manual_map(): 358 | url_manual_prefix = "https://docs.blender.org/manual/en/latest/" 359 | url_manual_mapping = ( 360 | ("bpy.ops.mesh.generateBuildingAction", "scene_layout/object/types.html"), 361 | ) 362 | return url_manual_prefix, url_manual_mapping 363 | 364 | def register(): 365 | 366 | bpy.utils.register_class(addObjectClass) 367 | bpy.utils.register_manual_map(add_object_manual_map) 368 | bpy.types.VIEW3D_MT_mesh_add.append(add_object_button) 369 | 370 | def unregister(): 371 | bpy.utils.unregister_class(addObjectClass) 372 | bpy.utils.unregister_manual_map(add_object_manual_map) 373 | bpy.types.VIEW3D_MT_mesh_add.remove(add_object_button) 374 | 375 | if __name__ == "__main__": 376 | register() -------------------------------------------------------------------------------- /methodsOfGeneration/__pycache__/methodGrid.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/__pycache__/methodGrid.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/__pycache__/methodTreemaps.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/__pycache__/methodTreemaps.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/Grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | 4 | from functions.miscFunctions import * 5 | from methodsOfGeneration.gridRelatedClasses.Room import room 6 | from methodsOfGeneration.gridRelatedClasses.TileWalker import tileWalker 7 | 8 | class grid: 9 | def __init__(self, gridSize = [12,16], roomsAmount = 5, indent = None): 10 | self.roomsAmount = roomsAmount 11 | self.gridSize = [gridSize[0] + 2, gridSize[1] + 2] #all the tiles on sides are reserved for ground 12 | 13 | self.grid = np.full(self.gridSize, None) 14 | self.grid[1:-1, 1:-1] = -1 15 | self.gridWeight = np.full(self.gridSize, 0) 16 | self.roomsInGrid = [] 17 | 18 | if indent != None: 19 | off = indent[2] 20 | off2 = indent[3] 21 | self.grid[1+off : indent[0]+1+off, 1+off2 : indent[1]+1+off2] = None 22 | 23 | for i in range(len(self.grid)): 24 | for j in range(len(self.grid[0])): 25 | if self.grid[i,j] == None: self.gridWeight[i,j] = 100 26 | 27 | self.area = np.count_nonzero(self.gridWeight == 0) 28 | 29 | def estimateAreaRoomsWant(self, roomList): 30 | self.areaRoomsWant = 0 31 | for Room in roomList: 32 | self.areaRoomsWant += Room.size ** 2 33 | 34 | def countDistanceFromWall(self, pos): 35 | gridTemp = np.zeros(self.gridSize) 36 | gridTemp[pos] = 1 37 | dist = 0 38 | positionsToCheck = [] 39 | 40 | if self.grid[pos] == None: 41 | return dist 42 | x = pos[0] 43 | y = pos[1] 44 | 45 | for fun in directions8List: 46 | positionsToCheck.append((fun(pos), fun, dist)) 47 | 48 | while(dist < max(self.gridSize)): 49 | current = positionsToCheck[0]; 50 | pos = current[0] 51 | fun = current[1] 52 | dist = current[2] + 1 53 | 54 | gridTemp[pos] = 1 55 | positionsToCheck.pop(0) 56 | if self.grid[pos] == None: 57 | return dist 58 | 59 | positionsToCheck.append((fun(pos), fun, dist)) 60 | 61 | def placeRoom(self, Room): 62 | #avoiding walls - bigger weight for close to walls 63 | tempGrid = np.copy(self.gridWeight) 64 | iterator = np.nditer(tempGrid, flags=['multi_index']) 65 | for field in iterator: 66 | pos = iterator.multi_index 67 | dist = self.countDistanceFromWall(pos) 68 | if (self.countDistanceFromWall(pos) < Room.distanceFromTheWall): 69 | tempGrid[pos] += Room.distanceFromTheWall - dist 70 | 71 | #bigger cost when far from wanted rooms 72 | for neighbourCandidate in self.roomsInGrid: 73 | if type(neighbourCandidate) in Room.wantedNeighbours: 74 | startPos = neighbourCandidate.occupiedIndices[0] 75 | 76 | lowX = max(0, startPos[0] - neighbourCandidate.distanceFromTheWall*2) 77 | highX = min(self.gridSize[0], startPos[0] + neighbourCandidate.distanceFromTheWall*2) 78 | 79 | lowY = max(0, startPos[1] - neighbourCandidate.distanceFromTheWall*2) 80 | highY = min(self.gridSize[1], startPos[1] + neighbourCandidate.distanceFromTheWall*2) 81 | 82 | tempGrid[lowX:highX, lowY:highY] -= 5 83 | tempGrid += 5 84 | 85 | #finding spot (in case of no places to put - allow worse weight) 86 | indicesTuples = [[]] 87 | idealWeigth = 0 88 | while (len(indicesTuples[0]) == 0): 89 | indicesTuples = np.where(tempGrid == idealWeigth) 90 | idealWeigth += 1 91 | 92 | indices = [] 93 | for i in range(len(indicesTuples[0])): 94 | indices.append((indicesTuples[0][i], indicesTuples[1][i])) 95 | placePicked = random.choice(indices) 96 | Room.occupiedIndices.append(placePicked) 97 | 98 | self.grid[placePicked] = Room.ID 99 | 100 | dist = int(Room.distanceFromTheWall) # parameter 101 | for i, num in enumerate(range(Room.distanceFromTheWall, 0, -1)): 102 | for j in range(-i, i+1): 103 | for k in range(-i, i+1): 104 | currentX = placePicked[0] + j 105 | currentY = placePicked[1] + k 106 | if 0 <= currentX < self.gridSize[0] and 0 <= currentY < self.gridSize[1]: 107 | if self.gridWeight[currentX, currentY] < num: 108 | self.gridWeight[currentX, currentY] = num 109 | self.gridWeight[placePicked] = 100 110 | self.roomsInGrid.append(Room) 111 | 112 | def fillEmpty(self, roomList): 113 | def locateUnassignedSpace(startPos): 114 | tempGrid = np.full(self.gridSize, 0) 115 | toCheck = [startPos] 116 | indices = [] 117 | while len(toCheck) > 0: 118 | current = toCheck[0] 119 | toCheck.pop(0) 120 | indices.append(current) 121 | tempGrid[current] = 1 122 | 123 | for fun in directions4List: 124 | if tempGrid[fun(current)] == 0 and self.grid[fun(current)] == -1: #I wasn't there, and place is unassigned to any room 125 | toCheck.append(fun(current)) 126 | return indices 127 | 128 | def findBestRoom(indices): 129 | tempRoom = room(occupiedIndices = indices, forceRoomID = -1) 130 | startPos = tempRoom.findStartPos(self) 131 | Walker = tileWalker(startPos, "right") 132 | 133 | start = True 134 | neighbours = np.zeros(self.roomsAmount) 135 | while not (Walker.position == startPos and Walker.direction[0] == RM) or start == True: 136 | start = False 137 | if Walker.lookLeft() != None: 138 | neighbours[self.grid[Walker.lookLeft()]] += 1 139 | Walker.makeStandardMovement(self, tempRoom) 140 | maxValue = max(neighbours) 141 | bestRooms = [i for i, j in enumerate(neighbours) if j == maxValue] 142 | bestRoomID = random.choice(bestRooms) 143 | return roomList[bestRoomID] 144 | 145 | for x in range(1, self.gridSize[0] - 1): 146 | for y in range(1, self.gridSize[1] - 1): 147 | if self.grid[x,y] == -1: 148 | indices = locateUnassignedSpace((x,y)) 149 | Room = findBestRoom(indices) 150 | Room.extendRoom(indices) 151 | Room.updateGrid(self) #this update should be in grid 152 | 153 | -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/Room.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | from copy import copy, deepcopy 4 | 5 | from methodsOfGeneration.gridRelatedClasses.TileWalker import tileWalker 6 | from functions.miscFunctions import * 7 | 8 | 9 | class room: 10 | currentID = 0 11 | 12 | def __init__(self, name = "unnamed", size = 4, wantedNeighbours = None, occupiedIndices = None, forceRoomID = None, outsideConnection = False): 13 | self.ID = room.currentID 14 | room.currentID += 1 15 | 16 | self.name = name 17 | self.size = size 18 | self.areaWanted = size ** 2 19 | self.outsideConnection = outsideConnection 20 | self.boundingArea = None 21 | 22 | self.estimatedAreaToGet = -1 #not calculated yet 23 | self.perfectArea = -1 24 | self.distanceFromTheWall = -1 25 | self.wantedNeighbours = wantedNeighbours 26 | 27 | if occupiedIndices == None: 28 | self.occupiedIndices = [] 29 | else: 30 | self.occupiedIndices = occupiedIndices 31 | 32 | if forceRoomID != None: 33 | self.ID = forceRoomID 34 | room.currentID -= 1 35 | 36 | self.canGrow = True 37 | self.LEdgeUsed = False 38 | 39 | self.blenderObject = None 40 | self.rectangleObject = None 41 | 42 | self.amountOfWindows = 0 43 | 44 | def updateGrid(self, Grid): 45 | for index in self.occupiedIndices: 46 | Grid.grid[index] = self.ID 47 | 48 | def estimateAreaToGet(self, area, areaRoomsWant): 49 | # print(Grid.area, Grid.areaRoomsWant) 50 | scale = float(area) / areaRoomsWant 51 | self.estimatedAreaToGet = self.areaWanted * scale 52 | self.distanceFromTheWall = max(int(math.sqrt(self.estimatedAreaToGet)/2), 1) 53 | 54 | def countPerfectArea(self, fieldSize): 55 | self.perfectArea = self.estimatedAreaToGet * fieldSize**2 56 | 57 | def findStartPos(self, Grid): 58 | minX = Grid.gridSize[0] 59 | minY = Grid.gridSize[1] 60 | 61 | #finding initial position 62 | candidates = [] 63 | for index in self.occupiedIndices: 64 | if index[0] < minX: 65 | minX = index[0] 66 | candidates = [index] 67 | elif index[0] == minX: 68 | candidates.append(index) 69 | for index in candidates: 70 | if index[1] < minY: 71 | minY = index[1] 72 | startPos = index 73 | return startPos 74 | 75 | def growRect(self, Grid): 76 | 77 | startPos = self.findStartPos(Grid) 78 | 79 | edge = [] 80 | edges = [] 81 | Walker = tileWalker(startPos, "right") 82 | 83 | start = True 84 | 85 | while not (Walker.position == startPos and Walker.direction[0] == RM) or start == True: 86 | start = False 87 | inspected = Walker.lookLeft() 88 | if Grid.grid[inspected] == -1: #empty space 89 | edge.append(inspected) 90 | 91 | if Grid.grid[Walker.lookForward()] != self.ID: #time to turn 92 | edges.append(edge.copy()) 93 | edge = [] 94 | Walker.makeStandardMovement(Grid, self) 95 | else: # can't have an edge here 96 | edge = [] #have to delete it, can't have edge there 97 | while Grid.grid[Walker.lookForward()] == self.ID: #move as far as possible 98 | Walker.moveForward() 99 | Walker.turnRight() 100 | 101 | if len(edges) == 0: 102 | self.canGrow = False 103 | else: 104 | mostTiles = 0 105 | longestLists = [] 106 | for i, edge in enumerate(edges): 107 | if len(edge) > mostTiles: 108 | longestLists = [edge] 109 | mostTiles = len(edge) 110 | elif len(edge) == mostTiles: 111 | longestLists.append(edge) 112 | 113 | pickedEdge = random.choice(longestLists) 114 | self.extendRoom(pickedEdge) 115 | self.updateGrid(Grid) 116 | #tweak /2 - disabled 117 | if len(self.occupiedIndices) > self.estimatedAreaToGet: #TRYING TO MAKE THEM SMALLER 118 | self.canGrow = False 119 | 120 | def extendRoom(self, indices): 121 | self.occupiedIndices.extend(indices) 122 | # TODO: GRID in room 123 | # Grid.updateGrid 124 | 125 | def growLShape(self, Grid): 126 | 127 | def timeToTurn(): 128 | if Grid.grid[Walker.lookForward()] != self.ID \ 129 | or Grid.grid[Walker.lookLeft()] == self.ID: #time to turn 130 | return True 131 | 132 | startPos = self.findStartPos(Grid) 133 | 134 | #TODO: bugfix - no candidates possible 135 | edge = [] 136 | edges = [] 137 | Walker = tileWalker(startPos, "right") 138 | 139 | start = True 140 | currentIsLShaped = False 141 | 142 | #grow loop 143 | while not (Walker.position == startPos and Walker.direction[0] == RM) or start == True: 144 | start = False 145 | if Grid.grid[Walker.lookLeft()] == -1: #empty space 146 | edge.append(Walker.lookLeft()) 147 | 148 | if timeToTurn(): 149 | edges.append((deepcopy(edge), deepcopy(currentIsLShaped))) 150 | edge = [] 151 | currentIsLShaped = False 152 | 153 | else: # obstacle on the left 154 | if self.LEdgeUsed: #move to the next edge 155 | edge = [] #have to delete it, can't have edge there 156 | currentIsLShaped = True 157 | else: # still can have an L-shaped edge 158 | if currentIsLShaped == True: 159 | edge = [] 160 | currentIsLShaped = True 161 | if len(edge) > 0: 162 | edges.append((deepcopy(edge), True)) 163 | edge.clear() 164 | 165 | if timeToTurn(): 166 | currentIsLShaped = False 167 | Walker.makeStandardMovement(Grid, self) 168 | # printResult(Grid, Walker, currentIsLShaped) 169 | 170 | #tweak - don't allow 1 sized edges 171 | newEdges = [] 172 | for edge in edges: 173 | if len(edge[0]) > 1: 174 | newEdges.append(edge) 175 | edges = newEdges 176 | 177 | #pick the longest edge 178 | mostTiles = 0 179 | longestLists = [] 180 | for i, edge in enumerate(edges): 181 | if not (self.LEdgeUsed == True and edge[1] == True): 182 | if len(edge[0]) > mostTiles: 183 | longestLists = [edge] 184 | mostTiles = len(edge[0]) 185 | elif len(edge[0]) == mostTiles: 186 | longestLists.append(edge) 187 | 188 | if longestLists == []: 189 | self.canGrow = False 190 | else: 191 | pickedEdge = random.choice(longestLists) 192 | if pickedEdge[1] == True: # this is an L-Shaped grow 193 | self.LEdgeUsed = True 194 | self.occupiedIndices.extend(pickedEdge[0]) 195 | self.updateGrid(Grid) 196 | 197 | def gridToPlanes(self, Grid, fieldSize): 198 | 199 | verts = [] 200 | 201 | startPos = self.findStartPos(Grid) 202 | Walker = tileWalker(startPos, "right") 203 | 204 | start = True 205 | while not (Walker.position == startPos and Walker.direction[0] == RM) or start == True: 206 | start = False 207 | direction = Walker.makeStandardMovement(Grid,self) 208 | if direction != "Forward" or (direction == "Forward" \ 209 | and Grid.grid[Walker.lookBottomRightCorner()] != Grid.grid[Walker.lookLeft()]): 210 | vert = Walker.placeVertex(fieldSize) 211 | # printResult(Grid, Walker, corner = vert) 212 | # printResult(Grid, Walker) 213 | verts.append((vert[0], vert[1], 0)) 214 | return verts 215 | 216 | # def findGeometricalCenter(self): 217 | # return self.center 218 | 219 | @staticmethod 220 | def resetCounter(): 221 | room.currentID = 0 -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/TileWalker.py: -------------------------------------------------------------------------------- 1 | from functions.miscFunctions import * 2 | 3 | class tileWalker: 4 | def __init__(self, position, initialDirection = "up"): 5 | self.position = position 6 | self.direction = (MT, LM, RM, MB,\ 7 | LT, RT, LB, RB) #always holds its: front, left, right, back in this order 8 | if initialDirection == "left": 9 | self.turnLeft() 10 | elif initialDirection == "right": 11 | self.turnRight() 12 | elif initialDirection == "down": 13 | self.turnRight(); self.turnRight() 14 | 15 | def turnLeft(self): 16 | d = self.direction 17 | self.direction = [d[1], d[3], d[0], d[2], \ 18 | d[6], d[4], d[7], d[5]] 19 | 20 | def turnRight(self): 21 | d = self.direction 22 | self.direction = [d[2], d[0], d[3], d[1], \ 23 | d[5], d[7], d[4], d[6]] 24 | 25 | def moveForward(self): 26 | self.position = self.direction[0](self.position) 27 | 28 | def lookLeft(self): 29 | return self.direction[1](self.position) 30 | 31 | def lookRight(self): 32 | return self.direction[2](self.position) 33 | 34 | def lookForward(self): 35 | return self.direction[0](self.position) 36 | 37 | def lookBottomRightCorner(self): 38 | return self.direction[6](self.position) 39 | 40 | def makeStandardMovement(self, Grid, Room): 41 | if Grid.grid[self.lookLeft()] == Room.ID: 42 | self.turnLeft() 43 | self.moveForward() 44 | return "Left" 45 | elif Grid.grid[self.lookForward()] != Room.ID: 46 | self.turnRight() 47 | return "Right" 48 | else: 49 | self.moveForward() 50 | return "Forward" 51 | 52 | def placeVertex(self, fieldSize): 53 | # if direction[0] == MT: 54 | # return RB(self.position) 55 | # elif direction[0] == LM: 56 | # return RT(self.position) 57 | # elif direction[0] == RM: 58 | # return LB(self.position) 59 | # elif direction[0] == MB: 60 | # return LT(self.position) 61 | if self.direction[0] == MT: 62 | return ((self.position[0] + 1)*fieldSize, self.position[1]*fieldSize) #right top 63 | elif self.direction[0] == LM: 64 | return ((self.position[0] +1)*fieldSize , (self.position[1]+1)*fieldSize) #right bottom 65 | elif self.direction[0] == RM: 66 | return (self.position[0]*fieldSize, self.position[1]*fieldSize) #left top 67 | elif self.direction[0] == MB: 68 | return (self.position[0]*fieldSize , (self.position[1]+1)*fieldSize) # left bottom -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/__pycache__/Grid.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/gridRelatedClasses/__pycache__/Grid.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/__pycache__/Room.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/gridRelatedClasses/__pycache__/Room.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/gridRelatedClasses/__pycache__/TileWalker.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/gridRelatedClasses/__pycache__/TileWalker.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/methodGrid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | import random 4 | 5 | from functions.miscFunctions import * 6 | 7 | from methodsOfGeneration.gridRelatedClasses.Grid import grid 8 | from methodsOfGeneration.gridRelatedClasses.Room import room 9 | from methodsOfGeneration.gridRelatedClasses.TileWalker import tileWalker 10 | 11 | from dataTypesClasses.RoomTypes import * 12 | 13 | def methodGrid(roomList, roomsAmount, gridSize, fieldSize = 1, indent = None): 14 | 15 | Grid = grid(gridSize, roomsAmount, indent) 16 | 17 | #nasty bug workaround - resets the index counter when using multiple times in blender 18 | r = diningroom() 19 | r.resetCounter() 20 | del r 21 | 22 | # roomList = [diningroom(), room("bedroom", 5), room("toilet", 4), room("kitchen", 3), room("bedroom", 4), 23 | # room("test1",3), room("test2",2), room("test3",3), room("test4",2), room("test5",3)] 24 | 25 | Grid.estimateAreaRoomsWant(roomList) 26 | 27 | for Room in roomList: 28 | Room.estimateAreaToGet(Grid.area, Grid.areaRoomsWant) 29 | # print(Room.ID, Room.name, Room.size, Room.areaWanted, Room.estimatedAreaToGet, Room.distanceFromTheWall) 30 | 31 | # placing rooms 32 | for Room in roomList: 33 | Grid.placeRoom(Room) 34 | 35 | # printResult(Grid) 36 | # quit() 37 | 38 | # rectangular growth 39 | roomsThatCanGrow = roomList.copy() 40 | while len(roomsThatCanGrow) != 0: 41 | roomsPicker = [] 42 | for room in roomsThatCanGrow: 43 | for i in range(room.size): 44 | roomsPicker.append(room) 45 | 46 | picked = random.choice(roomsPicker) 47 | picked.growRect(Grid) 48 | if picked.canGrow == False: 49 | roomsThatCanGrow.remove(picked) 50 | 51 | # printResult(Grid) 52 | # quit() 53 | # L-shaped growth 54 | 55 | for Room in roomList: 56 | Room.canGrow = True 57 | 58 | roomsThatCanGrow = roomList.copy() 59 | while len(roomsThatCanGrow) != 0: 60 | roomsPicker = [] 61 | for room in roomsThatCanGrow: 62 | for i in range(room.size): 63 | roomsPicker.append(room) 64 | 65 | picked = random.choice(roomsPicker) 66 | picked.growLShape(Grid) 67 | if picked.canGrow == False: 68 | roomsThatCanGrow.remove(picked) 69 | 70 | # for Room in roomList: 71 | # print(Room.LEdgeUsed) 72 | # print(Grid.area) 73 | # print(Grid.areaRoomsWant) 74 | # print(Grid.grid) 75 | 76 | # ------------------------- 77 | 78 | # printResult(Grid) 79 | # filling empty 80 | Grid.fillEmpty(roomList) 81 | # printResult(Grid) 82 | 83 | 84 | verts = [] 85 | faces = [] 86 | for Room in roomList: 87 | faceVerts = Room.gridToPlanes(Grid, fieldSize) 88 | faces.append(range(len(verts), len(verts)+len(faceVerts))) 89 | verts.extend(faceVerts) 90 | 91 | return verts, faces, Grid -------------------------------------------------------------------------------- /methodsOfGeneration/methodTreemaps.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from methodsOfGeneration.treemapsRelated.squarifiedTreemaps import Rectangle 4 | from dataTypesClasses.RoomTypes import * 5 | 6 | def methodTreemaps(roomList, size = (9,10)): 7 | 8 | # random.shuffle(roomList) 9 | 10 | #nasty bug workaround - resets the index counter when using multiple times in blender 11 | r = diningroom() 12 | r.resetCounter() 13 | del r 14 | 15 | area = size[0] * size[1] 16 | 17 | areaRoomsWant = 0 18 | for Room in roomList: 19 | areaRoomsWant += Room.size ** 2 20 | 21 | for Room in roomList: #Room.estimatedAreaToGet holds its area needed for the method 22 | Room.estimateAreaToGet(area, areaRoomsWant) 23 | 24 | BuildingPlan = Rectangle((0,0), size) 25 | 26 | areas = [Room.estimatedAreaToGet for Room in roomList] 27 | roomRectangles = BuildingPlan.placeRooms(areas) 28 | 29 | for i, Room in enumerate(roomList): 30 | Room.rectangleObject = roomRectangles[i] 31 | 32 | # for Room in roomList: 33 | # print(Room.name, Room.rectangleObject.LB(), Room.rectangleObject.RT(), Room.rectangleObject.area()) 34 | 35 | #list of all vertices 36 | allVerts = [] 37 | for Room in roomList: 38 | R = Room.rectangleObject 39 | functions = (R.LB, R.RB, R.RT, R.LT) 40 | for function in functions: 41 | if not function() in allVerts: 42 | allVerts.append(function()+[0]) 43 | 44 | #verts grouped by their face 45 | vertsInFaces = [] 46 | for Room in roomList: 47 | R = Room.rectangleObject 48 | faceVerts = [R.LB()+[0], R.RB()+[0], R.RT()+[0], R.LT()+[0]] 49 | vertsInFaces.append(faceVerts) 50 | 51 | def firstInBetween(vert1, vert2, allVerts): 52 | for vertBetween in allVerts: 53 | if round(vert1[0], 8) == round(vert2[0], 8) == round(vertBetween[0], 8): #rounding errors around 15th digit 54 | if vert1[1] < vertBetween[1] < vert2[1]\ 55 | or vert1[1] > vertBetween[1] > vert2[1]: 56 | return vertBetween 57 | if round(vert1[1], 8) == round(vert2[1], 8) == round(vertBetween[1], 8): 58 | if vert1[0] < vertBetween[0] < vert2[0]\ 59 | or vert1[0] > vertBetween[0] > vert2[0]: 60 | return vertBetween 61 | return None 62 | 63 | #add verts in middle of walls 64 | for faceID, face in enumerate(vertsInFaces): 65 | i = 0 66 | while i < len(face): 67 | vert1 = face[i-1] 68 | vert2 = face[i] 69 | vertToInsert = firstInBetween(vert1, vert2, allVerts) 70 | 71 | if faceID == 2 and i == 2: 72 | # print("NOW") 73 | print(i, vert1, vert2, vertToInsert) 74 | for verts in allVerts: 75 | print(verts) 76 | # print("END") 77 | 78 | if vertToInsert != None: 79 | face.insert(i, vertToInsert) 80 | i = 0 81 | else: 82 | i += 1 83 | 84 | #final list 85 | verts = [] 86 | faces = [] 87 | for face in vertsInFaces: 88 | faces.append(range(len(verts), len(verts)+len(face))) 89 | verts.extend(face) 90 | return verts, faces 91 | 92 | #tester 93 | 94 | # roomsAmount = 5 95 | # roomList = [diningroom(), bathroom(), bedroom(4), kitchen(), bedroom(), bedroom(), toilet(), pantry()] 96 | # roomList = roomList[:roomsAmount] 97 | # verts, faces = methodTreemaps(roomList, (10,13)) 98 | 99 | -------------------------------------------------------------------------------- /methodsOfGeneration/treemapsRelated/__pycache__/squarifiedTreemaps.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/methodsOfGeneration/treemapsRelated/__pycache__/squarifiedTreemaps.cpython-38.pyc -------------------------------------------------------------------------------- /methodsOfGeneration/treemapsRelated/squarifiedTreemaps.py: -------------------------------------------------------------------------------- 1 | class Rectangle: 2 | def __init__(self, LeftBottom, RightTop): #((x1, y1), (x2, y2)) 3 | 4 | self.x1 = LeftBottom[0] 5 | self.x2 = RightTop[0] 6 | self.y1 = LeftBottom[1] 7 | self.y2 = RightTop[1] 8 | 9 | def LB(self): 10 | return [self.x1, self.y1] 11 | 12 | def RB(self): 13 | return [self.x2, self.y1] 14 | 15 | def LT(self): 16 | return [self.x1, self.y2] 17 | 18 | def RT(self): 19 | return [self.x2, self.y2] 20 | 21 | def heigth(self): 22 | return abs(self.y2 - self.y1) 23 | 24 | def width(self): 25 | return abs(self.x2 - self.x1) 26 | 27 | def area(self): 28 | return self.heigth() * self.width() 29 | 30 | def isHorizontal(self): 31 | if self.heigth() > self.width(): return False 32 | else: return True 33 | 34 | def ratio(self): #0 for square 35 | ratio = max(self.width() / self.heigth(), self.heigth() / self.width()) 36 | return ratio - 1 37 | 38 | def divideHorizontal(self, area): #cuts are vertical lines 39 | divisionWidth = area / self.heigth() 40 | Rect1 = Rectangle(self.LB(), (self.x1+divisionWidth, self.y2)) 41 | Rect2 = Rectangle((self.x1+divisionWidth, self.y1), self.RT()) 42 | return Rect1, Rect2 43 | 44 | def divideVertical(self, area): #cuts are horizontal lines 45 | divisionHeight = area / self.width() 46 | Rect1 = Rectangle(self.LB(), (self.x2, self.y1+divisionHeight)) 47 | Rect2 = Rectangle((self.x1, self.y1+divisionHeight), self.RT()) 48 | return Rect1, Rect2 49 | 50 | def divideAuto(self, area): 51 | if self.isHorizontal(): 52 | Rect1, Rect2 = self.divideHorizontal(area) 53 | else: 54 | Rect1, Rect2 = self.divideVertical(area) 55 | return Rect1, Rect2 56 | 57 | def addRoomsInOneZone(self, areas): 58 | error = float('inf') #in first iteration, error is always acceptable 59 | for i in range(len(areas)): 60 | currentAreas = areas[:i+1] 61 | zoneArea = sum(currentAreas) 62 | Zone, NewLeftOver = self.divideAuto(zoneArea) 63 | newRooms = [] 64 | for area in currentAreas: #fills the zone with rooms 65 | if self.isHorizontal: 66 | Room, ZoneLeftOver = Zone.divideHorizontal(area) 67 | else: 68 | Room, ZoneLeftOver = Zone.divideVertical(area) 69 | # print(Room.LB(), Room.RT()) 70 | Zone = ZoneLeftOver 71 | newRooms.append(Room) 72 | 73 | #check if it is better than previous iteration 74 | newError = countSquareError(newRooms) 75 | 76 | if newError > error: #previous iteration was better 77 | break 78 | else: #this one is better, or first iteration 79 | rooms = newRooms 80 | error = newError 81 | LeftOver = NewLeftOver 82 | return rooms, LeftOver 83 | 84 | def placeRooms(self, areas): 85 | rooms, LeftOver = self.addRoomsInOneZone(areas) 86 | roomsLeft = areas[len(rooms):] 87 | if len(roomsLeft) > 0: 88 | newlyPlacedRooms = LeftOver.placeRooms(roomsLeft) 89 | rooms.extend(newlyPlacedRooms) 90 | return rooms 91 | 92 | def countSquareError(rooms): 93 | error = 0 94 | for Room in rooms: 95 | error += Room.ratio() 96 | return error/len(rooms) -------------------------------------------------------------------------------- /panelGUI.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import (StringProperty, 3 | BoolProperty, 4 | IntProperty, 5 | FloatProperty, 6 | FloatVectorProperty, 7 | EnumProperty, 8 | PointerProperty, 9 | ) 10 | from bpy.types import (Panel, 11 | Menu, 12 | Operator, 13 | PropertyGroup, 14 | ) 15 | 16 | class MyProperties(PropertyGroup): 17 | my_enum: EnumProperty( 18 | name="Dropdown:", 19 | description="Select generation method", 20 | items=[ ('M1', "Grid placement", ""), 21 | ('M2', "Squarified treemaps", ""),] 22 | ) 23 | 24 | 25 | class PanelGUI(bpy.types.Panel): 26 | """Creates a Panel in the dock on the World Properties dock""" 27 | bl_label = "Procedural building creator" 28 | bl_idname = "wojtryb_buildings" 29 | bl_space_type = 'PROPERTIES' 30 | bl_region_type = 'WINDOW' 31 | bl_context = "world" 32 | 33 | #GUI 34 | def draw(self, context): 35 | layout = self.layout 36 | 37 | obj = context.object 38 | 39 | row = layout.row() 40 | row.prop(context.scene, "seed") 41 | 42 | row = layout.row() 43 | row.prop(context.scene.my_tool, "my_enum", expand=True) 44 | 45 | row = layout.row() 46 | row.label(text="Main parameters - both methods", icon='WORLD_DATA') 47 | 48 | row = layout.row() 49 | row.prop(context.scene, "roomsAmount") 50 | 51 | row = layout.row() 52 | row.prop(context.scene, "sizeX") 53 | 54 | row.prop(context.scene, "sizeY") 55 | 56 | row = layout.row() 57 | row.label(text="Grid placement parameters", icon='WORLD_DATA') 58 | 59 | row = layout.row() 60 | row.prop(context.scene, "dynamicFieldSize") 61 | 62 | row = layout.row() 63 | row.prop(context.scene, "fieldSize") 64 | 65 | row = layout.row() 66 | row.prop(context.scene, "allowIndent") 67 | 68 | row = layout.row() 69 | row.prop(context.scene, "indentProbability") 70 | 71 | row = layout.row() 72 | row.prop(context.scene, "indentOffsetX", text = "Off_X") 73 | row.prop(context.scene, "indentOffsetY", text = "Off_Y") 74 | 75 | row = layout.row() 76 | 77 | row.prop(context.scene, "indentValueX", text = "Ind_X") 78 | row.prop(context.scene, "indentValueY", text = "Ind_Y") 79 | 80 | row = layout.row() 81 | row.label(text="Visual parameters", icon='WORLD_DATA') 82 | 83 | row = layout.row() 84 | row.prop(context.scene, "height") 85 | 86 | row = layout.row() 87 | row.prop(context.scene, "wallsThickness") 88 | 89 | row = layout.row() 90 | row.prop(context.scene, "doorPosition", text = "Door pos.") 91 | 92 | row = layout.row() 93 | row.operator("mesh.house") 94 | 95 | row = layout.row() 96 | row.operator("mesh.data") 97 | 98 | row = layout.row() 99 | row.prop(context.scene, "records") 100 | 101 | row = layout.row() 102 | row.operator("mesh.alldata") 103 | 104 | row = layout.row() 105 | row.prop(context.scene, "experiment") 106 | 107 | row = layout.row() 108 | row.prop(context.scene, "writePath") 109 | 110 | #creating variables for blender to store fields specified in GUI 111 | def register(): 112 | 113 | bpy.utils.register_class(PanelGUI) 114 | bpy.utils.register_class(MyProperties) 115 | 116 | bpy.types.Scene.my_tool = PointerProperty(type=MyProperties) 117 | 118 | bpy.types.Scene.seed = bpy.props.StringProperty \ 119 | ( 120 | name = "Seed", 121 | description = "Select the seed (leave blank for random generation)", 122 | default = "" 123 | ) 124 | 125 | bpy.types.Scene.roomsAmount = bpy.props.StringProperty \ 126 | ( 127 | name = "Rooms", 128 | description = "Select the amount of rooms to generate", 129 | default = "6" 130 | ) 131 | bpy.types.Scene.sizeX = bpy.props.StringProperty \ 132 | ( 133 | name = "SizeX", 134 | description = "Select the width of the building [m]", 135 | default = "10" 136 | ) 137 | bpy.types.Scene.sizeY = bpy.props.StringProperty \ 138 | ( 139 | name = "SizeY", 140 | description = "Select the height of the building [m]", 141 | default = "12" 142 | ) 143 | bpy.types.Scene.dynamicFieldSize = bpy.props.BoolProperty \ 144 | ( 145 | name="Dynamic field size", 146 | description="Specify if the grid size should be calculated automatically", 147 | default = False 148 | ) 149 | bpy.types.Scene.fieldSize = bpy.props.StringProperty \ 150 | ( 151 | name = "Field size", 152 | description = "Select the size of a grid field [m] (manual field size)", 153 | default = "1" 154 | ) 155 | bpy.types.Scene.allowIndent = bpy.props.BoolProperty \ 156 | ( 157 | name="Allow indent", 158 | description="Allow cutting indent in the building", 159 | default = False 160 | ) 161 | bpy.types.Scene.indentProbability = bpy.props.StringProperty \ 162 | ( 163 | name="Probability", 164 | description="Probability of cutting the indent", 165 | default = "0.5" 166 | ) 167 | bpy.types.Scene.indentOffsetX = bpy.props.StringProperty \ 168 | ( 169 | name="Offset X", 170 | description="Offset of the indent in x axis[m] (allows indent not in a building corner)", 171 | default = "1" 172 | ) 173 | bpy.types.Scene.indentOffsetY = bpy.props.StringProperty \ 174 | ( 175 | name="Offset Y", 176 | description="Offset of the indent in y axis[m] (allows indent not in a building corner)", 177 | default = "1" 178 | ) 179 | bpy.types.Scene.indentValueX = bpy.props.StringProperty \ 180 | ( 181 | name = "IndentValue X", 182 | description = "Size of the indent in x axis[m]", 183 | default = "3" 184 | ) 185 | bpy.types.Scene.indentValueY = bpy.props.StringProperty \ 186 | ( 187 | name = "IndentValue Y", 188 | description = "Size of the indent in y axis[m]", 189 | default = "3" 190 | ) 191 | bpy.types.Scene.height = bpy.props.StringProperty \ 192 | ( 193 | name = "Height", 194 | description = "Building height[m]", 195 | default = "1.5" 196 | ) 197 | bpy.types.Scene.wallsThickness = bpy.props.StringProperty \ 198 | ( 199 | name = "Thickness", 200 | description = "Thickness of the building walls[m]", 201 | default = "0.3" 202 | ) 203 | bpy.types.Scene.doorPosition = bpy.props.StringProperty \ 204 | ( 205 | name = "Door position", 206 | description = "Position of the doors (0-snap to wall on left, 1-snap to wall on right)", 207 | default = "0.5" 208 | ) 209 | bpy.types.Scene.records = bpy.props.StringProperty \ 210 | ( 211 | name = "Records", 212 | description = "Amount of buildings to generate", 213 | default = "20" 214 | ) 215 | bpy.types.Scene.experiment = bpy.props.StringProperty \ 216 | ( 217 | name = "Experiment", 218 | description = "ID of the experiment to conduct", 219 | default = "1" 220 | ) 221 | bpy.types.Scene.writePath = bpy.props.StringProperty \ 222 | ( 223 | name = "Write path", 224 | default = "", 225 | description = "Define the root path of the project", 226 | subtype = 'DIR_PATH' 227 | ) 228 | 229 | 230 | def unregister(): 231 | bpy.utils.unregister_class(PanelGUI) 232 | del bpy.types.Scene.roomsAmount 233 | 234 | if __name__ == "__main__": 235 | register() 236 | -------------------------------------------------------------------------------- /project.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtryb/proceduralBuildingGenerator/d7d21511e49b38b280da0a592d1583cca6470121/project.blend --------------------------------------------------------------------------------